@townco/ui 0.1.83 → 0.1.96

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 (63) hide show
  1. package/dist/core/hooks/use-chat-input.js +13 -6
  2. package/dist/core/hooks/use-chat-messages.d.ts +17 -0
  3. package/dist/core/hooks/use-chat-messages.js +294 -10
  4. package/dist/core/schemas/chat.d.ts +20 -0
  5. package/dist/core/schemas/chat.js +4 -0
  6. package/dist/core/schemas/index.d.ts +1 -0
  7. package/dist/core/schemas/index.js +1 -0
  8. package/dist/core/schemas/source.d.ts +22 -0
  9. package/dist/core/schemas/source.js +45 -0
  10. package/dist/core/store/chat-store.d.ts +4 -0
  11. package/dist/core/store/chat-store.js +54 -0
  12. package/dist/gui/components/Actions.d.ts +15 -0
  13. package/dist/gui/components/Actions.js +22 -0
  14. package/dist/gui/components/ChatInput.d.ts +9 -1
  15. package/dist/gui/components/ChatInput.js +24 -6
  16. package/dist/gui/components/ChatInputCommandMenu.d.ts +1 -0
  17. package/dist/gui/components/ChatInputCommandMenu.js +22 -5
  18. package/dist/gui/components/ChatInputParameters.d.ts +13 -0
  19. package/dist/gui/components/ChatInputParameters.js +67 -0
  20. package/dist/gui/components/ChatLayout.d.ts +2 -0
  21. package/dist/gui/components/ChatLayout.js +183 -61
  22. package/dist/gui/components/ChatPanelTabContent.d.ts +7 -0
  23. package/dist/gui/components/ChatPanelTabContent.js +17 -7
  24. package/dist/gui/components/ChatView.js +105 -15
  25. package/dist/gui/components/CitationChip.d.ts +15 -0
  26. package/dist/gui/components/CitationChip.js +72 -0
  27. package/dist/gui/components/EditableUserMessage.d.ts +18 -0
  28. package/dist/gui/components/EditableUserMessage.js +109 -0
  29. package/dist/gui/components/MessageActions.d.ts +16 -0
  30. package/dist/gui/components/MessageActions.js +97 -0
  31. package/dist/gui/components/MessageContent.js +22 -7
  32. package/dist/gui/components/Response.d.ts +3 -0
  33. package/dist/gui/components/Response.js +30 -3
  34. package/dist/gui/components/Sidebar.js +1 -1
  35. package/dist/gui/components/TodoSubline.js +1 -1
  36. package/dist/gui/components/WorkProgress.js +7 -0
  37. package/dist/gui/components/index.d.ts +6 -1
  38. package/dist/gui/components/index.js +6 -1
  39. package/dist/gui/hooks/index.d.ts +1 -0
  40. package/dist/gui/hooks/index.js +1 -0
  41. package/dist/gui/hooks/use-favicon.d.ts +6 -0
  42. package/dist/gui/hooks/use-favicon.js +47 -0
  43. package/dist/gui/hooks/use-scroll-to-bottom.d.ts +14 -0
  44. package/dist/gui/hooks/use-scroll-to-bottom.js +317 -1
  45. package/dist/gui/index.d.ts +1 -1
  46. package/dist/gui/index.js +1 -1
  47. package/dist/gui/lib/motion.js +6 -6
  48. package/dist/gui/lib/remark-citations.d.ts +28 -0
  49. package/dist/gui/lib/remark-citations.js +70 -0
  50. package/dist/sdk/client/acp-client.d.ts +38 -1
  51. package/dist/sdk/client/acp-client.js +67 -3
  52. package/dist/sdk/schemas/message.d.ts +40 -0
  53. package/dist/sdk/schemas/message.js +20 -0
  54. package/dist/sdk/transports/http.d.ts +24 -1
  55. package/dist/sdk/transports/http.js +189 -1
  56. package/dist/sdk/transports/stdio.d.ts +1 -0
  57. package/dist/sdk/transports/stdio.js +39 -0
  58. package/dist/sdk/transports/types.d.ts +46 -1
  59. package/dist/sdk/transports/websocket.d.ts +1 -0
  60. package/dist/sdk/transports/websocket.js +4 -0
  61. package/dist/tui/components/ChatView.js +3 -4
  62. package/package.json +5 -3
  63. package/src/styles/global.css +71 -0
