@townco/ui 0.1.68 → 0.1.70

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 (51) hide show
  1. package/dist/core/hooks/use-chat-messages.d.ts +6 -1
  2. package/dist/core/hooks/use-chat-session.d.ts +1 -1
  3. package/dist/core/hooks/use-tool-calls.d.ts +6 -1
  4. package/dist/core/schemas/chat.d.ts +10 -0
  5. package/dist/core/schemas/tool-call.d.ts +13 -8
  6. package/dist/core/schemas/tool-call.js +8 -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 +130 -138
  17. package/dist/gui/components/ChatView.js +42 -118
  18. package/dist/gui/components/HookNotification.d.ts +9 -0
  19. package/dist/gui/components/HookNotification.js +50 -0
  20. package/dist/gui/components/MessageContent.js +151 -39
  21. package/dist/gui/components/SessionHistory.d.ts +10 -0
  22. package/dist/gui/components/SessionHistory.js +101 -0
  23. package/dist/gui/components/SessionHistoryItem.d.ts +11 -0
  24. package/dist/gui/components/SessionHistoryItem.js +24 -0
  25. package/dist/gui/components/Sheet.d.ts +25 -0
  26. package/dist/gui/components/Sheet.js +36 -0
  27. package/dist/gui/components/Sidebar.d.ts +65 -0
  28. package/dist/gui/components/Sidebar.js +231 -0
  29. package/dist/gui/components/SidebarToggle.d.ts +3 -0
  30. package/dist/gui/components/SidebarToggle.js +9 -0
  31. package/dist/gui/components/SubAgentDetails.js +13 -2
  32. package/dist/gui/components/ToolCallList.js +3 -3
  33. package/dist/gui/components/ToolOperation.d.ts +11 -0
  34. package/dist/gui/components/ToolOperation.js +329 -0
  35. package/dist/gui/components/WorkProgress.d.ts +20 -0
  36. package/dist/gui/components/WorkProgress.js +79 -0
  37. package/dist/gui/components/index.d.ts +8 -1
  38. package/dist/gui/components/index.js +9 -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-mobile.d.ts +1 -0
  42. package/dist/gui/hooks/use-mobile.js +15 -0
  43. package/dist/gui/index.d.ts +1 -0
  44. package/dist/gui/index.js +2 -0
  45. package/dist/gui/lib/motion.d.ts +55 -0
  46. package/dist/gui/lib/motion.js +217 -0
  47. package/dist/sdk/schemas/message.d.ts +2 -2
  48. package/dist/sdk/schemas/session.d.ts +5 -0
  49. package/dist/sdk/transports/types.d.ts +5 -0
  50. package/package.json +8 -7
  51. package/src/styles/global.css +128 -1
@@ -0,0 +1,22 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Plus } from "lucide-react";
3
+ import { cn } from "../lib/utils.js";
4
+ import { IconButton } from "./IconButton.js";
5
+ import { SessionHistory } from "./SessionHistory.js";
6
+ import { Sidebar, SidebarContent, SidebarFooter, SidebarHeader, useSidebar, } from "./Sidebar.js";
7
+ export function AppSidebar({ client, currentSessionId, onSessionSelect, onNewSession, onRenameSession, onArchiveSession, onDeleteSession, footer, className, }) {
8
+ const { setOpenMobile } = useSidebar();
9
+ const handleNewSession = () => {
10
+ if (onNewSession) {
11
+ onNewSession();
12
+ }
13
+ else {
14
+ // Default behavior: clear session from URL and reload
15
+ const url = new URL(window.location.href);
16
+ url.searchParams.delete("session");
17
+ window.location.href = url.toString();
18
+ }
19
+ setOpenMobile(false);
20
+ };
21
+ return (_jsxs(Sidebar, { className: cn("group-data-[side=left]:border-r-0", className), children: [_jsx(SidebarHeader, { className: "h-16 py-5 pl-6 pr-4", children: _jsxs("div", { className: "flex flex-row items-center justify-between w-full", children: [_jsx("span", { className: "font-semibold text-lg", children: "Sessions" }), _jsx(IconButton, { onClick: handleNewSession, "aria-label": "New Chat", children: _jsx(Plus, { className: "size-4 text-muted-foreground" }) })] }) }), _jsx(SidebarContent, { children: _jsx(SessionHistory, { client: client, currentSessionId: currentSessionId, onSessionSelect: onSessionSelect, onRenameSession: onRenameSession, onArchiveSession: onArchiveSession, onDeleteSession: onDeleteSession }) }), footer && _jsx(SidebarFooter, { children: footer })] }));
22
+ }
@@ -8,6 +8,9 @@ interface ChatLayoutContextValue {
8
8
  setPanelSize: (size: PanelSize) => void;
9
9
  activeTab: PanelTabType;
10
10
  setActiveTab: (tab: PanelTabType) => void;
11
+ panelOpen: boolean;
12
+ setPanelOpen: (open: boolean) => void;
13
+ togglePanel: () => void;
11
14
  }
