@townco/ui 0.1.76 → 0.1.78

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/dist/core/hooks/use-chat-messages.d.ts +4 -4
  2. package/dist/core/hooks/use-chat-messages.js +4 -1
  3. package/dist/core/hooks/use-chat-session.d.ts +1 -1
  4. package/dist/core/hooks/use-chat-session.js +5 -1
  5. package/dist/core/hooks/use-subagent-stream.js +6 -6
  6. package/dist/core/hooks/use-tool-calls.d.ts +3 -3
  7. package/dist/core/hooks/use-tool-calls.js +1 -1
  8. package/dist/core/schemas/chat.d.ts +10 -10
  9. package/dist/core/schemas/tool-call.d.ts +8 -8
  10. package/dist/core/store/chat-store.js +1 -0
  11. package/dist/core/utils/tool-summary.js +8 -3
  12. package/dist/core/utils/tool-verbiage.js +1 -1
  13. package/dist/gui/components/AppSidebar.d.ts +1 -1
  14. package/dist/gui/components/AppSidebar.js +4 -3
  15. package/dist/gui/components/Button.d.ts +1 -1
  16. package/dist/gui/components/ChatEmptyState.js +1 -1
  17. package/dist/gui/components/ChatHeader.d.ts +1 -28
  18. package/dist/gui/components/ChatHeader.js +4 -71
  19. package/dist/gui/components/ChatLayout.d.ts +6 -2
  20. package/dist/gui/components/ChatLayout.js +82 -33
  21. package/dist/gui/components/ChatView.js +28 -45
  22. package/dist/gui/components/ContextUsageButton.d.ts +0 -1
  23. package/dist/gui/components/ContextUsageButton.js +10 -3
  24. package/dist/gui/components/HookNotification.js +2 -1
  25. package/dist/gui/components/MessageContent.js +24 -160
  26. package/dist/gui/components/SessionHistory.js +1 -2
  27. package/dist/gui/components/SessionHistoryItem.js +1 -1
  28. package/dist/gui/components/Sidebar.js +27 -42
  29. package/dist/gui/components/SubAgentDetails.js +10 -14
  30. package/dist/gui/components/TodoSubline.js +1 -0
  31. package/dist/gui/components/ToolOperation.js +16 -75
  32. package/dist/gui/components/WorkProgress.js +5 -3
  33. package/dist/gui/components/index.d.ts +0 -1
  34. package/dist/gui/components/resizable.d.ts +1 -1
  35. package/dist/gui/constants.d.ts +6 -0
  36. package/dist/gui/constants.js +8 -0
  37. package/dist/gui/hooks/index.d.ts +1 -0
  38. package/dist/gui/hooks/index.js +1 -0
  39. package/dist/gui/hooks/use-lock-body-scroll.d.ts +7 -0
  40. package/dist/gui/hooks/use-lock-body-scroll.js +29 -0
  41. package/dist/gui/lib/motion.d.ts +12 -0
  42. package/dist/gui/lib/motion.js +69 -0
  43. package/dist/sdk/schemas/message.d.ts +2 -2
  44. package/dist/sdk/schemas/session.d.ts +18 -18
  45. package/dist/sdk/transports/http.d.ts +1 -1
  46. package/dist/sdk/transports/http.js +9 -0
  47. package/dist/sdk/transports/stdio.js +2 -2
  48. package/dist/sdk/transports/types.d.ts +11 -0
  49. package/dist/sdk/transports/types.js +28 -1
  50. package/package.json +3 -5
  51. package/dist/gui/components/InvokingGroup.d.ts +0 -9
  52. package/dist/gui/components/InvokingGroup.js +0 -16
  53. package/dist/gui/components/SubagentStream.d.ts +0 -23
  54. package/dist/gui/components/SubagentStream.js +0 -98
  55. package/dist/gui/components/ToolCall.d.ts +0 -8
  56. package/dist/gui/components/ToolCall.js +0 -234
  57. package/dist/gui/components/ToolCallGroup.d.ts +0 -8
  58. package/dist/gui/components/ToolCallGroup.js +0 -29
@@ -1,10 +1,14 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { AnimatePresence, motion, useMotionValue } from "framer-motion";
3
- import { ArrowDown } from "lucide-react";
3
+ import { ArrowDown, X } from "lucide-react";
4
4
  import * as React from "react";
5
+ import { ASIDE_WIDTH_DEFAULT, ASIDE_WIDTH_MAX, ASIDE_WIDTH_MIN, } from "../constants.js";
6
+ import { useLockBodyScroll } from "../hooks/use-lock-body-scroll.js";
7
+ import { useIsMobile } from "../hooks/use-mobile.js";
5
8
  import { useScrollToBottom } from "../hooks/use-scroll-to-bottom.js";
6
- import { motionEasing } from "../lib/motion.js";
9
+ import { asideContentTransition, asideContentVariants, asideMobileTransition, asideMobileVariants, motionEasing, } from "../lib/motion.js";
7
10
  import { cn } from "../lib/utils.js";
11
+ import { IconButton } from "./IconButton.js";
8
12
  import { Toaster } from "./Sonner.js";
