@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.
Files changed (58) hide show
  1. package/dist/core/hooks/use-chat-messages.d.ts +4 -4
  2. package/dist/core/hooks/use-chat-messages.js +4 -1
  3. package/dist/core/hooks/use-chat-session.d.ts +1 -1
  4. package/dist/core/hooks/use-chat-session.js +5 -1
  5. package/dist/core/hooks/use-subagent-stream.js +6 -6
  6. package/dist/core/hooks/use-tool-calls.d.ts +3 -3
  7. package/dist/core/hooks/use-tool-calls.js +1 -1
  8. package/dist/core/schemas/chat.d.ts +10 -10
  9. package/dist/core/schemas/tool-call.d.ts +8 -8
  10. package/dist/core/store/chat-store.js +1 -0
  11. package/dist/core/utils/tool-summary.js +8 -3
  12. package/dist/core/utils/tool-verbiage.js +1 -1
  13. package/dist/gui/components/AppSidebar.d.ts +1 -1
  14. package/dist/gui/components/AppSidebar.js +4 -3
  15. package/dist/gui/components/Button.d.ts +1 -1
  16. package/dist/gui/components/ChatEmptyState.js +1 -1
  17. package/dist/gui/components/ChatHeader.d.ts +1 -28
  18. package/dist/gui/components/ChatHeader.js +4 -71
  19. package/dist/gui/components/ChatLayout.d.ts +6 -2
  20. package/dist/gui/components/ChatLayout.js +82 -33
  21. package/dist/gui/components/ChatView.js +28 -45
  22. package/dist/gui/components/ContextUsageButton.d.ts +0 -1
  23. package/dist/gui/components/ContextUsageButton.js +10 -3
  24. package/dist/gui/components/HookNotification.js +2 -1
  25. package/dist/gui/components/MessageContent.js +24 -160
  26. package/dist/gui/components/SessionHistory.js +1 -2
  27. package/dist/gui/components/SessionHistoryItem.js +1 -1
  28. package/dist/gui/components/Sidebar.js +27 -42
  29. package/dist/gui/components/SubAgentDetails.js +10 -14
  30. package/dist/gui/components/TodoSubline.js +1 -0
  31. package/dist/gui/components/ToolOperation.js +16 -75
  32. package/dist/gui/components/WorkProgress.js +5 -3
  33. package/dist/gui/components/index.d.ts +0 -1
  34. package/dist/gui/components/resizable.d.ts +1 -1
  35. package/dist/gui/constants.d.ts +6 -0
  36. package/dist/gui/constants.js +8 -0
  37. package/dist/gui/hooks/index.d.ts +1 -0
  38. package/dist/gui/hooks/index.js +1 -0
  39. package/dist/gui/hooks/use-lock-body-scroll.d.ts +7 -0
  40. package/dist/gui/hooks/use-lock-body-scroll.js +29 -0
  41. package/dist/gui/lib/motion.d.ts +12 -0
  42. package/dist/gui/lib/motion.js +69 -0
  43. package/dist/sdk/schemas/message.d.ts +2 -2
  44. package/dist/sdk/schemas/session.d.ts +18 -18
  45. package/dist/sdk/transports/http.d.ts +1 -1
  46. package/dist/sdk/transports/http.js +9 -0
  47. package/dist/sdk/transports/stdio.js +2 -2
  48. package/dist/sdk/transports/types.d.ts +11 -0
  49. package/dist/sdk/transports/types.js +28 -1
  50. package/package.json +3 -5
  51. package/dist/gui/components/InvokingGroup.d.ts +0 -9
  52. package/dist/gui/components/InvokingGroup.js +0 -16
  53. package/dist/gui/components/SubagentStream.d.ts +0 -23
  54. package/dist/gui/components/SubagentStream.js +0 -98
  55. package/dist/gui/components/ToolCall.d.ts +0 -8
  56. package/dist/gui/components/ToolCall.js +0 -234
  57. package/dist/gui/components/ToolCallGroup.d.ts +0 -8
  58. 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 currentModel = useChatStore((state) => state.currentModel);
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(motion.div, { initial: {
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(AnimatePresence, { mode: "popLayout", children: _jsx("div", { className: "flex flex-col gap-2 mb-1", children: groupedToolCalls.map((item, index) => renderToolCallOrGroup(item, index)) }) })), _jsx(motion.div, { initial: {
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(motion.div, { initial: {
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(motion.div, { initial: {
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 (_jsx(AnimatePresence, { mode: "popLayout", children: elements }));
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].id}`));
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, { toolCalls: [currentBatch[0]], isGrouped: false }) }, `tool-${currentBatch[0].id}`));
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(motion.div, { initial: {
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(motion.div, { initial: {
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].id}`));
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, { toolCalls: [preliminaryToolCalls[0]], isGrouped: false }) }, `tool-${preliminaryToolCalls[0].id}`));
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 (_jsx(AnimatePresence, { mode: "popLayout", children: elements }));
430
- })()) : (_jsxs("div", { className: "flex flex-col gap-2", children: [message.images && message.images.length > 0 && (_jsx(motion.div, { className: "flex flex-wrap gap-2", initial: {
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-sidebar-foreground/50 text-xs", 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", { children: [_jsx("div", { className: "px-2 py-1 text-sidebar-foreground/50 text-xs", 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", { children: [_jsx("div", { className: "px-2 py-1 text-sidebar-foreground/50 text-xs", 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", { children: [_jsx("div", { className: "px-2 py-1 text-sidebar-foreground/50 text-xs", 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", { children: [_jsx("div", { className: "px-2 py-1 text-sidebar-foreground/50 text-xs", 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)))] }))] }) }) }) }));
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, setOpenMobile]);
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
- state,
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(({ side = "left", variant = "sidebar", collapsible = "offcanvas", className, children, ...props }, ref) => {
91
- const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
92
- if (collapsible === "none") {
93
- return (_jsx("div", { className: cn("flex h-full w-[var(--sidebar-width)] flex-col bg-sidebar text-sidebar-foreground", className), ref: ref, ...props, children: children }));
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(Sheet, { onOpenChange: setOpenMobile, open: openMobile, ...props, children: _jsxs(SheetContent, { className: "w-[var(--sidebar-width)] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden", "data-mobile": "true", "data-sidebar": "sidebar", side: side, style: {
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
- return (_jsxs("div", { className: "group peer hidden text-sidebar-foreground md:block", "data-collapsible": state === "collapsed" ? collapsible : "", "data-side": side, "data-state": state, "data-variant": variant, ref: ref, children: [_jsx("div", { className: cn("relative w-[var(--sidebar-width)] bg-transparent transition-[width] duration-150 ease-linear", "group-data-[collapsible=offcanvas]:w-0", "group-data-[side=right]:rotate-180", variant === "floating" || variant === "inset"
101
- ? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]"
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
- return (_jsx("main", { className: cn("relative flex w-full flex-1 flex-col bg-background", "md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow", className), ref: ref, ...props }));
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-2", className), "data-sidebar": "group", ref: ref, ...props }));
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-1.5 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",
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-1.5", "peer-data-[size=lg]/menu-button:top-2.5", "group-data-[collapsible=icon]:hidden", showOnHover &&
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 React, { useCallback, useEffect, useRef, useState } from "react";
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 setIsExpanded = onExpandChange ?? setInternalIsExpanded;
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
- const streamOptions = shouldStream
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, isThinkingExpanded, isExpanded]);
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, idx) => 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-${idx}`)) : (_jsx(SubagentToolCallItem, { toolCall: block.toolCall }, block.toolCall.id)))
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)