@townco/ui 0.1.77 → 0.1.78
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 +4 -4
- package/dist/core/hooks/use-chat-messages.js +4 -1
- package/dist/core/hooks/use-chat-session.d.ts +1 -1
- package/dist/core/hooks/use-chat-session.js +5 -1
- package/dist/core/hooks/use-subagent-stream.js +6 -6
- package/dist/core/hooks/use-tool-calls.d.ts +3 -3
- package/dist/core/hooks/use-tool-calls.js +1 -1
- package/dist/core/schemas/chat.d.ts +10 -10
- package/dist/core/schemas/tool-call.d.ts +8 -8
- package/dist/core/store/chat-store.js +1 -0
- package/dist/core/utils/tool-summary.js +8 -3
- package/dist/core/utils/tool-verbiage.js +1 -1
- package/dist/gui/components/AppSidebar.d.ts +1 -1
- package/dist/gui/components/AppSidebar.js +4 -3
- package/dist/gui/components/Button.d.ts +1 -1
- package/dist/gui/components/ChatEmptyState.js +1 -1
- package/dist/gui/components/ChatHeader.d.ts +1 -28
- package/dist/gui/components/ChatHeader.js +4 -71
- package/dist/gui/components/ChatLayout.d.ts +6 -2
- package/dist/gui/components/ChatLayout.js +82 -33
- package/dist/gui/components/ChatView.js +28 -45
- package/dist/gui/components/ContextUsageButton.d.ts +0 -1
- package/dist/gui/components/ContextUsageButton.js +10 -3
- package/dist/gui/components/HookNotification.js +2 -1
- package/dist/gui/components/MessageContent.js +24 -160
- package/dist/gui/components/SessionHistory.js +1 -2
- package/dist/gui/components/SessionHistoryItem.js +1 -1
- package/dist/gui/components/Sidebar.js +27 -42
- package/dist/gui/components/SubAgentDetails.js +10 -14
- package/dist/gui/components/TodoSubline.js +1 -0
- package/dist/gui/components/ToolOperation.js +16 -75
- package/dist/gui/components/WorkProgress.js +5 -3
- package/dist/gui/components/index.d.ts +0 -1
- package/dist/gui/components/resizable.d.ts +1 -1
- package/dist/gui/constants.d.ts +6 -0
- package/dist/gui/constants.js +8 -0
- package/dist/gui/hooks/index.d.ts +1 -0
- package/dist/gui/hooks/index.js +1 -0
- package/dist/gui/hooks/use-lock-body-scroll.d.ts +7 -0
- package/dist/gui/hooks/use-lock-body-scroll.js +29 -0
- package/dist/gui/lib/motion.d.ts +12 -0
- package/dist/gui/lib/motion.js +69 -0
- package/dist/sdk/schemas/message.d.ts +2 -2
- package/dist/sdk/schemas/session.d.ts +18 -18
- package/dist/sdk/transports/http.d.ts +1 -1
- package/dist/sdk/transports/http.js +9 -0
- package/dist/sdk/transports/stdio.js +2 -2
- package/dist/sdk/transports/types.d.ts +11 -0
- package/dist/sdk/transports/types.js +28 -1
- package/package.json +3 -5
- package/dist/gui/components/InvokingGroup.d.ts +0 -9
- package/dist/gui/components/InvokingGroup.js +0 -16
- package/dist/gui/components/SubagentStream.d.ts +0 -23
- package/dist/gui/components/SubagentStream.js +0 -98
- package/dist/gui/components/ToolCall.d.ts +0 -8
- package/dist/gui/components/ToolCall.js +0 -234
- package/dist/gui/components/ToolCallGroup.d.ts +0 -8
- package/dist/gui/components/ToolCallGroup.js +0 -29
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { cva } from "class-variance-authority";
|
|
3
|
-
import { AnimatePresence, motion, useReducedMotion } from "framer-motion";
|
|
4
3
|
import * as React from "react";
|
|
5
4
|
import { useChatStore } from "../../core/store/chat-store.js";
|
|
6
5
|
import { isPreliminaryToolCall } from "../../core/utils/tool-call-state.js";
|
|
7
|
-
import { getDuration, getTransition, motionEasing, shimmerTransition, } from "../lib/motion.js";
|
|
8
6
|
import { cn } from "../lib/utils.js";
|
|
9
7
|
import { HookNotification } from "./HookNotification.js";
|
|
10
8
|
import { Reasoning } from "./Reasoning.js";
|
|
@@ -36,8 +34,7 @@ const messageContentVariants = cva("w-full rounded-2xl text-[var(--font-size)] f
|
|
|
36
34
|
export const MessageContent = React.forwardRef(({ role: roleProp, variant, isStreaming: isStreamingProp, message, thinkingDisplayStyle = "collapsible", className, children, ...props }, ref) => {
|
|
37
35
|
// Get streaming start time and current model from store
|
|
38
36
|
const streamingStartTime = useChatStore((state) => state.streamingStartTime);
|
|
39
|
-
const
|
|
40
|
-
const shouldReduceMotion = useReducedMotion();
|
|
37
|
+
const _currentModel = useChatStore((state) => state.currentModel);
|
|
41
38
|
// Use smart rendering if message is provided and no custom children
|
|
42
39
|
const useSmartRendering = message && !children;
|
|
43
40
|
// Derive props from message if using smart rendering
|
|
@@ -52,45 +49,7 @@ export const MessageContent = React.forwardRef(({ role: roleProp, variant, isStr
|
|
|
52
49
|
const hasThinking = !!thinking;
|
|
53
50
|
// Check if waiting (streaming but no content yet)
|
|
54
51
|
const isWaiting = message.isStreaming && !message.content && message.role === "assistant";
|
|
55
|
-
content = (_jsxs(_Fragment, { children: [message.role === "assistant" && hasThinking && (_jsx(
|
|
56
|
-
filter: "blur(12px)",
|
|
57
|
-
opacity: 0,
|
|
58
|
-
y: 12,
|
|
59
|
-
}, animate: {
|
|
60
|
-
filter: "blur(0px)",
|
|
61
|
-
opacity: 1,
|
|
62
|
-
y: 0,
|
|
63
|
-
}, exit: {
|
|
64
|
-
filter: "blur(12px)",
|
|
65
|
-
opacity: 0,
|
|
66
|
-
y: -12,
|
|
67
|
-
}, transition: getTransition(shouldReduceMotion ?? false, {
|
|
68
|
-
duration: 0.5,
|
|
69
|
-
ease: motionEasing.smooth,
|
|
70
|
-
}), children: _jsx(Reasoning, { content: thinking, isStreaming: message.isStreaming, mode: thinkingDisplayStyle, autoCollapse: true }) })), isWaiting && streamingStartTime && (_jsx(motion.div, { initial: {
|
|
71
|
-
filter: "blur(12px)",
|
|
72
|
-
opacity: 0,
|
|
73
|
-
y: 12,
|
|
74
|
-
}, animate: {
|
|
75
|
-
filter: "blur(0px)",
|
|
76
|
-
opacity: 1,
|
|
77
|
-
y: 0,
|
|
78
|
-
}, exit: {
|
|
79
|
-
filter: "blur(12px)",
|
|
80
|
-
opacity: 0,
|
|
81
|
-
y: -12,
|
|
82
|
-
}, transition: getTransition(shouldReduceMotion ?? false, {
|
|
83
|
-
duration: 0.4,
|
|
84
|
-
ease: motionEasing.smooth,
|
|
85
|
-
}), children: _jsx(motion.div, { className: "flex flex-col my-4 rounded-md px-1 -mx-1 w-fit", animate: {
|
|
86
|
-
backgroundPosition: ["-200% 0", "200% 0"],
|
|
87
|
-
}, transition: {
|
|
88
|
-
...shimmerTransition,
|
|
89
|
-
duration: getDuration(shouldReduceMotion ?? false, 1.5),
|
|
90
|
-
}, style: {
|
|
91
|
-
backgroundImage: "linear-gradient(90deg, transparent 5%, rgba(255, 255, 255, 0.75) 25%, transparent 35%)",
|
|
92
|
-
backgroundSize: "200% 100%",
|
|
93
|
-
}, children: _jsx("div", { className: "flex items-center gap-1.5", children: _jsx("span", { className: "text-paragraph-sm text-text-secondary/70", children: "Thinking..." }) }) }) })), 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: "Thinking..." }) }) })), message.role === "assistant" ? ((() => {
|
|
94
53
|
// Sort tool calls by content position
|
|
95
54
|
const sortedToolCalls = (message.toolCalls || [])
|
|
96
55
|
.slice()
|
|
@@ -111,6 +70,7 @@ export const MessageContent = React.forwardRef(({ role: roleProp, variant, isStr
|
|
|
111
70
|
});
|
|
112
71
|
}
|
|
113
72
|
else if (currentSelectingGroup.length === 1) {
|
|
73
|
+
// biome-ignore lint/style/noNonNullAssertion: Length check ensures element exists
|
|
114
74
|
result.push(currentSelectingGroup[0]);
|
|
115
75
|
}
|
|
116
76
|
currentSelectingGroup = [];
|
|
@@ -124,6 +84,7 @@ export const MessageContent = React.forwardRef(({ role: roleProp, variant, isStr
|
|
|
124
84
|
});
|
|
125
85
|
}
|
|
126
86
|
else if (currentConsecutiveGroup.length === 1) {
|
|
87
|
+
// biome-ignore lint/style/noNonNullAssertion: Length check ensures element exists
|
|
127
88
|
result.push(currentConsecutiveGroup[0]);
|
|
128
89
|
}
|
|
129
90
|
currentConsecutiveGroup = [];
|
|
@@ -207,22 +168,7 @@ export const MessageContent = React.forwardRef(({ role: roleProp, variant, isStr
|
|
|
207
168
|
const hasHookPositions = hookNotifications.some((n) => n.contentPosition !== undefined);
|
|
208
169
|
if (!hasHookPositions) {
|
|
209
170
|
// No positions - render hooks at top, then tool calls, then content
|
|
210
|
-
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(
|
|
211
|
-
filter: "blur(12px)",
|
|
212
|
-
opacity: 0,
|
|
213
|
-
y: 12,
|
|
214
|
-
}, animate: {
|
|
215
|
-
filter: "blur(0px)",
|
|
216
|
-
opacity: 1,
|
|
217
|
-
y: 0,
|
|
218
|
-
}, exit: {
|
|
219
|
-
filter: "blur(12px)",
|
|
220
|
-
opacity: 0,
|
|
221
|
-
y: -12,
|
|
222
|
-
}, transition: getTransition(shouldReduceMotion ?? false, {
|
|
223
|
-
duration: 0.4,
|
|
224
|
-
ease: motionEasing.smooth,
|
|
225
|
-
}), children: _jsx(Response, { content: message.content, isStreaming: message.isStreaming, showEmpty: false }) })] }));
|
|
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 }) })] }));
|
|
226
172
|
}
|
|
227
173
|
// Hooks have positions - render them inline with content
|
|
228
174
|
const elements = [];
|
|
@@ -233,22 +179,7 @@ export const MessageContent = React.forwardRef(({ role: roleProp, variant, isStr
|
|
|
233
179
|
if (position > currentPosition) {
|
|
234
180
|
const textChunk = message.content.slice(currentPosition, position);
|
|
235
181
|
if (textChunk) {
|
|
236
|
-
elements.push(_jsx(
|
|
237
|
-
filter: "blur(12px)",
|
|
238
|
-
opacity: 0,
|
|
239
|
-
y: 12,
|
|
240
|
-
}, animate: {
|
|
241
|
-
filter: "blur(0px)",
|
|
242
|
-
opacity: 1,
|
|
243
|
-
y: 0,
|
|
244
|
-
}, exit: {
|
|
245
|
-
filter: "blur(12px)",
|
|
246
|
-
opacity: 0,
|
|
247
|
-
y: -12,
|
|
248
|
-
}, transition: getTransition(shouldReduceMotion ?? false, {
|
|
249
|
-
duration: 0.4,
|
|
250
|
-
ease: motionEasing.smooth,
|
|
251
|
-
}), children: _jsx(Response, { content: textChunk, isStreaming: false, showEmpty: false }) }, `text-before-hook-${notification.id}`));
|
|
182
|
+
elements.push(_jsx("div", { children: _jsx(Response, { content: textChunk, isStreaming: false, showEmpty: false }) }, `text-before-hook-${notification.id}`));
|
|
252
183
|
}
|
|
253
184
|
}
|
|
254
185
|
// Add hook notification
|
|
@@ -259,22 +190,7 @@ export const MessageContent = React.forwardRef(({ role: roleProp, variant, isStr
|
|
|
259
190
|
if (currentPosition < message.content.length) {
|
|
260
191
|
const remainingText = message.content.slice(currentPosition);
|
|
261
192
|
if (remainingText) {
|
|
262
|
-
elements.push(_jsx(
|
|
263
|
-
filter: "blur(12px)",
|
|
264
|
-
opacity: 0,
|
|
265
|
-
y: 12,
|
|
266
|
-
}, animate: {
|
|
267
|
-
filter: "blur(0px)",
|
|
268
|
-
opacity: 1,
|
|
269
|
-
y: 0,
|
|
270
|
-
}, exit: {
|
|
271
|
-
filter: "blur(12px)",
|
|
272
|
-
opacity: 0,
|
|
273
|
-
y: -12,
|
|
274
|
-
}, transition: getTransition(shouldReduceMotion ?? false, {
|
|
275
|
-
duration: 0.4,
|
|
276
|
-
ease: motionEasing.smooth,
|
|
277
|
-
}), children: _jsx(Response, { content: remainingText, isStreaming: message.isStreaming, showEmpty: false }) }, "text-end-hooks"));
|
|
193
|
+
elements.push(_jsx("div", { children: _jsx(Response, { content: remainingText, isStreaming: message.isStreaming, showEmpty: false }) }, "text-end-hooks"));
|
|
278
194
|
}
|
|
279
195
|
}
|
|
280
196
|
// Add tool calls at the end
|
|
@@ -283,7 +199,7 @@ export const MessageContent = React.forwardRef(({ role: roleProp, variant, isStr
|
|
|
283
199
|
elements.push(renderToolCallOrGroup(item, index));
|
|
284
200
|
});
|
|
285
201
|
}
|
|
286
|
-
return
|
|
202
|
+
return _jsx(_Fragment, { children: elements });
|
|
287
203
|
}
|
|
288
204
|
// Render content interleaved with tool calls and hook notifications
|
|
289
205
|
// Group consecutive tool calls with the same batchId or same title
|
|
@@ -316,10 +232,14 @@ export const MessageContent = React.forwardRef(({ role: roleProp, variant, isStr
|
|
|
316
232
|
const flushBatch = () => {
|
|
317
233
|
if (currentBatch.length > 1) {
|
|
318
234
|
// Group multiple consecutive calls (by batchId or same title)
|
|
319
|
-
elements.push(_jsx(ToolOperation, { toolCalls: currentBatch, isGrouped: true }, `group-${currentBatchId || currentBatchTitle}-${currentBatch[0]
|
|
235
|
+
elements.push(_jsx(ToolOperation, { toolCalls: currentBatch, isGrouped: true }, `group-${currentBatchId || currentBatchTitle}-${currentBatch[0]?.id}`));
|
|
320
236
|
}
|
|
321
237
|
else if (currentBatch.length === 1) {
|
|
322
|
-
elements.push(_jsx("div", { children: _jsx(ToolOperation
|
|
238
|
+
elements.push(_jsx("div", { children: _jsx(ToolOperation
|
|
239
|
+
// biome-ignore lint/style/noNonNullAssertion: Length check ensures element exists
|
|
240
|
+
, {
|
|
241
|
+
// biome-ignore lint/style/noNonNullAssertion: Length check ensures element exists
|
|
242
|
+
toolCalls: [currentBatch[0]], isGrouped: false }) }, `tool-${currentBatch[0]?.id}`));
|
|
323
243
|
}
|
|
324
244
|
currentBatch = [];
|
|
325
245
|
currentBatchId = undefined;
|
|
@@ -337,22 +257,7 @@ export const MessageContent = React.forwardRef(({ role: roleProp, variant, isStr
|
|
|
337
257
|
const itemId = positionedItem.type === "toolCall"
|
|
338
258
|
? positionedItem.item.id
|
|
339
259
|
: positionedItem.item.id;
|
|
340
|
-
elements.push(_jsx(
|
|
341
|
-
filter: "blur(12px)",
|
|
342
|
-
opacity: 0,
|
|
343
|
-
y: 12,
|
|
344
|
-
}, animate: {
|
|
345
|
-
filter: "blur(0px)",
|
|
346
|
-
opacity: 1,
|
|
347
|
-
y: 0,
|
|
348
|
-
}, exit: {
|
|
349
|
-
filter: "blur(12px)",
|
|
350
|
-
opacity: 0,
|
|
351
|
-
y: -12,
|
|
352
|
-
}, transition: getTransition(shouldReduceMotion ?? false, {
|
|
353
|
-
duration: 0.4,
|
|
354
|
-
ease: motionEasing.smooth,
|
|
355
|
-
}), children: _jsx(Response, { content: textChunk, isStreaming: false, showEmpty: false }) }, `text-before-${itemId}`));
|
|
260
|
+
elements.push(_jsx("div", { children: _jsx(Response, { content: textChunk, isStreaming: false, showEmpty: false }) }, `text-before-${itemId}`));
|
|
356
261
|
}
|
|
357
262
|
}
|
|
358
263
|
if (positionedItem.type === "hookNotification") {
|
|
@@ -399,65 +304,24 @@ export const MessageContent = React.forwardRef(({ role: roleProp, variant, isStr
|
|
|
399
304
|
if (currentPosition < message.content.length) {
|
|
400
305
|
const remainingText = message.content.slice(currentPosition);
|
|
401
306
|
if (remainingText) {
|
|
402
|
-
elements.push(_jsx(
|
|
403
|
-
filter: "blur(12px)",
|
|
404
|
-
opacity: 0,
|
|
405
|
-
y: 12,
|
|
406
|
-
}, animate: {
|
|
407
|
-
filter: "blur(0px)",
|
|
408
|
-
opacity: 1,
|
|
409
|
-
y: 0,
|
|
410
|
-
}, exit: {
|
|
411
|
-
filter: "blur(12px)",
|
|
412
|
-
opacity: 0,
|
|
413
|
-
y: -12,
|
|
414
|
-
}, transition: getTransition(shouldReduceMotion ?? false, {
|
|
415
|
-
duration: 0.4,
|
|
416
|
-
ease: motionEasing.smooth,
|
|
417
|
-
}), children: _jsx(Response, { content: remainingText, isStreaming: message.isStreaming, showEmpty: false }) }, "text-end"));
|
|
307
|
+
elements.push(_jsx("div", { children: _jsx(Response, { content: remainingText, isStreaming: message.isStreaming, showEmpty: false }) }, "text-end"));
|
|
418
308
|
}
|
|
419
309
|
}
|
|
420
310
|
// Render preliminary (selecting) tool calls at the end, grouped
|
|
421
311
|
if (preliminaryToolCalls.length > 0) {
|
|
422
312
|
if (preliminaryToolCalls.length > 1) {
|
|
423
|
-
elements.push(_jsx(ToolOperation, { toolCalls: preliminaryToolCalls, isGrouped: true }, `selecting-group-${preliminaryToolCalls[0]
|
|
313
|
+
elements.push(_jsx(ToolOperation, { toolCalls: preliminaryToolCalls, isGrouped: true }, `selecting-group-${preliminaryToolCalls[0]?.id}`));
|
|
424
314
|
}
|
|
425
315
|
else {
|
|
426
|
-
elements.push(_jsx("div", { children: _jsx(ToolOperation
|
|
316
|
+
elements.push(_jsx("div", { children: _jsx(ToolOperation
|
|
317
|
+
// biome-ignore lint/style/noNonNullAssertion: Condition ensures element exists
|
|
318
|
+
, {
|
|
319
|
+
// biome-ignore lint/style/noNonNullAssertion: Condition ensures element exists
|
|
320
|
+
toolCalls: [preliminaryToolCalls[0]], isGrouped: false }) }, `tool-${preliminaryToolCalls[0]?.id}`));
|
|
427
321
|
}
|
|
428
322
|
}
|
|
429
|
-
return
|
|
430
|
-
})()) : (_jsxs("div", { className: "flex flex-col gap-2", children: [message.images && message.images.length > 0 && (_jsx(
|
|
431
|
-
filter: "blur(12px)",
|
|
432
|
-
opacity: 0,
|
|
433
|
-
y: 12,
|
|
434
|
-
}, animate: {
|
|
435
|
-
filter: "blur(0px)",
|
|
436
|
-
opacity: 1,
|
|
437
|
-
y: 0,
|
|
438
|
-
}, exit: {
|
|
439
|
-
filter: "blur(12px)",
|
|
440
|
-
opacity: 0,
|
|
441
|
-
y: -12,
|
|
442
|
-
}, transition: getTransition(shouldReduceMotion ?? false, {
|
|
443
|
-
duration: 0.5,
|
|
444
|
-
ease: motionEasing.smooth,
|
|
445
|
-
}), children: message.images.map((image, index) => (_jsx("img", { src: `data:${image.mimeType};base64,${image.data}`, alt: `Attachment ${index + 1}`, className: "max-w-[200px] max-h-[200px] rounded-lg object-cover" }, index))) })), message.content && (_jsx(motion.div, { className: "whitespace-pre-wrap", initial: {
|
|
446
|
-
filter: "blur(12px)",
|
|
447
|
-
opacity: 0,
|
|
448
|
-
y: 12,
|
|
449
|
-
}, animate: {
|
|
450
|
-
filter: "blur(0px)",
|
|
451
|
-
opacity: 1,
|
|
452
|
-
y: 0,
|
|
453
|
-
}, exit: {
|
|
454
|
-
filter: "blur(12px)",
|
|
455
|
-
opacity: 0,
|
|
456
|
-
y: -12,
|
|
457
|
-
}, transition: getTransition(shouldReduceMotion ?? false, {
|
|
458
|
-
duration: 0.4,
|
|
459
|
-
ease: motionEasing.smooth,
|
|
460
|
-
}), children: message.content }))] }))] }));
|
|
323
|
+
return _jsx(_Fragment, { children: elements });
|
|
324
|
+
})()) : (_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 }))] }))] }));
|
|
461
325
|
}
|
|
462
326
|
return (_jsx("div", { ref: ref, className: cn(messageContentVariants({ role, variant }), isStreaming && "animate-pulse-subtle", className), ...props, children: content }));
|
|
463
327
|
});
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { createLogger } from "@townco/core";
|
|
3
|
-
import { Loader2 } from "lucide-react";
|
|
4
3
|
import { useCallback, useEffect, useState } from "react";
|
|
5
4
|
import { SessionHistoryItem } from "./SessionHistoryItem.js";
|
|
6
5
|
import { SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarMenu, useSidebar, } from "./Sidebar.js";
|
|
@@ -97,5 +96,5 @@ export function SessionHistory({ client, currentSessionId, onSessionSelect, onRe
|
|
|
97
96
|
return (_jsx(SidebarGroup, { children: _jsx(SidebarGroupContent, { children: _jsx("div", { className: "flex w-full flex-row items-center justify-center gap-2 px-2 py-4 text-sm text-muted-foreground", children: "Your sessions will appear here once you start chatting!" }) }) }));
|
|
98
97
|
}
|
|
99
98
|
const groupedSessions = groupSessionsByDate(sessions);
|
|
100
|
-
return (_jsx(SidebarGroup, { children: _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: _jsxs("div", { className: "flex flex-col gap-6", children: [groupedSessions.today.length > 0 && (_jsxs("div", { children: [_jsx("div", { className: "px-2 py-1 text-
|
|
99
|
+
return (_jsx(SidebarGroup, { children: _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: _jsxs("div", { className: "flex flex-col gap-6", children: [groupedSessions.today.length > 0 && (_jsxs("div", { className: "flex flex-col", children: [_jsx("div", { className: "px-2 py-1 text-muted-foreground text-sm font-medium", children: "Today" }), groupedSessions.today.map((session) => (_jsx(SessionHistoryItem, { session: session, isActive: session.sessionId === currentSessionId, onSelect: handleSessionSelect, onRename: onRenameSession, onArchive: onArchiveSession, onDelete: onDeleteSession, setOpenMobile: setOpenMobile }, session.sessionId)))] })), groupedSessions.yesterday.length > 0 && (_jsxs("div", { className: "flex flex-col", children: [_jsx("div", { className: "px-2 py-1 text-muted-foreground text-sm font-medium", children: "Yesterday" }), groupedSessions.yesterday.map((session) => (_jsx(SessionHistoryItem, { session: session, isActive: session.sessionId === currentSessionId, onSelect: handleSessionSelect, onRename: onRenameSession, onArchive: onArchiveSession, onDelete: onDeleteSession, setOpenMobile: setOpenMobile }, session.sessionId)))] })), groupedSessions.lastWeek.length > 0 && (_jsxs("div", { className: "flex flex-col", children: [_jsx("div", { className: "px-2 py-1 text-muted-foreground text-sm font-medium", children: "Last 7 days" }), groupedSessions.lastWeek.map((session) => (_jsx(SessionHistoryItem, { session: session, isActive: session.sessionId === currentSessionId, onSelect: handleSessionSelect, onRename: onRenameSession, onArchive: onArchiveSession, onDelete: onDeleteSession, setOpenMobile: setOpenMobile }, session.sessionId)))] })), groupedSessions.lastMonth.length > 0 && (_jsxs("div", { className: "flex flex-col", children: [_jsx("div", { className: "px-2 py-1 text-muted-foreground text-sm font-medium", children: "Last 30 days" }), groupedSessions.lastMonth.map((session) => (_jsx(SessionHistoryItem, { session: session, isActive: session.sessionId === currentSessionId, onSelect: handleSessionSelect, onRename: onRenameSession, onArchive: onArchiveSession, onDelete: onDeleteSession, setOpenMobile: setOpenMobile }, session.sessionId)))] })), groupedSessions.older.length > 0 && (_jsxs("div", { className: "flex flex-col", children: [_jsx("div", { className: "px-2 py-1 text-muted-foreground text-sm font-medium", children: "Older" }), groupedSessions.older.map((session) => (_jsx(SessionHistoryItem, { session: session, isActive: session.sessionId === currentSessionId, onSelect: handleSessionSelect, onRename: onRenameSession, onArchive: onArchiveSession, onDelete: onDeleteSession, setOpenMobile: setOpenMobile }, session.sessionId)))] }))] }) }) }) }));
|
|
101
100
|
}
|
|
@@ -8,7 +8,7 @@ const PureSessionHistoryItem = ({ session, isActive, onSelect, onRename, onArchi
|
|
|
8
8
|
return (_jsxs(SidebarMenuItem, { children: [_jsx(SidebarMenuButton, { asChild: true, isActive: isActive, onClick: () => {
|
|
9
9
|
onSelect(session.sessionId);
|
|
10
10
|
setOpenMobile(false);
|
|
11
|
-
}, children: _jsx("button", { type: "button", className: "w-full", children: _jsx("span", { className: "truncate", children: session.firstUserMessage || "Empty session" }) }) }), _jsxs(DropdownMenu, { modal: true, children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(SidebarMenuAction, { className: "mr-0.5 data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground", showOnHover: !isActive, children: [_jsx(MoreHorizontal, {}), _jsx("span", { className: "sr-only", children: "More" })] }) }), _jsxs(DropdownMenuContent, { align: "end", side: "bottom", children: [_jsx(DropdownMenuItem, { className: "cursor-pointer", disabled: !onRename, onSelect: () => onRename?.(session.sessionId), children: "Rename" }), _jsx(DropdownMenuItem, { className: "cursor-pointer", disabled: !onArchive, onSelect: () => onArchive?.(session.sessionId), children: "Archive" }), _jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuItem, { className: cn("cursor-pointer text-destructive focus:bg-destructive/15 focus:text-destructive"), disabled: !onDelete, onSelect: () => onDelete?.(session.sessionId), children: "Delete" })] })] })] }));
|
|
11
|
+
}, className: "h-10 px-2 py-3", children: _jsx("button", { type: "button", className: "w-full text-base", children: _jsx("span", { className: "truncate", children: session.firstUserMessage || "Empty session" }) }) }), _jsxs(DropdownMenu, { modal: true, children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(SidebarMenuAction, { className: "mr-0.5 data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground", showOnHover: !isActive, children: [_jsx(MoreHorizontal, {}), _jsx("span", { className: "sr-only", children: "More" })] }) }), _jsxs(DropdownMenuContent, { align: "end", side: "bottom", children: [_jsx(DropdownMenuItem, { className: "cursor-pointer", disabled: !onRename, onSelect: () => onRename?.(session.sessionId), children: "Rename" }), _jsx(DropdownMenuItem, { className: "cursor-pointer", disabled: !onArchive, onSelect: () => onArchive?.(session.sessionId), children: "Archive" }), _jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuItem, { className: cn("cursor-pointer text-destructive focus:bg-destructive/15 focus:text-destructive"), disabled: !onDelete, onSelect: () => onDelete?.(session.sessionId), children: "Delete" })] })] })] }));
|
|
12
12
|
};
|
|
13
13
|
export const SessionHistoryItem = memo(PureSessionHistoryItem, (prevProps, nextProps) => {
|
|
14
14
|
if (prevProps.isActive !== nextProps.isActive) {
|
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Slot as SlotPrimitive } from "@radix-ui/react-slot";
|
|
3
3
|
import { cva } from "class-variance-authority";
|
|
4
|
+
import { AnimatePresence, motion } from "framer-motion";
|
|
4
5
|
import { PanelLeft } from "lucide-react";
|
|
5
6
|
import * as React from "react";
|
|
7
|
+
import { SIDEBAR_WIDTH_DESKTOP, SIDEBAR_WIDTH_MOBILE } from "../constants.js";
|
|
8
|
+
import { useLockBodyScroll } from "../hooks/use-lock-body-scroll.js";
|
|
6
9
|
import { useIsMobile } from "../hooks/use-mobile.js";
|
|
10
|
+
import { backdropVariants, sidebarContentTransition, sidebarContentVariants, sidebarDesktopVariants, sidebarMobileTransition, sidebarMobileVariants, sidebarTransition, } from "../lib/motion.js";
|
|
7
11
|
import { cn } from "../lib/utils.js";
|
|
8
12
|
import { Button } from "./Button.js";
|
|
9
13
|
import { Input } from "./Input.js";
|
|
10
|
-
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, } from "./Sheet.js";
|
|
11
14
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "./Tooltip.js";
|
|
12
15
|
const SIDEBAR_COOKIE_NAME = "sidebar_state";
|
|
13
16
|
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
|
|
14
|
-
const SIDEBAR_WIDTH = "16rem";
|
|
15
|
-
const SIDEBAR_WIDTH_MOBILE = "18rem";
|
|
16
|
-
const SIDEBAR_WIDTH_ICON = "3rem";
|
|
17
17
|
const SIDEBAR_KEYBOARD_SHORTCUT = "s";
|
|
18
18
|
const SidebarContext = React.createContext(null);
|
|
19
19
|
function useSidebar() {
|
|
@@ -39,6 +39,7 @@ const SidebarProvider = React.forwardRef(({ defaultOpen = true, open: openProp,
|
|
|
39
39
|
_setOpen(openState);
|
|
40
40
|
}
|
|
41
41
|
// This sets the cookie to keep the sidebar state.
|
|
42
|
+
// biome-ignore lint/suspicious/noDocumentCookie: Intentional cookie setting for sidebar state persistence
|
|
42
43
|
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
|
|
43
44
|
}, [setOpenProp, open]);
|
|
44
45
|
// Helper to toggle the sidebar.
|
|
@@ -46,7 +47,7 @@ const SidebarProvider = React.forwardRef(({ defaultOpen = true, open: openProp,
|
|
|
46
47
|
return isMobile
|
|
47
48
|
? setOpenMobile((open) => !open)
|
|
48
49
|
: setOpen((open) => !open);
|
|
49
|
-
}, [isMobile, setOpen
|
|
50
|
+
}, [isMobile, setOpen]);
|
|
50
51
|
// Adds a keyboard shortcut to toggle the sidebar (Cmd/Ctrl+Shift+S).
|
|
51
52
|
React.useEffect(() => {
|
|
52
53
|
const handleKeyDown = (event) => {
|
|
@@ -71,41 +72,20 @@ const SidebarProvider = React.forwardRef(({ defaultOpen = true, open: openProp,
|
|
|
71
72
|
openMobile,
|
|
72
73
|
setOpenMobile,
|
|
73
74
|
toggleSidebar,
|
|
74
|
-
}), [
|
|
75
|
-
|
|
76
|
-
open,
|
|
77
|
-
setOpen,
|
|
78
|
-
isMobile,
|
|
79
|
-
openMobile,
|
|
80
|
-
setOpenMobile,
|
|
81
|
-
toggleSidebar,
|
|
82
|
-
]);
|
|
83
|
-
return (_jsx(SidebarContext.Provider, { value: contextValue, children: _jsx(TooltipProvider, { delayDuration: 0, children: _jsx("div", { className: cn("group/sidebar-wrapper flex min-h-svh w-full has-[[data-variant=inset]]:bg-sidebar", className), ref: ref, style: {
|
|
84
|
-
"--sidebar-width": SIDEBAR_WIDTH,
|
|
85
|
-
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
|
|
86
|
-
...style,
|
|
87
|
-
}, ...props, children: children }) }) }));
|
|
75
|
+
}), [state, open, setOpen, isMobile, openMobile, toggleSidebar]);
|
|
76
|
+
return (_jsx(SidebarContext.Provider, { value: contextValue, children: _jsx(TooltipProvider, { delayDuration: 0, children: _jsx("div", { className: cn("group/sidebar-wrapper flex min-h-svh w-full has-[[data-variant=inset]]:bg-sidebar", className), ref: ref, style: style, ...props, children: children }) }) }));
|
|
88
77
|
});
|
|
89
78
|
SidebarProvider.displayName = "SidebarProvider";
|
|
90
|
-
const Sidebar = React.forwardRef(({
|
|
91
|
-
const { isMobile,
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
79
|
+
const Sidebar = React.forwardRef(({ className, children }, ref) => {
|
|
80
|
+
const { isMobile, open, openMobile, setOpenMobile } = useSidebar();
|
|
81
|
+
// Lock body scroll when mobile sidebar is open
|
|
82
|
+
useLockBodyScroll(isMobile && openMobile);
|
|
83
|
+
// Mobile: Sidebar with white overlay
|
|
95
84
|
if (isMobile) {
|
|
96
|
-
return (_jsx(
|
|
97
|
-
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
|
|
98
|
-
}, children: [_jsxs(SheetHeader, { className: "sr-only", children: [_jsx(SheetTitle, { children: "Sidebar" }), _jsx(SheetDescription, { children: "Displays the mobile sidebar." })] }), _jsx("div", { className: "flex h-full w-full flex-col", children: children })] }) }));
|
|
85
|
+
return (_jsx(AnimatePresence, { initial: false, children: openMobile && (_jsxs(_Fragment, { children: [_jsx(motion.div, { className: "fixed inset-0 z-40 bg-background/80", variants: backdropVariants, initial: "initial", animate: "animate", exit: "exit", transition: sidebarMobileTransition, onClick: () => setOpenMobile(false), "aria-label": "Close sidebar" }), _jsx(motion.aside, { ref: ref, className: cn("fixed inset-y-0 left-0 z-50", "bg-sidebar text-sidebar-foreground", "flex flex-col", "border-r border-sidebar-border", "max-h-screen overflow-hidden", className), style: { width: SIDEBAR_WIDTH_MOBILE }, variants: sidebarMobileVariants, initial: "initial", animate: "animate", exit: "exit", transition: sidebarMobileTransition, "data-sidebar": "sidebar", "data-mobile": "true", children: _jsx(motion.div, { className: "flex flex-col h-full overflow-y-auto", variants: sidebarContentVariants, initial: "initial", animate: "animate", transition: sidebarContentTransition, children: children }) })] })) }));
|
|
99
86
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
: "group-data-[collapsible=icon]:w-[var(--sidebar-width-icon)]") }), _jsx("div", { className: cn("fixed inset-y-0 z-10 hidden h-svh w-[var(--sidebar-width)] transition-[left,right,width] duration-150 ease-linear md:flex", side === "left"
|
|
103
|
-
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
|
|
104
|
-
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
|
|
105
|
-
// Adjust the padding for floating and inset variants.
|
|
106
|
-
variant === "floating" || variant === "inset"
|
|
107
|
-
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]"
|
|
108
|
-
: "group-data-[collapsible=icon]:w-[var(--sidebar-width-icon)] group-data-[side=left]:border-r group-data-[side=right]:border-l", className), ...props, children: _jsx("div", { className: "flex h-full w-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow", "data-sidebar": "sidebar", children: children }) })] }));
|
|
87
|
+
// Desktop: Fixed width sidebar with slide animation
|
|
88
|
+
return (_jsx("div", { className: "group peer hidden md:block", "data-state": open ? "expanded" : "collapsed", "data-side": "left", ref: ref, children: _jsx(AnimatePresence, { initial: false, mode: "wait", children: open && (_jsx(motion.aside, { className: cn("fixed inset-y-0 left-0 z-40", "bg-sidebar text-sidebar-foreground", "border-r border-sidebar-border", "flex flex-col", "max-h-screen overflow-hidden", className), style: { width: SIDEBAR_WIDTH_DESKTOP }, variants: sidebarDesktopVariants, initial: "initial", animate: "animate", exit: "exit", transition: sidebarTransition, "data-sidebar": "sidebar", children: _jsx(motion.div, { className: "flex flex-col h-full overflow-y-auto", variants: sidebarContentVariants, initial: "initial", animate: "animate", transition: sidebarContentTransition, children: children }) })) }) }));
|
|
109
89
|
});
|
|
110
90
|
Sidebar.displayName = "Sidebar";
|
|
111
91
|
const SidebarTrigger = React.forwardRef(({ className, onClick, ...props }, ref) => {
|
|
@@ -121,8 +101,13 @@ const SidebarRail = React.forwardRef(({ className, ...props }, ref) => {
|
|
|
121
101
|
return (_jsx("button", { type: "button", "aria-label": "Toggle Sidebar", className: cn("-translate-x-1/2 group-data-[side=left]:-right-4 absolute inset-y-0 z-20 hidden w-4 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-sidebar-border group-data-[side=right]:left-0 sm:flex", "[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize", "[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize", "group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:hover:bg-sidebar group-data-[collapsible=offcanvas]:after:left-full", "[[data-side=left][data-collapsible=offcanvas]_&]:-right-2", "[[data-side=right][data-collapsible=offcanvas]_&]:-left-2", className), "data-sidebar": "rail", onClick: toggleSidebar, ref: ref, tabIndex: -1, title: "Toggle Sidebar", ...props }));
|
|
122
102
|
});
|
|
123
103
|
SidebarRail.displayName = "SidebarRail";
|
|
124
|
-
const SidebarInset = React.forwardRef(({ className, ...props }, ref) => {
|
|
125
|
-
|
|
104
|
+
const SidebarInset = React.forwardRef(({ className, children, ...props }, ref) => {
|
|
105
|
+
const { isMobile, open } = useSidebar();
|
|
106
|
+
// Desktop: Add padding when sidebar is open
|
|
107
|
+
// Mobile: No changes (overlay handles visibility)
|
|
108
|
+
return (_jsx("main", { className: cn("relative flex w-full flex-1 flex-col bg-background transition-all duration-500",
|
|
109
|
+
// Add left padding on desktop when sidebar is open
|
|
110
|
+
!isMobile && open && "md:pl-64", className), ref: ref, ...props, children: children }));
|
|
126
111
|
});
|
|
127
112
|
SidebarInset.displayName = "SidebarInset";
|
|
128
113
|
const SidebarInput = React.forwardRef(({ className, ...props }, ref) => {
|
|
@@ -146,7 +131,7 @@ const SidebarContent = React.forwardRef(({ className, ...props }, ref) => {
|
|
|
146
131
|
});
|
|
147
132
|
SidebarContent.displayName = "SidebarContent";
|
|
148
133
|
const SidebarGroup = React.forwardRef(({ className, ...props }, ref) => {
|
|
149
|
-
return (_jsx("div", { className: cn("relative flex w-full min-w-0 flex-col p-
|
|
134
|
+
return (_jsx("div", { className: cn("relative flex w-full min-w-0 flex-col p-4", className), "data-sidebar": "group", ref: ref, ...props }));
|
|
150
135
|
});
|
|
151
136
|
SidebarGroup.displayName = "SidebarGroup";
|
|
152
137
|
const SidebarGroupLabel = React.forwardRef(({ className, asChild = false, ...props }, ref) => {
|
|
@@ -201,9 +186,9 @@ const SidebarMenuButton = React.forwardRef(({ asChild = false, isActive = false,
|
|
|
201
186
|
SidebarMenuButton.displayName = "SidebarMenuButton";
|
|
202
187
|
const SidebarMenuAction = React.forwardRef(({ className, asChild = false, showOnHover = false, ...props }, ref) => {
|
|
203
188
|
const Comp = asChild ? SlotPrimitive : "button";
|
|
204
|
-
return (_jsx(Comp, { className: cn("absolute top-
|
|
189
|
+
return (_jsx(Comp, { className: cn("absolute top-3 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 peer-hover/menu-button:text-sidebar-accent-foreground [&>svg]:size-4 [&>svg]:shrink-0",
|
|
205
190
|
// Increases the hit area of the button on mobile.
|
|
206
|
-
"after:-inset-2 after:absolute after:md:hidden", "peer-data-[size=sm]/menu-button:top-1", "peer-data-[size=default]/menu-button:top-
|
|
191
|
+
"after:-inset-2 after:absolute after:md:hidden", "peer-data-[size=sm]/menu-button:top-1", "peer-data-[size=default]/menu-button:top-2.5", "peer-data-[size=lg]/menu-button:top-2.5", "group-data-[collapsible=icon]:hidden", showOnHover &&
|
|
207
192
|
"group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0", className), "data-sidebar": "menu-action", ref: ref, type: asChild ? undefined : "button", ...props }));
|
|
208
193
|
});
|
|
209
194
|
SidebarMenuAction.displayName = "SidebarMenuAction";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { ChevronDown, Loader2 } from "lucide-react";
|
|
3
|
-
import
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
import { useSubagentStream } from "../../core/hooks/use-subagent-stream.js";
|
|
5
5
|
import { MarkdownRenderer } from "./MarkdownRenderer.js";
|
|
6
6
|
const SCROLL_THRESHOLD = 50; // px from bottom to consider "at bottom"
|
|
@@ -18,20 +18,23 @@ export function SubAgentDetails({ port, sessionId, host, parentStatus, agentName
|
|
|
18
18
|
const [internalIsExpanded, setInternalIsExpanded] = useState(false);
|
|
19
19
|
// Use controlled state if provided, otherwise use internal state
|
|
20
20
|
const isExpanded = controlledIsExpanded ?? internalIsExpanded;
|
|
21
|
-
const
|
|
21
|
+
const _setIsExpanded = onExpandChange ?? setInternalIsExpanded;
|
|
22
22
|
// Start with Thinking expanded while running to show the live stream
|
|
23
23
|
const [isThinkingExpanded, setIsThinkingExpanded] = useState(true);
|
|
24
24
|
const [isNearBottom, setIsNearBottom] = useState(true);
|
|
25
25
|
const thinkingContainerRef = useRef(null);
|
|
26
26
|
// Only use SSE streaming if not in replay mode and port/sessionId provided
|
|
27
27
|
const shouldStream = !isReplay && port !== undefined && sessionId !== undefined;
|
|
28
|
-
|
|
28
|
+
// Memoize stream options to prevent unnecessary reconnections
|
|
29
|
+
const streamOptions = useMemo(() => shouldStream
|
|
29
30
|
? {
|
|
31
|
+
// biome-ignore lint/style/noNonNullAssertion: shouldStream condition ensures port is defined
|
|
30
32
|
port: port,
|
|
33
|
+
// biome-ignore lint/style/noNonNullAssertion: shouldStream condition ensures sessionId is defined
|
|
31
34
|
sessionId: sessionId,
|
|
32
35
|
...(host !== undefined ? { host } : {}),
|
|
33
36
|
}
|
|
34
|
-
: null;
|
|
37
|
+
: null, [shouldStream, port, sessionId, host]);
|
|
35
38
|
const { messages: streamedMessages, isStreaming: hookIsStreaming, error, } = useSubagentStream(streamOptions);
|
|
36
39
|
// Use stored messages if available, otherwise use streamed messages
|
|
37
40
|
const messages = storedMessages && storedMessages.length > 0
|
|
@@ -82,14 +85,7 @@ export function SubAgentDetails({ port, sessionId, host, parentStatus, agentName
|
|
|
82
85
|
if (isNearBottom && (isRunning || hasContent)) {
|
|
83
86
|
scrollToBottom();
|
|
84
87
|
}
|
|
85
|
-
}, [
|
|
86
|
-
currentMessage?.content,
|
|
87
|
-
currentMessage?.toolCalls,
|
|
88
|
-
isNearBottom,
|
|
89
|
-
isRunning,
|
|
90
|
-
hasContent,
|
|
91
|
-
scrollToBottom,
|
|
92
|
-
]);
|
|
88
|
+
}, [isNearBottom, isRunning, hasContent, scrollToBottom]);
|
|
93
89
|
// Set up scroll listener
|
|
94
90
|
useEffect(() => {
|
|
95
91
|
const container = thinkingContainerRef.current;
|
|
@@ -99,7 +95,7 @@ export function SubAgentDetails({ port, sessionId, host, parentStatus, agentName
|
|
|
99
95
|
container.addEventListener("scroll", handleScroll, { passive: true });
|
|
100
96
|
checkScrollPosition(); // Check initial position
|
|
101
97
|
return () => container.removeEventListener("scroll", handleScroll);
|
|
102
|
-
}, [checkScrollPosition
|
|
98
|
+
}, [checkScrollPosition]);
|
|
103
99
|
// When thinking section expands, scroll to bottom and reset follow state
|
|
104
100
|
useEffect(() => {
|
|
105
101
|
if (isThinkingExpanded) {
|
|
@@ -123,7 +119,7 @@ export function SubAgentDetails({ port, sessionId, host, parentStatus, agentName
|
|
|
123
119
|
return (_jsxs("div", { children: [!isExpanded && (_jsx("div", { className: "w-full max-w-md", children: previewText ? (_jsx("p", { className: "text-paragraph-sm text-muted-foreground/70 truncate", children: previewText })) : queryFirstLine ? (_jsx("p", { className: "text-paragraph-sm text-muted-foreground/50 truncate", children: queryFirstLine })) : 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: "Stream" }), _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.contentBlocks &&
|
|
124
120
|
currentMessage.contentBlocks.length > 0
|
|
125
121
|
? // Render interleaved content blocks
|
|
126
|
-
currentMessage.contentBlocks.map((block,
|
|
122
|
+
currentMessage.contentBlocks.map((block, blockIdx) => block.type === "text" ? (_jsx("div", { className: "text-[11px] text-foreground prose prose-sm dark:prose-invert max-w-none prose-p:my-1 prose-pre:my-1 prose-code:text-[10px]", children: _jsx(MarkdownRenderer, { content: block.text }) }, `text-${block.text.slice(0, 30)}-${blockIdx}`)) : (_jsx(SubagentToolCallItem, { toolCall: block.toolCall }, block.toolCall.id)))
|
|
127
123
|
: // Fallback to legacy content with markdown
|
|
128
124
|
currentMessage.content && (_jsx("div", { className: "text-[11px] text-foreground prose prose-sm dark:prose-invert max-w-none prose-p:my-1 prose-pre:my-1 prose-code:text-[10px]", children: _jsx(MarkdownRenderer, { content: 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 max-h-[200px] overflow-y-auto rounded-md bg-muted/30 border border-border/50 px-2 py-2 prose prose-sm dark:prose-invert max-w-none prose-p:my-1 prose-pre:my-1 prose-code:text-[10px]", children: _jsx(MarkdownRenderer, { content: currentMessage.content }) })] }))] }))] }));
|
|
129
125
|
}
|
|
@@ -7,6 +7,7 @@ export function TodoSubline({ todos, className }) {
|
|
|
7
7
|
return null;
|
|
8
8
|
// Find the most important item to display
|
|
9
9
|
// Priority: in_progress > completed > first item
|
|
10
|
+
// biome-ignore lint/correctness/useHookAtTopLevel: Hook call at component top level
|
|
10
11
|
const displayItem = React.useMemo(() => {
|
|
11
12
|
const inProgress = todos.find((t) => t.status === "in_progress");
|
|
12
13
|
if (inProgress)
|