12
15
  declare const ChatLayoutContext: React.Context<ChatLayoutContextValue | undefined>;
13
16
  declare const useChatLayoutContext: () => ChatLayoutContextValue;
@@ -36,6 +39,8 @@ export interface ChatLayoutMessagesProps extends React.HTMLAttributes<HTMLDivEle
36
39
  onScrollChange?: (isAtBottom: boolean) => void;
37
40
  /** Whether to show scroll to bottom button */
38
41
  showScrollToBottom?: boolean;
42
+ /** Whether to scroll to bottom on initial mount (default: true) */
43
+ initialScrollToBottom?: boolean;
39
44
  }
40
45
  declare const ChatLayoutMessages: React.ForwardRefExoticComponent<ChatLayoutMessagesProps & React.RefAttributes<HTMLDivElement>>;
41
46
  export interface ChatLayoutFooterProps extends React.HTMLAttributes<HTMLDivElement> {
@@ -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,117 @@ 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 hasInitialScrolledRef = React.useRef(false); // Track if initial scroll has happened
53
66
  // Merge refs
54
67
  React.useImperativeHandle(ref, () => scrollContainerRef.current);
55
- // Check if at bottom
56
- const checkIfAtBottom = React.useCallback(() => {
68
+ // Check if user is at bottom of scroll
69
+ const checkScrollPosition = React.useCallback(() => {
57
70
  const container = scrollContainerRef.current;
58
71
  if (!container)
59
- return true;
72
+ return false;
60
73
  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") => {
74
+ const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
75
+ const isAtBottom = distanceFromBottom < 100; // 100px threshold
76
+ setShowScrollButton(!isAtBottom && showScrollToBottom);
77
+ onScrollChange?.(isAtBottom);
78
+ return isAtBottom;
79
+ }, [onScrollChange, showScrollToBottom]);
80
+ // Handle scroll events - update button visibility
81
+ const handleScroll = React.useCallback(() => {
82
+ checkScrollPosition();
83
+ }, [checkScrollPosition]);
84
+ // Scroll to bottom function (for button click)
85
+ const scrollToBottom = React.useCallback((smooth = true) => {
65
86
  const container = scrollContainerRef.current;
66
87
  if (!container)
67
88
  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
- }
79
89
  container.scrollTo({
80
90
  top: container.scrollHeight,
81
- behavior,
91
+ behavior: smooth ? "smooth" : "auto",
82
92
  });
83
93
  }, []);
84
- // Handle user scroll events
94
+ // Auto-scroll when content changes ONLY if user is currently at the bottom
85
95
  React.useEffect(() => {
86
96
  const container = scrollContainerRef.current;
87
97
  if (!container)
88
98
  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);
108
- };
109
- }, [checkIfAtBottom, onScrollChange]);
110
- // Auto-scroll when content changes using MutationObserver + ResizeObserver
99
+ // Check if user is CURRENTLY at the bottom (not just "was" at bottom)
100
+ const { scrollTop, scrollHeight, clientHeight } = container;
101
+ const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
102
+ const isCurrentlyAtBottom = distanceFromBottom < 100;
103
+ // Only auto-scroll if user is at the bottom right now
104
+ if (isCurrentlyAtBottom) {
105
+ requestAnimationFrame(() => {
106
+ container.scrollTop = container.scrollHeight;
107
+ });
108
+ }
109
+ // Update the scroll button visibility
110
+ setShowScrollButton(!isCurrentlyAtBottom && showScrollToBottom);
111
+ }, [children, showScrollToBottom]);
112
+ // Scroll to bottom on initial mount only (for session replay)
111
113
  React.useEffect(() => {
114
+ if (!initialScrollToBottom)
115
+ return undefined;
112
116
  const container = scrollContainerRef.current;
113
117
  if (!container)
114
- return;
115
- const scrollIfNeeded = () => {
116
- // Only auto-scroll if user was at bottom and isn't actively scrolling
117
- if (!isAtBottomRef.current || isUserScrollingRef.current) {
118
- return;
119
- }
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;
129
- requestAnimationFrame(() => {
130
- if (isLikelyNewMessage) {
131
- scrollToBottom("smooth");
132
- }
133
- else {
134
- scrollToBottom("instant");
135
- }
136
- setIsAtBottom(true);
137
- isAtBottomRef.current = true;
138
- });
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);
118
+ return undefined;
119
+ // Only scroll on initial mount, not on subsequent renders
120
+ if (!hasInitialScrolledRef.current) {
121
+ // Use a small delay to let initial content render
122
+ const timeout = setTimeout(() => {
123
+ container.scrollTop = container.scrollHeight;
124
+ hasInitialScrolledRef.current = true;
125
+ }, 100);
126
+ return () => clearTimeout(timeout);
153
127
  }
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" }) }))] }));
128
+ return undefined;
129
+ }, [initialScrollToBottom]);
130
+ 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, ...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
131
  });
