@townco/ui 0.1.68 → 0.1.70

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist/core/hooks/use-chat-messages.d.ts +6 -1
  2. package/dist/core/hooks/use-chat-session.d.ts +1 -1
  3. package/dist/core/hooks/use-tool-calls.d.ts +6 -1
  4. package/dist/core/schemas/chat.d.ts +10 -0
  5. package/dist/core/schemas/tool-call.d.ts +13 -8
  6. package/dist/core/schemas/tool-call.js +8 -0
  7. package/dist/core/utils/tool-call-state.d.ts +30 -0
  8. package/dist/core/utils/tool-call-state.js +73 -0
  9. package/dist/core/utils/tool-summary.d.ts +13 -0
  10. package/dist/core/utils/tool-summary.js +172 -0
  11. package/dist/core/utils/tool-verbiage.d.ts +28 -0
  12. package/dist/core/utils/tool-verbiage.js +185 -0
  13. package/dist/gui/components/AppSidebar.d.ts +22 -0
  14. package/dist/gui/components/AppSidebar.js +22 -0
  15. package/dist/gui/components/ChatLayout.d.ts +5 -0
  16. package/dist/gui/components/ChatLayout.js +130 -138
  17. package/dist/gui/components/ChatView.js +42 -118
  18. package/dist/gui/components/HookNotification.d.ts +9 -0
  19. package/dist/gui/components/HookNotification.js +50 -0
  20. package/dist/gui/components/MessageContent.js +151 -39
  21. package/dist/gui/components/SessionHistory.d.ts +10 -0
  22. package/dist/gui/components/SessionHistory.js +101 -0
  23. package/dist/gui/components/SessionHistoryItem.d.ts +11 -0
  24. package/dist/gui/components/SessionHistoryItem.js +24 -0
  25. package/dist/gui/components/Sheet.d.ts +25 -0
  26. package/dist/gui/components/Sheet.js +36 -0
  27. package/dist/gui/components/Sidebar.d.ts +65 -0
  28. package/dist/gui/components/Sidebar.js +231 -0
  29. package/dist/gui/components/SidebarToggle.d.ts +3 -0
  30. package/dist/gui/components/SidebarToggle.js +9 -0
  31. package/dist/gui/components/SubAgentDetails.js +13 -2
  32. package/dist/gui/components/ToolCallList.js +3 -3
  33. package/dist/gui/components/ToolOperation.d.ts +11 -0
  34. package/dist/gui/components/ToolOperation.js +329 -0
  35. package/dist/gui/components/WorkProgress.d.ts +20 -0
  36. package/dist/gui/components/WorkProgress.js +79 -0
  37. package/dist/gui/components/index.d.ts +8 -1
  38. package/dist/gui/components/index.js +9 -1
  39. package/dist/gui/hooks/index.d.ts +1 -0
  40. package/dist/gui/hooks/index.js +1 -0
  41. package/dist/gui/hooks/use-mobile.d.ts +1 -0
  42. package/dist/gui/hooks/use-mobile.js +15 -0
  43. package/dist/gui/index.d.ts +1 -0
  44. package/dist/gui/index.js +2 -0
  45. package/dist/gui/lib/motion.d.ts +55 -0
  46. package/dist/gui/lib/motion.js +217 -0
  47. package/dist/sdk/schemas/message.d.ts +2 -2
  48. package/dist/sdk/schemas/session.d.ts +5 -0
  49. package/dist/sdk/transports/types.d.ts +5 -0
  50. package/package.json +8 -7
  51. package/src/styles/global.css +128 -1