9
13
  const ChatLayoutContext = React.createContext(undefined);
10
14
  const useChatLayoutContext = () => {
@@ -18,17 +22,29 @@ const ChatLayoutRoot = React.forwardRef(({ defaultSidebarOpen = false, defaultPa
18
22
  const [sidebarOpen, setSidebarOpen] = React.useState(defaultSidebarOpen);
19
23
  const [panelSize, setPanelSize] = React.useState(defaultPanelSize);
20
24
  const [activeTab, setActiveTab] = React.useState(defaultActiveTab);
21
- // Right panel state (derived from panelSize for backwards compatibility)
22
- const [panelOpen, setPanelOpen] = React.useState(defaultPanelSize !== "hidden");
25
+ // Derive panelOpen from panelSize (single source of truth)
26
+ const panelOpen = React.useMemo(() => panelSize !== "hidden", [panelSize]);
27
+ // Track if toggle is in progress to prevent rapid toggling
28
+ const isTogglingRef = React.useRef(false);
29
+ // Track if aside panel is being dragged (for Main reflow coordination)
30
+ const [isDraggingAside, setIsDraggingAside] = React.useState(false);
23
31
  // Helper to toggle the right panel
24
32
  const togglePanel = React.useCallback(() => {
33
+ // Prevent rapid toggling during animation
34
+ if (isTogglingRef.current) {
35
+ return;
36
+ }
37
+ isTogglingRef.current = true;
25
38
  setPanelSize((size) => (size === "hidden" ? "large" : "hidden"));
26
- setPanelOpen((open) => !open);
39
+ // Reset after animation duration + buffer
40
+ setTimeout(() => {
41
+ isTogglingRef.current = false;
42
+ }, 600); // 500ms animation + 100ms buffer
43
+ }, []);
44
+ // Helper to set panel open/closed (for backwards compatibility)
45
+ const setPanelOpen = React.useCallback((open) => {
46
+ setPanelSize(open ? "large" : "hidden");
27
47
  }, []);
28
- // Sync panelOpen with panelSize
29
- React.useEffect(() => {
30
- setPanelOpen(panelSize !== "hidden");
31
- }, [panelSize]);
32
48
  return (_jsx(ChatLayoutContext.Provider, { value: {
33
49
  sidebarOpen,
34
50
  setSidebarOpen,
@@ -42,6 +58,8 @@ const ChatLayoutRoot = React.forwardRef(({ defaultSidebarOpen = false, defaultPa
42
58
  panelOpen,
43
59
  setPanelOpen,
44
60
  togglePanel,
61
+ isDraggingAside,
62
+ setIsDraggingAside,
45
63
  }, 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 }) }));
46
64
  });
47
65
  ChatLayoutRoot.displayName = "ChatLayout.Root";
@@ -50,10 +68,17 @@ const ChatLayoutHeader = React.forwardRef(({ className, children, ...props }, re
50
68
  });
51
69
  ChatLayoutHeader.displayName = "ChatLayout.Header";
52
70
  const ChatLayoutMain = React.forwardRef(({ className, children }, ref) => {
53
- return (_jsx(motion.div, { ref: ref, layout: true, transition: {
54
- duration: 0.5,
55
- ease: motionEasing.smooth,
56
- }, className: cn("flex flex-1 flex-col overflow-hidden h-full min-w-0", className), children: children }));
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 }));
57
82
  });
58
83
  ChatLayoutMain.displayName = "ChatLayout.Main";