@@ -1,12 +1,12 @@
1
- import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { AnimatePresence, motion, useMotionValue } from "framer-motion";
3
- import { ArrowDown, X } from "lucide-react";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { AnimatePresence, motion } from "framer-motion";
3
+ import { ArrowDown, ArrowUp, X } from "lucide-react";
4
4
  import * as React from "react";
5
5
  import { ASIDE_WIDTH_DEFAULT, ASIDE_WIDTH_MAX, ASIDE_WIDTH_MIN, } from "../constants.js";
6
6
  import { useLockBodyScroll } from "../hooks/use-lock-body-scroll.js";
7
7
  import { useIsMobile } from "../hooks/use-mobile.js";
8
8
  import { useScrollToBottom } from "../hooks/use-scroll-to-bottom.js";
9
- import { asideContentTransition, asideContentVariants, asideMobileTransition, asideMobileVariants, motionEasing, } from "../lib/motion.js";
9
+ import { asideContentTransition, asideContentVariants, asideMobileTransition, asideMobileVariants, } from "../lib/motion.js";
10
10
  import { cn } from "../lib/utils.js";
11
11
  import { IconButton } from "./IconButton.js";
12
12
  import { Toaster } from "./Sonner.js";
@@ -28,6 +28,8 @@ const ChatLayoutRoot = React.forwardRef(({ defaultSidebarOpen = false, defaultPa
28
28
  const isTogglingRef = React.useRef(false);
29
29
  // Track if aside panel is being dragged (for Main reflow coordination)
30
30
  const [isDraggingAside, setIsDraggingAside] = React.useState(false);
31
+ // Track aside panel width (for main content padding)
32
+ const [asideWidth, setAsideWidth] = React.useState(ASIDE_WIDTH_DEFAULT);
31
33
  // Helper to toggle the right panel
32
34
  const togglePanel = React.useCallback(() => {
33
35
  // Prevent rapid toggling during animation
@@ -39,12 +41,25 @@ const ChatLayoutRoot = React.forwardRef(({ defaultSidebarOpen = false, defaultPa
39
41
  // Reset after animation duration + buffer
40
42
  setTimeout(() => {
41
43
  isTogglingRef.current = false;
42
- }, 600); // 500ms animation + 100ms buffer
44
+ }, 350); // 250ms animation + 100ms buffer
43
45
  }, []);
44
46
  // Helper to set panel open/closed (for backwards compatibility)
45
47
  const setPanelOpen = React.useCallback((open) => {
46
48
  setPanelSize(open ? "large" : "hidden");
47
49
  }, []);
50
+ // Keyboard shortcut to toggle right panel (Cmd/Ctrl+B)
51
+ React.useEffect(() => {
52
+ const handleKeyDown = (event) => {
53
+ if (event.key.toLowerCase() === "b" &&
54
+ (event.metaKey || event.ctrlKey) &&
55
+ !event.shiftKey) {
56
+ event.preventDefault();
57
+ togglePanel();
58
+ }
59
+ };
60
+ window.addEventListener("keydown", handleKeyDown);
61
+ return () => window.removeEventListener("keydown", handleKeyDown);
62
+ }, [togglePanel]);
48
63
  return (_jsx(ChatLayoutContext.Provider, { value: {
49
64
  sidebarOpen,
50
65
  setSidebarOpen,
@@ -60,6 +75,8 @@ const ChatLayoutRoot = React.forwardRef(({ defaultSidebarOpen = false, defaultPa
60
75
  togglePanel,
61
76
  isDraggingAside,
62
77
  setIsDraggingAside,
78
+ asideWidth,
79
+ setAsideWidth,
63
80
  }, 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 }) }));
64
81
  });
65
82
  ChatLayoutRoot.displayName = "ChatLayout.Root";
@@ -68,17 +85,13 @@ const ChatLayoutHeader = React.forwardRef(({ className, children, ...props }, re
68
85
  });
69
86
  ChatLayoutHeader.displayName = "ChatLayout.Header";
70
87
  const ChatLayoutMain = React.forwardRef(({ className, children }, ref) => {
71
- const { isDraggingAside } = useChatLayoutContext();
72
- return (_jsx(motion.div, { ref: ref, layout: true, transition: isDraggingAside
73
- ? {
74
- // Instant reflow during aside drag
75
- duration: 0,
76
- }
77
- : {
78
- // Smooth animation for panel open/close
79
- duration: 0.5,
80
- ease: motionEasing.smooth,
81
- }, className: cn("flex flex-1 flex-col overflow-hidden h-full min-w-0", className), children: children }));
88
+ const { panelOpen, isDraggingAside, asideWidth } = useChatLayoutContext();
89
+ return (_jsx("div", { ref: ref, className: cn("flex flex-1 flex-col overflow-hidden h-full min-w-0",
90
+ // Use CSS transition for smooth reflow (like left sidebar does)
91
+ !isDraggingAside && "transition-[padding] duration-250", className), style: {
92
+ // Add padding when panel is open to make room for the fixed aside
93
+ paddingRight: panelOpen ? asideWidth : 0,
94
+ }, children: children }));
82
95
  });
83
96
  ChatLayoutMain.displayName = "ChatLayout.Main";
84
97
  const ChatLayoutBody = React.forwardRef(({ showToaster = true, className, children, ...props }, ref) => {
@@ -86,7 +99,86 @@ const ChatLayoutBody = React.forwardRef(({ showToaster = true, className, childr
86
99
  });
87
100
  ChatLayoutBody.displayName = "ChatLayout.Body";
88
101
  const ChatLayoutMessages = React.forwardRef(({ className, children, onScrollChange, showScrollToBottom = true, initialScrollToBottom = true, ...props }, ref) => {
89
- const { containerRef, endRef, isAtBottom, scrollToBottom } = useScrollToBottom();
102
+ const { containerRef, endRef, isAtBottom, scrollToBottom, isUserMessageAboveFold, hasMoreUserMessagesBelow, userMessagesAboveCount, userMessagesBelowCount, scrollToPreviousUserMessage, scrollToNextUserMessage, scrollToUserMessageByIndex, getUserMessagePreviews, } = useScrollToBottom();
103
+ // State for hover menu
104
+ const [showMessageMenu, setShowMessageMenu] = React.useState(false);
105
+ const [messagePreviews, setMessagePreviews] = React.useState([]);
106
+ const hoverTimeoutRef = React.useRef(null);
107
+ // State for navigator pill visibility (only show when scrolling or cursor in top region)
108
+ const [isScrolling, setIsScrolling] = React.useState(false);
109
+ const [isCursorInTopRegion, setIsCursorInTopRegion] = React.useState(false);
110
+ const [isHoveringNav, setIsHoveringNav] = React.useState(false);
111
+ const scrollTimeoutRef = React.useRef(null);
112
+ // Track scrolling activity
113
+ React.useEffect(() => {
114
+ const container = containerRef.current;
115
+ if (!container)
116
+ return;
117
+ const handleScroll = () => {
118
+ setIsScrolling(true);
119
+ // Clear existing timeout
120
+ if (scrollTimeoutRef.current) {
121
+ clearTimeout(scrollTimeoutRef.current);
122
+ }
123
+ // Hide after 1.5 seconds of no scrolling
124
+ scrollTimeoutRef.current = setTimeout(() => {
125
+ setIsScrolling(false);
126
+ }, 1500);
127
+ };
128
+ container.addEventListener("scroll", handleScroll, { passive: true });
129
+ return () => {
130
+ container.removeEventListener("scroll", handleScroll);
131
+ if (scrollTimeoutRef.current) {
132
+ clearTimeout(scrollTimeoutRef.current);
133
+ }
134
+ };
135
+ }, [containerRef]);
136
+ // Track cursor position (show pill when cursor is in top 100px of the container)
137
+ React.useEffect(() => {
138
+ const container = containerRef.current;
139
+ if (!container)
140
+ return;
141
+ const handleMouseMove = (e) => {
142
+ const containerRect = container.getBoundingClientRect();
143
+ const relativeY = e.clientY - containerRect.top;
144
+ const isInTopRegion = relativeY >= 0 && relativeY <= 100;
145
+ setIsCursorInTopRegion(isInTopRegion);
146
+ };
147
+ const handleMouseLeave = () => {
148
+ setIsCursorInTopRegion(false);
149
+ };
150
+ container.addEventListener("mousemove", handleMouseMove);
151
+ container.addEventListener("mouseleave", handleMouseLeave);
152
+ return () => {
153
+ container.removeEventListener("mousemove", handleMouseMove);
154
+ container.removeEventListener("mouseleave", handleMouseLeave);
155
+ };
156
+ }, [containerRef]);
157
+ // Show navigator pill only when scrolling, cursor in top region, or hovering over the nav
158
+ const showNavigatorPill = isUserMessageAboveFold &&
159
+ (isScrolling || isCursorInTopRegion || isHoveringNav);
160
+ // Handle mouse enter on navigation component
161
+ const handleNavMouseEnter = React.useCallback(() => {
162
+ if (hoverTimeoutRef.current) {
163
+ clearTimeout(hoverTimeoutRef.current);
164
+ }
165
+ setIsHoveringNav(true);
166
+ const previews = getUserMessagePreviews();
167
+ setMessagePreviews(previews);
168
+ setShowMessageMenu(true);
169
+ }, [getUserMessagePreviews]);
170
+ // Handle mouse leave with delay
171
+ const handleNavMouseLeave = React.useCallback(() => {
172
+ hoverTimeoutRef.current = setTimeout(() => {
173
+ setShowMessageMenu(false);
174
+ setIsHoveringNav(false);
175
+ }, 150);
176
+ }, []);
177
+ // Handle click on a message in the menu
178
+ const handleMessageClick = React.useCallback((index) => {
179
+ scrollToUserMessageByIndex(index, "smooth");
180
+ setShowMessageMenu(false);
181
+ }, [scrollToUserMessageByIndex]);
90
182
  const hasInitialScrolledRef = React.useRef(false);
91
183
  // Merge refs
92
184
  React.useImperativeHandle(ref, () => containerRef.current);
@@ -117,7 +209,17 @@ const ChatLayoutMessages = React.forwardRef(({ className, children, onScrollChan
117
209
  return (_jsxs("div", { className: "relative flex-1 overflow-hidden", children: [
118
210
  _jsxs("div", { ref: containerRef, className: cn("h-full overflow-y-auto flex flex-col", className), ...props, children: [
119
211
  _jsx("div", { className: "mx-auto max-w-chat flex-1 w-full flex flex-col", children: children }), _jsx("div", { ref: endRef, className: "shrink-0" })
120
- ] }), showScrollButton && (_jsx("button", { type: "button", onClick: () => scrollToBottom("smooth"), 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" }) }))] }));
212
+ ] }), showNavigatorPill && (_jsxs("nav", { className: "absolute top-4 left-1/2 -translate-x-1/2 z-10", onMouseEnter: handleNavMouseEnter, onMouseLeave: handleNavMouseLeave, "aria-label": "User message navigation", children: [
213
+ _jsxs("div", { className: cn("flex items-center gap-0.5 rounded-full", "bg-card border border-border shadow-lg", "animate-in fade-in slide-in-from-top-2"), children: [userMessagesAboveCount > 0 && (_jsxs("button", { type: "button", onClick: () => scrollToPreviousUserMessage("smooth"), className: cn("flex items-center gap-1 py-2 pl-2.5 pr-2", hasMoreUserMessagesBelow
214
+ ? "rounded-l-full"
215
+ : "rounded-full pr-2.5", "text-foreground", "hover:bg-accent hover:text-accent-foreground", "transition-colors duration-150"), "aria-label": `${userMessagesAboveCount} previous user message${userMessagesAboveCount !== 1 ? "s" : ""}`, children: [
216
+ _jsx(ArrowUp, { className: "size-4" }), _jsx("span", { className: "text-xs font-medium min-w-[1ch]", children: userMessagesAboveCount })
217
+ ] })), userMessagesAboveCount > 0 && hasMoreUserMessagesBelow && (_jsx("div", { className: "w-px h-4 bg-border" })), hasMoreUserMessagesBelow && (_jsxs("button", { type: "button", onClick: () => scrollToNextUserMessage("smooth"), className: cn("flex items-center gap-1 py-2 pl-2 pr-2.5", userMessagesAboveCount > 0
218
+ ? "rounded-r-full"
219
+ : "rounded-full pl-2.5", "text-foreground", "hover:bg-accent hover:text-accent-foreground", "transition-colors duration-150"), "aria-label": `${userMessagesBelowCount} next user message${userMessagesBelowCount !== 1 ? "s" : ""}`, children: [
220
+ _jsx("span", { className: "text-xs font-medium min-w-[1ch]", children: userMessagesBelowCount }), _jsx(ArrowDown, { className: "size-4" })
221
+ ] }))] }), showMessageMenu && messagePreviews.length > 0 && (_jsx("div", { className: cn("absolute top-full left-1/2 -translate-x-1/2 mt-2", "w-72 max-h-64 overflow-y-auto", "bg-card border border-border rounded-lg shadow-xl", "animate-in fade-in slide-in-from-top-1 duration-150"), children: _jsx("div", { className: "p-1", children: messagePreviews.map(({ index, preview }) => (_jsxs("button", { type: "button", onClick: () => handleMessageClick(index), className: cn("w-full text-left px-3 py-2 rounded-md", "text-sm text-foreground", "hover:bg-accent hover:text-accent-foreground", "transition-colors duration-100", "truncate"), children: [
222
+ _jsxs("span", { className: "text-muted-foreground mr-2 text-xs", children: [index + 1, "."] }), preview] }, index))) }) }))] })), showScrollButton && (_jsx("button", { type: "button", onClick: () => scrollToBottom("smooth"), 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" }) }))] }));
121
223
  });
122
224
  ChatLayoutMessages.displayName = "ChatLayout.Messages";
123
225
  const ChatLayoutFooter = React.forwardRef(({ className, children, ...props }, ref) => {
@@ -132,11 +234,11 @@ const ChatLayoutSidebar = React.forwardRef(({ className, children, ...props }, r
132
234
  });
133
235
  ChatLayoutSidebar.displayName = "ChatLayout.Sidebar";
134
236
  const ChatLayoutAside = React.forwardRef(({ breakpoint = "md", onClose, className, children }, ref) => {
135
- const { panelSize, togglePanel, setIsDraggingAside } = useChatLayoutContext();
237
+ const { panelSize, togglePanel, setIsDraggingAside, setAsideWidth } = useChatLayoutContext();
136
238
  // State for committed width (persisted between renders)
137
239
  const [committedWidth, setCommittedWidth] = React.useState(ASIDE_WIDTH_DEFAULT);
138
- // Motion value for real-time drag updates
139
- const widthMotionValue = useMotionValue(committedWidth);
240
+ // State for current drag width (real-time updates during drag)
241
+ const [dragWidth, setDragWidth] = React.useState(committedWidth);
140
242
  // Track if currently dragging for visual feedback
141
243
  const [isDragging, setIsDragging] = React.useState(false);
142
244
  // Track drag start position and width
@@ -144,14 +246,34 @@ const ChatLayoutAside = React.forwardRef(({ breakpoint = "md", onClose, classNam
144
246
  const dragStartWidth = React.useRef(committedWidth);
145
247
  // Hidden state - don't render
146
248
  const isVisible = panelSize !== "hidden";
249
+ // Track if panel just mounted to animate from 0 width
250
+ const [hasAnimatedIn, setHasAnimatedIn] = React.useState(false);
251
+ // Reset animation state when panel becomes hidden
252
+ React.useEffect(() => {
253
+ if (!isVisible) {
254
+ setHasAnimatedIn(false);
255
+ return;
256
+ }
257
+ // Small delay to ensure the element is in the DOM before transitioning
258
+ const timer = requestAnimationFrame(() => {
259
+ setHasAnimatedIn(true);
260
+ });
261
+ return () => cancelAnimationFrame(timer);
262
+ }, [isVisible]);
147
263
  // Detect mobile viewport (< 768px)
148
264
  const isMobile = useIsMobile();
149
265
  // Lock body scroll when panel is open on mobile
150
266
  useLockBodyScroll(isMobile && isVisible);
151
- // Sync motion value when committed width changes
267
+ // Sync drag width when committed width changes
152
268
  React.useEffect(() => {
153
- widthMotionValue.set(committedWidth);
154
- }, [committedWidth, widthMotionValue]);
269
+ setDragWidth(committedWidth);
270
+ }, [committedWidth]);
271
+ // Sync aside width to context (for main content padding)
272
+ React.useEffect(() => {
273
+ if (isVisible) {
274
+ setAsideWidth(isDragging ? dragWidth : committedWidth);
275
+ }
276
+ }, [isVisible, isDragging, dragWidth, committedWidth, setAsideWidth]);
155
277
  // Handle pointer down to start drag
156
278
  const handlePointerDown = React.useCallback((e) => {
157
279
  e.preventDefault();
@@ -168,8 +290,8 @@ const ChatLayoutAside = React.forwardRef(({ breakpoint = "md", onClose, classNam
168
290
  return;
169
291
  const deltaX = e.clientX - dragStartX.current;
170
292
  const newWidth = Math.max(ASIDE_WIDTH_MIN, Math.min(ASIDE_WIDTH_MAX, dragStartWidth.current - deltaX));
171
- widthMotionValue.set(newWidth);
172
- }, [isDragging, widthMotionValue]);
293
+ setDragWidth(newWidth);
294
+ }, [isDragging]);
173
295
  // Handle pointer up to end drag
174
296
  const handlePointerUp = React.useCallback((e) => {
175
297
  if (!isDragging)
@@ -177,45 +299,45 @@ const ChatLayoutAside = React.forwardRef(({ breakpoint = "md", onClose, classNam
177
299
  const deltaX = e.clientX - dragStartX.current;
178
300
  const newWidth = Math.max(ASIDE_WIDTH_MIN, Math.min(ASIDE_WIDTH_MAX, dragStartWidth.current - deltaX));
179
301
  setCommittedWidth(newWidth);
180
- widthMotionValue.set(newWidth);
302
+ setDragWidth(newWidth);
181
303
  setIsDragging(false);
182
304
  setIsDraggingAside(false); // Notify context drag ended
183
305
  // Release pointer capture
184
306
  e.target.releasePointerCapture(e.pointerId);
185
- }, [isDragging, widthMotionValue, setIsDraggingAside]);
186
- return (_jsx(AnimatePresence, { initial: false, mode: "wait", children: isVisible && (_jsxs(_Fragment, { children: [isMobile && (_jsx(motion.aside, { variants: asideMobileVariants, initial: "initial", animate: "animate", exit: "exit", transition: asideMobileTransition, className: cn("fixed inset-0 z-50 bg-card", "flex flex-col",
187
- // Only visible on mobile (< 768px)
188
- "block md:hidden", className), "data-aside": "aside", "data-mobile": "true", children: _jsxs(motion.div, { className: "flex flex-col h-full", variants: asideContentVariants, initial: "initial", animate: "animate", transition: asideContentTransition, children: [
189
- _jsx("div", { className: "flex justify-end px-4 pt-3 shrink-0", children: _jsx(IconButton, { onClick: () => {
190
- onClose?.();
191
- togglePanel();
192
- }, "aria-label": "Close panel", children: _jsx(X, { className: "size-4" }) }) }), _jsx("div", { className: "flex-1 overflow-y-auto", children: children })
193
- ] }) }, "aside-mobile")), !isMobile && (_jsx(motion.aside, { ref: ref, initial: { width: 0, opacity: 0 }, animate: {
194
- width: committedWidth,
195
- opacity: 1,
196
- }, exit: { width: 0, opacity: 0 }, style: {
197
- // Use motion value during drag for instant updates
198
- width: isDragging ? widthMotionValue : undefined,
199
- }, transition: isDragging
200
- ? {
201
- // Instant updates during drag (no animation)
202
- duration: 0,
203
- }
204
- : {
205
- // Smooth animation for open/close
206
- duration: 0.5,
207
- ease: motionEasing.smooth,
208
- }, className: cn(
209
- // Hidden by default, visible at breakpoint
210
- "hidden h-full border-l border-border bg-card overflow-hidden relative",
211
- // Breakpoint visibility
212
- breakpoint === "sm" && "sm:block", breakpoint === "md" && "md:block", breakpoint === "lg" && "lg:block", breakpoint === "xl" && "xl:block", breakpoint === "2xl" && "2xl:block", className), "data-aside": "aside", children: _jsxs(motion.div, { className: "flex flex-col h-full", variants: asideContentVariants, initial: "initial", animate: "animate", transition: asideContentTransition, children: [
213
- _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: cn("h-full overflow-y-auto", isDragging ? "w-full" : undefined), style: !isDragging
214
- ? {
215
- width: `${committedWidth}px`,
216
- }
217
- : undefined, children: children })
218
- ] }) }, "aside-desktop"))] })) }));
307
+ }, [isDragging, setIsDraggingAside]);
308
+ // Determine which component to show based on visibility and device
309
+ const showMobile = isVisible && isMobile;
310
+ const showDesktop = isVisible && !isMobile;
311
+ return (_jsxs(AnimatePresence, { initial: false, children: [showMobile && (_jsx(motion.aside, { variants: asideMobileVariants, initial: "initial", animate: "animate", exit: "exit", transition: asideMobileTransition, className: cn("fixed inset-0 z-50 bg-card", "flex flex-col",
312
+ // Only visible on mobile (< 768px)
313
+ "block md:hidden", className), "data-aside": "aside", "data-mobile": "true", children: _jsxs(motion.div, { className: "flex flex-col h-full", variants: asideContentVariants, initial: "initial", animate: "animate", transition: asideContentTransition, children: [
314
+ _jsx("div", { className: "flex justify-end px-4 pt-3 shrink-0", children: _jsx(IconButton, { onClick: () => {
315
+ onClose?.();
316
+ togglePanel();
317
+ }, "aria-label": "Close panel", children: _jsx(X, { className: "size-4" }) }) }), _jsx("div", { className: "flex-1 overflow-y-auto", children: children })
318
+ ] }) }, "aside-mobile")), showDesktop && (_jsx("aside", { ref: ref, style: {
319
+ // Animate from 0 on mount, then to committedWidth
320
+ width: isDragging
321
+ ? dragWidth
322
+ : hasAnimatedIn
323
+ ? committedWidth
324
+ : 0,
325
+ transition: isDragging
326
+ ? "none"
327
+ : "width 0.25s cubic-bezier(0.25, 0.1, 0.25, 1)",
328
+ }, className: cn(
329
+ // Fixed position on the right (like left sidebar)
330
+ "fixed inset-y-0 right-0 z-40",
331
+ // Hidden by default, visible at breakpoint
332
+ "hidden h-full border-l border-border bg-card overflow-hidden",
333
+ // Breakpoint visibility
334
+ breakpoint === "sm" && "sm:block", breakpoint === "md" && "md:block", breakpoint === "lg" && "lg:block", breakpoint === "xl" && "xl:block", breakpoint === "2xl" && "2xl:block", className), "data-aside": "aside", children: _jsxs("div", { className: "flex flex-col h-full", children: [
335
+ _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: cn("h-full overflow-y-auto", isDragging ? "w-full" : undefined), style: !isDragging
336
+ ? {
337
+ width: `${committedWidth}px`,
338
+ }
339
+ : undefined, children: children })
340
+ ] }) }, "aside-desktop"))] }));
219
341
  });
220
342
  ChatLayoutAside.displayName = "ChatLayout.Aside";
221
343
  /* -------------------------------------------------------------------------------------------------
@@ -18,6 +18,8 @@ export interface FilesTabContentProps extends React.HTMLAttributes<HTMLDivElemen
18
18
  export declare const FilesTabContent: React.ForwardRefExoticComponent<FilesTabContentProps & React.RefAttributes<HTMLDivElement>>;
19
19
  export interface SourcesTabContentProps extends React.HTMLAttributes<HTMLDivElement> {
20
20
  sources?: SourceItem[];
21
+ /** ID of the source to highlight (for scroll-to behavior) */
22
+ highlightedSourceId?: string;
21
23
  }
22
24
  export declare const SourcesTabContent: React.ForwardRefExoticComponent<SourcesTabContentProps & React.RefAttributes<HTMLDivElement>>;
23
25
  export interface DatabaseTabContentProps extends React.HTMLAttributes<HTMLDivElement> {
@@ -30,6 +32,11 @@ export interface SettingsTabContentProps extends React.HTMLAttributes<HTMLDivEle
30
32
  description?: string;
31
33
  prettyName?: string;
32
34
  icon?: string;
35
+ children?: Array<{
36
+ name: string;
37
+ prettyName: string;
38
+ icon?: string;
39
+ }>;
33
40
  }>;
34
41
  mcps?: Array<{
35
42
  name: string;
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Globe } from "lucide-react";
2
3
  import * as React from "react";
3
- import { mockSourceData } from "../data/mockSourceData.js";
4
4
  import { cn } from "../lib/utils.js";
5
5
  import { FileSystemView } from "./FileSystemView.js";
6
6
  import { SourceListItem } from "./SourceListItem.js";
@@ -31,10 +31,14 @@ export const FilesTabContent = React.forwardRef(({ files = [], provider, onFileS
31
31
  }, onDownload: handleDownload, onRename: handleRename, onDelete: handleDelete, className: "h-full" }) }));
32
32
  });
33
33
  FilesTabContent.displayName = "FilesTabContent";
34
- export const SourcesTabContent = React.forwardRef(({ sources, className, ...props }, ref) => {
35
- // Use mock data if no sources provided or if empty array
36
- const displaySources = sources && sources.length > 0 ? sources : mockSourceData;
37
- return (_jsx("div", { ref: ref, className: cn("space-y-2", className), ...props, children: displaySources.map((source) => (_jsx(SourceListItem, { source: source }, source.id))) }));
34
+ export const SourcesTabContent = React.forwardRef(({ sources = [], highlightedSourceId, className, ...props }, ref) => {
35
+ // Show empty state if no sources
36
+ if (sources.length === 0) {
37
+ return (_jsxs("div", { ref: ref, className: cn("flex flex-col items-center justify-center h-full text-center py-8", className), ...props, children: [
38
+ _jsx(Globe, { className: "size-8 text-muted-foreground/50 mb-3" }), _jsx("p", { className: "text-paragraph-sm text-muted-foreground", children: "No sources yet" }), _jsx("p", { className: "text-caption text-muted-foreground/70 mt-1", children: "Sources from web searches and fetches will appear here" })
39
+ ] }));
40
+ }
41
+ return (_jsx("div", { ref: ref, className: cn("space-y-2", className), ...props, children: sources.map((source) => (_jsx(SourceListItem, { source: source, isSelected: source.id === highlightedSourceId }, source.id))) }));
38
42
  });
39
43
  SourcesTabContent.displayName = "SourcesTabContent";
40
44
  export const DatabaseTabContent = React.forwardRef(({ data, className, ...props }, ref) => {
@@ -46,11 +50,17 @@ export const DatabaseTabContent = React.forwardRef(({ data, className, ...props
46
50
  ] }));
47
51
  });
48
52
  DatabaseTabContent.displayName = "DatabaseTabContent";
53
+ const ToolItem = ({ tool }) => {
54
+ const hasChildren = tool.children && tool.children.length > 0;
55
+ return (_jsxs("div", { className: "p-3 border border-border rounded-lg bg-muted/30", children: [
56
+ _jsx("div", { className: "font-medium text-paragraph-sm", children: tool.prettyName || tool.name }), tool.description && !hasChildren && (_jsx("div", { className: "text-caption text-muted-foreground mt-1 line-clamp-1", children: tool.description })), hasChildren && (_jsxs("div", { className: "text-caption text-muted-foreground mt-1", children: ["Built-in tools:", " ", tool.children
57
+ ?.map((child) => child.prettyName || child.name)
58
+ .join(", ")] }))] }));
59
+ };
49
60
  export const SettingsTabContent = React.forwardRef(({ tools = [], mcps = [], subagents = [], className, ...props }, ref) => {
50
61
  return (_jsxs("div", { ref: ref, className: cn("space-y-6", className), ...props, children: [
51
62
  _jsxs("div", { className: "space-y-3 p-2", children: [
52
- _jsx("h3", { className: "font-semibold text-subheading", children: "Tools" }), tools.length > 0 ? (_jsx("div", { className: "space-y-2", children: tools.map((tool) => (_jsxs("div", { className: "p-3 border border-border rounded-lg bg-muted/30", children: [
53
- _jsx("div", { className: "font-medium text-paragraph-sm", children: tool.prettyName || tool.name }), tool.description && (_jsx("div", { className: "text-caption text-muted-foreground mt-1 line-clamp-1", children: tool.description }))] }, tool.name))) })) : (_jsx("p", { className: "text-paragraph-sm text-muted-foreground", children: "No tools available" }))] }), _jsxs("div", { className: "space-y-3 p-2", children: [
63
+ _jsx("h3", { className: "font-semibold text-subheading", children: "Tools" }), tools.length > 0 ? (_jsx("div", { className: "space-y-2", children: tools.map((tool) => (_jsx(ToolItem, { tool: tool }, tool.name))) })) : (_jsx("p", { className: "text-paragraph-sm text-muted-foreground", children: "No tools available" }))] }), _jsxs("div", { className: "space-y-3 p-2", children: [
54
64
  _jsx("h3", { className: "font-semibold text-subheading", children: "MCP Servers" }), mcps.length > 0 ? (_jsx("div", { className: "space-y-2", children: mcps.map((mcp) => (_jsxs("div", { className: "p-3 border border-border rounded-lg bg-muted/30", children: [
55
65
  _jsx("div", { className: "font-medium text-paragraph-sm", children: mcp.name }), _jsxs("div", { className: "text-caption text-muted-foreground mt-1", children: ["Transport: ", mcp.transport] })
56
66
  ] }, mcp.name))) })) : (_jsx("p", { className: "text-paragraph-sm text-muted-foreground", children: "No MCP servers connected" }))] }), _jsxs("div", { className: "space-y-3 p-2", children: [