@townco/ui 0.1.67 → 0.1.69

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 (56) hide show
  1. package/dist/core/hooks/use-chat-messages.d.ts +5 -0
  2. package/dist/core/hooks/use-tool-calls.d.ts +5 -0
  3. package/dist/core/hooks/use-tool-calls.js +2 -1
  4. package/dist/core/schemas/chat.d.ts +10 -0
  5. package/dist/core/schemas/tool-call.d.ts +96 -0
  6. package/dist/core/schemas/tool-call.js +12 -0
  7. package/dist/core/utils/tool-call-state.d.ts +30 -0
  8. package/dist/core/utils/tool-call-state.js +73 -0
  9. package/dist/core/utils/tool-summary.d.ts +13 -0
  10. package/dist/core/utils/tool-summary.js +172 -0
  11. package/dist/core/utils/tool-verbiage.d.ts +28 -0
  12. package/dist/core/utils/tool-verbiage.js +185 -0
  13. package/dist/gui/components/AppSidebar.d.ts +22 -0
  14. package/dist/gui/components/AppSidebar.js +22 -0
  15. package/dist/gui/components/ChatLayout.d.ts +5 -0
  16. package/dist/gui/components/ChatLayout.js +239 -132
  17. package/dist/gui/components/ChatView.js +42 -118
  18. package/dist/gui/components/MessageContent.js +199 -49
  19. package/dist/gui/components/SessionHistory.d.ts +10 -0
  20. package/dist/gui/components/SessionHistory.js +101 -0
  21. package/dist/gui/components/SessionHistoryItem.d.ts +11 -0
  22. package/dist/gui/components/SessionHistoryItem.js +24 -0
  23. package/dist/gui/components/Sheet.d.ts +25 -0
  24. package/dist/gui/components/Sheet.js +36 -0
  25. package/dist/gui/components/Sidebar.d.ts +65 -0
  26. package/dist/gui/components/Sidebar.js +231 -0
  27. package/dist/gui/components/SidebarToggle.d.ts +3 -0
  28. package/dist/gui/components/SidebarToggle.js +9 -0
  29. package/dist/gui/components/SubAgentDetails.d.ts +13 -6
  30. package/dist/gui/components/SubAgentDetails.js +29 -14
  31. package/dist/gui/components/ToolCallList.js +3 -3
  32. package/dist/gui/components/ToolOperation.d.ts +11 -0
  33. package/dist/gui/components/ToolOperation.js +289 -0
  34. package/dist/gui/components/WorkProgress.d.ts +20 -0
  35. package/dist/gui/components/WorkProgress.js +79 -0
  36. package/dist/gui/components/index.d.ts +8 -1
  37. package/dist/gui/components/index.js +9 -1
  38. package/dist/gui/hooks/index.d.ts +1 -0
  39. package/dist/gui/hooks/index.js +1 -0
  40. package/dist/gui/hooks/use-mobile.d.ts +1 -0
  41. package/dist/gui/hooks/use-mobile.js +15 -0
  42. package/dist/gui/index.d.ts +1 -0
  43. package/dist/gui/index.js +2 -0
  44. package/dist/gui/lib/motion.d.ts +55 -0
  45. package/dist/gui/lib/motion.js +217 -0
  46. package/dist/sdk/schemas/session.d.ts +102 -6
  47. package/dist/sdk/transports/http.js +105 -37
  48. package/dist/sdk/transports/types.d.ts +5 -0
  49. package/package.json +8 -7
  50. package/src/styles/global.css +128 -1
  51. package/dist/gui/components/InvokingGroup.d.ts +0 -9
  52. package/dist/gui/components/InvokingGroup.js +0 -16
  53. package/dist/gui/components/ToolCall.d.ts +0 -8
  54. package/dist/gui/components/ToolCall.js +0 -226
  55. package/dist/gui/components/ToolCallGroup.d.ts +0 -8
  56. package/dist/gui/components/ToolCallGroup.js +0 -29
@@ -1,8 +1,9 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { AnimatePresence, motion, useMotionValue } from "framer-motion";
2
3
  import { ArrowDown } from "lucide-react";
3
4
  import * as React from "react";
5
+ import { motionEasing } from "../lib/motion.js";
4
6
  import { cn } from "../lib/utils.js";
