@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.
- package/dist/core/hooks/use-chat-messages.d.ts +6 -1
- package/dist/core/hooks/use-chat-session.d.ts +1 -1
- package/dist/core/hooks/use-tool-calls.d.ts +6 -1
- package/dist/core/schemas/chat.d.ts +10 -0
- package/dist/core/schemas/tool-call.d.ts +13 -8
- package/dist/core/schemas/tool-call.js +8 -0
- package/dist/core/utils/tool-call-state.d.ts +30 -0
- package/dist/core/utils/tool-call-state.js +73 -0
- package/dist/core/utils/tool-summary.d.ts +13 -0
- package/dist/core/utils/tool-summary.js +172 -0
- package/dist/core/utils/tool-verbiage.d.ts +28 -0
- package/dist/core/utils/tool-verbiage.js +185 -0
- package/dist/gui/components/AppSidebar.d.ts +22 -0
- package/dist/gui/components/AppSidebar.js +22 -0
- package/dist/gui/components/ChatLayout.d.ts +5 -0
- package/dist/gui/components/ChatLayout.js +130 -138
- package/dist/gui/components/ChatView.js +42 -118
- package/dist/gui/components/HookNotification.d.ts +9 -0
- package/dist/gui/components/HookNotification.js +50 -0
- package/dist/gui/components/MessageContent.js +151 -39
- package/dist/gui/components/SessionHistory.d.ts +10 -0
- package/dist/gui/components/SessionHistory.js +101 -0
- package/dist/gui/components/SessionHistoryItem.d.ts +11 -0
- package/dist/gui/components/SessionHistoryItem.js +24 -0
- package/dist/gui/components/Sheet.d.ts +25 -0
- package/dist/gui/components/Sheet.js +36 -0
- package/dist/gui/components/Sidebar.d.ts +65 -0
- package/dist/gui/components/Sidebar.js +231 -0
- package/dist/gui/components/SidebarToggle.d.ts +3 -0
- package/dist/gui/components/SidebarToggle.js +9 -0
- package/dist/gui/components/SubAgentDetails.js +13 -2
- package/dist/gui/components/ToolCallList.js +3 -3
- package/dist/gui/components/ToolOperation.d.ts +11 -0
- package/dist/gui/components/ToolOperation.js +329 -0
- package/dist/gui/components/WorkProgress.d.ts +20 -0
- package/dist/gui/components/WorkProgress.js +79 -0
- package/dist/gui/components/index.d.ts +8 -1
- package/dist/gui/components/index.js +9 -1
- package/dist/gui/hooks/index.d.ts +1 -0
- package/dist/gui/hooks/index.js +1 -0
- package/dist/gui/hooks/use-mobile.d.ts +1 -0
- package/dist/gui/hooks/use-mobile.js +15 -0
- package/dist/gui/index.d.ts +1 -0
- package/dist/gui/index.js +2 -0
- package/dist/gui/lib/motion.d.ts +55 -0
- package/dist/gui/lib/motion.js +217 -0
- package/dist/sdk/schemas/message.d.ts +2 -2
- package/dist/sdk/schemas/session.d.ts +5 -0
- package/dist/sdk/transports/types.d.ts +5 -0
- package/package.json +8 -7
- 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
|
|
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
|
-
|
|
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
|
|
34
|
-
return (_jsx(
|
|
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
|
|
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
|
|
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
|
|
72
|
+
return false;
|
|
60
73
|
const { scrollTop, scrollHeight, clientHeight } = container;
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
|
144
|
+
const ChatLayoutAside = React.forwardRef(({ breakpoint = "lg", className, children }, ref) => {
|
|
184
145
|
const { panelSize } = useChatLayoutContext();
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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,
|
|
4
|
-
import {
|
|
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,
|
|
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
|
-
//
|
|
35
|
-
|
|
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,
|
|
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: [
|
|
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,
|
|
137
|
-
const {
|
|
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: [
|
|
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: [
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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;
|