@@ -0,0 +1,50 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { AlertCircle, Archive, CheckCircle2, ChevronDown, Scissors, } from "lucide-react";
3
+ import React, { useState } from "react";
4
+ /**
5
+ * Get display information for a hook type
6
+ */
7
+ function getHookDisplayInfo(hookType, _callback) {
8
+ if (hookType === "context_size") {
9
+ return {
10
+ icon: Archive,
11
+ title: "Context Compacted",
12
+ };
13
+ }
14
+ if (hookType === "tool_response") {
15
+ return {
16
+ icon: Scissors,
17
+ title: "Tool Response Compacted",
18
+ };
19
+ }
20
+ // Fallback for unknown hook types
21
+ return {
22
+ icon: Archive,
23
+ title: `Hook Executed`,
24
+ };
25
+ }
26
+ /**
27
+ * Format a number with thousand separators
28
+ */
29
+ function formatNumber(num) {
30
+ return num.toLocaleString();
31
+ }
32
+ /**
33
+ * HookNotification component - displays a hook notification inline with messages
34
+ * Only shows completed or error states (not intermediate "triggered" state)
35
+ */
36
+ export function HookNotification({ notification }) {
37
+ const [isExpanded, setIsExpanded] = useState(false);
38
+ const { icon: IconComponent, title } = getHookDisplayInfo(notification.hookType, notification.callback);
39
+ const isCompleted = notification.status === "completed";
40
+ const isError = notification.status === "error";
41
+ // Build subtitle showing key info
42
+ let subtitle = "";
43
+ if (isCompleted && notification.metadata?.tokensSaved !== undefined) {
44
+ subtitle = `${formatNumber(notification.metadata.tokensSaved)} tokens saved`;
45
+ }
46
+ else if (isError && notification.error) {
47
+ subtitle = notification.error;
48
+ }
49
+ return (_jsxs("div", { className: "flex flex-col my-3", children: [_jsxs("button", { type: "button", className: "flex flex-col items-start gap-0.5 cursor-pointer bg-transparent border-none p-0 text-left group w-fit", onClick: () => setIsExpanded(!isExpanded), "aria-expanded": isExpanded, children: [_jsxs("div", { className: "flex items-center gap-1.5 text-[11px] font-medium text-muted-foreground", children: [_jsx("div", { className: isError ? "text-destructive" : "text-muted-foreground", children: _jsx(IconComponent, { className: "h-3 w-3" }) }), _jsx("span", { className: "text-paragraph-sm text-muted-foreground", children: title }), isCompleted && _jsx(CheckCircle2, { className: "h-3 w-3 text-green-500" }), isError && _jsx(AlertCircle, { className: "h-3 w-3 text-destructive" }), _jsx(ChevronDown, { className: `h-3 w-3 text-muted-foreground/70 transition-transform duration-200 ${isExpanded ? "rotate-180" : ""}` })] }), subtitle && (_jsx("span", { className: `text-paragraph-sm pl-4.5 ${isError ? "text-destructive/70" : "text-muted-foreground/70"}`, children: subtitle }))] }), isExpanded && (_jsxs("div", { className: "mt-2 text-sm border border-border rounded-lg bg-card overflow-hidden w-full", children: [_jsxs("div", { className: "p-3 border-b border-border", children: [_jsx("div", { className: "text-[10px] font-bold text-muted-foreground uppercase tracking-wider mb-1.5 font-sans", children: "Hook Details" }), _jsxs("div", { className: "space-y-1 text-[11px]", children: [_jsxs("div", { className: "flex gap-2", children: [_jsx("span", { className: "text-muted-foreground", children: "Type:" }), _jsx("span", { className: "text-foreground font-mono", children: notification.hookType })] }), _jsxs("div", { className: "flex gap-2", children: [_jsx("span", { className: "text-muted-foreground", children: "Callback:" }), _jsx("span", { className: "text-foreground font-mono", children: notification.callback })] })] })] }), notification.metadata && (_jsxs("div", { className: "p-3 border-b border-border last:border-0", children: [_jsx("div", { className: "text-[10px] font-bold text-muted-foreground uppercase tracking-wider mb-1.5 font-sans", children: "Result" }), _jsxs("div", { className: "space-y-1 text-[11px]", children: [notification.metadata.action && (_jsxs("div", { className: "flex gap-2", children: [_jsx("span", { className: "text-muted-foreground", children: "Action:" }), _jsx("span", { className: "text-foreground", children: notification.metadata.action })] })), notification.metadata.messagesRemoved !== undefined && (_jsxs("div", { className: "flex gap-2", children: [_jsx("span", { className: "text-muted-foreground", children: "Messages Removed:" }), _jsx("span", { className: "text-foreground", children: formatNumber(notification.metadata.messagesRemoved) })] })), notification.metadata.tokensSaved !== undefined && (_jsxs("div", { className: "flex gap-2", children: [_jsx("span", { className: "text-muted-foreground", children: "Tokens Saved:" }), _jsx("span", { className: "text-green-500 font-medium", children: formatNumber(notification.metadata.tokensSaved) })] }))] })] })), notification.error && (_jsxs("div", { className: "p-3 border-b border-border last:border-0", children: [_jsx("div", { className: "text-[10px] font-bold text-destructive uppercase tracking-wider mb-1.5 font-sans", children: "Error" }), _jsx("div", { className: "text-[11px] text-destructive font-mono", children: notification.error })] })), notification.completedAt && (_jsxs("div", { className: "p-2 bg-muted/50 border-t border-border text-[10px] text-muted-foreground font-sans", children: ["Executed:", " ", new Date(notification.completedAt).toLocaleTimeString()] }))] }))] }));
50
+ }
@@ -1,13 +1,14 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { cva } from "class-variance-authority";
3
+ import { AnimatePresence, motion, useReducedMotion } from "framer-motion";
3
4
  import * as React from "react";
4
5
  import { useChatStore } from "../../core/store/chat-store.js";
6
+ import { isPreliminaryToolCall } from "../../core/utils/tool-call-state.js";
7
+ import { getDuration, getTransition, motionEasing, shimmerTransition, } from "../lib/motion.js";
5
8
  import { cn } from "../lib/utils.js";
6
- import { InvokingGroup } from "./InvokingGroup.js";
7
9
  import { Reasoning } from "./Reasoning.js";
8
10
  import { Response } from "./Response.js";