171
132
  ChatLayoutMessages.displayName = "ChatLayout.Messages";
172
133
  const ChatLayoutFooter = React.forwardRef(({ className, children, ...props }, ref) => {
@@ -180,32 +141,63 @@ const ChatLayoutSidebar = React.forwardRef(({ className, children, ...props }, r
180
141
  return (_jsx("div", { ref: ref, className: cn("border-r border-border bg-card w-64 overflow-y-auto", className), ...props, children: children }));
181
142
  });
182
143
  ChatLayoutSidebar.displayName = "ChatLayout.Sidebar";
183
- const ChatLayoutAside = React.forwardRef(({ breakpoint = "lg", className, children, ...props }, ref) => {
144
+ const ChatLayoutAside = React.forwardRef(({ breakpoint = "lg", className, children }, ref) => {
184
145
  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
- }, []);
146
+ // State for committed width (persisted between renders)
147
+ const [committedWidth, setCommittedWidth] = React.useState(450);
148
+ // Motion value for real-time drag updates
149
+ const widthMotionValue = useMotionValue(committedWidth);
150
+ // Track if currently dragging for visual feedback
151
+ const [isDragging, setIsDragging] = React.useState(false);
152
+ // Track drag start position and width
153
+ const dragStartX = React.useRef(0);
154
+ const dragStartWidth = React.useRef(committedWidth);
199
155
  // 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 }) })] }));
156
+ const isVisible = panelSize !== "hidden";
157
+ // Sync motion value when committed width changes
158
+ React.useEffect(() => {
159
+ widthMotionValue.set(committedWidth);
160
+ }, [committedWidth, widthMotionValue]);
161
+ // Handle pointer down to start drag
162
+ const handlePointerDown = React.useCallback((e) => {
163
+ e.preventDefault();
164
+ setIsDragging(true);
165
+ dragStartX.current = e.clientX;
166
+ dragStartWidth.current = committedWidth;
167
+ // Capture pointer for smooth dragging
168
+ e.target.setPointerCapture(e.pointerId);
169
+ }, [committedWidth]);
170
+ // Handle pointer move during drag
171
+ const handlePointerMove = React.useCallback((e) => {
172
+ if (!isDragging)
173
+ return;
174
+ const deltaX = e.clientX - dragStartX.current;
175
+ const newWidth = Math.max(250, Math.min(800, dragStartWidth.current - deltaX));
176
+ widthMotionValue.set(newWidth);
177
+ }, [isDragging, widthMotionValue]);
178
+ // Handle pointer up to end drag
179
+ const handlePointerUp = React.useCallback((e) => {
180
+ if (!isDragging)
181
+ return;
182
+ const deltaX = e.clientX - dragStartX.current;
183
+ const newWidth = Math.max(250, Math.min(800, dragStartWidth.current - deltaX));
184
+ setCommittedWidth(newWidth);
185
+ widthMotionValue.set(newWidth);
186
+ setIsDragging(false);
187
+ // Release pointer capture
188
+ e.target.releasePointerCapture(e.pointerId);
189
+ }, [isDragging, widthMotionValue]);
190
+ 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: {
191
+ duration: 0.5,
192
+ ease: motionEasing.smooth,
193
+ }, style: {
194
+ // Use motion value during drag for instant updates
195
+ width: isDragging ? widthMotionValue : undefined,
196
+ }, className: cn(
197
+ // Hidden by default, visible at breakpoint
198
+ "hidden h-full border-l border-border bg-card overflow-hidden relative",
199
+ // Breakpoint visibility
200
+ 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
201
  });
210
202
  ChatLayoutAside.displayName = "ChatLayout.Aside";
211
203
  /* -------------------------------------------------------------------------------------------------
@@ -1,12 +1,11 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { createLogger } from "@townco/core";
3
- import { ArrowUp, Bug, ChevronDown, ChevronUp, Code, PanelRight, Plus, Settings, Sparkles, X, } from "lucide-react";
4
- import { useCallback, useEffect, useState } from "react";
3
+ import { ArrowUp, Bug, ChevronUp, PanelRight, Settings, X } from "lucide-react";
4
+ import { useEffect, useState } from "react";
5
5
  import { useChatMessages, useChatSession, useToolCalls, } from "../../core/hooks/index.js";
6
6
  import { selectTodosForCurrentSession, useChatStore, } from "../../core/store/chat-store.js";
7
- import { calculateTokenPercentage, formatTokenPercentage, } from "../../core/utils/model-context.js";
8
7
  import { cn } from "../lib/utils.js";
9
- import { ChatEmptyState, ChatHeader, ChatInputActions, ChatInputAttachment, ChatInputCommandMenu, ChatInputField, ChatInputRoot, ChatInputSubmit, ChatInputToolbar, ChatInputVoiceInput, ChatLayout, ContextUsageButton, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, FilesTabContent, IconButton, Message, MessageContent, PanelTabsHeader, SettingsTabContent, SourcesTabContent, Tabs, TabsContent, ThemeToggle, TodoTabContent, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "./index.js";
8
+ import { AppSidebar, ChatEmptyState, ChatHeader, ChatInputActions, ChatInputAttachment, ChatInputCommandMenu, ChatInputField, ChatInputRoot, ChatInputSubmit, ChatInputToolbar, ChatInputVoiceInput, ChatLayout, ContextUsageButton, FilesTabContent, IconButton, Message, MessageContent, PanelTabsHeader, SettingsTabContent, SidebarInset, SidebarProvider, SidebarToggle, SourcesTabContent, Tabs, TabsContent, ThemeToggle, TodoTabContent, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "./index.js";
10
9
  const logger = createLogger("gui");
11
10
  // Helper component to provide openFiles callback
12
11
  function OpenFilesButton({ children, }) {
@@ -31,85 +30,8 @@ function OpenFilesButton({ children, }) {
31
30
  };
32
31
  return _jsx(_Fragment, { children: children({ openFiles, openSettings }) });
33
32
  }
34
- // Hook to handle sidebar keyboard shortcut (Cmd+B / Ctrl+B)
35
- function SidebarHotkey() {
36
- const { panelSize, setPanelSize } = ChatLayout.useChatLayoutContext();
37
- useEffect(() => {
38
- const handleKeyDown = (e) => {
39
- // Cmd+B (Mac) or Ctrl+B (Windows/Linux)
40
- if ((e.metaKey || e.ctrlKey) && e.key === "b") {
41
- e.preventDefault();
42
- setPanelSize(panelSize === "hidden" ? "small" : "hidden");
43
- }
44
- };
45
- window.addEventListener("keydown", handleKeyDown);
46
- return () => window.removeEventListener("keydown", handleKeyDown);
47
- }, [panelSize, setPanelSize]);
48
- return null;
49
- }
50
- // Format relative time from date string
51
- function formatRelativeTime(dateString) {
52
- const date = new Date(dateString);
53
- const now = new Date();
54
- const diffMs = now.getTime() - date.getTime();
55
- const diffMins = Math.floor(diffMs / 60000);
56
- const diffHours = Math.floor(diffMs / 3600000);
57
- const diffDays = Math.floor(diffMs / 86400000);
58
- if (diffMins < 1)
59
- return "Just now";
60
- if (diffMins < 60)
61
- return `${diffMins}m ago`;
62
- if (diffHours < 24)
63
- return `${diffHours}h ago`;
64
- if (diffDays < 7)
65
- return `${diffDays}d ago`;
66
- return date.toLocaleDateString();
67
- }
68
- // Session switcher dropdown component
69
- function SessionSwitcher({ agentName, client, currentSessionId, }) {
70
- const [sessions, setSessions] = useState([]);
71
- const [isLoading, setIsLoading] = useState(false);
72
- const [isOpen, setIsOpen] = useState(false);
73
- const fetchSessions = useCallback(async () => {
74
- if (!client)
75
- return;
76
- setIsLoading(true);
77
- try {
78
- const sessionList = await client.listSessions();
79
- setSessions(sessionList);
80
- }
81
- catch (error) {
82
- logger.error("Failed to fetch sessions", { error });
83
- }
84
- finally {
85
- setIsLoading(false);
86
- }
87
- }, [client]);
88
- // Fetch sessions when dropdown opens
89
- useEffect(() => {
90
- if (isOpen) {
91
- fetchSessions();
92
- }
93
- }, [isOpen, fetchSessions]);
94
- const handleNewSession = () => {
95
- // Clear session from URL and reload to start fresh
96
- const url = new URL(window.location.href);
97
- url.searchParams.delete("session");
98
- window.location.href = url.toString();
99
- };
100
- const handleSessionSelect = (sessionId) => {
101
- if (sessionId === currentSessionId) {
102
- setIsOpen(false);
103
- return;
104
- }
105
- // Update URL with session ID and reload
106
- const url = new URL(window.location.href);
107
- url.searchParams.set("session", sessionId);
108
- window.location.href = url.toString();
109
- };
110
- return (_jsxs(DropdownMenu, { open: isOpen, onOpenChange: setIsOpen, children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs("button", { type: "button", className: "flex items-center gap-1 text-heading-4 text-foreground hover:text-foreground/80 transition-colors cursor-pointer", children: [agentName, _jsx(ChevronDown, { className: cn("size-4 text-muted-foreground transition-transform duration-200", isOpen && "rotate-180") })] }) }), _jsxs(DropdownMenuContent, { align: "start", className: "w-72", children: [_jsxs(DropdownMenuLabel, { className: "flex items-center justify-between", children: [_jsx("span", { children: "Sessions" }), isLoading && (_jsx("span", { className: "text-caption text-muted-foreground", children: "Loading..." }))] }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { onClick: handleNewSession, className: "gap-2", children: [_jsx(Plus, { className: "size-4" }), _jsx("span", { children: "New Session" })] }), sessions.length > 0 && _jsx(DropdownMenuSeparator, {}), _jsx("div", { className: "max-h-64 overflow-y-auto", children: sessions.map((session) => (_jsxs(DropdownMenuItem, { onClick: () => handleSessionSelect(session.sessionId), className: cn("flex flex-col items-start gap-0.5 py-2", session.sessionId === currentSessionId &&
111
- "bg-muted/50 font-medium"), children: [_jsxs("div", { className: "flex w-full items-center justify-between gap-2", children: [_jsx("span", { className: "truncate text-paragraph-sm", children: session.firstUserMessage || "Empty session" }), session.sessionId === currentSessionId && (_jsx("span", { className: "shrink-0 text-caption text-primary", children: "Current" }))] }), _jsxs("span", { className: "text-caption text-muted-foreground", children: [formatRelativeTime(session.updatedAt), " \u2022 ", session.messageCount, " ", "messages"] })] }, session.sessionId))) }), sessions.length === 0 && !isLoading && (_jsx("div", { className: "px-2 py-4 text-center text-paragraph-sm text-muted-foreground", children: "No previous sessions" }))] })] }));
112
- }
33
+ // Note: Keyboard shortcut (Cmd+B / Ctrl+B) for toggling the right panel
34
+ // is now handled internally by ChatLayout.Root
113
35
  // Chat input with attachment handling
114
36
  function ChatInputWithAttachments({ client, startSession, placeholder, latestContextSize, currentModel, commandMenuItems, }) {
115
37
  const attachedFiles = useChatStore((state) => state.input.attachedFiles);
@@ -128,21 +50,19 @@ function AsideTabs({ todos, tools, mcps, subagents, }) {
128
50
  return (_jsxs(Tabs, { value: activeTab, onValueChange: (value) => setActiveTab(value), className: "flex flex-col h-full", children: [_jsx("div", { className: cn("border-b border-border bg-card", "px-4 py-2 h-16", "flex items-center", "[border-bottom-width:0.5px]"), children: _jsx(PanelTabsHeader, { showIcons: true, visibleTabs: ["todo", "files", "sources", "settings"], variant: "compact" }) }), _jsx(TabsContent, { value: "todo", className: "flex-1 p-4 mt-0", children: _jsx(TodoTabContent, { todos: todos }) }), _jsx(TabsContent, { value: "files", className: "flex-1 p-4 mt-0", children: _jsx(FilesTabContent, {}) }), _jsx(TabsContent, { value: "sources", className: "flex-1 p-4 mt-0", children: _jsx(SourcesTabContent, {}) }), _jsx(TabsContent, { value: "settings", className: "flex-1 p-4 mt-0 overflow-y-auto", children: _jsx(SettingsTabContent, { tools: tools ?? [], mcps: mcps ?? [], subagents: subagents ?? [] }) })] }));
129
51
  }
130
52
  // Mobile header component that uses ChatHeader context
131
- function MobileHeader({ agentName, showHeader, client, currentSessionId, }) {
53
+ function MobileHeader({ agentName, showHeader, }) {
132
54
  const { isExpanded, setIsExpanded } = ChatHeader.useChatHeaderContext();
133
- return (_jsxs("div", { className: "flex lg:hidden items-center flex-1", children: [showHeader && (_jsx("div", { className: "flex items-center gap-2 flex-1", children: _jsx(SessionSwitcher, { agentName: agentName, client: client, currentSessionId: currentSessionId }) })), !showHeader && _jsx("div", { className: "flex-1" }), _jsx(ThemeToggle, {}), _jsx(IconButton, { "aria-label": "Toggle menu", onClick: () => setIsExpanded(!isExpanded), children: _jsx(ChevronUp, { className: cn("size-4 text-muted-foreground transition-transform duration-200", isExpanded ? "" : "rotate-180") }) })] }));
55
+ return (_jsxs("div", { className: "flex lg:hidden items-center flex-1", children: [_jsxs("div", { className: "flex items-center gap-2 flex-1", children: [_jsx(SidebarToggle, {}), showHeader && (_jsx("span", { className: "text-heading-4 text-foreground", children: agentName }))] }), _jsx(ThemeToggle, {}), _jsx(IconButton, { "aria-label": "Toggle menu", onClick: () => setIsExpanded(!isExpanded), children: _jsx(ChevronUp, { className: cn("size-4 text-muted-foreground transition-transform duration-200", isExpanded ? "" : "rotate-180") }) })] }));
134
56
  }
135
57
  // Header component that uses ChatLayout context (must be inside ChatLayout.Root)
136
- function AppChatHeader({ agentName, todos, sources, showHeader, sessionId, debuggerUrl, tools, mcps, subagents, client, }) {
137
- const { panelSize, setPanelSize } = ChatLayout.useChatLayoutContext();
58
+ function AppChatHeader({ agentName, todos, sources, showHeader, sessionId, debuggerUrl, tools, mcps, subagents, }) {
59
+ const { togglePanel, panelOpen } = ChatLayout.useChatLayoutContext();
138
60
  const debuggerLink = debuggerUrl
139
61
  ? sessionId
140
62
  ? `${debuggerUrl}/sessions/${sessionId}`
141
63
  : debuggerUrl
142
64
  : null;
143
- return (_jsxs(ChatHeader.Root, { className: cn("border-b border-border bg-card relative lg:p-0", "[border-bottom-width:0.5px]"), children: [_jsxs("div", { className: "hidden lg:flex items-center w-full h-16 py-5 pl-6 pr-4", children: [showHeader && (_jsx("div", { className: "flex items-center gap-2 flex-1", children: _jsx(SessionSwitcher, { agentName: agentName, client: client, currentSessionId: sessionId }) })), !showHeader && _jsx("div", { className: "flex-1" }), debuggerUrl && (_jsx(TooltipProvider, { children: _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx(IconButton, { "aria-label": "View session in debugger", disabled: !debuggerLink, asChild: !!debuggerLink, children: debuggerLink ? (_jsx("a", { href: debuggerLink, target: "_blank", rel: "noopener noreferrer", children: _jsx(Bug, { className: "size-4 text-muted-foreground" }) })) : (_jsx(Bug, { className: "size-4 text-muted-foreground" })) }) }), _jsx(TooltipContent, { children: _jsx("p", { children: sessionId ? "View session in debugger" : "Open debugger" }) })] }) })), _jsx(ThemeToggle, {}), _jsx(IconButton, { "aria-label": "Toggle sidebar", onClick: () => {
144
- setPanelSize(panelSize === "hidden" ? "small" : "hidden");
145
- }, children: _jsx(PanelRight, { className: "size-4 text-muted-foreground" }) })] }), _jsx(MobileHeader, { agentName: agentName, showHeader: showHeader, client: client, currentSessionId: sessionId }), _jsx(ChatHeader.ExpandablePanel, { className: cn("pt-6 pb-8 px-6", "border-b border-border bg-card", "shadow-[0_4px_16px_0_rgba(0,0,0,0.04)]", "[border-bottom-width:0.5px]"), children: _jsxs(Tabs, { defaultValue: "todo", className: "w-full", children: [_jsx(PanelTabsHeader, { showIcons: true, visibleTabs: ["todo", "files", "sources", "settings"], variant: "default" }), _jsx(TabsContent, { value: "todo", className: "mt-4", children: _jsx(TodoTabContent, { todos: todos }) }), _jsx(TabsContent, { value: "files", className: "mt-4", children: _jsx(FilesTabContent, {}) }), _jsx(TabsContent, { value: "sources", className: "mt-4", children: _jsx(SourcesTabContent, { sources: sources }) }), _jsx(TabsContent, { value: "settings", className: "mt-4", children: _jsx(SettingsTabContent, { tools: tools ?? [], mcps: mcps ?? [], subagents: subagents ?? [] }) })] }) })] }));
65
+ return (_jsxs(ChatHeader.Root, { className: cn("border-b border-border bg-card relative lg:p-0", "[border-bottom-width:0.5px]"), children: [_jsxs("div", { className: "hidden lg:flex items-center w-full h-16 py-5 pl-6 pr-4", children: [_jsxs("div", { className: "flex items-center gap-2 flex-1", children: [_jsx(SidebarToggle, {}), showHeader && (_jsx("span", { className: "text-heading-4 text-foreground", children: agentName }))] }), debuggerUrl && (_jsx(TooltipProvider, { children: _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx(IconButton, { "aria-label": "View session in debugger", disabled: !debuggerLink, asChild: !!debuggerLink, children: debuggerLink ? (_jsx("a", { href: debuggerLink, target: "_blank", rel: "noopener noreferrer", children: _jsx(Bug, { className: "size-4 text-muted-foreground" }) })) : (_jsx(Bug, { className: "size-4 text-muted-foreground" })) }) }), _jsx(TooltipContent, { children: _jsx("p", { children: sessionId ? "View session in debugger" : "Open debugger" }) })] }) })), _jsx(ThemeToggle, {}), _jsx(IconButton, { "aria-label": "Toggle panel", onClick: togglePanel, "data-state": panelOpen ? "open" : "closed", children: _jsx(PanelRight, { className: "size-4 text-muted-foreground" }) })] }), _jsx(MobileHeader, { agentName: agentName, showHeader: showHeader }), _jsx(ChatHeader.ExpandablePanel, { className: cn("pt-6 pb-8 px-6", "border-b border-border bg-card", "shadow-[0_4px_16px_0_rgba(0,0,0,0.04)]", "[border-bottom-width:0.5px]"), children: _jsxs(Tabs, { defaultValue: "todo", className: "w-full", children: [_jsx(PanelTabsHeader, { showIcons: true, visibleTabs: ["todo", "files", "sources", "settings"], variant: "default" }), _jsx(TabsContent, { value: "todo", className: "mt-4", children: _jsx(TodoTabContent, { todos: todos }) }), _jsx(TabsContent, { value: "files", className: "mt-4", children: _jsx(FilesTabContent, {}) }), _jsx(TabsContent, { value: "sources", className: "mt-4", children: _jsx(SourcesTabContent, { sources: sources }) }), _jsx(TabsContent, { value: "settings", className: "mt-4", children: _jsx(SettingsTabContent, { tools: tools ?? [], mcps: mcps ?? [], subagents: subagents ?? [] }) })] }) })] }));
146
66
  }
147
67
  export function ChatView({ client, initialSessionId, error: initError, debuggerUrl, }) {
148
68
  // Use shared hooks from @townco/ui/core - MUST be called before any early returns
@@ -291,32 +211,36 @@ export function ChatView({ client, initialSessionId, error: initError, debuggerU
291
211
  },
292
212
  },
293
213
  ];
294
- return (_jsxs(ChatLayout.Root, { defaultPanelSize: "hidden", defaultActiveTab: "todo", children: [_jsx(SidebarHotkey, {}), _jsxs(ChatLayout.Main, { children: [!hideTopBar && (_jsx(AppChatHeader, { agentName: agentName, todos: todos, sources: sources, showHeader: messages.length > 0, sessionId: sessionId, tools: agentTools, mcps: agentMcps, subagents: agentSubagents, client: client, ...(debuggerUrl && { debuggerUrl }) })), connectionStatus === "error" && error && (_jsx("div", { className: "border-b border-destructive/20 bg-destructive/10 px-6 py-4", children: _jsxs("div", { className: "flex items-start justify-between gap-4", children: [_jsxs("div", { className: "flex-1", children: [_jsx("h3", { className: "mb-1 text-paragraph-sm font-semibold text-destructive", children: "Connection Error" }), _jsx("p", { className: "whitespace-pre-line text-paragraph-sm text-foreground", children: error })] }), _jsx("button", { type: "button", onClick: connect, className: "rounded-lg bg-destructive px-4 py-2 text-paragraph-sm font-medium text-destructive-foreground transition-colors hover:bg-destructive-hover", children: "Retry" })] }) })), _jsxs(ChatLayout.Body, { children: [_jsx(ChatLayout.Messages, { children: messages.length === 0 ? (_jsx(OpenFilesButton, { children: ({ openFiles, openSettings }) => (_jsx("div", { className: "flex flex-1 items-center px-4", children: _jsx(ChatEmptyState, { title: agentName, titleElement: _jsx(SessionSwitcher, { agentName: agentName, client: client, currentSessionId: sessionId }), description: agentDescription, suggestedPrompts: suggestedPrompts, onPromptClick: (prompt) => {
295
- sendMessage(prompt);
296
- setPlaceholder("Type a message or / for commands...");
297
- logger.info("Prompt clicked", { prompt });
298
- }, onPromptHover: handlePromptHover, onPromptLeave: handlePromptLeave, onOpenFiles: openFiles, onOpenSettings: openSettings, toolsAndMcpsCount: agentTools.length +
299
- agentMcps.length +
300
- agentSubagents.length }) })) })) : (_jsx("div", { className: "flex flex-col px-4", children: messages.map((message, index) => {
301
- // Calculate dynamic spacing based on message sequence
302
- const isFirst = index === 0;
303
- const previousMessage = isFirst ? null : messages[index - 1];
304
- let spacingClass = "mt-2";
305
- if (isFirst) {
306
- // First message needs more top margin if it's an assistant initial message
307
- spacingClass =
308
- message.role === "assistant" ? "mt-8" : "mt-2";
309
- }
310
- else if (message.role === "user") {
311
- // User message usually starts a new turn
312
- spacingClass =
313
- previousMessage?.role === "user" ? "mt-4" : "mt-4";
314
- }
315
- else if (message.role === "assistant") {
316
- // Assistant message is usually a response
317
- spacingClass =
318
- previousMessage?.role === "assistant" ? "mt-2" : "mt-6";
319
- }
320
- return (_jsx(Message, { message: message, className: spacingClass, isLastMessage: index === messages.length - 1, children: _jsx(MessageContent, { message: message, thinkingDisplayStyle: "collapsible" }) }, message.id));
321
- }) })) }), _jsx(ChatLayout.Footer, { children: _jsx(ChatInputWithAttachments, { client: client, startSession: startSession, placeholder: placeholder, latestContextSize: latestContextSize, currentModel: currentModel, commandMenuItems: commandMenuItems }) })] })] }), isLargeScreen && (_jsx(ChatLayout.Aside, { breakpoint: "lg", children: _jsx(AsideTabs, { todos: todos, tools: agentTools, mcps: agentMcps, subagents: agentSubagents }) }))] }));
214
+ return (_jsxs(SidebarProvider, { defaultOpen: false, children: [_jsx(AppSidebar, { client: client, currentSessionId: sessionId }), _jsx(SidebarInset, { children: _jsxs(ChatLayout.Root, { defaultPanelSize: "hidden", defaultActiveTab: "todo", children: [_jsxs(ChatLayout.Main, { children: [!hideTopBar && (_jsx(AppChatHeader, { agentName: agentName, todos: todos, sources: sources, showHeader: messages.length > 0, sessionId: sessionId, tools: agentTools, mcps: agentMcps, subagents: agentSubagents, ...(debuggerUrl && { debuggerUrl }) })), connectionStatus === "error" && error && (_jsx("div", { className: "border-b border-destructive/20 bg-destructive/10 px-6 py-4", children: _jsxs("div", { className: "flex items-start justify-between gap-4", children: [_jsxs("div", { className: "flex-1", children: [_jsx("h3", { className: "mb-1 text-paragraph-sm font-semibold text-destructive", children: "Connection Error" }), _jsx("p", { className: "whitespace-pre-line text-paragraph-sm text-foreground", children: error })] }), _jsx("button", { type: "button", onClick: connect, className: "rounded-lg bg-destructive px-4 py-2 text-paragraph-sm font-medium text-destructive-foreground transition-colors hover:bg-destructive-hover", children: "Retry" })] }) })), _jsxs(ChatLayout.Body, { children: [_jsx(ChatLayout.Messages, { children: messages.length === 0 ? (_jsx(OpenFilesButton, { children: ({ openFiles, openSettings }) => (_jsx("div", { className: "flex flex-1 items-center px-4", children: _jsx(ChatEmptyState, { title: agentName, description: agentDescription, suggestedPrompts: suggestedPrompts, onPromptClick: (prompt) => {
215
+ sendMessage(prompt);
216
+ setPlaceholder("Type a message or / for commands...");
217
+ logger.info("Prompt clicked", { prompt });
218
+ }, onPromptHover: handlePromptHover, onPromptLeave: handlePromptLeave, onOpenFiles: openFiles, onOpenSettings: openSettings, toolsAndMcpsCount: agentTools.length +
219
+ agentMcps.length +
220
+ agentSubagents.length }) })) })) : (_jsx("div", { className: "flex flex-col px-4", children: messages.map((message, index) => {
221
+ // Calculate dynamic spacing based on message sequence
222
+ const isFirst = index === 0;
223
+ const previousMessage = isFirst
224
+ ? null
225
+ : messages[index - 1];
226
+ let spacingClass = "mt-2";
227
+ if (isFirst) {
228
+ // First message needs more top margin if it's an assistant initial message
229
+ spacingClass =
230
+ message.role === "assistant" ? "mt-8" : "mt-2";
231
+ }
232
+ else if (message.role === "user") {
233
+ // User message usually starts a new turn
234
+ spacingClass =
235
+ previousMessage?.role === "user" ? "mt-4" : "mt-4";
236
+ }
237
+ else if (message.role === "assistant") {
238
+ // Assistant message is usually a response
239
+ spacingClass =
240
+ previousMessage?.role === "assistant"
241
+ ? "mt-2"
242
+ : "mt-6";
243
+ }
244
+ return (_jsx(Message, { message: message, className: spacingClass, isLastMessage: index === messages.length - 1, children: _jsx(MessageContent, { message: message, thinkingDisplayStyle: "collapsible" }) }, message.id));
245
+ }) })) }), _jsx(ChatLayout.Footer, { children: _jsx(ChatInputWithAttachments, { client: client, startSession: startSession, placeholder: placeholder, latestContextSize: latestContextSize, currentModel: currentModel, commandMenuItems: commandMenuItems }) })] })] }), isLargeScreen && (_jsx(ChatLayout.Aside, { breakpoint: "lg", children: _jsx(AsideTabs, { todos: todos, tools: agentTools, mcps: agentMcps, subagents: agentSubagents }) }))] }) })] }));
322
246
  }
@@ -0,0 +1,9 @@
1
+ import type { HookNotificationDisplay } from "../../core/schemas/chat.js";
2
+ export interface HookNotificationProps {
3
+ notification: HookNotificationDisplay;
4
+ }
5
+ /**
6
+ * HookNotification component - displays a hook notification inline with messages
7
+ * Only shows completed or error states (not intermediate "triggered" state)
8
+ */
9
+ export declare function HookNotification({ notification }: HookNotificationProps): import("react/jsx-runtime").JSX.Element;