@townco/ui 0.1.115 → 0.1.117
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.d.ts +25 -0
- package/dist/core/hooks/use-tool-calls.d.ts +50 -0
- package/dist/core/schemas/chat.d.ts +50 -2
- package/dist/core/schemas/tool-call.d.ts +91 -3
- package/dist/core/schemas/tool-call.js +22 -0
- package/dist/gui/components/ChatView.js +2 -25
- package/dist/gui/components/ContextUsageButton.d.ts +16 -12
- package/dist/gui/components/ContextUsageButton.js +85 -45
- package/dist/gui/components/EditableUserMessage.js +1 -1
- package/dist/gui/components/Message.js +2 -2
- package/dist/gui/components/MessageActions.js +1 -1
- package/dist/gui/components/MessageContent.js +29 -20
- package/dist/gui/components/Response.js +72 -1
- package/dist/gui/components/SubagentStream.d.ts +23 -0
- package/dist/gui/components/SubagentStream.js +98 -0
- package/dist/gui/components/ToolCall.js +10 -2
- package/dist/gui/components/ToolOperation.js +33 -6
- package/dist/gui/components/Tooltip.js +3 -1
- package/dist/sdk/schemas/session.d.ts +50 -2
- package/package.json +3 -3
|
@@ -13,11 +13,11 @@ import { ToolOperation } from "./ToolOperation.js";
|
|
|
13
13
|
* Provides the content container with role-based styling
|
|
14
14
|
* Handles automatic rendering of thinking, waiting states, and content
|
|
15
15
|
*/
|
|
16
|
-
const messageContentVariants = cva("w-full rounded-2xl text-[var(--font-size)] font-[var(--font-family)] leading-relaxed break-words transition-colors", {
|
|
16
|
+
const messageContentVariants = cva("w-full min-w-0 rounded-2xl text-[var(--font-size)] font-[var(--font-family)] leading-relaxed break-words [overflow-wrap:anywhere] transition-colors", {
|
|
17
17
|
variants: {
|
|
18
18
|
role: {
|
|
19
19
|
user: "bg-secondary text-foreground px-4 py-4 text-paragraph",
|
|
20
|
-
assistant: "text-foreground text-paragraph
|
|
20
|
+
assistant: "text-foreground text-paragraph",
|
|
21
21
|
system: "bg-card border border-border text-foreground opacity-80 text-caption px-4 py-3",
|
|
22
22
|
},
|
|
23
23
|
variant: {
|
|
@@ -40,6 +40,11 @@ function countCitations(text) {
|
|
|
40
40
|
count++;
|
|
41
41
|
return count;
|
|
42
42
|
}
|
|
43
|
+
function trimTrailingNewlines(text) {
|
|
44
|
+
// Avoid rendering an extra blank line at the end of assistant markdown
|
|
45
|
+
// (especially noticeable right before tool-operation blocks).
|
|
46
|
+
return text.replace(/\n+$/g, "");
|
|
47
|
+
}
|
|
43
48
|
export const MessageContent = React.forwardRef(({ role: roleProp, variant, isStreaming: isStreamingProp, message, thinkingDisplayStyle = "collapsible", className, children, ...props }, ref) => {
|
|
44
49
|
// Get streaming start time and current model from store
|
|
45
50
|
const streamingStartTime = useChatStore((state) => state.streamingStartTime);
|
|
@@ -70,7 +75,7 @@ export const MessageContent = React.forwardRef(({ role: roleProp, variant, isStr
|
|
|
70
75
|
const hasThinking = !!thinking;
|
|
71
76
|
// Check if waiting (streaming but no content yet)
|
|
72
77
|
const isWaiting = message.isStreaming && !message.content && message.role === "assistant";
|
|
73
|
-
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
|
|
78
|
+
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 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" ? ((() => {
|
|
74
79
|
// Sort tool calls by content position
|
|
75
80
|
const sortedToolCalls = (message.toolCalls || [])
|
|
76
81
|
.slice()
|
|
@@ -210,7 +215,7 @@ export const MessageContent = React.forwardRef(({ role: roleProp, variant, isStr
|
|
|
210
215
|
const hasHookPositions = hookNotifications.some((n) => n.contentPosition !== undefined);
|
|
211
216
|
if (!hasHookPositions) {
|
|
212
217
|
// No positions - render hooks at top, then content, then tool calls
|
|
213
|
-
return (_jsxs(
|
|
218
|
+
return (_jsxs("div", { className: "flex flex-col gap-2", children: [hookNotifications.length > 0 && (_jsx("div", { className: "flex flex-col gap-2", children: hookNotifications.map((notification) => (_jsx(HookNotification, { notification: notification }, notification.id))) })), _jsx("div", { children: _jsx(Response, { content: trimTrailingNewlines(message.content), isStreaming: message.isStreaming, showEmpty: false, sources: sourcesForResponse, messageId: message.id, citationIndexOffset: 0 }) }), groupedToolCalls.length > 0 && (_jsx("div", { className: "flex flex-col gap-2", children: groupedToolCalls.map((item, index) => renderToolCallOrGroup(item, index)) })), shouldShowThinkingIndicator && (_jsx("div", { className: "flex flex-col 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: "Thinking..." }) }) }))] }));
|
|
214
219
|
}
|
|
215
220
|
// Hooks have positions - render them inline with content
|
|
216
221
|
const elements = [];
|
|
@@ -221,9 +226,10 @@ export const MessageContent = React.forwardRef(({ role: roleProp, variant, isStr
|
|
|
221
226
|
// Add text before this hook notification
|
|
222
227
|
if (position > currentPosition) {
|
|
223
228
|
const textChunk = message.content.slice(currentPosition, position);
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
229
|
+
const trimmedChunk = trimTrailingNewlines(textChunk);
|
|
230
|
+
if (trimmedChunk) {
|
|
231
|
+
elements.push(_jsx("div", { children: _jsx(Response, { content: trimmedChunk, isStreaming: false, showEmpty: false, sources: sourcesForResponse, messageId: message.id, citationIndexOffset: citationOffset }) }, `text-before-hook-${notification.id}`));
|
|
232
|
+
citationOffset += countCitations(trimmedChunk);
|
|
227
233
|
}
|
|
228
234
|
}
|
|
229
235
|
// Add hook notification
|
|
@@ -233,9 +239,10 @@ export const MessageContent = React.forwardRef(({ role: roleProp, variant, isStr
|
|
|
233
239
|
// Add remaining text
|
|
234
240
|
if (currentPosition < message.content.length) {
|
|
235
241
|
const remainingText = message.content.slice(currentPosition);
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
242
|
+
const trimmedRemaining = trimTrailingNewlines(remainingText);
|
|
243
|
+
if (trimmedRemaining) {
|
|
244
|
+
elements.push(_jsx("div", { children: _jsx(Response, { content: trimmedRemaining, isStreaming: message.isStreaming, showEmpty: false, sources: sourcesForResponse, messageId: message.id, citationIndexOffset: citationOffset }) }, "text-end-hooks"));
|
|
245
|
+
citationOffset += countCitations(trimmedRemaining);
|
|
239
246
|
}
|
|
240
247
|
}
|
|
241
248
|
// Add tool calls at the end
|
|
@@ -244,7 +251,7 @@ export const MessageContent = React.forwardRef(({ role: roleProp, variant, isStr
|
|
|
244
251
|
elements.push(renderToolCallOrGroup(item, index));
|
|
245
252
|
});
|
|
246
253
|
}
|
|
247
|
-
return _jsx(
|
|
254
|
+
return _jsx("div", { className: "flex flex-col gap-2", children: elements });
|
|
248
255
|
}
|
|
249
256
|
// Render content interleaved with tool calls and hook notifications
|
|
250
257
|
// Group consecutive tool calls with the same batchId or same title
|
|
@@ -299,12 +306,13 @@ export const MessageContent = React.forwardRef(({ role: roleProp, variant, isStr
|
|
|
299
306
|
// Flush any pending batch before adding text
|
|
300
307
|
flushBatch();
|
|
301
308
|
const textChunk = message.content.slice(currentPosition, position);
|
|
302
|
-
|
|
309
|
+
const trimmedChunk = trimTrailingNewlines(textChunk);
|
|
310
|
+
if (trimmedChunk) {
|
|
303
311
|
const itemId = positionedItem.type === "toolCall"
|
|
304
312
|
? positionedItem.item.id
|
|
305
313
|
: positionedItem.item.id;
|
|
306
|
-
elements.push(_jsx("div", { children: _jsx(Response, { content:
|
|
307
|
-
citationOffset += countCitations(
|
|
314
|
+
elements.push(_jsx("div", { children: _jsx(Response, { content: trimmedChunk, isStreaming: false, showEmpty: false, sources: sourcesForResponse, messageId: message.id, citationIndexOffset: citationOffset }) }, `text-before-${itemId}`));
|
|
315
|
+
citationOffset += countCitations(trimmedChunk);
|
|
308
316
|
}
|
|
309
317
|
}
|
|
310
318
|
if (positionedItem.type === "hookNotification") {
|
|
@@ -357,9 +365,10 @@ export const MessageContent = React.forwardRef(({ role: roleProp, variant, isStr
|
|
|
357
365
|
// Add remaining text after the last non-preliminary tool call
|
|
358
366
|
if (currentPosition < message.content.length) {
|
|
359
367
|
const remainingText = message.content.slice(currentPosition);
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
368
|
+
const trimmedRemaining = trimTrailingNewlines(remainingText);
|
|
369
|
+
if (trimmedRemaining) {
|
|
370
|
+
elements.push(_jsx("div", { children: _jsx(Response, { content: trimmedRemaining, isStreaming: message.isStreaming, showEmpty: false, sources: sourcesForResponse, messageId: message.id, citationIndexOffset: citationOffset }) }, "text-end"));
|
|
371
|
+
citationOffset += countCitations(trimmedRemaining);
|
|
363
372
|
}
|
|
364
373
|
}
|
|
365
374
|
// Render preliminary (selecting) tool calls at the end, grouped
|
|
@@ -377,10 +386,10 @@ export const MessageContent = React.forwardRef(({ role: roleProp, variant, isStr
|
|
|
377
386
|
}
|
|
378
387
|
// Add thinking indicator if all tool calls are complete but message is still streaming
|
|
379
388
|
if (shouldShowThinkingIndicator) {
|
|
380
|
-
elements.push(_jsx("div", { className: "flex flex-col
|
|
389
|
+
elements.push(_jsx("div", { className: "flex flex-col 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: "Thinking..." }) }) }, "thinking-indicator"));
|
|
381
390
|
}
|
|
382
|
-
return _jsx(
|
|
383
|
-
})()) : (_jsxs("div", { className: "flex flex-col gap-2", children: [message.images && message.images.length > 0 && (_jsx("div", { className: "flex flex-wrap gap-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" }, `image-${image.mimeType}-${image.data.slice(0, 20)}`))) })), message.content && (_jsx("div", { className: "whitespace-pre-wrap", children: message.content }))] }))] }));
|
|
391
|
+
return _jsx("div", { className: "flex flex-col gap-2", children: elements });
|
|
392
|
+
})()) : (_jsxs("div", { className: "flex flex-col gap-2", children: [message.images && message.images.length > 0 && (_jsx("div", { className: "flex flex-wrap gap-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" }, `image-${image.mimeType}-${image.data.slice(0, 20)}`))) })), message.content && (_jsx("div", { className: "whitespace-pre-wrap break-words [overflow-wrap:anywhere]", children: message.content }))] }))] }));
|
|
384
393
|
}
|
|
385
394
|
return (_jsx("div", { ref: ref, className: cn(messageContentVariants({ role, variant }), isStreaming && "animate-pulse-subtle", className), ...props, children: content }));
|
|
386
395
|
});
|
|
@@ -1,13 +1,79 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { createLogger } from "@townco/core";
|
|
4
|
+
import { Check, Copy, Download } from "lucide-react";
|
|
4
5
|
import * as React from "react";
|
|
5
6
|
import remarkGfm from "remark-gfm";
|
|
7
|
+
import { toast } from "sonner";
|
|
6
8
|
import { Streamdown } from "streamdown";
|
|
7
9
|
import { remarkCitations } from "../lib/remark-citations.js";
|
|
8
10
|
import { cn } from "../lib/utils.js";
|
|
11
|
+
import { Action, Actions } from "./Actions.js";
|
|
9
12
|
import { CitationChip } from "./CitationChip.js";
|
|
10
13
|
const logger = createLogger("Response", "debug");
|
|
14
|
+
function escapeCsvCell(value) {
|
|
15
|
+
// Escape double quotes by doubling them, and wrap in quotes if needed.
|
|
16
|
+
const mustQuote = /[",\n\r]/.test(value);
|
|
17
|
+
const escaped = value.replaceAll('"', '""');
|
|
18
|
+
return mustQuote ? `"${escaped}"` : escaped;
|
|
19
|
+
}
|
|
20
|
+
function tableToCsv(table) {
|
|
21
|
+
const rows = Array.from(table.querySelectorAll("tr"));
|
|
22
|
+
return rows
|
|
23
|
+
.map((row) => {
|
|
24
|
+
const cells = Array.from(row.querySelectorAll("th,td"));
|
|
25
|
+
return cells
|
|
26
|
+
.map((cell) => escapeCsvCell((cell.textContent || "").trim()))
|
|
27
|
+
.join(",");
|
|
28
|
+
})
|
|
29
|
+
.join("\n");
|
|
30
|
+
}
|
|
31
|
+
function downloadTextFile(filename, content, mime) {
|
|
32
|
+
const blob = new Blob([content], { type: mime });
|
|
33
|
+
const url = URL.createObjectURL(blob);
|
|
34
|
+
const a = document.createElement("a");
|
|
35
|
+
a.href = url;
|
|
36
|
+
a.download = filename;
|
|
37
|
+
document.body.appendChild(a);
|
|
38
|
+
a.click();
|
|
39
|
+
document.body.removeChild(a);
|
|
40
|
+
URL.revokeObjectURL(url);
|
|
41
|
+
}
|
|
42
|
+
function TableWithActions(tableProps) {
|
|
43
|
+
const { className, ...rest } = tableProps;
|
|
44
|
+
const tableRef = React.useRef(null);
|
|
45
|
+
const [isCopied, setIsCopied] = React.useState(false);
|
|
46
|
+
const handleCopy = React.useCallback(async () => {
|
|
47
|
+
const table = tableRef.current;
|
|
48
|
+
if (!table)
|
|
49
|
+
return;
|
|
50
|
+
try {
|
|
51
|
+
const csv = tableToCsv(table);
|
|
52
|
+
await navigator.clipboard.writeText(csv);
|
|
53
|
+
setIsCopied(true);
|
|
54
|
+
toast.success("Copied table as CSV");
|
|
55
|
+
setTimeout(() => setIsCopied(false), 1500);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
toast.error("Failed to copy table");
|
|
59
|
+
}
|
|
60
|
+
}, []);
|
|
61
|
+
const handleDownload = React.useCallback(() => {
|
|
62
|
+
const table = tableRef.current;
|
|
63
|
+
if (!table)
|
|
64
|
+
return;
|
|
65
|
+
const csv = tableToCsv(table);
|
|
66
|
+
downloadTextFile("table.csv", csv, "text/csv;charset=utf-8");
|
|
67
|
+
toast.success("Downloaded table.csv");
|
|
68
|
+
}, []);
|
|
69
|
+
return (_jsxs("div", { className: "group/table", children: [
|
|
70
|
+
_jsx("div", { className: "overflow-x-auto", children: _jsx("table", { ref: tableRef, className: cn("min-w-full border-collapse border border-border rounded-md", className), ...rest }) }), _jsxs(Actions, { className: cn("mt-2 flex justify-end", "opacity-0 group-hover/table:opacity-100 transition-opacity",
|
|
71
|
+
// Keep layout stable and avoid accidental clicks when hidden
|
|
72
|
+
"pointer-events-none group-hover/table:pointer-events-auto"), children: [
|
|
73
|
+
_jsx(Action, { onClick: handleCopy, tooltip: isCopied ? "Copied!" : "Copy", children: isCopied ? (_jsx(Check, { className: "size-4" })) : (_jsx(Copy, { className: "size-4" })) }), _jsx(Action, { onClick: handleDownload, tooltip: "Download CSV", children: _jsx(Download, { className: "size-4" }) })
|
|
74
|
+
] })
|
|
75
|
+
] }));
|
|
76
|
+
}
|
|
11
77
|
export const Response = React.memo(React.forwardRef(({ content, isStreaming = false, showEmpty = true, emptyMessage = "", sources = [], messageId, citationIndexOffset = 0, className, ...props }, ref) => {
|
|
12
78
|
// State to force remount when sources change
|
|
13
79
|
const [remountKey, setRemountKey] = React.useState(0);
|
|
@@ -41,6 +107,11 @@ export const Response = React.memo(React.forwardRef(({ content, isStreaming = fa
|
|
|
41
107
|
// running local index + the provided chunk offset.
|
|
42
108
|
const citationCounter = { current: 0 };
|
|
43
109
|
return {
|
|
110
|
+
table: (props) => (_jsx(TableWithActions, { ...props })),
|
|
111
|
+
thead: (props) => (_jsx("thead", { className: cn("bg-card border-b border-border", props.className), ...props })),
|
|
112
|
+
tr: (props) => (_jsx("tr", { className: cn("border-b border-border hover:bg-card transition-colors", props.className), ...props })),
|
|
113
|
+
th: (props) => (_jsx("th", { className: cn("px-4 py-2 text-left font-semibold text-foreground border-r border-border last:border-r-0", props.className), ...props })),
|
|
114
|
+
td: (props) => (_jsx("td", { className: cn("px-4 py-2 text-foreground border-r border-border last:border-r-0", props.className), ...props })),
|
|
44
115
|
// Custom span component that intercepts citation markers
|
|
45
116
|
// The remark-citations plugin creates spans with class "citation-marker"
|
|
46
117
|
// and data-citation-id attribute
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface SubagentStreamProps {
|
|
2
|
+
/** Sub-agent HTTP port */
|
|
3
|
+
port: number;
|
|
4
|
+
/** Sub-agent session ID */
|
|
5
|
+
sessionId: string;
|
|
6
|
+
/** Optional host (defaults to localhost) */
|
|
7
|
+
host?: string;
|
|
8
|
+
/** Parent tool call status - use this to determine if sub-agent is running */
|
|
9
|
+
parentStatus?: "pending" | "in_progress" | "completed" | "failed";
|
|
10
|
+
/** Sub-agent name (for display) */
|
|
11
|
+
agentName?: string | undefined;
|
|
12
|
+
/** Query sent to the sub-agent */
|
|
13
|
+
query?: string | undefined;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* SubagentStream component - displays streaming content from a sub-agent.
|
|
17
|
+
*
|
|
18
|
+
* This component:
|
|
19
|
+
* - Connects directly to the sub-agent's SSE endpoint
|
|
20
|
+
* - Displays streaming text and tool calls
|
|
21
|
+
* - Renders in a collapsible section (collapsed by default)
|
|
22
|
+
*/
|
|
23
|
+
export declare function SubagentStream({ port, sessionId, host, parentStatus, agentName, query, }: SubagentStreamProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ChevronDown, CircleDot, Loader2 } from "lucide-react";
|
|
3
|
+
import React, { useCallback, useEffect, useRef, useState } from "react";
|
|
4
|
+
import { useSubagentStream } from "../../core/hooks/use-subagent-stream.js";
|
|
5
|
+
const SCROLL_THRESHOLD = 50; // px from bottom to consider "at bottom"
|
|
6
|
+
/**
|
|
7
|
+
* SubagentStream component - displays streaming content from a sub-agent.
|
|
8
|
+
*
|
|
9
|
+
* This component:
|
|
10
|
+
* - Connects directly to the sub-agent's SSE endpoint
|
|
11
|
+
* - Displays streaming text and tool calls
|
|
12
|
+
* - Renders in a collapsible section (collapsed by default)
|
|
13
|
+
*/
|
|
14
|
+
export function SubagentStream({ port, sessionId, host, parentStatus, agentName, query, }) {
|
|
15
|
+
const [isExpanded, setIsExpanded] = useState(false); // Start collapsed for parallel ops
|
|
16
|
+
const [isThinkingExpanded, setIsThinkingExpanded] = useState(true);
|
|
17
|
+
const [isNearBottom, setIsNearBottom] = useState(true);
|
|
18
|
+
const thinkingContainerRef = useRef(null);
|
|
19
|
+
const { messages, isStreaming: hookIsStreaming, error } = useSubagentStream({
|
|
20
|
+
port,
|
|
21
|
+
sessionId,
|
|
22
|
+
...(host !== undefined ? { host } : {}),
|
|
23
|
+
});
|
|
24
|
+
// Use parent status as primary indicator, fall back to hook's streaming state
|
|
25
|
+
// Parent is "in_progress" means sub-agent is definitely still running
|
|
26
|
+
const isRunning = parentStatus === "in_progress" || parentStatus === "pending" || hookIsStreaming;
|
|
27
|
+
// Get the current/latest message
|
|
28
|
+
const currentMessage = messages[messages.length - 1];
|
|
29
|
+
const hasContent = currentMessage &&
|
|
30
|
+
(currentMessage.content ||
|
|
31
|
+
(currentMessage.toolCalls && currentMessage.toolCalls.length > 0));
|
|
32
|
+
// Auto-collapse Thinking when completed (so Output is the primary view)
|
|
33
|
+
const prevIsRunningRef = useRef(isRunning);
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (prevIsRunningRef.current && !isRunning) {
|
|
36
|
+
// Just completed - collapse thinking to show output
|
|
37
|
+
setIsThinkingExpanded(false);
|
|
38
|
+
}
|
|
39
|
+
prevIsRunningRef.current = isRunning;
|
|
40
|
+
}, [isRunning]);
|
|
41
|
+
// Check if user is near bottom of scroll area
|
|
42
|
+
const checkScrollPosition = useCallback(() => {
|
|
43
|
+
const container = thinkingContainerRef.current;
|
|
44
|
+
if (!container)
|
|
45
|
+
return;
|
|
46
|
+
const { scrollTop, scrollHeight, clientHeight } = container;
|
|
47
|
+
const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
|
|
48
|
+
setIsNearBottom(distanceFromBottom < SCROLL_THRESHOLD);
|
|
49
|
+
}, []);
|
|
50
|
+
// Scroll to bottom
|
|
51
|
+
const scrollToBottom = useCallback(() => {
|
|
52
|
+
const container = thinkingContainerRef.current;
|
|
53
|
+
if (!container)
|
|
54
|
+
return;
|
|
55
|
+
container.scrollTop = container.scrollHeight;
|
|
56
|
+
}, []);
|
|
57
|
+
// Auto-scroll when content changes and user is near bottom
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (isNearBottom && (isRunning || hasContent)) {
|
|
60
|
+
scrollToBottom();
|
|
61
|
+
}
|
|
62
|
+
}, [currentMessage?.content, currentMessage?.toolCalls, isNearBottom, isRunning, hasContent, scrollToBottom]);
|
|
63
|
+
// Set up scroll listener
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
const container = thinkingContainerRef.current;
|
|
66
|
+
if (!container)
|
|
67
|
+
return;
|
|
68
|
+
const handleScroll = () => checkScrollPosition();
|
|
69
|
+
container.addEventListener("scroll", handleScroll, { passive: true });
|
|
70
|
+
checkScrollPosition(); // Check initial position
|
|
71
|
+
return () => container.removeEventListener("scroll", handleScroll);
|
|
72
|
+
}, [checkScrollPosition, isThinkingExpanded, isExpanded]);
|
|
73
|
+
// Get last line of streaming content for preview
|
|
74
|
+
const lastLine = currentMessage?.content
|
|
75
|
+
? currentMessage.content.split("\n").filter(Boolean).pop() || ""
|
|
76
|
+
: "";
|
|
77
|
+
const previewText = lastLine.length > 100 ? `${lastLine.slice(0, 100)}...` : lastLine;
|
|
78
|
+
return (_jsxs("div", { children: [!isExpanded && (_jsx("button", { type: "button", onClick: () => setIsExpanded(true), className: "w-full max-w-md text-left cursor-pointer bg-transparent border-none p-0", children: previewText ? (_jsx("p", { className: `text-paragraph-sm text-muted-foreground truncate ${isRunning ? "animate-pulse" : ""}`, children: previewText })) : isRunning ? (_jsx("p", { className: "text-paragraph-sm text-muted-foreground/50 italic animate-pulse", children: "Waiting for response..." })) : null })), isExpanded && (_jsxs("div", { className: "space-y-3", children: [(agentName || query) && (_jsxs("div", { children: [_jsx("div", { className: "text-[10px] font-bold text-muted-foreground uppercase tracking-wider mb-1.5 font-sans", children: "Input" }), _jsxs("div", { className: "text-[11px] font-mono space-y-1", children: [agentName && (_jsxs("div", { children: [_jsx("span", { className: "text-muted-foreground", children: "agentName: " }), _jsx("span", { className: "text-foreground", children: agentName })] })), query && (_jsxs("div", { children: [_jsx("span", { className: "text-muted-foreground", children: "query: " }), _jsx("span", { className: "text-foreground", children: query })] }))] })] })), _jsxs("div", { children: [_jsxs("button", { type: "button", onClick: () => setIsThinkingExpanded(!isThinkingExpanded), className: "flex items-center gap-2 cursor-pointer bg-transparent border-none p-0 text-left group", children: [_jsx("div", { className: "text-[10px] font-bold text-muted-foreground uppercase tracking-wider font-sans", children: "Thinking" }), _jsx(ChevronDown, { className: `h-3 w-3 text-muted-foreground/70 transition-transform duration-200 ${isThinkingExpanded ? "rotate-180" : ""}` })] }), isThinkingExpanded && (_jsxs("div", { ref: thinkingContainerRef, className: "mt-2 rounded-md overflow-hidden bg-muted/30 border border-border/50 max-h-[200px] overflow-y-auto", children: [error && (_jsxs("div", { className: "px-2 py-2 text-[11px] text-destructive", children: ["Error: ", error] })), !error && !hasContent && isRunning && (_jsx("div", { className: "px-2 py-2 text-[11px] text-muted-foreground", children: "Waiting for sub-agent response..." })), currentMessage && (_jsxs("div", { className: "px-2 py-2 space-y-2", children: [currentMessage.toolCalls &&
|
|
79
|
+
currentMessage.toolCalls.length > 0 && (_jsx("div", { className: "space-y-1", children: currentMessage.toolCalls.map((tc) => (_jsx(SubagentToolCallItem, { toolCall: tc }, tc.id))) })), currentMessage.content && (_jsxs("div", { className: "text-[11px] text-foreground whitespace-pre-wrap font-mono", children: [currentMessage.content, currentMessage.isStreaming && (_jsx("span", { className: "inline-block w-1.5 h-3 bg-primary/70 ml-0.5 animate-pulse" }))] }))] }))] }))] }), !isRunning && currentMessage?.content && (_jsxs("div", { children: [_jsx("div", { className: "text-[10px] font-bold text-muted-foreground uppercase tracking-wider mb-1.5 font-sans", children: "Output" }), _jsx("div", { className: "text-[11px] text-foreground whitespace-pre-wrap font-mono max-h-[200px] overflow-y-auto rounded-md bg-muted/30 border border-border/50 px-2 py-2", children: currentMessage.content })] }))] }))] }));
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Simple tool call display for sub-agent tool calls
|
|
83
|
+
*/
|
|
84
|
+
function SubagentToolCallItem({ toolCall }) {
|
|
85
|
+
const statusIcon = {
|
|
86
|
+
pending: "...",
|
|
87
|
+
in_progress: "",
|
|
88
|
+
completed: "",
|
|
89
|
+
failed: "",
|
|
90
|
+
}[toolCall.status];
|
|
91
|
+
const statusColor = {
|
|
92
|
+
pending: "text-muted-foreground",
|
|
93
|
+
in_progress: "text-blue-500",
|
|
94
|
+
completed: "text-green-500",
|
|
95
|
+
failed: "text-destructive",
|
|
96
|
+
}[toolCall.status];
|
|
97
|
+
return (_jsxs("div", { className: "flex items-center gap-2 text-[10px] text-muted-foreground", children: [_jsx("span", { className: statusColor, children: statusIcon }), _jsx("span", { className: "font-medium", children: toolCall.prettyName || toolCall.title }), toolCall.status === "in_progress" && (_jsx(Loader2, { className: "h-2.5 w-2.5 animate-spin" }))] }));
|
|
98
|
+
}
|
|
@@ -44,7 +44,15 @@ export function ToolCall({ toolCall }) {
|
|
|
44
44
|
const { resolvedTheme } = useTheme();
|
|
45
45
|
// Detect TodoWrite tool and subagent
|
|
46
46
|
const isTodoWrite = toolCall.title === "todo_write";
|
|
47
|
-
|
|
47
|
+
// A subagent call can be detected by:
|
|
48
|
+
// - Live: has port and sessionId (but no stored messages yet)
|
|
49
|
+
// - Replay: has stored subagentMessages
|
|
50
|
+
const hasLiveSubagent = !!(toolCall.subagentPort && toolCall.subagentSessionId);
|
|
51
|
+
const hasStoredSubagent = !!(toolCall.subagentMessages && toolCall.subagentMessages.length > 0);
|
|
52
|
+
const isSubagentCall = hasLiveSubagent || hasStoredSubagent;
|
|
53
|
+
// Use replay mode if we have stored messages - they should take precedence
|
|
54
|
+
// over trying to connect to SSE (which won't work for replayed sessions)
|
|
55
|
+
const isReplaySubagent = hasStoredSubagent;
|
|
48
56
|
// Safely access ChatLayout context - will be undefined if not within ChatLayout
|
|
49
57
|
const layoutContext = React.useContext(ChatLayout.Context);
|
|
50
58
|
// Click handler: toggle sidepanel for TodoWrite, subagent details for subagents, expand for others
|
|
@@ -139,7 +147,7 @@ export function ToolCall({ toolCall }) {
|
|
|
139
147
|
if (isPreliminary) {
|
|
140
148
|
return (_jsx("div", { className: "flex flex-col my-4", children: _jsxs("span", { className: "text-paragraph-sm text-muted-foreground/50", children: ["Invoking ", displayName] }) }));
|
|
141
149
|
}
|
|
142
|
-
return (_jsxs("div", { className: "flex flex-col my-4", children: [_jsxs("button", { type: "button", className: "flex flex-col items-start gap-0.5 cursor-pointer bg-transparent border-none p-0 text-left group w-fit", onClick: handleHeaderClick, "aria-expanded": isTodoWrite ? undefined : isExpanded, children: [_jsxs("div", { className: "flex items-center gap-1.5 text-[11px] font-medium text-muted-foreground", children: [_jsx("div", { className: iconColorClass, title: statusTooltip, children: _jsx(IconComponent, { className: "h-3 w-3" }) }), _jsx("span", { className: "text-paragraph-sm text-muted-foreground", children: displayName }), hasError && _jsx(AlertCircle, { className: "h-3 w-3 text-destructive" }), isTodoWrite ? (_jsx(ChevronRight, { className: "h-3 w-3 text-muted-foreground/70" })) : (_jsx(ChevronDown, { className: `h-3 w-3 text-muted-foreground/70 transition-transform duration-200 ${isExpanded ? "rotate-180" : ""}` }))] }), toolCall.subline && (_jsx("span", { className: "text-paragraph-sm text-muted-foreground/70 pl-4.5", children: toolCall.subline }))] }), !isTodoWrite && isSubagentCall && (_jsx("div", { className: "pl-4.5", children: _jsx(SubAgentDetails, { port: toolCall.subagentPort, sessionId: toolCall.subagentSessionId, parentStatus: toolCall.status, agentName: toolCall.rawInput?.agentName, query: toolCall.rawInput?.query, isExpanded: isSubagentExpanded, onExpandChange: setIsSubagentExpanded }) })), !isTodoWrite && !isSubagentCall && isExpanded && (_jsxs("div", { className: "mt-2 text-sm border border-border rounded-lg bg-card overflow-hidden w-full", children: [toolCall.rawInput &&
|
|
150
|
+
return (_jsxs("div", { className: "flex flex-col my-4", children: [_jsxs("button", { type: "button", className: "flex flex-col items-start gap-0.5 cursor-pointer bg-transparent border-none p-0 text-left group w-fit", onClick: handleHeaderClick, "aria-expanded": isTodoWrite ? undefined : isExpanded, children: [_jsxs("div", { className: "flex items-center gap-1.5 text-[11px] font-medium text-muted-foreground", children: [_jsx("div", { className: iconColorClass, title: statusTooltip, children: _jsx(IconComponent, { className: "h-3 w-3" }) }), _jsx("span", { className: "text-paragraph-sm text-muted-foreground", children: displayName }), hasError && _jsx(AlertCircle, { className: "h-3 w-3 text-destructive" }), isTodoWrite ? (_jsx(ChevronRight, { className: "h-3 w-3 text-muted-foreground/70" })) : (_jsx(ChevronDown, { className: `h-3 w-3 text-muted-foreground/70 transition-transform duration-200 ${isExpanded ? "rotate-180" : ""}` }))] }), toolCall.subline && (_jsx("span", { className: "text-paragraph-sm text-muted-foreground/70 pl-4.5", children: toolCall.subline }))] }), !isTodoWrite && isSubagentCall && (_jsx("div", { className: "pl-4.5", children: _jsx(SubAgentDetails, { port: toolCall.subagentPort, sessionId: toolCall.subagentSessionId, parentStatus: toolCall.status, agentName: toolCall.rawInput?.agentName, query: toolCall.rawInput?.query, isExpanded: isSubagentExpanded, onExpandChange: setIsSubagentExpanded, storedMessages: toolCall.subagentMessages, isReplay: isReplaySubagent }) })), !isTodoWrite && !isSubagentCall && isExpanded && (_jsxs("div", { className: "mt-2 text-sm border border-border rounded-lg bg-card overflow-hidden w-full", children: [toolCall.rawInput &&
|
|
143
151
|
Object.keys(toolCall.rawInput).length > 0 &&
|
|
144
152
|
!toolCall.subagentPort && (_jsxs("div", { className: "p-3 border-b border-border", children: [_jsx("div", { className: "text-[10px] font-bold text-muted-foreground uppercase tracking-wider mb-1.5 font-sans", children: "Input" }), _jsx("div", { className: "text-[11px] font-mono text-foreground", children: _jsx(JsonView, { value: toolCall.rawInput, collapsed: false, displayDataTypes: false, displayObjectSize: false, enableClipboard: true, style: jsonStyle }) })] })), toolCall.locations && toolCall.locations.length > 0 && (_jsxs("div", { className: "p-3 border-b border-border", children: [_jsx("div", { className: "text-[10px] font-bold text-muted-foreground uppercase tracking-wider mb-1.5 font-sans", children: "Files" }), _jsx("ul", { className: "space-y-1", children: toolCall.locations.map((loc) => (_jsxs("li", { className: "font-mono text-[11px] text-foreground bg-muted px-1.5 py-0.5 rounded w-fit", children: [loc.path, loc.line !== null &&
|
|
145
153
|
loc.line !== undefined &&
|
|
@@ -5,6 +5,7 @@ import React, { useEffect, useRef, useState } from "react";
|
|
|
5
5
|
import { getGroupDisplayState, getToolCallDisplayState, getToolCallStateVerbiage, isPreliminaryToolCall, } from "../../core/utils/tool-call-state.js";
|
|
6
6
|
import { generateSmartSummary } from "../../core/utils/tool-summary.js";
|
|
7
7
|
import * as ChatLayout from "./ChatLayout.js";
|
|
8
|
+
import { ContextUsageIndicator } from "./ContextUsageButton.js";
|
|
8
9
|
import { SubAgentDetails } from "./SubAgentDetails.js";
|
|
9
10
|
import { useTheme } from "./ThemeProvider.js";
|
|
10
11
|
import { TodoSubline } from "./TodoSubline.js";
|
|
@@ -141,6 +142,20 @@ export function ToolOperation({ toolCalls, isGrouped = false, autoMinimize = tru
|
|
|
141
142
|
// Detect subagent calls (subagents now run in-process, messages in subagentMessages)
|
|
142
143
|
const isSubagentCall = !!(singleToolCall?.subagentMessages &&
|
|
143
144
|
singleToolCall.subagentMessages.length > 0);
|
|
145
|
+
// Extract latest subagent context size (if provided by backend on subagent messages)
|
|
146
|
+
const subagentContextSize = React.useMemo(() => {
|
|
147
|
+
if (!singleToolCall?.subagentMessages)
|
|
148
|
+
return null;
|
|
149
|
+
const messages = singleToolCall.subagentMessages;
|
|
150
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
151
|
+
const msg = messages[i];
|
|
152
|
+
const cs = msg?._meta?.context_size ?? msg?.context_size;
|
|
153
|
+
if (cs != null)
|
|
154
|
+
return cs;
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
}, [singleToolCall?.subagentMessages]);
|
|
158
|
+
const subagentHeaderContextSize = subagentContextSize;
|
|
144
159
|
// State for subagent expansion
|
|
145
160
|
const [isSubagentExpanded, setIsSubagentExpanded] = useState(false);
|
|
146
161
|
// Safely access ChatLayout context
|
|
@@ -226,7 +241,7 @@ export function ToolOperation({ toolCalls, isGrouped = false, autoMinimize = tru
|
|
|
226
241
|
const displayText = getDisplayText();
|
|
227
242
|
// For preliminary/selecting states, show simple non-expandable text
|
|
228
243
|
if (isSelecting && !isGrouped) {
|
|
229
|
-
return (_jsx("div", { className: "flex flex-col
|
|
244
|
+
return (_jsx("div", { className: "flex flex-col rounded-md px-1 -mx-1 w-fit", children: _jsxs("div", { className: "flex items-center gap-1.5", children: [
|
|
230
245
|
_jsx("div", { className: "text-text-secondary/70", children: _jsx(IconComponent, { className: "h-3 w-3" }) }), _jsxs("span", { className: "text-paragraph-xs text-text-muted", children: [displayText, singleToolCall?.startedAt && (_jsx(RunningDuration, { startTime: singleToolCall.startedAt, isRunning: !singleToolCall.subagentCompleted }))] })
|
|
231
246
|
] }) }));
|
|
232
247
|
}
|
|
@@ -243,17 +258,17 @@ export function ToolOperation({ toolCalls, isGrouped = false, autoMinimize = tru
|
|
|
243
258
|
})();
|
|
244
259
|
// Check if all tool calls in the group are completed
|
|
245
260
|
const allCompleted = toolCalls.every((tc) => tc.subagentCompleted);
|
|
246
|
-
return (_jsxs("div", { className: "flex flex-col
|
|
261
|
+
return (_jsxs("div", { className: "flex flex-col rounded-md px-1 -mx-1 w-fit", children: [
|
|
247
262
|
_jsxs("div", { className: "flex items-center gap-1.5", children: [
|
|
248
263
|
_jsx("div", { className: "text-text-secondary/70", children: _jsx(ListVideo, { className: "h-3 w-3" }) }), _jsx("span", { className: "text-paragraph-sm text-text-secondary/70", children: "Selecting tools" }), selectingStartTime && (_jsx(RunningDuration, { startTime: selectingStartTime, isRunning: !allCompleted })), _jsx("span", { className: "text-[10px] bg-muted px-1.5 py-0.5 rounded text-text-secondary/70", children: toolCalls.length })
|
|
249
264
|
] }), _jsx("span", { className: "text-paragraph-sm text-text-secondary/70 pl-4.5", children: displayText })
|
|
250
265
|
] }));
|
|
251
266
|
}
|
|
252
267
|
// Full display (for single tool call or expanded group, includes minimized state)
|
|
253
|
-
return (_jsxs("div", { className: "flex flex-col
|
|
268
|
+
return (_jsxs("div", { className: "flex flex-col", children: [
|
|
254
269
|
_jsxs("button", { type: "button", className: "flex flex-col items-start gap-0.5 cursor-pointer bg-transparent border-none p-0 text-left group w-fit rounded-md px-1 -mx-1", onClick: handleHeaderClick, "aria-expanded": isTodoWrite ? undefined : isExpanded, children: [
|
|
255
270
|
_jsxs("div", { className: "flex items-center gap-1.5", children: [
|
|
256
|
-
_jsx("div", { className: "text-text-secondary/70 group-hover:text-text-secondary transition-colors", children: _jsx(IconComponent, { className: "h-3 w-3" }) }), _jsxs("span", { className: "text-paragraph-sm text-text-secondary/70 group-hover:text-text-secondary transition-colors", children: [isGrouped && _jsx("span", { className: "mr-1", children: "Parallel operation" }), !isGrouped && displayText] }), !isGrouped &&
|
|
271
|
+
_jsx("div", { className: "text-text-secondary/70 group-hover:text-text-secondary transition-colors", children: _jsx(IconComponent, { className: "h-3 w-3" }) }), _jsxs("span", { className: "text-paragraph-sm text-text-secondary/70 group-hover:text-text-secondary transition-colors", children: [isGrouped && _jsx("span", { className: "mr-1", children: "Parallel operation" }), !isGrouped && displayText] }), isSubagentCall && (_jsx(ContextUsageIndicator, { contextSize: subagentHeaderContextSize, size: 12, className: "text-text-secondary/70 group-hover:text-text-secondary transition-colors" })), !isGrouped &&
|
|
257
272
|
singleToolCall?.startedAt &&
|
|
258
273
|
displayState === "executing" && (_jsx(RunningDuration, { startTime: singleToolCall.startedAt, isRunning: !singleToolCall.subagentCompleted })), isGrouped &&
|
|
259
274
|
displayState === "executing" &&
|
|
@@ -316,7 +331,7 @@ export function ToolOperation({ toolCalls, isGrouped = false, autoMinimize = tru
|
|
|
316
331
|
return null;
|
|
317
332
|
})()] }), !isTodoWrite && isSubagentCall && singleToolCall && (_jsx("div", { className: "pl-4.5 mt-2", children: _jsx(SubAgentDetails, { parentStatus: singleToolCall.status, agentName: singleToolCall.rawInput?.agentName, query: singleToolCall.rawInput?.query, isExpanded: isSubagentExpanded, onExpandChange: setIsSubagentExpanded, storedMessages: singleToolCall.subagentMessages }) })), !isTodoWrite && isExpanded && (_jsx("div", { className: "mt-1", children: isGrouped ? (
|
|
318
333
|
// Render individual tool calls in group
|
|
319
|
-
_jsx("div", { className: "flex flex-col gap-
|
|
334
|
+
_jsx("div", { className: "flex flex-col gap-2 mt-1", children: toolCalls.map((toolCall) => {
|
|
320
335
|
const hookNotification = hookNotifications.find((n) => n.toolCallId === toolCall.id);
|
|
321
336
|
return (_jsx(GroupedToolCallItem, { toolCall: toolCall, ...(hookNotification ? { hookNotification } : {}) }, toolCall.id));
|
|
322
337
|
}) })) : (
|
|
@@ -333,6 +348,18 @@ function GroupedToolCallItem({ toolCall, hookNotification, }) {
|
|
|
333
348
|
const [isExpanded, setIsExpanded] = useState(false);
|
|
334
349
|
// Detect subagent calls (subagents now run in-process, messages in subagentMessages)
|
|
335
350
|
const isSubagentCall = !!(toolCall.subagentMessages && toolCall.subagentMessages.length > 0);
|
|
351
|
+
// Extract latest subagent context size (if provided by backend on subagent messages)
|
|
352
|
+
const subagentContextSize = React.useMemo(() => {
|
|
353
|
+
const messages = toolCall.subagentMessages ?? [];
|
|
354
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
355
|
+
const msg = messages[i];
|
|
356
|
+
const cs = msg?._meta?.context_size ?? msg?.context_size;
|
|
357
|
+
if (cs != null)
|
|
358
|
+
return cs;
|
|
359
|
+
}
|
|
360
|
+
return null;
|
|
361
|
+
}, [toolCall.subagentMessages]);
|
|
362
|
+
const subagentHeaderContextSize = subagentContextSize;
|
|
336
363
|
// Detect compaction for this individual tool call
|
|
337
364
|
const hasCompaction = !!((hookNotification?.status === "completed" &&
|
|
338
365
|
hookNotification.metadata?.action &&
|
|
@@ -351,7 +378,7 @@ function GroupedToolCallItem({ toolCall, hookNotification, }) {
|
|
|
351
378
|
_jsxs("button", { type: "button", className: "flex items-center gap-1.5 cursor-pointer bg-transparent border-none p-0 text-left group w-fit", onClick: () => setIsExpanded(!isExpanded), "aria-expanded": isExpanded, children: [
|
|
352
379
|
_jsx("div", { className: "text-text-secondary/70 group-hover:text-text-secondary transition-colors", children: _jsx(CircleDot, { className: "h-3 w-3" }) }), _jsx("span", { className: "text-paragraph-sm text-text-secondary/70 group-hover:text-text-secondary transition-colors", children: toolCall.subagentMessages?.[0]?._meta?.semanticName ||
|
|
353
380
|
toolCall.rawInput?.agentName ||
|
|
354
|
-
"Subagent" }), toolCall.subagentMessages?.[0]?._meta?.semanticName && (_jsxs("span", { className: "text-muted-foreground text-[10px] ml-1", children: ["(", toolCall.rawInput?.agentName || "subagent", ")"] })), isRunning && toolCall.startedAt && (_jsx(RunningDuration, { startTime: toolCall.startedAt })), isFailed && (_jsx("span", { title: toolCall.error || "Operation failed", children: _jsx(AlertCircle, { className: "h-3 w-3 text-destructive" }) })), hasCompaction && (_jsx(TooltipProvider, { delayDuration: 0, children: _jsxs(Tooltip, { children: [
|
|
381
|
+
"Subagent" }), toolCall.subagentMessages?.[0]?._meta?.semanticName && (_jsxs("span", { className: "text-muted-foreground text-[10px] ml-1", children: ["(", toolCall.rawInput?.agentName || "subagent", ")"] })), _jsx(ContextUsageIndicator, { contextSize: subagentHeaderContextSize, size: 12, className: "text-text-secondary/70 group-hover:text-text-secondary transition-colors" }), isRunning && toolCall.startedAt && (_jsx(RunningDuration, { startTime: toolCall.startedAt })), isFailed && (_jsx("span", { title: toolCall.error || "Operation failed", children: _jsx(AlertCircle, { className: "h-3 w-3 text-destructive" }) })), hasCompaction && (_jsx(TooltipProvider, { delayDuration: 0, children: _jsxs(Tooltip, { children: [
|
|
355
382
|
_jsx(TooltipTrigger, { asChild: true, children: _jsx("span", { children: isTruncation ? (_jsx(ScissorsLineDashed, { className: "h-3 w-3 text-destructive" })) : (_jsx(FoldVertical, { className: "h-3 w-3 text-text-secondary/70" })) }) }), _jsx(TooltipContent, { children: (() => {
|
|
356
383
|
const meta = toolCall._meta;
|
|
357
384
|
const percentage = meta?.originalTokens && meta?.finalTokens
|
|
@@ -5,6 +5,8 @@ import { cn } from "../lib/utils.js";
|
|
|
5
5
|
const TooltipProvider = TooltipPrimitive.Provider;
|
|
6
6
|
const Tooltip = TooltipPrimitive.Root;
|
|
7
7
|
const TooltipTrigger = TooltipPrimitive.Trigger;
|
|
8
|
-
const TooltipContent = React.forwardRef(({ className, sideOffset = 4, ...props }, ref) => (_jsx(TooltipPrimitive.Content, { ref: ref, sideOffset: sideOffset, className: cn(
|
|
8
|
+
const TooltipContent = React.forwardRef(({ className, sideOffset = 4, ...props }, ref) => (_jsx(TooltipPrimitive.Portal, { children: _jsx(TooltipPrimitive.Content, { ref: ref, sideOffset: sideOffset, className: cn(
|
|
9
|
+
// High z-index so tooltips render above overlays/modals in the chat UI.
|
|
10
|
+
"z-[9999] overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", className), ...props }) })));
|
|
9
11
|
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
|
|
10
12
|
export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger };
|
|
@@ -348,12 +348,36 @@ export declare const SessionUpdate: z.ZodUnion<readonly [z.ZodObject<{
|
|
|
348
348
|
}, z.core.$strip>;
|
|
349
349
|
}, z.core.$strip>], "type">>>;
|
|
350
350
|
isStreaming: z.ZodOptional<z.ZodBoolean>;
|
|
351
|
+
context_size: z.ZodOptional<z.ZodObject<{
|
|
352
|
+
systemPromptTokens: z.ZodNumber;
|
|
353
|
+
toolOverheadTokens: z.ZodOptional<z.ZodNumber>;
|
|
354
|
+
mcpOverheadTokens: z.ZodOptional<z.ZodNumber>;
|
|
355
|
+
userMessagesTokens: z.ZodNumber;
|
|
356
|
+
assistantMessagesTokens: z.ZodNumber;
|
|
357
|
+
toolInputTokens: z.ZodNumber;
|
|
358
|
+
toolResultsTokens: z.ZodNumber;
|
|
359
|
+
totalEstimated: z.ZodNumber;
|
|
360
|
+
llmReportedInputTokens: z.ZodOptional<z.ZodNumber>;
|
|
361
|
+
modelContextWindow: z.ZodOptional<z.ZodNumber>;
|
|
362
|
+
}, z.core.$strip>>;
|
|
351
363
|
_meta: z.ZodOptional<z.ZodObject<{
|
|
352
364
|
semanticName: z.ZodOptional<z.ZodString>;
|
|
353
365
|
agentDefinitionName: z.ZodOptional<z.ZodString>;
|
|
354
366
|
currentActivity: z.ZodOptional<z.ZodString>;
|
|
355
367
|
statusGenerating: z.ZodOptional<z.ZodBoolean>;
|
|
356
|
-
|
|
368
|
+
context_size: z.ZodOptional<z.ZodObject<{
|
|
369
|
+
systemPromptTokens: z.ZodNumber;
|
|
370
|
+
toolOverheadTokens: z.ZodOptional<z.ZodNumber>;
|
|
371
|
+
mcpOverheadTokens: z.ZodOptional<z.ZodNumber>;
|
|
372
|
+
userMessagesTokens: z.ZodNumber;
|
|
373
|
+
assistantMessagesTokens: z.ZodNumber;
|
|
374
|
+
toolInputTokens: z.ZodNumber;
|
|
375
|
+
toolResultsTokens: z.ZodNumber;
|
|
376
|
+
totalEstimated: z.ZodNumber;
|
|
377
|
+
llmReportedInputTokens: z.ZodOptional<z.ZodNumber>;
|
|
378
|
+
modelContextWindow: z.ZodOptional<z.ZodNumber>;
|
|
379
|
+
}, z.core.$strip>>;
|
|
380
|
+
}, z.core.$loose>>;
|
|
357
381
|
}, z.core.$strip>>>;
|
|
358
382
|
subagentStreaming: z.ZodOptional<z.ZodBoolean>;
|
|
359
383
|
subagentCompleted: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -563,12 +587,36 @@ export declare const SessionUpdate: z.ZodUnion<readonly [z.ZodObject<{
|
|
|
563
587
|
}, z.core.$strip>;
|
|
564
588
|
}, z.core.$strip>], "type">>>;
|
|
565
589
|
isStreaming: z.ZodOptional<z.ZodBoolean>;
|
|
590
|
+
context_size: z.ZodOptional<z.ZodObject<{
|
|
591
|
+
systemPromptTokens: z.ZodNumber;
|
|
592
|
+
toolOverheadTokens: z.ZodOptional<z.ZodNumber>;
|
|
593
|
+
mcpOverheadTokens: z.ZodOptional<z.ZodNumber>;
|
|
594
|
+
userMessagesTokens: z.ZodNumber;
|
|
595
|
+
assistantMessagesTokens: z.ZodNumber;
|
|
596
|
+
toolInputTokens: z.ZodNumber;
|
|
597
|
+
toolResultsTokens: z.ZodNumber;
|
|
598
|
+
totalEstimated: z.ZodNumber;
|
|
599
|
+
llmReportedInputTokens: z.ZodOptional<z.ZodNumber>;
|
|
600
|
+
modelContextWindow: z.ZodOptional<z.ZodNumber>;
|
|
601
|
+
}, z.core.$strip>>;
|
|
566
602
|
_meta: z.ZodOptional<z.ZodObject<{
|
|
567
603
|
semanticName: z.ZodOptional<z.ZodString>;
|
|
568
604
|
agentDefinitionName: z.ZodOptional<z.ZodString>;
|
|
569
605
|
currentActivity: z.ZodOptional<z.ZodString>;
|
|
570
606
|
statusGenerating: z.ZodOptional<z.ZodBoolean>;
|
|
571
|
-
|
|
607
|
+
context_size: z.ZodOptional<z.ZodObject<{
|
|
608
|
+
systemPromptTokens: z.ZodNumber;
|
|
609
|
+
toolOverheadTokens: z.ZodOptional<z.ZodNumber>;
|
|
610
|
+
mcpOverheadTokens: z.ZodOptional<z.ZodNumber>;
|
|
611
|
+
userMessagesTokens: z.ZodNumber;
|
|
612
|
+
assistantMessagesTokens: z.ZodNumber;
|
|
613
|
+
toolInputTokens: z.ZodNumber;
|
|
614
|
+
toolResultsTokens: z.ZodNumber;
|
|
615
|
+
totalEstimated: z.ZodNumber;
|
|
616
|
+
llmReportedInputTokens: z.ZodOptional<z.ZodNumber>;
|
|
617
|
+
modelContextWindow: z.ZodOptional<z.ZodNumber>;
|
|
618
|
+
}, z.core.$strip>>;
|
|
619
|
+
}, z.core.$loose>>;
|
|
572
620
|
}, z.core.$strip>>>;
|
|
573
621
|
subagentCompleted: z.ZodOptional<z.ZodBoolean>;
|
|
574
622
|
_meta: z.ZodOptional<z.ZodObject<{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@townco/ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.117",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"@radix-ui/react-slot": "^1.2.4",
|
|
50
50
|
"@radix-ui/react-tabs": "^1.1.13",
|
|
51
51
|
"@radix-ui/react-tooltip": "^1.2.8",
|
|
52
|
-
"@townco/core": "0.0.
|
|
52
|
+
"@townco/core": "0.0.95",
|
|
53
53
|
"@types/mdast": "^4.0.4",
|
|
54
54
|
"@uiw/react-json-view": "^2.0.0-alpha.39",
|
|
55
55
|
"class-variance-authority": "^0.7.1",
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
"zustand": "^5.0.8"
|
|
68
68
|
},
|
|
69
69
|
"devDependencies": {
|
|
70
|
-
"@townco/tsconfig": "0.1.
|
|
70
|
+
"@townco/tsconfig": "0.1.114",
|
|
71
71
|
"@types/node": "^24.10.0",
|
|
72
72
|
"@types/react": "^19.2.2",
|
|
73
73
|
"@types/unist": "^3.0.3",
|