59
84
  const ChatLayoutBody = React.forwardRef(({ showToaster = true, className, children, ...props }, ref) => {
@@ -89,7 +114,7 @@ const ChatLayoutMessages = React.forwardRef(({ className, children, onScrollChan
89
114
  }, [initialScrollToBottom, containerRef]);
90
115
  // Show scroll button when not at bottom
91
116
  const showScrollButton = !isAtBottom && showScrollToBottom;
92
- return (_jsxs("div", { className: "relative flex-1 overflow-hidden", children: [_jsxs("div", { ref: containerRef, className: cn("h-full overflow-y-auto flex flex-col", className), ...props, children: [_jsx("div", { className: "mx-auto max-w-chat flex-1 w-full flex flex-col", children: children }), _jsx("div", { ref: endRef, className: "min-h-[24px] min-w-[24px] shrink-0" })] }), 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" }) }))] }));
117
+ return (_jsxs("div", { className: "relative flex-1 overflow-hidden", children: [_jsxs("div", { ref: containerRef, className: cn("h-full overflow-y-auto flex flex-col", className), ...props, children: [_jsx("div", { className: "mx-auto max-w-chat flex-1 w-full flex flex-col", children: children }), _jsx("div", { ref: endRef, className: "shrink-0" })] }), 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" }) }))] }));
93
118
  });
94
119
  ChatLayoutMessages.displayName = "ChatLayout.Messages";
95
120
  const ChatLayoutFooter = React.forwardRef(({ className, children, ...props }, ref) => {
@@ -103,10 +128,10 @@ const ChatLayoutSidebar = React.forwardRef(({ className, children, ...props }, r
103
128
  return (_jsx("div", { ref: ref, className: cn("border-r border-border bg-card w-64 overflow-y-auto", className), ...props, children: children }));
104
129
  });
105
130
  ChatLayoutSidebar.displayName = "ChatLayout.Sidebar";
106
- const ChatLayoutAside = React.forwardRef(({ breakpoint = "lg", className, children }, ref) => {
107
- const { panelSize } = useChatLayoutContext();
131
+ const ChatLayoutAside = React.forwardRef(({ breakpoint = "md", onClose, className, children }, ref) => {
132
+ const { panelSize, togglePanel, setIsDraggingAside } = useChatLayoutContext();
108
133
  // State for committed width (persisted between renders)
109
- const [committedWidth, setCommittedWidth] = React.useState(450);
134
+ const [committedWidth, setCommittedWidth] = React.useState(ASIDE_WIDTH_DEFAULT);
110
135
  // Motion value for real-time drag updates
111
136
  const widthMotionValue = useMotionValue(committedWidth);
112
137
  // Track if currently dragging for visual feedback
@@ -116,6 +141,10 @@ const ChatLayoutAside = React.forwardRef(({ breakpoint = "lg", className, childr
116
141
  const dragStartWidth = React.useRef(committedWidth);
117
142
  // Hidden state - don't render
118
143
  const isVisible = panelSize !== "hidden";
144
+ // Detect mobile viewport (< 768px)
145
+ const isMobile = useIsMobile();
146
+ // Lock body scroll when panel is open on mobile
147
+ useLockBodyScroll(isMobile && isVisible);
119
148
  // Sync motion value when committed width changes
120
149
  React.useEffect(() => {
121
150
  widthMotionValue.set(committedWidth);
@@ -124,17 +153,18 @@ const ChatLayoutAside = React.forwardRef(({ breakpoint = "lg", className, childr
124
153
  const handlePointerDown = React.useCallback((e) => {
125
154
  e.preventDefault();
126
155
  setIsDragging(true);
156
+ setIsDraggingAside(true); // Notify context for Main reflow
127
157
  dragStartX.current = e.clientX;
128
158
  dragStartWidth.current = committedWidth;
129
159
  // Capture pointer for smooth dragging
130
160
  e.target.setPointerCapture(e.pointerId);
131
- }, [committedWidth]);
161
+ }, [committedWidth, setIsDraggingAside]);
132
162
  // Handle pointer move during drag
133
163
  const handlePointerMove = React.useCallback((e) => {
134
164
  if (!isDragging)
135
165
  return;
136
166
  const deltaX = e.clientX - dragStartX.current;
137
- const newWidth = Math.max(250, Math.min(800, dragStartWidth.current - deltaX));
167
+ const newWidth = Math.max(ASIDE_WIDTH_MIN, Math.min(ASIDE_WIDTH_MAX, dragStartWidth.current - deltaX));
138
168
  widthMotionValue.set(newWidth);
139
169
  }, [isDragging, widthMotionValue]);
140
170
  // Handle pointer up to end drag
@@ -142,24 +172,43 @@ const ChatLayoutAside = React.forwardRef(({ breakpoint = "lg", className, childr
142
172
  if (!isDragging)
143
173
  return;
144
174
  const deltaX = e.clientX - dragStartX.current;
145
- const newWidth = Math.max(250, Math.min(800, dragStartWidth.current - deltaX));
175
+ const newWidth = Math.max(ASIDE_WIDTH_MIN, Math.min(ASIDE_WIDTH_MAX, dragStartWidth.current - deltaX));
146
176
  setCommittedWidth(newWidth);
147
177
  widthMotionValue.set(newWidth);
148
178
  setIsDragging(false);
179
+ setIsDraggingAside(false); // Notify context drag ended
149
180
  // Release pointer capture
150
181
  e.target.releasePointerCapture(e.pointerId);
151
- }, [isDragging, widthMotionValue]);
152
- 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: {
153
- duration: 0.5,
154
- ease: motionEasing.smooth,
155
- }, style: {
156
- // Use motion value during drag for instant updates
157
- width: isDragging ? widthMotionValue : undefined,
158
- }, className: cn(
159
- // Hidden by default, visible at breakpoint
160
- "hidden h-full border-l border-border bg-card overflow-hidden relative",
161
- // Breakpoint visibility
162
- 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")) }));
182
+ }, [isDragging, widthMotionValue, setIsDraggingAside]);
183
+ 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",
184
+ // Only visible on mobile (< 768px)
185
+ "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: [_jsx("div", { className: "flex justify-end px-4 pt-3 shrink-0", children: _jsx(IconButton, { onClick: () => {
186
+ onClose?.();
187
+ togglePanel();
188
+ }, "aria-label": "Close panel", children: _jsx(X, { className: "size-4" }) }) }), _jsx("div", { className: "flex-1 overflow-y-auto", children: children })] }) }, "aside-mobile")), !isMobile && (_jsx(motion.aside, { ref: ref, initial: { width: 0, opacity: 0 }, animate: {
189
+ width: committedWidth,
190
+ opacity: 1,
191
+ }, exit: { width: 0, opacity: 0 }, style: {
192
+ // Use motion value during drag for instant updates
193
+ width: isDragging ? widthMotionValue : undefined,
194
+ }, transition: isDragging
195
+ ? {
196
+ // Instant updates during drag (no animation)
197
+ duration: 0,
198
+ }
199
+ : {
200
+ // Smooth animation for open/close
201
+ duration: 0.5,
202
+ ease: motionEasing.smooth,
203
+ }, className: cn(
204
+ // Hidden by default, visible at breakpoint
205
+ "hidden h-full border-l border-border bg-card overflow-hidden relative",
206
+ // Breakpoint visibility
207
+ 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: [_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
208
+ ? {
209
+ width: `${committedWidth}px`,
210
+ }
211
+ : undefined, children: children })] }) }, "aside-desktop"))] })) }));
163
212
  });