9
- import { ToolCall } from "./ToolCall.js";
10
- import { ToolCallGroup } from "./ToolCallGroup.js";
11
+ import { ToolOperation } from "./ToolOperation.js";
11
12
  /**
12
13
  * MessageContent component inspired by shadcn.io/ai
13
14
  * Provides the content container with role-based styling
@@ -35,6 +36,7 @@ export const MessageContent = React.forwardRef(({ role: roleProp, variant, isStr
35
36
  // Get streaming start time and current model from store
36
37
  const streamingStartTime = useChatStore((state) => state.streamingStartTime);
37
38
  const currentModel = useChatStore((state) => state.currentModel);
39
+ const shouldReduceMotion = useReducedMotion();
38
40
  // Use smart rendering if message is provided and no custom children
39
41
  const useSmartRendering = message && !children;
40
42
  // Derive props from message if using smart rendering
@@ -49,33 +51,68 @@ export const MessageContent = React.forwardRef(({ role: roleProp, variant, isStr
49
51
  const hasThinking = !!thinking;
50
52
  // Check if waiting (streaming but no content yet)
51
53
  const isWaiting = message.isStreaming && !message.content && message.role === "assistant";
52
- content = (_jsxs(_Fragment, { children: [message.role === "assistant" && hasThinking && (_jsx(Reasoning, { content: thinking, isStreaming: message.isStreaming, mode: thinkingDisplayStyle, autoCollapse: true })), isWaiting && streamingStartTime && (_jsx("div", { className: "flex items-center gap-2", children: _jsx("div", { className: "size-2.5 rounded-full bg-foreground animate-pulse-scale" }) })), message.role === "assistant" ? ((() => {
54
+ content = (_jsxs(_Fragment, { children: [message.role === "assistant" && hasThinking && (_jsx(motion.div, { initial: {
55
+ filter: "blur(12px)",
56
+ opacity: 0,
57
+ y: 12,
58
+ }, animate: {
59
+ filter: "blur(0px)",
60
+ opacity: 1,
61
+ y: 0,
62
+ }, exit: {
63
+ filter: "blur(12px)",
64
+ opacity: 0,
65
+ y: -12,
66
+ }, transition: getTransition(shouldReduceMotion ?? false, {
67
+ duration: 0.5,
68
+ ease: motionEasing.smooth,
69
+ }), children: _jsx(Reasoning, { content: thinking, isStreaming: message.isStreaming, mode: thinkingDisplayStyle, autoCollapse: true }) })), isWaiting && streamingStartTime && (_jsx(motion.div, { initial: {
70
+ filter: "blur(12px)",
71
+ opacity: 0,
72
+ y: 12,
73
+ }, animate: {
74
+ filter: "blur(0px)",
75
+ opacity: 1,
76
+ y: 0,
77
+ }, exit: {
78
+ filter: "blur(12px)",
79
+ opacity: 0,
80
+ y: -12,
81
+ }, transition: getTransition(shouldReduceMotion ?? false, {
82
+ duration: 0.4,
83
+ ease: motionEasing.smooth,
84
+ }), children: _jsx(motion.div, { className: "flex flex-col my-4 rounded-md px-1 -mx-1 w-fit", animate: {
85
+ backgroundPosition: ["-200% 0", "200% 0"],
86
+ }, transition: {
87
+ ...shimmerTransition,
88
+ duration: getDuration(shouldReduceMotion ?? false, 1.5),
89
+ }, style: {
90
+ backgroundImage: "linear-gradient(90deg, transparent 5%, rgba(255, 255, 255, 0.75) 25%, transparent 35%)",
91
+ backgroundSize: "200% 100%",
92
+ }, children: _jsx("div", { className: "flex items-center gap-1.5", children: _jsx("span", { className: "text-paragraph-sm text-text-secondary/70", children: "Thinking..." }) }) }) })), message.role === "assistant" ? ((() => {
53
93
  // Sort tool calls by content position
54
94
  const sortedToolCalls = (message.toolCalls || [])
55
95
  .slice()
56
96
  .sort((a, b) => (a.contentPosition ?? Infinity) -
57
97
  (b.contentPosition ?? Infinity));
58
- // Helper to check if a tool call is preliminary (invoking)
59
- const isPreliminary = (tc) => tc.status === "pending" &&
60
- (!tc.rawInput || Object.keys(tc.rawInput).length === 0);
61
98
  // Helper to group tool calls by batchId, consecutive same-title calls, or consecutive preliminary calls
62
99
  const groupToolCalls = (toolCalls) => {
63
100
  const result = [];
64
101
  const batchGroups = new Map();
65
- let currentInvokingGroup = [];
102
+ let currentSelectingGroup = [];
66
103
  let currentConsecutiveGroup = [];
67
104
  let currentConsecutiveTitle = null;
68
- const flushInvokingGroup = () => {
69
- if (currentInvokingGroup.length > 1) {
105
+ const flushSelectingGroup = () => {
106
+ if (currentSelectingGroup.length > 1) {
70
107
  result.push({
71
- type: "invoking",
72
- toolCalls: currentInvokingGroup,
108
+ type: "selecting",
109
+ toolCalls: currentSelectingGroup,
73
110
  });
74
111
  }
75
- else if (currentInvokingGroup.length === 1) {
76
- result.push(currentInvokingGroup[0]);
112
+ else if (currentSelectingGroup.length === 1) {
113
+ result.push(currentSelectingGroup[0]);
77
114
  }
78
- currentInvokingGroup = [];
115
+ currentSelectingGroup = [];
79
116
  };
80
117
  const flushConsecutiveGroup = () => {
81
118
  if (currentConsecutiveGroup.length > 1) {
@@ -94,7 +131,7 @@ export const MessageContent = React.forwardRef(({ role: roleProp, variant, isStr
94
131
  for (const tc of toolCalls) {
95
132
  // Handle batch groups (explicit batchId)
96
133
  if (tc.batchId) {
97
- flushInvokingGroup();
134
+ flushSelectingGroup();
98
135
  flushConsecutiveGroup();
99
136
  const existing = batchGroups.get(tc.batchId);
100
137
  if (existing) {
@@ -106,14 +143,14 @@ export const MessageContent = React.forwardRef(({ role: roleProp, variant, isStr
106
143
  result.push({ type: "batch", toolCalls: group });
107
144
  }
108
145
  }
109
- // Handle consecutive preliminary (invoking) tool calls
110
- else if (isPreliminary(tc)) {
146
+ // Handle consecutive preliminary (selecting) tool calls
147
+ else if (isPreliminaryToolCall(tc)) {
111
148
  flushConsecutiveGroup();
112
- currentInvokingGroup.push(tc);
149
+ currentSelectingGroup.push(tc);
113
150
  }
114
151
  // Regular tool call - group consecutive same-title calls (e.g., subagent)
115
152
  else {
116
- flushInvokingGroup();
153
+ flushSelectingGroup();
117
154
  // Check if this continues a consecutive group
118
155
  if (currentConsecutiveTitle === tc.title) {
119
156
  currentConsecutiveGroup.push(tc);
@@ -127,7 +164,7 @@ export const MessageContent = React.forwardRef(({ role: roleProp, variant, isStr
127
164
  }
128
165
  }
129
166
  // Flush any remaining groups
130
- flushInvokingGroup();
167
+ flushSelectingGroup();
131
168
  flushConsecutiveGroup();
132
169
  return result;
133
170
  };
@@ -137,22 +174,37 @@ export const MessageContent = React.forwardRef(({ role: roleProp, variant, isStr
137
174
  if (typeof item === "object" &&
138
175
  "type" in item &&
139
176
  item.type === "batch") {
140
- return (_jsx(ToolCallGroup, { toolCalls: item.toolCalls }, `batch-${item.toolCalls[0]?.batchId || index}`));
177
+ return (_jsx(ToolOperation, { toolCalls: item.toolCalls, isGrouped: true }, `batch-${item.toolCalls[0]?.batchId || index}`));
141
178
  }
142
- // Invoking group (consecutive preliminary tool calls)
179
+ // Selecting group (consecutive preliminary tool calls)
143
180
  if (typeof item === "object" &&
144
181
  "type" in item &&
145
- item.type === "invoking") {
146
- return (_jsx(InvokingGroup, { toolCalls: item.toolCalls }, `invoking-${item.toolCalls[0]?.id || index}`));
182
+ item.type === "selecting") {
183
+ return (_jsx(ToolOperation, { toolCalls: item.toolCalls, isGrouped: true }, `selecting-${item.toolCalls[0]?.id || index}`));
147
184
  }
148
185
  // Single tool call
149
- return (_jsx(ToolCall, { toolCall: item }, item.id));
186
+ return (_jsx(ToolOperation, { toolCalls: [item], isGrouped: false }, item.id));
150
187
  };
151
- // If no tool calls or they don't have positions, render old way
188
+ // If no tool calls or they don't have positions, render simplified way
152
189
  if (sortedToolCalls.length === 0 ||
153
190
  !sortedToolCalls.some((tc) => tc.contentPosition !== undefined)) {
154
191
  const groupedToolCalls = groupToolCalls(sortedToolCalls);
155
- return (_jsxs(_Fragment, { children: [groupedToolCalls.length > 0 && (_jsx("div", { className: "flex flex-col gap-2 mb-1", children: groupedToolCalls.map((item, index) => renderToolCallOrGroup(item, index)) })), _jsx(Response, { content: message.content, isStreaming: message.isStreaming, showEmpty: false })] }));
192
+ return (_jsxs(_Fragment, { children: [groupedToolCalls.length > 0 && (_jsx(AnimatePresence, { mode: "popLayout", children: _jsx("div", { className: "flex flex-col gap-2 mb-1", children: groupedToolCalls.map((item, index) => renderToolCallOrGroup(item, index)) }) })), _jsx(motion.div, { initial: {
193
+ filter: "blur(12px)",
194
+ opacity: 0,
195
+ y: 12,
196
+ }, animate: {
197
+ filter: "blur(0px)",
198
+ opacity: 1,
199
+ y: 0,
200
+ }, exit: {
201
+ filter: "blur(12px)",
202
+ opacity: 0,
203
+ y: -12,
204
+ }, transition: getTransition(shouldReduceMotion ?? false, {
205
+ duration: 0.4,
206
+ ease: motionEasing.smooth,
207
+ }), children: _jsx(Response, { content: message.content, isStreaming: message.isStreaming, showEmpty: false }) })] }));
156
208
  }
157
209
  // Render content interleaved with tool calls
158
210
  // Group consecutive tool calls with the same batchId or same title
@@ -164,18 +216,18 @@ export const MessageContent = React.forwardRef(({ role: roleProp, variant, isStr
164
216
  const flushBatch = () => {
165
217
  if (currentBatch.length > 1) {
166
218
  // Group multiple consecutive calls (by batchId or same title)
167
- elements.push(_jsx(ToolCallGroup, { toolCalls: currentBatch }, `group-${currentBatchId || currentBatchTitle}-${currentBatch[0].id}`));
219
+ elements.push(_jsx(ToolOperation, { toolCalls: currentBatch, isGrouped: true }, `group-${currentBatchId || currentBatchTitle}-${currentBatch[0].id}`));
168
220
  }
169
221
  else if (currentBatch.length === 1) {
170
- elements.push(_jsx("div", { children: _jsx(ToolCall, { toolCall: currentBatch[0] }) }, `tool-${currentBatch[0].id}`));
222
+ elements.push(_jsx("div", { children: _jsx(ToolOperation, { toolCalls: [currentBatch[0]], isGrouped: false }) }, `tool-${currentBatch[0].id}`));
171
223
  }
172
224
  currentBatch = [];
173
225
  currentBatchId = undefined;
174
226
  currentBatchTitle = undefined;
175
227
  };
176
228
  // Separate preliminary tool calls - they should render at the end, not break text
177
- const preliminaryToolCalls = sortedToolCalls.filter(isPreliminary);
178
- const nonPreliminaryToolCalls = sortedToolCalls.filter((tc) => !isPreliminary(tc));
229
+ const preliminaryToolCalls = sortedToolCalls.filter(isPreliminaryToolCall);
230
+ const nonPreliminaryToolCalls = sortedToolCalls.filter((tc) => !isPreliminaryToolCall(tc));
179
231
  // Process non-preliminary tool calls inline with text
180
232
  nonPreliminaryToolCalls.forEach((toolCall, index) => {
181
233
  const position = toolCall.contentPosition ?? message.content.length;
@@ -185,7 +237,22 @@ export const MessageContent = React.forwardRef(({ role: roleProp, variant, isStr
185
237
  flushBatch();
186
238
  const textChunk = message.content.slice(currentPosition, position);
187
239
  if (textChunk) {
188
- elements.push(_jsx(Response, { content: textChunk, isStreaming: false, showEmpty: false }, `text-before-${toolCall.id}`));
240
+ elements.push(_jsx(motion.div, { initial: {
241
+ filter: "blur(12px)",
242
+ opacity: 0,
243
+ y: 12,
244
+ }, animate: {
245
+ filter: "blur(0px)",
246
+ opacity: 1,
247
+ y: 0,
248
+ }, exit: {
249
+ filter: "blur(12px)",
250
+ opacity: 0,
251
+ y: -12,
252
+ }, transition: getTransition(shouldReduceMotion ?? false, {
253
+ duration: 0.4,
254
+ ease: motionEasing.smooth,
255
+ }), children: _jsx(Response, { content: textChunk, isStreaming: false, showEmpty: false }) }, `text-before-${toolCall.id}`));
189
256
  }
190
257
  }
191
258
  // Check if this tool call should be batched (by batchId or consecutive same title)
@@ -223,20 +290,65 @@ export const MessageContent = React.forwardRef(({ role: roleProp, variant, isStr
223
290
  if (currentPosition < message.content.length) {
224
291
  const remainingText = message.content.slice(currentPosition);
225
292
  if (remainingText) {
226
- elements.push(_jsx(Response, { content: remainingText, isStreaming: message.isStreaming, showEmpty: false }, "text-end"));
293
+ elements.push(_jsx(motion.div, { initial: {
294
+ filter: "blur(12px)",
295
+ opacity: 0,
296
+ y: 12,
297
+ }, animate: {
298
+ filter: "blur(0px)",
299
+ opacity: 1,
300
+ y: 0,
301
+ }, exit: {
302
+ filter: "blur(12px)",
303
+ opacity: 0,
304
+ y: -12,
305
+ }, transition: getTransition(shouldReduceMotion ?? false, {
306
+ duration: 0.4,
307
+ ease: motionEasing.smooth,
308
+ }), children: _jsx(Response, { content: remainingText, isStreaming: message.isStreaming, showEmpty: false }) }, "text-end"));
227
309
  }
228
310
  }
229
- // Render preliminary (invoking) tool calls at the end, grouped
311
+ // Render preliminary (selecting) tool calls at the end, grouped
230
312
  if (preliminaryToolCalls.length > 0) {
231
313
  if (preliminaryToolCalls.length > 1) {
232
- elements.push(_jsx(InvokingGroup, { toolCalls: preliminaryToolCalls }, `invoking-group-${preliminaryToolCalls[0].id}`));
314
+ elements.push(_jsx(ToolOperation, { toolCalls: preliminaryToolCalls, isGrouped: true }, `selecting-group-${preliminaryToolCalls[0].id}`));
233
315
  }
234
316
  else {
235
- elements.push(_jsx("div", { children: _jsx(ToolCall, { toolCall: preliminaryToolCalls[0] }) }, `tool-${preliminaryToolCalls[0].id}`));
317
+ elements.push(_jsx("div", { children: _jsx(ToolOperation, { toolCalls: [preliminaryToolCalls[0]], isGrouped: false }) }, `tool-${preliminaryToolCalls[0].id}`));
236
318
  }
237
319
  }
238
- return _jsx(_Fragment, { children: elements });
239
- })()) : (_jsxs("div", { className: "flex flex-col gap-2", children: [message.images && message.images.length > 0 && (_jsx("div", { className: "flex flex-wrap gap-2", children: message.images.map((image, index) => (_jsx("img", { src: `data:${image.mimeType};base64,${image.data}`, alt: `Attachment ${index + 1}`, className: "max-w-[200px] max-h-[200px] rounded-lg object-cover" }, index))) })), message.content && (_jsx("div", { className: "whitespace-pre-wrap", children: message.content }))] }))] }));
320
+ return (_jsx(AnimatePresence, { mode: "popLayout", children: elements }));
321
+ })()) : (_jsxs("div", { className: "flex flex-col gap-2", children: [message.images && message.images.length > 0 && (_jsx(motion.div, { className: "flex flex-wrap gap-2", initial: {
322
+ filter: "blur(12px)",
323
+ opacity: 0,
324
+ y: 12,
325
+ }, animate: {
326
+ filter: "blur(0px)",
327
+ opacity: 1,
328
+ y: 0,
329
+ }, exit: {
330
+ filter: "blur(12px)",
331
+ opacity: 0,
332
+ y: -12,
333
+ }, transition: getTransition(shouldReduceMotion ?? false, {
334
+ duration: 0.5,
335
+ ease: motionEasing.smooth,
336
+ }), children: message.images.map((image, index) => (_jsx("img", { src: `data:${image.mimeType};base64,${image.data}`, alt: `Attachment ${index + 1}`, className: "max-w-[200px] max-h-[200px] rounded-lg object-cover" }, index))) })), message.content && (_jsx(motion.div, { className: "whitespace-pre-wrap", initial: {
337
+ filter: "blur(12px)",
338
+ opacity: 0,
339
+ y: 12,
340
+ }, animate: {
341
+ filter: "blur(0px)",
342
+ opacity: 1,
343
+ y: 0,
344
+ }, exit: {
345
+ filter: "blur(12px)",
346
+ opacity: 0,
347
+ y: -12,
348
+ }, transition: getTransition(shouldReduceMotion ?? false, {
349
+ duration: 0.4,
350
+ ease: motionEasing.smooth,
351
+ }), children: message.content }))] }))] }));
240
352
  }
241
353
  return (_jsx("div", { ref: ref, className: cn(messageContentVariants({ role, variant }), isStreaming && "animate-pulse-subtle", className), ...props, children: content }));
242
354
  });
@@ -0,0 +1,10 @@
1
+ import type { AcpClient } from "../../sdk/client/index.js";
2
+ export interface SessionHistoryProps {
3
+ client: AcpClient | null;
4
+ currentSessionId: string | null;
5
+ onSessionSelect?: ((sessionId: string) => void) | undefined;
6
+ onRenameSession?: ((sessionId: string) => void) | undefined;
7
+ onArchiveSession?: ((sessionId: string) => void) | undefined;
8
+ onDeleteSession?: ((sessionId: string) => void) | undefined;
9
+ }
10
+ export declare function SessionHistory({ client, currentSessionId, onSessionSelect, onRenameSession, onArchiveSession, onDeleteSession, }: SessionHistoryProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,101 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { createLogger } from "@townco/core";
3
+ import { Loader2 } from "lucide-react";
4
+ import { useCallback, useEffect, useState } from "react";
5
+ import { SessionHistoryItem } from "./SessionHistoryItem.js";
6
+ import { SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarMenu, useSidebar, } from "./Sidebar.js";
7
+ const logger = createLogger("session-history");
8
+ const isToday = (date) => {
9
+ const today = new Date();
10
+ return (date.getDate() === today.getDate() &&
11
+ date.getMonth() === today.getMonth() &&
12
+ date.getFullYear() === today.getFullYear());
13
+ };
14
+ const isYesterday = (date) => {
15
+ const yesterday = new Date();
16
+ yesterday.setDate(yesterday.getDate() - 1);
17
+ return (date.getDate() === yesterday.getDate() &&
18
+ date.getMonth() === yesterday.getMonth() &&
19
+ date.getFullYear() === yesterday.getFullYear());
20
+ };
21
+ const groupSessionsByDate = (sessions) => {
22
+ const now = new Date();
23
+ const oneWeekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
24
+ const oneMonthAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
25
+ return sessions.reduce((groups, session) => {
26
+ const sessionDate = new Date(session.updatedAt);
27
+ if (isToday(sessionDate)) {
28
+ groups.today.push(session);
29
+ }
30
+ else if (isYesterday(sessionDate)) {
31
+ groups.yesterday.push(session);
32
+ }
33
+ else if (sessionDate > oneWeekAgo) {
34
+ groups.lastWeek.push(session);
35
+ }
36
+ else if (sessionDate > oneMonthAgo) {
37
+ groups.lastMonth.push(session);
38
+ }
39
+ else {
40
+ groups.older.push(session);
41
+ }
42
+ return groups;
43
+ }, {
44
+ today: [],
45
+ yesterday: [],
46
+ lastWeek: [],
47
+ lastMonth: [],
48
+ older: [],
49
+ });
50
+ };
51
+ export function SessionHistory({ client, currentSessionId, onSessionSelect, onRenameSession, onArchiveSession, onDeleteSession, }) {
52
+ const { setOpenMobile } = useSidebar();
53
+ const [sessions, setSessions] = useState([]);
54
+ const [isLoading, setIsLoading] = useState(false);
55
+ const fetchSessions = useCallback(async () => {
56
+ if (!client)
57
+ return;
58
+ setIsLoading(true);
59
+ try {
60
+ const sessionList = await client.listSessions();
61
+ setSessions(sessionList);
62
+ }
63
+ catch (error) {
64
+ logger.error("Failed to fetch sessions", { error });
65
+ }
66
+ finally {
67
+ setIsLoading(false);
68
+ }
69
+ }, [client]);
70
+ // Fetch sessions on mount and when client changes
71
+ useEffect(() => {
72
+ fetchSessions();
73
+ }, [fetchSessions]);
74
+ const handleSessionSelect = (sessionId) => {
75
+ if (sessionId === currentSessionId) {
76
+ return;
77
+ }
78
+ if (onSessionSelect) {
79
+ onSessionSelect(sessionId);
80
+ }
81
+ else {
82
+ // Default behavior: update URL with session ID
83
+ const url = new URL(window.location.href);
84
+ url.searchParams.set("session", sessionId);
85
+ window.location.href = url.toString();
86
+ }
87
+ };
88
+ if (!client) {
89
+ return (_jsx(SidebarGroup, { children: _jsx(SidebarGroupContent, { children: _jsx("div", { className: "flex w-full flex-row items-center justify-center gap-2 px-2 text-sm text-muted-foreground", children: "Connect to an agent to see session history" }) }) }));
90
+ }
91
+ if (isLoading) {
92
+ return (_jsxs(SidebarGroup, { children: [_jsx(SidebarGroupLabel, { children: "Today" }), _jsx(SidebarGroupContent, { children: _jsx("div", { className: "flex flex-col", children: [44, 32, 28, 64, 52].map((item) => (_jsx("div", { className: "flex h-8 items-center gap-2 rounded-md px-2", children: _jsx("div", { className: "h-4 flex-1 rounded-md bg-sidebar-accent-foreground/10 animate-pulse", style: {
93
+ maxWidth: `${item}%`,
94
+ } }) }, item))) }) })] }));
95
+ }
96
+ if (sessions.length === 0) {
97
+ return (_jsx(SidebarGroup, { children: _jsx(SidebarGroupContent, { children: _jsx("div", { className: "flex w-full flex-row items-center justify-center gap-2 px-2 py-4 text-sm text-muted-foreground", children: "Your sessions will appear here once you start chatting!" }) }) }));
98
+ }
99
+ const groupedSessions = groupSessionsByDate(sessions);
100
+ return (_jsx(SidebarGroup, { children: _jsx(SidebarGroupContent, { children: _jsx(SidebarMenu, { children: _jsxs("div", { className: "flex flex-col gap-6", children: [groupedSessions.today.length > 0 && (_jsxs("div", { children: [_jsx("div", { className: "px-2 py-1 text-sidebar-foreground/50 text-xs", children: "Today" }), groupedSessions.today.map((session) => (_jsx(SessionHistoryItem, { session: session, isActive: session.sessionId === currentSessionId, onSelect: handleSessionSelect, onRename: onRenameSession, onArchive: onArchiveSession, onDelete: onDeleteSession, setOpenMobile: setOpenMobile }, session.sessionId)))] })), groupedSessions.yesterday.length > 0 && (_jsxs("div", { children: [_jsx("div", { className: "px-2 py-1 text-sidebar-foreground/50 text-xs", children: "Yesterday" }), groupedSessions.yesterday.map((session) => (_jsx(SessionHistoryItem, { session: session, isActive: session.sessionId === currentSessionId, onSelect: handleSessionSelect, onRename: onRenameSession, onArchive: onArchiveSession, onDelete: onDeleteSession, setOpenMobile: setOpenMobile }, session.sessionId)))] })), groupedSessions.lastWeek.length > 0 && (_jsxs("div", { children: [_jsx("div", { className: "px-2 py-1 text-sidebar-foreground/50 text-xs", children: "Last 7 days" }), groupedSessions.lastWeek.map((session) => (_jsx(SessionHistoryItem, { session: session, isActive: session.sessionId === currentSessionId, onSelect: handleSessionSelect, onRename: onRenameSession, onArchive: onArchiveSession, onDelete: onDeleteSession, setOpenMobile: setOpenMobile }, session.sessionId)))] })), groupedSessions.lastMonth.length > 0 && (_jsxs("div", { children: [_jsx("div", { className: "px-2 py-1 text-sidebar-foreground/50 text-xs", children: "Last 30 days" }), groupedSessions.lastMonth.map((session) => (_jsx(SessionHistoryItem, { session: session, isActive: session.sessionId === currentSessionId, onSelect: handleSessionSelect, onRename: onRenameSession, onArchive: onArchiveSession, onDelete: onDeleteSession, setOpenMobile: setOpenMobile }, session.sessionId)))] })), groupedSessions.older.length > 0 && (_jsxs("div", { children: [_jsx("div", { className: "px-2 py-1 text-sidebar-foreground/50 text-xs", children: "Older" }), groupedSessions.older.map((session) => (_jsx(SessionHistoryItem, { session: session, isActive: session.sessionId === currentSessionId, onSelect: handleSessionSelect, onRename: onRenameSession, onArchive: onArchiveSession, onDelete: onDeleteSession, setOpenMobile: setOpenMobile }, session.sessionId)))] }))] }) }) }) }));
101
+ }
@@ -0,0 +1,11 @@
1
+ import type { SessionSummary } from "../../sdk/transports/index.js";
2
+ export interface SessionHistoryItemProps {
3
+ session: SessionSummary;
4
+ isActive: boolean;
5
+ onSelect: (sessionId: string) => void;
6
+ onRename?: ((sessionId: string) => void) | undefined;
7
+ onArchive?: ((sessionId: string) => void) | undefined;
8
+ onDelete?: ((sessionId: string) => void) | undefined;
9
+ setOpenMobile: (open: boolean) => void;
10
+ }
11
+ export declare const SessionHistoryItem: import("react").MemoExoticComponent<({ session, isActive, onSelect, onRename, onArchive, onDelete, setOpenMobile, }: SessionHistoryItemProps) => import("react/jsx-runtime").JSX.Element>;
@@ -0,0 +1,24 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { MoreHorizontal } from "lucide-react";
3
+ import { memo } from "react";
4
+ import { cn } from "../lib/utils.js";
5
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "./DropdownMenu.js";
6
+ import { SidebarMenuAction, SidebarMenuButton, SidebarMenuItem, } from "./Sidebar.js";
7
+ const PureSessionHistoryItem = ({ session, isActive, onSelect, onRename, onArchive, onDelete, setOpenMobile, }) => {
8
+ return (_jsxs(SidebarMenuItem, { children: [_jsx(SidebarMenuButton, { asChild: true, isActive: isActive, onClick: () => {
9
+ onSelect(session.sessionId);
10
+ setOpenMobile(false);
11
+ }, children: _jsx("button", { type: "button", className: "w-full", children: _jsx("span", { className: "truncate", children: session.firstUserMessage || "Empty session" }) }) }), _jsxs(DropdownMenu, { modal: true, children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(SidebarMenuAction, { className: "mr-0.5 data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground", showOnHover: !isActive, children: [_jsx(MoreHorizontal, {}), _jsx("span", { className: "sr-only", children: "More" })] }) }), _jsxs(DropdownMenuContent, { align: "end", side: "bottom", children: [_jsx(DropdownMenuItem, { className: "cursor-pointer", disabled: !onRename, onSelect: () => onRename?.(session.sessionId), children: "Rename" }), _jsx(DropdownMenuItem, { className: "cursor-pointer", disabled: !onArchive, onSelect: () => onArchive?.(session.sessionId), children: "Archive" }), _jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuItem, { className: cn("cursor-pointer text-destructive focus:bg-destructive/15 focus:text-destructive"), disabled: !onDelete, onSelect: () => onDelete?.(session.sessionId), children: "Delete" })] })] })] }));
12
+ };
13
+ export const SessionHistoryItem = memo(PureSessionHistoryItem, (prevProps, nextProps) => {
14
+ if (prevProps.isActive !== nextProps.isActive) {
15
+ return false;
16
+ }
17
+ if (prevProps.session.sessionId !== nextProps.session.sessionId) {
18
+ return false;
19
+ }
20
+ if (prevProps.session.firstUserMessage !== nextProps.session.firstUserMessage) {
21
+ return false;
22
+ }
23
+ return true;
24
+ });
@@ -0,0 +1,25 @@
1
+ import * as DialogPrimitive from "@radix-ui/react-dialog";
2
+ import { type VariantProps } from "class-variance-authority";
3
+ import * as React from "react";
4
+ declare const Sheet: React.FC<DialogPrimitive.DialogProps>;
5
+ declare const SheetTrigger: React.ForwardRefExoticComponent<DialogPrimitive.DialogTriggerProps & React.RefAttributes<HTMLButtonElement>>;
6
+ declare const SheetClose: React.ForwardRefExoticComponent<DialogPrimitive.DialogCloseProps & React.RefAttributes<HTMLButtonElement>>;
7
+ declare const SheetPortal: React.FC<DialogPrimitive.DialogPortalProps>;
8
+ declare const SheetOverlay: React.ForwardRefExoticComponent<Omit<DialogPrimitive.DialogOverlayProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
9
+ declare const sheetVariants: (props?: ({
10
+ side?: "top" | "right" | "bottom" | "left" | null | undefined;
11
+ } & import("class-variance-authority/types").ClassProp) | undefined) => string;
12
+ interface SheetContentProps extends React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>, VariantProps<typeof sheetVariants> {
13
+ }
14
+ declare const SheetContent: React.ForwardRefExoticComponent<SheetContentProps & React.RefAttributes<HTMLDivElement>>;
15
+ declare const SheetHeader: {
16
+ ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>): import("react/jsx-runtime").JSX.Element;
17
+ displayName: string;
18
+ };
19
+ declare const SheetFooter: {
20
+ ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>): import("react/jsx-runtime").JSX.Element;
21
+ displayName: string;
22
+ };
23
+ declare const SheetTitle: React.ForwardRefExoticComponent<Omit<DialogPrimitive.DialogTitleProps & React.RefAttributes<HTMLHeadingElement>, "ref"> & React.RefAttributes<HTMLHeadingElement>>;
24
+ declare const SheetDescription: React.ForwardRefExoticComponent<Omit<DialogPrimitive.DialogDescriptionProps & React.RefAttributes<HTMLParagraphElement>, "ref"> & React.RefAttributes<HTMLParagraphElement>>;
25
+ export { Sheet, SheetPortal, SheetOverlay, SheetTrigger, SheetClose, SheetContent, SheetHeader, SheetFooter, SheetTitle, SheetDescription, };
@@ -0,0 +1,36 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import * as DialogPrimitive from "@radix-ui/react-dialog";
3
+ import { cva } from "class-variance-authority";
4
+ import { X } from "lucide-react";
5
+ import * as React from "react";
6
+ import { cn } from "../lib/utils.js";
7
+ const Sheet = DialogPrimitive.Root;
8
+ const SheetTrigger = DialogPrimitive.Trigger;
9
+ const SheetClose = DialogPrimitive.Close;
10
+ const SheetPortal = DialogPrimitive.Portal;
11
+ const SheetOverlay = React.forwardRef(({ className, ...props }, ref) => (_jsx(DialogPrimitive.Overlay, { className: cn("fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", className), ...props, ref: ref })));
12
+ SheetOverlay.displayName = DialogPrimitive.Overlay.displayName;
13
+ const sheetVariants = cva("fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500", {
14
+ variants: {
15
+ side: {
16
+ top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
17
+ bottom: "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
18
+ left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
19
+ right: "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
20
+ },
21
+ },
22
+ defaultVariants: {
23
+ side: "right",
24
+ },
25
+ });
26
+ const SheetContent = React.forwardRef(({ side = "right", className, children, ...props }, ref) => (_jsxs(SheetPortal, { children: [_jsx(SheetOverlay, {}), _jsxs(DialogPrimitive.Content, { ref: ref, className: cn(sheetVariants({ side }), className), ...props, children: [children, _jsxs(DialogPrimitive.Close, { className: "absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary", children: [_jsx(X, { className: "h-4 w-4" }), _jsx("span", { className: "sr-only", children: "Close" })] })] })] })));
27
+ SheetContent.displayName = DialogPrimitive.Content.displayName;
28
+ const SheetHeader = ({ className, ...props }) => (_jsx("div", { className: cn("flex flex-col space-y-2 text-center sm:text-left", className), ...props }));
29
+ SheetHeader.displayName = "SheetHeader";
30
+ const SheetFooter = ({ className, ...props }) => (_jsx("div", { className: cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className), ...props }));
31
+ SheetFooter.displayName = "SheetFooter";
32
+ const SheetTitle = React.forwardRef(({ className, ...props }, ref) => (_jsx(DialogPrimitive.Title, { ref: ref, className: cn("text-lg font-semibold text-foreground", className), ...props })));
33
+ SheetTitle.displayName = DialogPrimitive.Title.displayName;
34
+ const SheetDescription = React.forwardRef(({ className, ...props }, ref) => (_jsx(DialogPrimitive.Description, { ref: ref, className: cn("text-sm text-muted-foreground", className), ...props })));
35
+ SheetDescription.displayName = DialogPrimitive.Description.displayName;
36
+ export { Sheet, SheetPortal, SheetOverlay, SheetTrigger, SheetClose, SheetContent, SheetHeader, SheetFooter, SheetTitle, SheetDescription, };