5
- import { ResizableHandle, ResizablePanel, ResizablePanelGroup, } from "./resizable.js";
6
7
  import { Toaster } from "./Sonner.js";
7
8
  const ChatLayoutContext = React.createContext(undefined);
8
9
  const useChatLayoutContext = () => {
@@ -16,157 +17,232 @@ const ChatLayoutRoot = React.forwardRef(({ defaultSidebarOpen = false, defaultPa
16
17
  const [sidebarOpen, setSidebarOpen] = React.useState(defaultSidebarOpen);
17
18
  const [panelSize, setPanelSize] = React.useState(defaultPanelSize);
18
19
  const [activeTab, setActiveTab] = React.useState(defaultActiveTab);
20
+ // Right panel state (derived from panelSize for backwards compatibility)
21
+ const [panelOpen, setPanelOpen] = React.useState(defaultPanelSize !== "hidden");
22
+ // Helper to toggle the right panel
23
+ const togglePanel = React.useCallback(() => {
24
+ setPanelSize((size) => (size === "hidden" ? "large" : "hidden"));
25
+ setPanelOpen((open) => !open);
26
+ }, []);
27
+ // Sync panelOpen with panelSize
28
+ React.useEffect(() => {
29
+ setPanelOpen(panelSize !== "hidden");
30
+ }, [panelSize]);
19
31
  return (_jsx(ChatLayoutContext.Provider, { value: {
20
32
  sidebarOpen,
21
33
  setSidebarOpen,
22
34
  panelSize,
23
- setPanelSize,
35
+ setPanelSize: (size) => {
36
+ setPanelSize(size);
37
+ setPanelOpen(size !== "hidden");
38
+ },
24
39
  activeTab,
25
40
  setActiveTab,
26
- }, children: _jsx("div", { ref: ref, className: cn("flex h-screen flex-row bg-background text-foreground", className), ...props, children: _jsx(ResizablePanelGroup, { direction: "horizontal", className: "flex-1", children: children }) }) }));
41
+ panelOpen,
42
+ setPanelOpen,
43
+ togglePanel,
44
+ }, children: _jsx("div", { ref: ref, "data-panel-state": panelOpen ? "expanded" : "collapsed", className: cn("flex h-screen flex-row bg-background text-foreground overflow-hidden", className), ...props, children: children }) }));
27
45
  });
28
46
  ChatLayoutRoot.displayName = "ChatLayout.Root";
29
47
  const ChatLayoutHeader = React.forwardRef(({ className, children, ...props }, ref) => {
30
48
  return (_jsx("div", { ref: ref, className: cn("relative z-10 border-b border-border bg-card shrink-0", className), ...props, children: children }));
31
49
  });
32
50
  ChatLayoutHeader.displayName = "ChatLayout.Header";