164
213
  ChatLayoutAside.displayName = "ChatLayout.Aside";
165
214
  /* -------------------------------------------------------------------------------------------------
@@ -1,11 +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, ChevronUp, PanelRight, Settings, X } from "lucide-react";
3
+ import { ArrowUp, Bug, PanelRight, Settings, X } from "lucide-react";
4
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
7
  import { cn } from "../lib/utils.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";
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, TodoTabContent, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "./index.js";
9
9
  const logger = createLogger("gui");
10
10
  // Helper component to provide openFiles callback
11
11
  function OpenFilesButton({ children, }) {
@@ -33,7 +33,7 @@ function OpenFilesButton({ children, }) {
33
33
  // Note: Keyboard shortcut (Cmd+B / Ctrl+B) for toggling the right panel
34
34
  // is now handled internally by ChatLayout.Root
35
35
  // Chat input with attachment handling
36
- function ChatInputWithAttachments({ client, startSession, placeholder, latestContextSize, commandMenuItems, }) {
36
+ function ChatInputWithAttachments({ client, startSession, placeholder, commandMenuItems, }) {
37
37
  const attachedFiles = useChatStore((state) => state.input.attachedFiles);
38
38
  const addFileAttachment = useChatStore((state) => state.addFileAttachment);
39
39
  const removeFileAttachment = useChatStore((state) => state.removeFileAttachment);
@@ -42,27 +42,22 @@ function ChatInputWithAttachments({ client, startSession, placeholder, latestCon
42
42
  addFileAttachment(file);
43
43
  }
44
44
  };
45
- return (_jsxs(ChatInputRoot, { client: client, startSession: startSession, children: [_jsx(ChatInputCommandMenu, { commands: commandMenuItems }), attachedFiles.length > 0 && (_jsx("div", { className: "flex flex-wrap gap-2 p-3 border-b border-border", children: attachedFiles.map((file, index) => (_jsxs("div", { className: "relative group rounded-md overflow-hidden border border-border", children: [_jsx("img", { src: `data:${file.mimeType};base64,${file.data}`, alt: file.name, className: "h-20 w-20 object-cover" }), _jsx("button", { type: "button", onClick: () => removeFileAttachment(index), className: "absolute top-1 right-1 p-1 rounded-full bg-background/80 hover:bg-background opacity-0 group-hover:opacity-100 transition-opacity", children: _jsx(X, { className: "size-3" }) })] }, index))) })), _jsx(ChatInputField, { placeholder: placeholder, autoFocus: true, onFilesDropped: handleFilesSelected }), _jsxs(ChatInputToolbar, { children: [_jsxs("div", { className: "flex items-baseline gap-1", children: [_jsx(ChatInputActions, {}), _jsx(ChatInputAttachment, { onFilesSelected: handleFilesSelected }), latestContextSize != null && (_jsx(ContextUsageButton, { contextSize: latestContextSize }))] }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx(ChatInputVoiceInput, {}), _jsx(ChatInputSubmit, { children: _jsx(ArrowUp, { className: "size-4" }) })] })] })] }));
45
+ return (_jsxs(ChatInputRoot, { client: client, startSession: startSession, children: [_jsx(ChatInputCommandMenu, { commands: commandMenuItems }), attachedFiles.length > 0 && (_jsx("div", { className: "flex flex-wrap gap-2 p-3 border-b border-border", children: attachedFiles.map((file, index) => (_jsxs("div", { className: "relative group rounded-md overflow-hidden border border-border", children: [_jsx("img", { src: `data:${file.mimeType};base64,${file.data}`, alt: file.name, className: "h-20 w-20 object-cover" }), _jsx("button", { type: "button", onClick: () => removeFileAttachment(index), className: "absolute top-1 right-1 p-1 rounded-full bg-background/80 hover:bg-background opacity-0 group-hover:opacity-100 transition-opacity", children: _jsx(X, { className: "size-3" }) })] }, `attached-${file.name}-${file.data.slice(0, 20)}`))) })), _jsx(ChatInputField, { placeholder: placeholder, autoFocus: true, onFilesDropped: handleFilesSelected }), _jsxs(ChatInputToolbar, { children: [_jsxs("div", { className: "flex items-baseline gap-1", children: [_jsx(ChatInputActions, {}), _jsx(ChatInputAttachment, { onFilesSelected: handleFilesSelected })] }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx(ChatInputVoiceInput, {}), _jsx(ChatInputSubmit, { children: _jsx(ArrowUp, { className: "size-4" }) })] })] })] }));
46
46
  }
47
47
  // Controlled Tabs component for the aside panel
48
48
  function AsideTabs({ todos, tools, mcps, subagents, }) {
49
49
  const { activeTab, setActiveTab } = ChatLayout.useChatLayoutContext();
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 ?? [] }) })] }));
51
- }
52
- // Mobile header component that uses ChatHeader context
53
- function MobileHeader({ agentName, showHeader, }) {
54
- const { isExpanded, setIsExpanded } = ChatHeader.useChatHeaderContext();
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") }) })] }));
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"), 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 ?? [] }) })] }));
56
51
  }
57
52
  // Header component that uses ChatLayout context (must be inside ChatLayout.Root)
58
- function AppChatHeader({ agentName, todos, sources, showHeader, sessionId, debuggerUrl, tools, mcps, subagents, }) {
53
+ function AppChatHeader({ agentName, showHeader, sessionId, debuggerUrl, }) {
59
54
  const { togglePanel, panelOpen } = ChatLayout.useChatLayoutContext();
60
55
  const debuggerLink = debuggerUrl
61
56
  ? sessionId
62
57
  ? `${debuggerUrl}/sessions/${sessionId}`
63
58
  : debuggerUrl
64
59
  : null;
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 ?? [] }) })] }) })] }));
60
+ return (_jsx(ChatHeader.Root, { className: cn("border-b border-border bg-card h-16"), children: _jsxs("div", { className: "flex items-center w-full py-5 px-4 lg:px-4", children: [_jsxs("div", { className: "flex items-center gap-2 flex-1", children: [_jsx(SidebarToggle, {}), showHeader && agentName && (_jsx("span", { className: "text-heading-4 text-foreground", children: agentName }))] }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx(ContextUsageButton, {}), 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(IconButton, { "aria-label": "Toggle panel", onClick: togglePanel, "data-state": panelOpen ? "open" : "closed", children: _jsx(PanelRight, { className: "size-4 text-muted-foreground" }) })] })] }) }));
66
61
  }
67
62
  export function ChatView({ client, initialSessionId, error: initError, debuggerUrl, }) {
68
63
  // Use shared hooks from @townco/ui/core - MUST be called before any early returns
@@ -70,18 +65,16 @@ export function ChatView({ client, initialSessionId, error: initError, debuggerU
70
65
  const { messages, sendMessage } = useChatMessages(client, startSession);
71
66
  useToolCalls(client); // Still need to subscribe to tool call events
72
67
  const error = useChatStore((state) => state.error);
73
- const [agentName, setAgentName] = useState("Agent");
74
- const [agentDescription, setAgentDescription] = useState("This research agent can help you find and summarize information, analyze sources, track tasks, and answer questions about your research. Start by typing a message below to begin your investigation.");
75
- const [suggestedPrompts, setSuggestedPrompts] = useState([
76
- "Search the web for the latest news on top tech company earnings, produce a summary for each company, and then a macro trend analysis of the tech industry. Use your todo list",
77
- "What can you help me with?",
78
- ]);
68
+ const [agentName, setAgentName] = useState(undefined);
69
+ const [agentDescription, setAgentDescription] = useState(undefined);
70
+ const [suggestedPrompts, setSuggestedPrompts] = useState(undefined);
79
71
  const [agentTools, setAgentTools] = useState([]);
80
72
  const [agentMcps, setAgentMcps] = useState([]);
81
73
  const [agentSubagents, setAgentSubagents] = useState([]);
82
- const [isLargeScreen, setIsLargeScreen] = useState(typeof window !== "undefined" ? window.innerWidth >= 1024 : true);
83
74
  const [placeholder, setPlaceholder] = useState("Type a message or / for commands...");
84
75
  const [hideTopBar, setHideTopBar] = useState(false);
76
+ const todos = useChatStore(selectTodosForCurrentSession);
77
+ const _latestContextSize = useChatStore((state) => state.latestContextSize);
85
78
  // Log connection status changes
86
79
  useEffect(() => {
87
80
  logger.debug("Connection status changed", { status: connectionStatus });
@@ -90,8 +83,9 @@ export function ChatView({ client, initialSessionId, error: initError, debuggerU
90
83
  }
91
84
  }, [connectionStatus, error]);
92
85
  // Get agent name from session metadata or client info
86
+ // Re-run when connectionStatus changes to pick up agentInfo after connect()
93
87
  useEffect(() => {
94
- if (client) {
88
+ if (client && connectionStatus === "connected") {
95
89
  // Try to get from current session first
96
90
  const session = client.getCurrentSession();
97
91
  if (session?.metadata?.agentName) {
@@ -107,12 +101,16 @@ export function ChatView({ client, initialSessionId, error: initError, debuggerU
107
101
  else if (agentInfo.name) {
108
102
  setAgentName(agentInfo.name);
109
103
  }
110
- if (agentInfo.description) {
111
- setAgentDescription(agentInfo.description);
112
- }
104
+ // Set description - use agent's description or a default
105
+ setAgentDescription(agentInfo.description ||
106
+ "Start a conversation below to begin working with this agent.");
113
107
  if (agentInfo.suggestedPrompts && agentInfo.suggestedPrompts.length > 0) {
114
108
  setSuggestedPrompts(agentInfo.suggestedPrompts);
115
109
  }
110
+ else {
111
+ // Set default prompts if none provided
112
+ setSuggestedPrompts([]);
113
+ }
116
114
  // Get tools, MCPs, and subagents
117
115
  if (agentInfo.tools && agentInfo.tools.length > 0) {
118
116
  setAgentTools(agentInfo.tools);
@@ -132,21 +130,7 @@ export function ChatView({ client, initialSessionId, error: initError, debuggerU
132
130
  setHideTopBar(true);
133
131
  }
134
132
  }
135
- }, [client, sessionId, connectionStatus]);
136
- // Monitor screen size changes and update isLargeScreen state
137
- useEffect(() => {
138
- const mediaQuery = window.matchMedia("(min-width: 1024px)");
139
- const handleChange = (e) => {
140
- setIsLargeScreen(e.matches);
141
- };
142
- // Set initial value
143
- setIsLargeScreen(mediaQuery.matches);
144
- // Listen for changes
145
- mediaQuery.addEventListener("change", handleChange);
146
- return () => {
147
- mediaQuery.removeEventListener("change", handleChange);
148
- };
149
- }, []);
133
+ }, [client, connectionStatus]);
150
134
  // Handle prompt hover - temporarily show the full prompt as placeholder
151
135
  const handlePromptHover = (prompt) => {
152
136
  setPlaceholder(prompt);
@@ -159,9 +143,8 @@ export function ChatView({ client, initialSessionId, error: initError, debuggerU
159
143
  if (initError) {
160
144
  return (_jsx("div", { className: "flex items-center justify-center h-screen bg-background", children: _jsxs("div", { className: "text-center p-8 max-w-md", children: [_jsx("h1", { className: "text-2xl font-bold text-destructive mb-4", children: "Initialization Error" }), _jsx("p", { className: "text-foreground mb-4", children: initError }), _jsx("p", { className: "text-sm text-muted-foreground", children: "Failed to initialize the ACP client. Check the console for details." })] }) }));
161
145
  }
162
- const todos = useChatStore(selectTodosForCurrentSession);
163
146
  // Dummy sources data based on Figma design
164
- const sources = [
147
+ const _sources = [
165
148
  {
166
149
  id: "1",
167
150
  title: "Boeing Scores Early Wins at Dubai Airshow",
@@ -195,8 +178,6 @@ export function ChatView({ client, initialSessionId, error: initError, debuggerU
195
178
  favicon: "https://www.google.com/s2/favicons?domain=theverge.com&sz=32",
196
179
  },
197
180
  ];
198
- // Get the latest context size from the session context
199
- const latestContextSize = useChatStore((state) => state.latestContextSize);
200
181
  // Command menu items for chat input
201
182
  const commandMenuItems = [
202
183
  {
@@ -210,13 +191,15 @@ export function ChatView({ client, initialSessionId, error: initError, debuggerU
210
191
  },
211
192
  },
212
193
  ];
213
- 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) => {
194
+ 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, showHeader: messages.length > 0, sessionId: sessionId, ...(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 ? (
195
+ // Only render empty state once agent info is loaded
196
+ agentName !== undefined ? (_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) => {
214
197
  sendMessage(prompt);
215
198
  setPlaceholder("Type a message or / for commands...");
216
199
  logger.info("Prompt clicked", { prompt });
217
200
  }, onPromptHover: handlePromptHover, onPromptLeave: handlePromptLeave, onOpenFiles: openFiles, onOpenSettings: openSettings, toolsAndMcpsCount: agentTools.length +
218
201
  agentMcps.length +
219
- agentSubagents.length }) })) })) : (_jsx("div", { className: "flex flex-col px-4", children: messages.map((message, index) => {
202
+ agentSubagents.length }) })) })) : null) : (_jsx("div", { className: "flex flex-col px-4", children: messages.map((message, index) => {
220
203
  // Calculate dynamic spacing based on message sequence
221
204
  const isFirst = index === 0;
222
205
  const previousMessage = isFirst
@@ -241,5 +224,5 @@ export function ChatView({ client, initialSessionId, error: initError, debuggerU
241
224
  : "mt-6";
242
225
  }
243
226
  return (_jsx(Message, { message: message, className: spacingClass, isLastMessage: index === messages.length - 1, children: _jsx(MessageContent, { message: message, thinkingDisplayStyle: "collapsible" }) }, message.id));
244
- }) })) }), _jsx(ChatLayout.Footer, { children: _jsx(ChatInputWithAttachments, { client: client, startSession: startSession, placeholder: placeholder, latestContextSize: latestContextSize, commandMenuItems: commandMenuItems }) })] })] }), isLargeScreen && (_jsx(ChatLayout.Aside, { breakpoint: "lg", children: _jsx(AsideTabs, { todos: todos, tools: agentTools, mcps: agentMcps, subagents: agentSubagents }) }))] }) })] }));
227
+ }) })) }), _jsx(ChatLayout.Footer, { children: _jsx(ChatInputWithAttachments, { client: client, startSession: startSession, placeholder: placeholder, commandMenuItems: commandMenuItems }) })] })] }), _jsx(ChatLayout.Aside, { breakpoint: "md", children: _jsx(AsideTabs, { todos: todos, tools: agentTools, mcps: agentMcps, subagents: agentSubagents }) })] }) })] }));
245
228
  }
@@ -12,6 +12,5 @@ export interface ContextSize {
12
12
  modelContextWindow?: number;
13
13
  }
14
14
  export interface ContextUsageButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
15
- contextSize: ContextSize;
16
15
  }
17
16
  export declare const ContextUsageButton: React.ForwardRefExoticComponent<ContextUsageButtonProps & React.RefAttributes<HTMLButtonElement>>;
@@ -1,12 +1,19 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import * as React from "react";
3
+ import { useChatStore } from "../../core/store/chat-store.js";
3
4
  import { cn } from "../lib/utils.js";
4
5
  import { Button } from "./Button.js";
5
6
  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "./Tooltip.js";
6
7
  // Default context window for backward compatibility (should not be used in practice)
7
8
  const DEFAULT_MODEL_CONTEXT_WINDOW = 200000;
8
- export const ContextUsageButton = React.forwardRef(({ contextSize, className, ...props }, ref) => {
9
- // Use max of estimated and LLM-reported tokens (same logic as backend hook executor)
9
+ export const ContextUsageButton = React.forwardRef(({ className, ...props }, ref) => {
10
+ const latestContextSize = useChatStore((state) => state.latestContextSize);
11
+ // Don't render if no context size available
12
+ if (latestContextSize == null) {
13
+ return null;
14
+ }
15
+ const contextSize = latestContextSize;
16
+ // Use max of estimated and LLM-reported tokens (LLM reported as fallback if higher)
10
17
  const actualTokens = Math.max(contextSize.totalEstimated, contextSize.llmReportedInputTokens ?? 0);
11
18
  // Use model context window from backend, or default for backward compatibility
12
19
  const modelContextWindow = contextSize.modelContextWindow ?? DEFAULT_MODEL_CONTEXT_WINDOW;
@@ -36,7 +43,7 @@ export const ContextUsageButton = React.forwardRef(({ contextSize, className, ..
36
43
  const center = size / 2;
37
44
  const circumference = 2 * Math.PI * radius;
38
45
  const offset = circumference - (clampedPercentage / 100) * circumference;
39
- return (_jsx(TooltipProvider, { children: _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx(Button, { ref: ref, type: "button", variant: "ghost", size: "icon", className: cn("rounded-full cursor-default", colorClass, className), ...props, children: _jsxs("svg", { width: size, height: size, viewBox: `0 0 ${size} ${size}`, className: "transform -rotate-90", children: [_jsx("circle", { cx: center, cy: center, r: radius, stroke: "currentColor", strokeWidth: strokeWidth, fill: "transparent", className: "opacity-20" }), _jsx("circle", { cx: center, cy: center, r: radius, stroke: "currentColor", strokeWidth: strokeWidth, fill: "transparent", strokeDasharray: circumference, strokeDashoffset: offset, strokeLinecap: "round", className: "transition-all duration-300 ease-in-out" })] }) }) }), _jsx(TooltipContent, { side: "top", align: "center", className: "p-3", children: _jsxs("div", { className: "space-y-2 font-mono text-xs", children: [_jsxs("div", { className: "space-y-1", children: [_jsxs("div", { className: "flex justify-between gap-6", children: [_jsx("span", { className: "text-muted-foreground", children: "System Prompt:" }), _jsxs("div", { className: "flex gap-1 items-baseline", children: [_jsx("span", { children: contextSize.systemPromptTokens.toLocaleString() }), _jsxs("span", { className: "text-muted-foreground text-[10px]", children: ["(", calculatePercentage(contextSize.systemPromptTokens), ")"] })] })] }), contextSize.toolOverheadTokens !== undefined &&
46
+ return (_jsx(TooltipProvider, { children: _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx(Button, { ref: ref, type: "button", variant: "ghost", size: "icon", className: cn("rounded-full cursor-default", colorClass, className), ...props, children: _jsxs("svg", { width: size, height: size, viewBox: `0 0 ${size} ${size}`, className: "transform -rotate-90", children: [_jsx("title", { children: "Context usage indicator" }), _jsx("circle", { cx: center, cy: center, r: radius, stroke: "currentColor", strokeWidth: strokeWidth, fill: "transparent", className: "opacity-20" }), _jsx("circle", { cx: center, cy: center, r: radius, stroke: "currentColor", strokeWidth: strokeWidth, fill: "transparent", strokeDasharray: circumference, strokeDashoffset: offset, strokeLinecap: "round", className: "transition-all duration-300 ease-in-out" })] }) }) }), _jsx(TooltipContent, { side: "top", align: "center", className: "p-3", children: _jsxs("div", { className: "space-y-2 font-mono text-xs", children: [_jsxs("div", { className: "space-y-1", children: [_jsxs("div", { className: "flex justify-between gap-6", children: [_jsx("span", { className: "text-muted-foreground", children: "System Prompt:" }), _jsxs("div", { className: "flex gap-1 items-baseline", children: [_jsx("span", { children: contextSize.systemPromptTokens.toLocaleString() }), _jsxs("span", { className: "text-muted-foreground text-[10px]", children: ["(", calculatePercentage(contextSize.systemPromptTokens), ")"] })] })] }), contextSize.toolOverheadTokens !== undefined &&
40
47
  contextSize.toolOverheadTokens > 0 && (_jsxs("div", { className: "flex justify-between gap-6", children: [_jsx("span", { className: "text-muted-foreground", children: "Tools Definition:" }), _jsxs("div", { className: "flex gap-1 items-baseline", children: [_jsx("span", { children: contextSize.toolOverheadTokens.toLocaleString() }), _jsxs("span", { className: "text-muted-foreground text-[10px]", children: ["(", calculatePercentage(contextSize.toolOverheadTokens), ")"] })] })] })), contextSize.mcpOverheadTokens !== undefined &&
41
48
  contextSize.mcpOverheadTokens > 0 && (_jsxs("div", { className: "flex justify-between gap-6", children: [_jsx("span", { className: "text-muted-foreground", children: "MCPs Definition:" }), _jsxs("div", { className: "flex gap-1 items-baseline", children: [_jsx("span", { children: contextSize.mcpOverheadTokens.toLocaleString() }), _jsxs("span", { className: "text-muted-foreground text-[10px]", children: ["(", calculatePercentage(contextSize.mcpOverheadTokens), ")"] })] })] })), _jsxs("div", { className: "flex justify-between gap-6", children: [_jsx("span", { className: "text-muted-foreground", children: "User Messages:" }), _jsxs("div", { className: "flex gap-1 items-baseline", children: [_jsx("span", { children: contextSize.userMessagesTokens.toLocaleString() }), _jsxs("span", { className: "text-muted-foreground text-[10px]", children: ["(", calculatePercentage(contextSize.userMessagesTokens), ")"] })] })] }), _jsxs("div", { className: "flex justify-between gap-6", children: [_jsx("span", { className: "text-muted-foreground", children: "Assistant Messages:" }), _jsxs("div", { className: "flex gap-1 items-baseline", children: [_jsx("span", { children: contextSize.assistantMessagesTokens.toLocaleString() }), _jsxs("span", { className: "text-muted-foreground text-[10px]", children: ["(", calculatePercentage(contextSize.assistantMessagesTokens), ")"] })] })] }), _jsxs("div", { className: "flex justify-between gap-6", children: [_jsx("span", { className: "text-muted-foreground", children: "Tool Inputs:" }), _jsxs("div", { className: "flex gap-1 items-baseline", children: [_jsx("span", { children: contextSize.toolInputTokens.toLocaleString() }), _jsxs("span", { className: "text-muted-foreground text-[10px]", children: ["(", calculatePercentage(contextSize.toolInputTokens), ")"] })] })] }), _jsxs("div", { className: "flex justify-between gap-6", children: [_jsx("span", { className: "text-muted-foreground", children: "Tool Results:" }), _jsxs("div", { className: "flex gap-1 items-baseline", children: [_jsx("span", { children: contextSize.toolResultsTokens.toLocaleString() }), _jsxs("span", { className: "text-muted-foreground text-[10px]", children: ["(", calculatePercentage(contextSize.toolResultsTokens), ")"] })] })] })] }), _jsx("div", { className: "border-t border-border pt-2", children: _jsxs("div", { className: cn("flex justify-end gap-2 font-semibold", colorClass), children: [_jsx("span", { children: actualTokens.toLocaleString() }), _jsxs("span", { children: ["(", formattedPercentage, ")"] })] }) })] }) })] }) }));
42
49
  });
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { AlertCircle, AlertTriangle, Archive, CheckCircle2, ChevronDown, Loader2, Scissors, } from "lucide-react";
3
- import React, { useEffect, useState } from "react";
3
+ import { useEffect, useState } from "react";
4
4
  /**
5
5
  * Get display information for a hook type
6
6
  */
@@ -55,6 +55,7 @@ export function HookNotification({ notification }) {
55
55
  }
56
56
  // Calculate initial elapsed time
57
57
  const updateElapsed = () => {
58
+ // biome-ignore lint/style/noNonNullAssertion: triggeredAt is guaranteed to exist for hook_triggered notifications
58
59
  const elapsed = (Date.now() - notification.triggeredAt) / 1000;
59
60
  setElapsedTime(elapsed);
60
61
  };