33
- const ChatLayoutMain = React.forwardRef(({ className, children, ...props }, ref) => {
34
- return (_jsx(ResizablePanel, { defaultSize: 70, minSize: 50, children: _jsx("div", { ref: ref, className: cn("flex flex-1 flex-col overflow-hidden h-full", className), ...props, children: children }) }));
51
+ const ChatLayoutMain = React.forwardRef(({ className, children }, ref) => {
52
+ return (_jsx(motion.div, { ref: ref, layout: true, transition: {
53
+ duration: 0.5,
54
+ ease: motionEasing.smooth,
55
+ }, className: cn("flex flex-1 flex-col overflow-hidden h-full min-w-0", className), children: children }));
35
56
  });
36
57
  ChatLayoutMain.displayName = "ChatLayout.Main";
37
58
  const ChatLayoutBody = React.forwardRef(({ showToaster = true, className, children, ...props }, ref) => {
38
59
  return (_jsxs("div", { ref: ref, className: cn("relative flex flex-1 flex-col overflow-hidden", className), ...props, children: [children, showToaster && _jsx(Toaster, {})] }));
39
60
  });
40
61
  ChatLayoutBody.displayName = "ChatLayout.Body";
41
- const ChatLayoutMessages = React.forwardRef(({ className, children, onScrollChange, showScrollToBottom = true, ...props }, ref) => {
62
+ const ChatLayoutMessages = React.forwardRef(({ className, children, onScrollChange, showScrollToBottom = true, initialScrollToBottom = true, ...props }, ref) => {
63
+ const [showScrollButton, setShowScrollButton] = React.useState(false);
42
64
  const scrollContainerRef = React.useRef(null);
43
- const [isAtBottom, setIsAtBottom] = React.useState(true);
44
- const isAtBottomRef = React.useRef(true);
45
- const isUserScrollingRef = React.useRef(false);
46
- const lastScrollHeightRef = React.useRef(0);
47
- const lastMutationTimeRef = React.useRef(0);
48
- const isSmoothScrollingRef = React.useRef(false); // Protect smooth scrolls from interruption
49
- // Keep ref in sync with state
50
- React.useEffect(() => {
51
- isAtBottomRef.current = isAtBottom;
52
- }, [isAtBottom]);
65
+ const wasAtBottomRef = React.useRef(true); // Track if user was at bottom before content update
66
+ const isAutoScrollingRef = React.useRef(false); // Track if we're programmatically scrolling
67
+ const hasInitialScrolledRef = React.useRef(false); // Track if initial scroll has happened
53
68
  // Merge refs
54
69
  React.useImperativeHandle(ref, () => scrollContainerRef.current);
55
- // Check if at bottom
56
- const checkIfAtBottom = React.useCallback(() => {
70
+ // Check if user is at bottom of scroll
71
+ const checkScrollPosition = React.useCallback(() => {
57
72
  const container = scrollContainerRef.current;
58
73
  if (!container)
59
- return true;
74
+ return false;
60
75
  const { scrollTop, scrollHeight, clientHeight } = container;
61
- return scrollTop + clientHeight >= scrollHeight - 100;
62
- }, []);
63
- // Scroll to bottom with protection for smooth scrolls
64
- const scrollToBottom = React.useCallback((behavior = "smooth") => {
76
+ const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
77
+ const isAtBottom = distanceFromBottom < 100; // 100px threshold
78
+ setShowScrollButton(!isAtBottom && showScrollToBottom);
79
+ onScrollChange?.(isAtBottom);
80
+ return isAtBottom;
81
+ }, [onScrollChange, showScrollToBottom]);
82
+ // Handle scroll events
83
+ const handleScroll = React.useCallback(() => {
84
+ // If this is a programmatic scroll, don't update wasAtBottomRef
85
+ if (isAutoScrollingRef.current) {
86
+ return;
87
+ }
88
+ // This is a user-initiated scroll, update the position
89
+ const isAtBottom = checkScrollPosition();
90
+ wasAtBottomRef.current = isAtBottom;
91
+ }, [checkScrollPosition]);
92
+ // Scroll to bottom function
93
+ const scrollToBottom = React.useCallback((smooth = true) => {
65
94
  const container = scrollContainerRef.current;
66
95
  if (!container)
67
96
  return;
68
- // If we're doing a smooth scroll, don't interrupt with instant
69
- if (isSmoothScrollingRef.current && behavior === "instant") {
70
- return;
71
- }
72
- if (behavior === "smooth") {
73
- isSmoothScrollingRef.current = true;
74
- // Clear the smooth scroll protection after animation completes
75
- setTimeout(() => {
76
- isSmoothScrollingRef.current = false;
77
- }, 500);
78
- }
97
+ // Mark that we're about to programmatically scroll
98
+ isAutoScrollingRef.current = true;
99
+ wasAtBottomRef.current = true; // Set immediately for instant scrolls
79
100
  container.scrollTo({
80
101
  top: container.scrollHeight,
81
- behavior,
102
+ behavior: smooth ? "smooth" : "auto",
82
103
  });
104
+ // Clear the flag after scroll completes
105
+ // For instant scrolling, clear immediately; for smooth, wait
106
+ setTimeout(() => {
107
+ isAutoScrollingRef.current = false;
108
+ }, smooth ? 300 : 0);
83
109
  }, []);
84
- // Handle user scroll events
110
+ // Auto-scroll when content changes if user was at bottom
85
111
  React.useEffect(() => {
86
112
  const container = scrollContainerRef.current;
87
113
  if (!container)
88
114
  return;
89
- let scrollTimeout;
90
- const handleScroll = () => {
91
- // Mark as user scrolling
92
- isUserScrollingRef.current = true;
93
- clearTimeout(scrollTimeout);
94
- // Update isAtBottom state
95
- const atBottom = checkIfAtBottom();
96
- setIsAtBottom(atBottom);
97
- isAtBottomRef.current = atBottom;
98
- onScrollChange?.(atBottom);
99
- // Reset user scrolling flag after scroll ends
100
- scrollTimeout = setTimeout(() => {
101
- isUserScrollingRef.current = false;
102
- }, 150);
103
- };
104
- container.addEventListener("scroll", handleScroll, { passive: true });
105
- return () => {
106
- container.removeEventListener("scroll", handleScroll);
107
- clearTimeout(scrollTimeout);
115
+ // If user was at the bottom, scroll to new content
116
+ if (wasAtBottomRef.current && !isAutoScrollingRef.current) {
117
+ // Use requestAnimationFrame to ensure DOM has updated
118
+ requestAnimationFrame(() => {
119
+ scrollToBottom(false); // Use instant scroll for streaming to avoid jarring smooth animations
120
+ });
121
+ }
122
+ // Update scroll position state (but don't change wasAtBottomRef if we're auto-scrolling)
123
+ if (!isAutoScrollingRef.current) {
124
+ checkScrollPosition();
125
+ }
126
+ }, [children, scrollToBottom, checkScrollPosition]);
127
+ // Track last scroll height to detect when content stops loading
128
+ const lastScrollHeightRef = React.useRef(0);
129
+ const scrollStableCountRef = React.useRef(0);
130
+ // Scroll to bottom on initial mount and during session loading
131
+ // Keep scrolling until content stabilizes (no more changes)
132
+ React.useEffect(() => {
133
+ if (!initialScrollToBottom)
134
+ return;
135
+ const container = scrollContainerRef.current;
136
+ if (!container)
137
+ return;
138
+ const scrollToBottomInstant = () => {
139
+ container.scrollTop = container.scrollHeight;
140
+ wasAtBottomRef.current = true;
108
141
  };
109
- }, [checkIfAtBottom, onScrollChange]);
110
- // Auto-scroll when content changes using MutationObserver + ResizeObserver
142
+ // Check if content has stabilized (scrollHeight hasn't changed)
143
+ const currentHeight = container.scrollHeight;
144
+ if (currentHeight === lastScrollHeightRef.current) {
145
+ scrollStableCountRef.current++;
146
+ }
147
+ else {
148
+ scrollStableCountRef.current = 0;
149
+ lastScrollHeightRef.current = currentHeight;
150
+ }
151
+ // If content is still loading (height changing) or we haven't scrolled yet,
152
+ // keep auto-scrolling. Stop after content is stable for a few renders.
153
+ if (scrollStableCountRef.current < 3) {
154
+ isAutoScrollingRef.current = true;
155
+ scrollToBottomInstant();
156
+ hasInitialScrolledRef.current = true;
157
+ }
158
+ else {
159
+ // Content is stable, stop auto-scrolling
160
+ isAutoScrollingRef.current = false;
161
+ }
162
+ }, [initialScrollToBottom, children]);
163
+ // Also use a timer-based approach as backup for session replay
164
+ // which may not trigger children changes
111
165
  React.useEffect(() => {
166
+ if (!initialScrollToBottom)
167
+ return;
112
168
  const container = scrollContainerRef.current;
113
169
  if (!container)
114
170
  return;
115
- const scrollIfNeeded = () => {
116
- // Only auto-scroll if user was at bottom and isn't actively scrolling
117
- if (!isAtBottomRef.current || isUserScrollingRef.current) {
171
+ // Keep scrolling to bottom for the first 2 seconds of session load
172
+ // to catch async message replay
173
+ let cancelled = false;
174
+ const scrollInterval = setInterval(() => {
175
+ if (cancelled)
118
176
  return;
177
+ if (container.scrollHeight > container.clientHeight) {
178
+ isAutoScrollingRef.current = true;
179
+ container.scrollTop = container.scrollHeight;
180
+ wasAtBottomRef.current = true;
181
+ hasInitialScrolledRef.current = true;
119
182
  }
120
- const now = Date.now();
121
- const timeSinceLastMutation = now - lastMutationTimeRef.current;
122
- const currentScrollHeight = container.scrollHeight;
123
- const heightDelta = currentScrollHeight - lastScrollHeightRef.current;
124
- lastMutationTimeRef.current = now;
125
- lastScrollHeightRef.current = currentScrollHeight;
126
- // Detect if this is likely a new message (large height increase + time gap)
127
- // vs streaming (small incremental changes)
128
- const isLikelyNewMessage = heightDelta > 80 && timeSinceLastMutation > 150;
183
+ }, 100);
184
+ // Stop after 2 seconds
185
+ const timeout = setTimeout(() => {
186
+ clearInterval(scrollInterval);
187
+ isAutoScrollingRef.current = false;
188
+ }, 2000);
189
+ return () => {
190
+ cancelled = true;
191
+ clearInterval(scrollInterval);
192
+ clearTimeout(timeout);
193
+ };
194
+ }, [initialScrollToBottom]); // Only run once on mount
195
+ // Check scroll position on mount
196
+ React.useEffect(() => {
197
+ if (!isAutoScrollingRef.current) {
198
+ const isAtBottom = checkScrollPosition();
199
+ wasAtBottomRef.current = isAtBottom;
200
+ }
201
+ }, [checkScrollPosition]);
202
+ // Detect user interaction with scroll area (wheel, touch) - IMMEDIATELY break auto-scroll
203
+ const handleUserInteraction = React.useCallback(() => {
204
+ // Immediately mark that user is interacting
205
+ isAutoScrollingRef.current = false;
206
+ // For wheel/touch events, temporarily break auto-scroll
207
+ // The actual scroll event will update wasAtBottomRef properly
208
+ // This prevents the race condition where content updates before scroll completes
209
+ const container = scrollContainerRef.current;
210
+ if (!container)
211
+ return;
212
+ // Check current position BEFORE the scroll happens
213
+ const { scrollTop, scrollHeight, clientHeight } = container;
214
+ const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
215
+ // If user is not currently at the bottom, definitely break auto-scroll
216
+ if (distanceFromBottom >= 100) {
217
+ wasAtBottomRef.current = false;
218
+ }
219
+ // If they are at bottom, the scroll event will determine if they stay there
220
+ }, []);
221
+ // Handle keyboard navigation
222
+ const handleKeyDown = React.useCallback((e) => {
223
+ // If user presses arrow keys, page up/down, home/end - they're scrolling
224
+ const scrollKeys = [
225
+ "ArrowUp",
226
+ "ArrowDown",
227
+ "PageUp",
228
+ "PageDown",
229
+ "Home",
230
+ "End",
231
+ ];
232
+ if (scrollKeys.includes(e.key)) {
233
+ isAutoScrollingRef.current = false;
234
+ // Check position on next frame after the scroll happens
129
235
  requestAnimationFrame(() => {
130
- if (isLikelyNewMessage) {
131
- scrollToBottom("smooth");
132
- }
133
- else {
134
- scrollToBottom("instant");
135
- }
136
- setIsAtBottom(true);
137
- isAtBottomRef.current = true;
236
+ const container = scrollContainerRef.current;
237
+ if (!container)
238
+ return;
239
+ const { scrollTop, scrollHeight, clientHeight } = container;
240
+ const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
241
+ wasAtBottomRef.current = distanceFromBottom < 100;
138
242
  });
139
- };
140
- // Watch for DOM changes (new messages, content updates)
141
- const mutationObserver = new MutationObserver(scrollIfNeeded);
142
- mutationObserver.observe(container, {
143
- childList: true,
144
- subtree: true,
145
- characterData: true,
146
- });
147
- // Watch for size changes (images loading, content expanding)
148
- const resizeObserver = new ResizeObserver(scrollIfNeeded);
149
- resizeObserver.observe(container);
150
- // Also observe direct children for size changes
151
- for (const child of Array.from(container.children)) {
152
- resizeObserver.observe(child);
153
243
  }
154
- // Initialize scroll height
155
- lastScrollHeightRef.current = container.scrollHeight;
156
- return () => {
157
- mutationObserver.disconnect();
158
- resizeObserver.disconnect();
159
- };
160
- }, [scrollToBottom]);
161
- // Button click handler - smooth scroll and re-engage auto-scroll
162
- const handleScrollToBottomClick = React.useCallback(() => {
163
- setIsAtBottom(true);
164
- isAtBottomRef.current = true;
165
- scrollToBottom("smooth");
166
- }, [scrollToBottom]);
167
- return (_jsxs("div", { className: "relative flex-1 overflow-hidden", children: [_jsx("div", { ref: scrollContainerRef, className: cn("h-full overflow-y-auto flex flex-col", className), tabIndex: 0, ...props, children: _jsx("div", { className: "mx-auto max-w-chat flex-1 w-full flex flex-col", children: children }) }), showScrollToBottom && (_jsx("button", { type: "button", onClick: handleScrollToBottomClick, className: cn("absolute bottom-4 left-1/2 -translate-x-1/2 z-10", "flex items-center justify-center p-2 rounded-full", "bg-card border border-border shadow-lg", "text-foreground", "hover:bg-accent hover:text-accent-foreground", "transition-all duration-200 ease-in-out", isAtBottom
168
- ? "pointer-events-none scale-0 opacity-0"
169
- : "pointer-events-auto scale-100 opacity-100"), "aria-label": "Scroll to bottom", children: _jsx(ArrowDown, { className: "size-4" }) }))] }));
244
+ }, []);
245
+ return (_jsxs("div", { className: "relative flex-1 overflow-hidden", children: [_jsx("div", { ref: scrollContainerRef, className: cn("h-full overflow-y-auto flex flex-col", className), onScroll: handleScroll, onWheel: handleUserInteraction, onTouchStart: handleUserInteraction, onKeyDown: handleKeyDown, tabIndex: 0, ...props, children: _jsx("div", { className: "mx-auto max-w-chat flex-1 w-full flex flex-col", children: children }) }), showScrollButton && (_jsx("button", { type: "button", onClick: () => scrollToBottom(true), className: cn("absolute bottom-4 left-1/2 -translate-x-1/2 z-10", "flex items-center justify-center p-2 rounded-full", "bg-card border border-border shadow-lg", "text-foreground", "hover:bg-accent hover:text-accent-foreground", "transition-all duration-200 ease-in-out", "animate-in fade-in slide-in-from-bottom-2"), "aria-label": "Scroll to bottom", children: _jsx(ArrowDown, { className: "size-4" }) }))] }));
170
246
  });
171
247
  ChatLayoutMessages.displayName = "ChatLayout.Messages";
172
248
  const ChatLayoutFooter = React.forwardRef(({ className, children, ...props }, ref) => {
@@ -180,32 +256,63 @@ const ChatLayoutSidebar = React.forwardRef(({ className, children, ...props }, r
180
256
  return (_jsx("div", { ref: ref, className: cn("border-r border-border bg-card w-64 overflow-y-auto", className), ...props, children: children }));
181
257
  });
182
258
  ChatLayoutSidebar.displayName = "ChatLayout.Sidebar";
183
- const ChatLayoutAside = React.forwardRef(({ breakpoint = "lg", className, children, ...props }, ref) => {
259
+ const ChatLayoutAside = React.forwardRef(({ breakpoint = "lg", className, children }, ref) => {
184
260
  const { panelSize } = useChatLayoutContext();
185
- const [minSizePercent, setMinSizePercent] = React.useState(28);
186
- // Convert 450px minimum to percentage based on window width
187
- React.useEffect(() => {
188
- const updateMinSize = () => {
189
- const minPixels = 450;
190
- const minPercent = (minPixels / window.innerWidth) * 100;
191
- setMinSizePercent(Math.max(minPercent, 28)); // Never less than 28% or 450px
192
- };
193
- updateMinSize();
194
- window.addEventListener("resize", updateMinSize);
195
- return () => {
196
- window.removeEventListener("resize", updateMinSize);
197
- };
198
- }, []);
261
+ // State for committed width (persisted between renders)
262
+ const [committedWidth, setCommittedWidth] = React.useState(450);
263
+ // Motion value for real-time drag updates
264
+ const widthMotionValue = useMotionValue(committedWidth);
265
+ // Track if currently dragging for visual feedback
266
+ const [isDragging, setIsDragging] = React.useState(false);
267
+ // Track drag start position and width
268
+ const dragStartX = React.useRef(0);
269
+ const dragStartWidth = React.useRef(committedWidth);
199
270
  // Hidden state - don't render
200
- if (panelSize === "hidden")
201
- return null;
202
- return (_jsxs(_Fragment, { children: [_jsx(ResizableHandle, { withHandle: true, className: "group-hover:opacity-100 opacity-0 transition-opacity" }), _jsx(ResizablePanel, { defaultSize: 30, minSize: minSizePercent, maxSize: 40, className: "group", children: _jsx("div", { ref: ref, className: cn(
203
- // Hidden by default, visible at breakpoint
204
- "hidden h-full border-l border-border bg-card overflow-y-auto transition-all duration-300",
205
- // Breakpoint visibility
206
- breakpoint === "md" && "md:block", breakpoint === "lg" && "lg:block", breakpoint === "xl" && "xl:block", breakpoint === "2xl" && "2xl:block",
207
- // Size variants - width is now controlled by ResizablePanel
208
- className), ...props, children: children }) })] }));
271
+ const isVisible = panelSize !== "hidden";
272
+ // Sync motion value when committed width changes
273
+ React.useEffect(() => {
274
+ widthMotionValue.set(committedWidth);
275
+ }, [committedWidth, widthMotionValue]);
276
+ // Handle pointer down to start drag
277
+ const handlePointerDown = React.useCallback((e) => {
278
+ e.preventDefault();
279
+ setIsDragging(true);
280
+ dragStartX.current = e.clientX;
281
+ dragStartWidth.current = committedWidth;
282
+ // Capture pointer for smooth dragging
283
+ e.target.setPointerCapture(e.pointerId);
284
+ }, [committedWidth]);
285
+ // Handle pointer move during drag
286
+ const handlePointerMove = React.useCallback((e) => {
287
+ if (!isDragging)
288
+ return;
289
+ const deltaX = e.clientX - dragStartX.current;
290
+ const newWidth = Math.max(250, Math.min(800, dragStartWidth.current - deltaX));
291
+ widthMotionValue.set(newWidth);
292
+ }, [isDragging, widthMotionValue]);
293
+ // Handle pointer up to end drag
294
+ const handlePointerUp = React.useCallback((e) => {
295
+ if (!isDragging)
296
+ return;
297
+ const deltaX = e.clientX - dragStartX.current;
298
+ const newWidth = Math.max(250, Math.min(800, dragStartWidth.current - deltaX));
299
+ setCommittedWidth(newWidth);
300
+ widthMotionValue.set(newWidth);
301
+ setIsDragging(false);
302
+ // Release pointer capture
303
+ e.target.releasePointerCapture(e.pointerId);
304
+ }, [isDragging, widthMotionValue]);
305
+ return (_jsx(AnimatePresence, { initial: false, mode: "wait", children: isVisible && (_jsxs(motion.div, { ref: ref, layout: true, initial: { width: 0, opacity: 0 }, animate: { width: committedWidth, opacity: 1 }, exit: { width: 0, opacity: 0 }, transition: {
306
+ duration: 0.5,
307
+ ease: motionEasing.smooth,
308
+ }, style: {
309
+ // Use motion value during drag for instant updates
310
+ width: isDragging ? widthMotionValue : undefined,
311
+ }, className: cn(
312
+ // Hidden by default, visible at breakpoint
313
+ "hidden h-full border-l border-border bg-card overflow-hidden relative",
314
+ // Breakpoint visibility
315
+ breakpoint === "md" && "md:block", breakpoint === "lg" && "lg:block", breakpoint === "xl" && "xl:block", breakpoint === "2xl" && "2xl:block", className), children: [_jsx("div", { onPointerDown: handlePointerDown, onPointerMove: handlePointerMove, onPointerUp: handlePointerUp, className: cn("absolute left-0 inset-y-0 w-2 -ml-1 cursor-col-resize z-10", "group flex items-center justify-center", "hover:bg-primary/5 transition-colors delay-300", isDragging && "bg-primary/10") }), _jsx("div", { className: "h-full overflow-y-auto", style: { width: committedWidth }, children: children })] }, "aside-panel")) }));
209
316
  });
210
317
  ChatLayoutAside.displayName = "ChatLayout.Aside";
211
318
  /* -------------------------------------------------------------------------------------------------