@townco/ui 0.1.77 → 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,12 +1,9 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import JsonView from "@uiw/react-json-view";
3
- import { AnimatePresence, motion, useReducedMotion } from "framer-motion";
4
3
  import { AlertCircle, BrainCircuit, CheckSquare, ChevronDown, ChevronRight, CircleDot, Cloud, Edit, FileText, Globe, Image, Link, ListVideo, Search, Wrench, } from "lucide-react";
5
4
  import React, { useEffect, useState } from "react";
6
- import { areAllToolCallsCompleted, getGroupDisplayState, getToolCallDisplayState, getToolCallStateVerbiage, hasAnyToolCallFailed, isPreliminaryToolCall, } from "../../core/utils/tool-call-state.js";
5
+ import { getGroupDisplayState, getToolCallDisplayState, getToolCallStateVerbiage, isPreliminaryToolCall, } from "../../core/utils/tool-call-state.js";
7
6
  import { generateSmartSummary } from "../../core/utils/tool-summary.js";
8
- import { expandCollapseVariants, fadeInVariants, getDuration, getTransition, motionDuration, motionEasing, rotateVariants, shimmerTransition, standardTransition, } from "../lib/motion.js";
9
- import { cn } from "../lib/utils.js";
10
7
  import * as ChatLayout from "./ChatLayout.js";
11
8
  import { SubAgentDetails } from "./SubAgentDetails.js";
12
9
  import { useTheme } from "./ThemeProvider.js";
@@ -35,8 +32,6 @@ export function ToolOperation({ toolCalls, isGrouped = false, autoMinimize = tru
35
32
  const [isExpanded, setIsExpanded] = useState(false);
36
33
  const [isMinimized, setIsMinimized] = useState(false);
37
34
  const [userInteracted, setUserInteracted] = useState(false);
38
- const { resolvedTheme } = useTheme();
39
- const shouldReduceMotion = useReducedMotion();
40
35
  // For single tool call, extract it
41
36
  const singleToolCall = toolCalls.length === 1 ? toolCalls[0] : null;
42
37
  const isTodoWrite = singleToolCall?.title === "todo_write";
@@ -63,7 +58,6 @@ export function ToolOperation({ toolCalls, isGrouped = false, autoMinimize = tru
63
58
  const isCompleted = displayState === "completed";
64
59
  const isFailed = displayState === "failed";
65
60
  const isSelecting = displayState === "selecting";
66
- const isActive = displayState === "executing";
67
61
  // Auto-minimize when completed (only if user hasn't manually interacted)
68
62
  useEffect(() => {
69
63
  if (autoMinimize && isCompleted && !isMinimized && !userInteracted) {
@@ -136,64 +130,14 @@ export function ToolOperation({ toolCalls, isGrouped = false, autoMinimize = tru
136
130
  const displayText = getDisplayText();
137
131
  // For preliminary/selecting states, show simple non-expandable text
138
132
  if (isSelecting && !isGrouped) {
139
- return (_jsx(motion.div, { className: "flex flex-col my-4 rounded-md px-1 -mx-1 w-fit", animate: {
140
- backgroundPosition: ["200% 0", "-200% 0"],
141
- }, transition: {
142
- ...shimmerTransition,
143
- duration: getDuration(shouldReduceMotion ?? false, 1.5),
144
- }, style: {
145
- backgroundImage: "linear-gradient(90deg, transparent 5%, rgba(255, 255, 255, 0.75) 25%, transparent 35%)",
146
- backgroundSize: "200% 100%",
147
- }, children: _jsx("span", { className: "text-paragraph-sm text-text-secondary/50", children: displayText }) }));
133
+ return (_jsx("div", { className: "flex flex-col my-4 rounded-md px-1 -mx-1 w-fit", children: _jsx("span", { className: "text-paragraph-sm text-text-secondary/50", children: displayText }) }));
148
134
  }
149
135
  // If it's a grouped preliminary (selecting) state
150
136
  if (isSelecting && isGrouped) {
151
- return (_jsxs(motion.div, { className: "flex flex-col my-4 rounded-md px-1 -mx-1 w-fit", animate: {
152
- backgroundPosition: ["200% 0", "-200% 0"],
153
- }, transition: {
154
- ...shimmerTransition,
155
- duration: getDuration(shouldReduceMotion ?? false, 1.5),
156
- }, style: {
157
- backgroundImage: "linear-gradient(90deg, transparent 5%, rgba(255, 255, 255, 0.75) 25%, transparent 35%)",
158
- backgroundSize: "200% 100%",
159
- }, children: [_jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx("div", { className: "text-text-secondary/70", children: _jsx(ListVideo, { className: "h-3 w-3" }) }), _jsx("span", { className: "text-paragraph-sm text-text-secondary/70", children: "Selecting tools" }), _jsx("span", { className: "text-[10px] bg-muted px-1.5 py-0.5 rounded text-text-secondary/70", children: toolCalls.length })] }), _jsx("span", { className: "text-paragraph-sm text-text-secondary/70 pl-4.5", children: displayText })] }));
137
+ return (_jsxs("div", { className: "flex flex-col my-4 rounded-md px-1 -mx-1 w-fit", children: [_jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx("div", { className: "text-text-secondary/70", children: _jsx(ListVideo, { className: "h-3 w-3" }) }), _jsx("span", { className: "text-paragraph-sm text-text-secondary/70", children: "Selecting tools" }), _jsx("span", { className: "text-[10px] bg-muted px-1.5 py-0.5 rounded text-text-secondary/70", children: toolCalls.length })] }), _jsx("span", { className: "text-paragraph-sm text-text-secondary/70 pl-4.5", children: displayText })] }));
160
138
  }
161
139
  // Full display (for single tool call or expanded group, includes minimized state)
162
- return (_jsxs(motion.div, { className: "flex flex-col my-4", initial: {
163
- filter: "blur(12px)",
164
- opacity: 0,
165
- y: 12,
166
- }, animate: {
167
- filter: "blur(0px)",
168
- opacity: 1,
169
- y: 0,
170
- }, exit: {
171
- filter: "blur(12px)",
172
- opacity: 0,
173
- y: -12,
174
- }, transition: getTransition(shouldReduceMotion ?? false, {
175
- duration: 0.5,
176
- ease: motionEasing.smooth,
177
- }), children: [_jsxs(motion.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 rounded-md px-1 -mx-1", onClick: handleHeaderClick, "aria-expanded": isTodoWrite ? undefined : isExpanded, animate: isActive || isSelecting
178
- ? {
179
- backgroundPosition: ["200% 0", "-200% 0"],
180
- }
181
- : {}, transition: isActive || isSelecting
182
- ? {
183
- ...shimmerTransition,
184
- duration: getDuration(shouldReduceMotion ?? false, 1.5),
185
- }
186
- : {}, style: isActive || isSelecting
187
- ? {
188
- backgroundImage: "linear-gradient(90deg, transparent 5%, rgba(255, 255, 255, 0.75) 25%, transparent 35%)",
189
- backgroundSize: "200% 100%",
190
- }
191
- : {}, children: [_jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx("div", { className: "text-text-secondary/70 group-hover:text-text-secondary transition-colors", children: _jsx(IconComponent, { className: "h-3 w-3" }) }), _jsxs("span", { className: "text-paragraph-sm text-text-secondary/70 group-hover:text-text-secondary transition-colors", children: [isGrouped && _jsx("span", { className: "mr-1", children: "Parallel operation" }), !isGrouped && displayText] }), isGrouped && (_jsx("span", { className: "text-[10px] bg-muted px-1.5 py-0.5 rounded text-text-secondary/70", children: toolCalls.length })), isFailed && _jsx(AlertCircle, { className: "h-3 w-3 text-destructive" }), isTodoWrite ? (_jsx(ChevronRight, { className: "h-3 w-3 text-text-secondary/70 group-hover:text-text-secondary transition-colors" })) : (_jsx(motion.div, { animate: {
192
- rotate: isExpanded ? 180 : 0,
193
- }, transition: getTransition(shouldReduceMotion ?? false, {
194
- duration: motionDuration.normal,
195
- ease: motionEasing.smooth,
196
- }), className: "h-3 w-3 text-text-secondary/70 group-hover:text-text-secondary transition-colors", children: _jsx(ChevronDown, { className: "h-3 w-3" }) }))] }), !isGrouped &&
140
+ return (_jsxs("div", { className: "flex flex-col my-4", 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 rounded-md px-1 -mx-1", onClick: handleHeaderClick, "aria-expanded": isTodoWrite ? undefined : isExpanded, children: [_jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx("div", { className: "text-text-secondary/70 group-hover:text-text-secondary transition-colors", children: _jsx(IconComponent, { className: "h-3 w-3" }) }), _jsxs("span", { className: "text-paragraph-sm text-text-secondary/70 group-hover:text-text-secondary transition-colors", children: [isGrouped && _jsx("span", { className: "mr-1", children: "Parallel operation" }), !isGrouped && displayText] }), isGrouped && (_jsx("span", { className: "text-[10px] bg-muted px-1.5 py-0.5 rounded text-text-secondary/70", children: toolCalls.length })), isFailed && _jsx(AlertCircle, { className: "h-3 w-3 text-destructive" }), isTodoWrite ? (_jsx(ChevronRight, { className: "h-3 w-3 text-text-secondary/70 group-hover:text-text-secondary transition-colors" })) : (_jsx("div", { className: `h-3 w-3 text-text-secondary/70 group-hover:text-text-secondary transition-all duration-200 ${isExpanded ? "rotate-180" : ""}`, children: _jsx(ChevronDown, { className: "h-3 w-3" }) }))] }), !isGrouped &&
197
141
  singleToolCall &&
198
142
  (isTodoWrite && singleToolCall.rawInput?.todos ? (_jsx(TodoSubline, { todos: singleToolCall.rawInput.todos, className: "text-paragraph-sm text-text-secondary/70 pl-4.5" })) : singleToolCall.subline ? (_jsx("span", { className: "text-paragraph-sm text-text-secondary/70 pl-4.5", children: singleToolCall.subline })) : null), !isGrouped && toolHookNotification && (_jsx("span", { className: "text-paragraph-sm text-text-secondary/70 pl-4.5", children: toolHookNotification.status === "triggered"
199
143
  ? "Compacting response..."
@@ -218,19 +162,16 @@ export function ToolOperation({ toolCalls, isGrouped = false, autoMinimize = tru
218
162
  return (_jsxs("span", { className: "text-paragraph-sm text-text-secondary/70 pl-4.5", children: [compactedCount, " response", compactedCount > 1 ? "s" : "", " ", "compacted"] }));
219
163
  }
220
164
  return null;
221
- })()] }), !isTodoWrite && isSubagentCall && singleToolCall && (_jsx("div", { className: "pl-4.5 mt-2", children: _jsx(SubAgentDetails, { port: singleToolCall.subagentPort, sessionId: singleToolCall.subagentSessionId, parentStatus: singleToolCall.status, agentName: singleToolCall.rawInput?.agentName, query: singleToolCall.rawInput?.query, isExpanded: isSubagentExpanded, onExpandChange: setIsSubagentExpanded, storedMessages: singleToolCall.subagentMessages, isReplay: isReplaySubagent }) })), _jsx(AnimatePresence, { initial: false, children: !isTodoWrite && isExpanded && (_jsx(motion.div, { initial: "collapsed", animate: "expanded", exit: "collapsed", variants: expandCollapseVariants, transition: getTransition(shouldReduceMotion ?? false, {
222
- duration: motionDuration.normal,
223
- ease: motionEasing.smooth,
224
- }), className: "mt-1", children: isGrouped ? (
225
- // Render individual tool calls in group
226
- _jsx("div", { className: "flex flex-col gap-4 mt-2", children: toolCalls.map((toolCall) => {
227
- const hookNotification = hookNotifications.find((n) => n.toolCallId === toolCall.id);
228
- return (_jsx(GroupedToolCallItem, { toolCall: toolCall, ...(hookNotification ? { hookNotification } : {}) }, toolCall.id));
229
- }) })) : (
230
- // Render single tool call details
231
- singleToolCall && (_jsx(ToolOperationDetails, { toolCall: singleToolCall, ...(toolHookNotification
232
- ? { hookNotification: toolHookNotification }
233
- : {}) }))) }, "expanded-content")) })] }));
165
+ })()] }), !isTodoWrite && isSubagentCall && singleToolCall && (_jsx("div", { className: "pl-4.5 mt-2", children: _jsx(SubAgentDetails, { port: singleToolCall.subagentPort, sessionId: singleToolCall.subagentSessionId, parentStatus: singleToolCall.status, agentName: singleToolCall.rawInput?.agentName, query: singleToolCall.rawInput?.query, isExpanded: isSubagentExpanded, onExpandChange: setIsSubagentExpanded, storedMessages: singleToolCall.subagentMessages, isReplay: isReplaySubagent }) })), !isTodoWrite && isExpanded && (_jsx("div", { className: "mt-1", children: isGrouped ? (
166
+ // Render individual tool calls in group
167
+ _jsx("div", { className: "flex flex-col gap-4 mt-2", children: toolCalls.map((toolCall) => {
168
+ const hookNotification = hookNotifications.find((n) => n.toolCallId === toolCall.id);
169
+ return (_jsx(GroupedToolCallItem, { toolCall: toolCall, ...(hookNotification ? { hookNotification } : {}) }, toolCall.id));
170
+ }) })) : (
171
+ // Render single tool call details
172
+ singleToolCall && (_jsx(ToolOperationDetails, { toolCall: singleToolCall, ...(toolHookNotification
173
+ ? { hookNotification: toolHookNotification }
174
+ : {}) }))) }))] }));
234
175
  }
235
176
  /**
236
177
  * Component to render a single tool call within a grouped parallel operation
@@ -290,7 +231,7 @@ function ToolOperationDetails({ toolCall, hookNotification, }) {
290
231
  "--w-rjv-type-nan-color": resolvedTheme === "dark" ? "#ef4444" : "#dc2626",
291
232
  "--w-rjv-type-undefined-color": resolvedTheme === "dark" ? "#ef4444" : "#dc2626",
292
233
  };
293
- return (_jsxs("div", { className: "text-sm border border-border rounded-lg bg-card overflow-hidden w-full", children: [toolCall.locations && toolCall.locations.length > 0 && (_jsxs("div", { className: "p-3 border-b border-border", children: [_jsx("div", { className: "text-[10px] font-bold text-text-secondary uppercase tracking-wider mb-1.5 font-sans", children: "Files" }), _jsx("ul", { className: "space-y-1", children: toolCall.locations.map((loc) => (_jsxs("li", { className: "font-mono text-[11px] text-foreground bg-muted px-1.5 py-0.5 rounded w-fit", children: [loc.path, loc.line !== null && loc.line !== undefined && `:${loc.line}`] }, `${loc.path}:${loc.line ?? ""}`))) })] })), toolCall.rawInput && Object.keys(toolCall.rawInput).length > 0 && (_jsxs("div", { className: "p-3 border-b border-border", children: [_jsx("div", { className: "text-[10px] font-bold text-text-secondary uppercase tracking-wider mb-1.5 font-sans", children: "Input" }), _jsx("div", { className: "text-[11px] font-mono text-foreground", children: _jsx(JsonView, { value: toolCall.rawInput, collapsed: false, displayDataTypes: false, displayObjectSize: false, enableClipboard: true, style: jsonStyle }) })] })), ((toolCall.content && toolCall.content.length > 0) ||
234
+ return (_jsxs("div", { className: "text-sm border border-border rounded-lg bg-card overflow-hidden w-full", children: [toolCall.locations && toolCall.locations.length > 0 && (_jsxs("div", { className: "p-3 border-b border-border", children: [_jsx("div", { className: "text-[10px] font-bold text-text-secondary uppercase tracking-wider mb-1.5 font-sans", children: "Files" }), _jsx("ul", { className: "space-y-1", children: toolCall.locations.map((loc) => (_jsxs("li", { className: "font-mono text-[11px] text-foreground bg-muted px-1.5 py-0.5 rounded w-fit", children: [loc.path, loc.line !== null && loc.line !== undefined && `:${loc.line}`] }, `${loc.path}:${loc.line ?? ""}`))) })] })), toolCall.rawInput && Object.keys(toolCall.rawInput).length > 0 && (_jsxs("div", { className: "p-3 border-b border-border", children: [_jsx("div", { className: "text-[10px] font-bold text-text-secondary uppercase tracking-wider mb-1.5 font-sans", children: "Input" }), _jsx("div", { className: "text-[11px] font-mono text-foreground", children: _jsx(JsonView, { value: toolCall.rawInput, collapsed: false, displayDataTypes: false, displayObjectSize: false, enableClipboard: true, shortenTextAfterLength: 80, style: jsonStyle }) })] })), ((toolCall.content && toolCall.content.length > 0) ||
294
235
  toolCall.error) && (_jsxs("div", { className: "p-3 border-b border-border last:border-0", children: [_jsx("div", { className: "text-[10px] font-bold text-text-secondary uppercase tracking-wider mb-1.5 font-sans", children: "Output" }), _jsxs("div", { className: "space-y-2 text-[11px] text-foreground", children: [toolCall.content?.map((block, idx) => {
295
236
  // Generate a stable key based on content
296
237
  const getBlockKey = () => {
@@ -314,7 +255,7 @@ function ToolOperationDetails({ toolCall, hookNotification, }) {
314
255
  try {
315
256
  const parsed = JSON.parse(text);
316
257
  if (typeof parsed === "object" && parsed !== null) {
317
- return (_jsx("div", { className: "text-[11px]", children: _jsx(JsonView, { value: parsed, collapsed: false, displayDataTypes: false, displayObjectSize: false, enableClipboard: true, style: jsonStyle }) }, key));
258
+ return (_jsx("div", { className: "text-[11px]", children: _jsx(JsonView, { value: parsed, collapsed: false, displayDataTypes: false, displayObjectSize: false, enableClipboard: true, shortenTextAfterLength: 80, style: jsonStyle }) }, key));
318
259
  }
319
260
  }
320
261
  catch {
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import * as React from "react";
3
- import { getGroupDisplayState, isPreliminaryToolCall, } from "../../core/utils/tool-call-state.js";
3
+ import { isPreliminaryToolCall } from "../../core/utils/tool-call-state.js";
4
4
  import { cn } from "../lib/utils.js";
5
5
  import { Reasoning } from "./Reasoning.js";
6
6
  import { ToolOperation } from "./ToolOperation.js";
@@ -22,6 +22,7 @@ export function WorkProgress({ thinking, isThinkingStreaming = false, toolCalls
22
22
  result.push({ type: "selecting", toolCalls: selectingGroup });
23
23
  }
24
24
  else {
25
+ // biome-ignore lint/style/noNonNullAssertion: else branch ensures array has exactly one element
25
26
  result.push({ type: "single", toolCall: selectingGroup[0] });
26
27
  }
27
28
  selectingGroup = [];
@@ -64,14 +65,15 @@ export function WorkProgress({ thinking, isThinkingStreaming = false, toolCalls
64
65
  if (!hasThinking && !hasToolCalls) {
65
66
  return null;
66
67
  }
67
- return (_jsxs("div", { className: cn("work-progress", className), children: [hasThinking && (_jsx(Reasoning, { content: thinking, isStreaming: isThinkingStreaming, mode: thinkingDisplayStyle, autoCollapse: autoCollapseThinking })), groupedToolCalls.map((group, index) => {
68
+ return (_jsxs("div", { className: cn("work-progress", className), children: [hasThinking && (_jsx(Reasoning, { content: thinking, isStreaming: isThinkingStreaming, mode: thinkingDisplayStyle, autoCollapse: autoCollapseThinking })), groupedToolCalls.map((group, _index) => {
68
69
  if (group.type === "batch") {
69
70
  // Parallel operations group
70
71
  return (_jsx(ToolOperation, { toolCalls: group.toolCalls, isGrouped: true, autoMinimize: true }, `batch-${group.batchId}`));
71
72
  }
72
73
  if (group.type === "selecting") {
73
74
  // Multiple selecting operations
74
- return (_jsx(ToolOperation, { toolCalls: group.toolCalls, isGrouped: true, autoMinimize: false }, `selecting-${index}`));
75
+ const selectingKey = group.toolCalls.map((tc) => tc.id).join("-");
76
+ return (_jsx(ToolOperation, { toolCalls: group.toolCalls, isGrouped: true, autoMinimize: false }, `selecting-${selectingKey}`));
75
77
  }
76
78
  // Single tool call
77
79
  return (_jsx(ToolOperation, { toolCalls: [group.toolCall], isGrouped: false, autoMinimize: true }, group.toolCall.id));
@@ -6,7 +6,6 @@ export { AppSidebar, type AppSidebarProps, } from "./AppSidebar.js";
6
6
  export { Button, type ButtonProps, buttonVariants } from "./Button.js";
7
7
  export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, } from "./Card.js";
8
8
  export { ChatEmptyState, type ChatEmptyStateProps } from "./ChatEmptyState.js";
9
- export type { ConnectionStatus } from "./ChatHeader.js";
10
9
  export * as ChatHeader from "./ChatHeader.js";
11
10
  export { Actions as ChatInputActions, Attachment as ChatInputAttachment, type ChatInputActionsProps, type ChatInputAttachmentProps, type ChatInputCommandMenuProps, type ChatInputFieldProps, type ChatInputRootProps, type ChatInputSubmitProps, type ChatInputToolbarProps, type ChatInputVoiceInputProps, CommandMenu as ChatInputCommandMenu, type CommandMenuItem, Field as ChatInputField, Root as ChatInputRoot, Submit as ChatInputSubmit, Toolbar as ChatInputToolbar, VoiceInput as ChatInputVoiceInput, } from "./ChatInput.js";
12
11
  export * as ChatLayout from "./ChatLayout.js";
@@ -1,5 +1,5 @@
1
1
  import * as ResizablePrimitive from "react-resizable-panels";
2
- declare const ResizablePanelGroup: ({ className, ...props }: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) => import("react/jsx-runtime").JSX.Element;
2
+ declare const ResizablePanelGroup: React.FC<React.ComponentProps<typeof ResizablePrimitive.PanelGroup>>;
3
3
  declare const ResizablePanel: ({ className, ...props }: React.ComponentProps<typeof ResizablePrimitive.Panel>) => import("react/jsx-runtime").JSX.Element;
4
4
  declare const ResizableHandle: ({ withHandle, className, ...props }: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
5
5
  withHandle?: boolean;
@@ -0,0 +1,6 @@
1
+ export declare const SIDEBAR_WIDTH_DESKTOP = 256;
2
+ export declare const SIDEBAR_WIDTH_MOBILE = "calc(100vw - 104px)";
3
+ export declare const SIDEBAR_TAP_ZONE = 104;
4
+ export declare const ASIDE_WIDTH_DEFAULT = 450;
5
+ export declare const ASIDE_WIDTH_MIN = 250;
6
+ export declare const ASIDE_WIDTH_MAX = 800;
@@ -0,0 +1,8 @@
1
+ // Sidebar constants
2
+ export const SIDEBAR_WIDTH_DESKTOP = 256;
3
+ export const SIDEBAR_WIDTH_MOBILE = "calc(100vw - 104px)";
4
+ export const SIDEBAR_TAP_ZONE = 104;
5
+ // Aside panel constants
6
+ export const ASIDE_WIDTH_DEFAULT = 450;
7
+ export const ASIDE_WIDTH_MIN = 250;
8
+ export const ASIDE_WIDTH_MAX = 800;
@@ -1,2 +1,3 @@
1
+ export { useLockBodyScroll } from "./use-lock-body-scroll.js";
1
2
  export { useIsMobile } from "./use-mobile.js";
2
3
  export { useScrollToBottom } from "./use-scroll-to-bottom.js";
@@ -1,2 +1,3 @@
1
+ export { useLockBodyScroll } from "./use-lock-body-scroll.js";
1
2
  export { useIsMobile } from "./use-mobile.js";
2
3
  export { useScrollToBottom } from "./use-scroll-to-bottom.js";
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Hook to lock body scroll when a modal/drawer is open
3
+ * Primarily used for mobile sidebar overlay to prevent background scrolling
4
+ *
5
+ * @param lock - Whether to lock the body scroll
6
+ */
7
+ export declare function useLockBodyScroll(lock: boolean): void;
@@ -0,0 +1,29 @@
1
+ import { useEffect } from "react";
2
+ /**
3
+ * Hook to lock body scroll when a modal/drawer is open
4
+ * Primarily used for mobile sidebar overlay to prevent background scrolling
5
+ *
6
+ * @param lock - Whether to lock the body scroll
7
+ */
8
+ export function useLockBodyScroll(lock) {
9
+ useEffect(() => {
10
+ if (!lock)
11
+ return;
12
+ // Store current scroll position
13
+ const scrollY = window.scrollY;
14
+ // Lock body scroll
15
+ document.body.style.position = "fixed";
16
+ document.body.style.top = `-${scrollY}px`;
17
+ document.body.style.width = "100%";
18
+ document.body.style.overflow = "hidden";
19
+ return () => {
20
+ // Unlock body scroll
21
+ document.body.style.position = "";
22
+ document.body.style.top = "";
23
+ document.body.style.width = "";
24
+ document.body.style.overflow = "";
25
+ // Restore scroll position
26
+ window.scrollTo(0, scrollY);
27
+ };
28
+ }, [lock]);
29
+ }
@@ -53,3 +53,15 @@ export declare function getTransition(shouldReduceMotion: boolean, transition?:
53
53
  * Get duration with reduced motion consideration
54
54
  */
55
55
  export declare function getDuration(shouldReduceMotion: boolean, duration?: number): number;
56
+ export declare const sidebarTransition: Transition;
57
+ export declare const sidebarMobileTransition: Transition;
58
+ export declare const sidebarDesktopVariants: Variants;
59
+ export declare const sidebarMobileVariants: Variants;
60
+ export declare const sidebarContentVariants: Variants;
61
+ export declare const sidebarContentTransition: Transition;
62
+ export declare const backdropVariants: Variants;
63
+ export declare const asideDesktopVariants: Variants;
64
+ export declare const asideMobileVariants: Variants;
65
+ export declare const asideContentVariants: Variants;
66
+ export declare const asideContentTransition: Transition;
67
+ export declare const asideMobileTransition: Transition;
@@ -215,3 +215,72 @@ export function getTransition(shouldReduceMotion, transition = standardTransitio
215
215
  export function getDuration(shouldReduceMotion, duration = motionDuration.normal) {
216
216
  return shouldReduceMotion ? 0.01 : duration;
217
217
  }
218
+ // ============================================================================
219
+ // Sidebar Animations (AppSidebar)
220
+ // ============================================================================
221
+ export const sidebarTransition = {
222
+ duration: 0.5,
223
+ ease: motionEasing.smooth,
224
+ };
225
+ export const sidebarMobileTransition = {
226
+ duration: 0.3,
227
+ ease: motionEasing.smooth,
228
+ };
229
+ // Desktop: Slide animation (fixed width, no reflow)
230
+ export const sidebarDesktopVariants = {
231
+ initial: { x: "-100%" },
232
+ animate: { x: 0 },
233
+ exit: { x: "-100%" },
234
+ };
235
+ // Mobile: Slide animation (overlay from left)
236
+ export const sidebarMobileVariants = {
237
+ initial: { x: "-100%" },
238
+ animate: { x: 0 },
239
+ exit: { x: "-100%" },
240
+ };
241
+ // Sidebar content fade-in (snappy, only on open)
242
+ export const sidebarContentVariants = {
243
+ initial: { opacity: 0, x: -20 },
244
+ animate: { opacity: 1, x: 0 },
245
+ };
246
+ export const sidebarContentTransition = {
247
+ duration: 0.5,
248
+ ease: motionEasing.smooth,
249
+ delay: 0.25,
250
+ };
251
+ // Backdrop animation for mobile overlay
252
+ export const backdropVariants = {
253
+ initial: { opacity: 0 },
254
+ animate: { opacity: 1 },
255
+ exit: { opacity: 0 },
256
+ };
257
+ // ============================================================================
258
+ // Aside Panel Animations (ChatLayout.Aside)
259
+ // ============================================================================
260
+ // Desktop: Width-based slide animation (inline panel, main content reflows)
261
+ export const asideDesktopVariants = {
262
+ initial: { width: 0 },
263
+ animate: { width: "auto" },
264
+ exit: { width: 0 },
265
+ };
266
+ // Mobile: Slide animation from right (full-screen overlay)
267
+ export const asideMobileVariants = {
268
+ initial: { x: "100%" },
269
+ animate: { x: 0 },
270
+ exit: { x: "100%" },
271
+ };
272
+ // Aside content fade-in (delayed, snappy)
273
+ export const asideContentVariants = {
274
+ initial: { opacity: 0, x: 20 },
275
+ animate: { opacity: 1, x: 0 },
276
+ };
277
+ export const asideContentTransition = {
278
+ duration: 0.35,
279
+ ease: motionEasing.smooth,
280
+ delay: 0.1,
281
+ };
282
+ // Mobile transition (faster, matches sidebar)
283
+ export const asideMobileTransition = {
284
+ duration: 0.3,
285
+ ease: motionEasing.smooth,
286
+ };
@@ -13,9 +13,9 @@ export type MessageRole = z.infer<typeof MessageRole>;
13
13
  * Content type for messages
14
14
  */
15
15
  export declare const ContentType: z.ZodEnum<{
16
+ file: "file";
16
17
  text: "text";
17
18
  image: "image";
18
- file: "file";
19
19
  tool_call: "tool_call";
20
20
  tool_result: "tool_result";
21
21
  }>;
@@ -25,9 +25,9 @@ export type ContentType = z.infer<typeof ContentType>;
25
25
  */
26
26
  export declare const BaseContent: z.ZodObject<{
27
27
  type: z.ZodEnum<{
28
+ file: "file";
28
29
  text: "text";
29
30
  image: "image";
30
- file: "file";
31
31
  tool_call: "tool_call";
32
32
  tool_result: "tool_result";
33
33
  }>;
@@ -5,11 +5,11 @@ import { z } from "zod";
5
5
  export declare const SessionStatus: z.ZodEnum<{
6
6
  error: "error";
7
7
  active: "active";
8
- disconnected: "disconnected";
8
+ idle: "idle";
9
9
  connecting: "connecting";
10
10
  connected: "connected";
11
- idle: "idle";
12
11
  streaming: "streaming";
12
+ disconnected: "disconnected";
13
13
  }>;
14
14
  export type SessionStatus = z.infer<typeof SessionStatus>;
15
15
  /**
@@ -42,11 +42,11 @@ export declare const Session: z.ZodObject<{
42
42
  status: z.ZodEnum<{
43
43
  error: "error";
44
44
  active: "active";
45
- disconnected: "disconnected";
45
+ idle: "idle";
46
46
  connecting: "connecting";
47
47
  connected: "connected";
48
- idle: "idle";
49
48
  streaming: "streaming";
49
+ disconnected: "disconnected";
50
50
  }>;
51
51
  config: z.ZodObject<{
52
52
  agentPath: z.ZodString;
@@ -118,11 +118,11 @@ export declare const SessionUpdate: z.ZodUnion<readonly [z.ZodObject<{
118
118
  status: z.ZodOptional<z.ZodEnum<{
119
119
  error: "error";
120
120
  active: "active";
121
- disconnected: "disconnected";
121
+ idle: "idle";
122
122
  connecting: "connecting";
123
123
  connected: "connected";
124
- idle: "idle";
125
124
  streaming: "streaming";
125
+ disconnected: "disconnected";
126
126
  }>>;
127
127
  message: z.ZodOptional<z.ZodObject<{
128
128
  id: z.ZodString;
@@ -197,9 +197,9 @@ export declare const SessionUpdate: z.ZodUnion<readonly [z.ZodObject<{
197
197
  other: "other";
198
198
  }>;
199
199
  status: z.ZodEnum<{
200
- completed: "completed";
201
200
  pending: "pending";
202
201
  in_progress: "in_progress";
202
+ completed: "completed";
203
203
  failed: "failed";
204
204
  }>;
205
205
  contentPosition: z.ZodOptional<z.ZodNumber>;
@@ -265,9 +265,9 @@ export declare const SessionUpdate: z.ZodUnion<readonly [z.ZodObject<{
265
265
  prettyName: z.ZodOptional<z.ZodString>;
266
266
  icon: z.ZodOptional<z.ZodString>;
267
267
  status: z.ZodEnum<{
268
- completed: "completed";
269
268
  pending: "pending";
270
269
  in_progress: "in_progress";
270
+ completed: "completed";
271
271
  failed: "failed";
272
272
  }>;
273
273
  content: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
@@ -310,9 +310,9 @@ export declare const SessionUpdate: z.ZodUnion<readonly [z.ZodObject<{
310
310
  prettyName: z.ZodOptional<z.ZodString>;
311
311
  icon: z.ZodOptional<z.ZodString>;
312
312
  status: z.ZodEnum<{
313
- completed: "completed";
314
313
  pending: "pending";
315
314
  in_progress: "in_progress";
315
+ completed: "completed";
316
316
  failed: "failed";
317
317
  }>;
318
318
  content: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
@@ -355,11 +355,11 @@ export declare const SessionUpdate: z.ZodUnion<readonly [z.ZodObject<{
355
355
  status: z.ZodOptional<z.ZodEnum<{
356
356
  error: "error";
357
357
  active: "active";
358
- disconnected: "disconnected";
358
+ idle: "idle";
359
359
  connecting: "connecting";
360
360
  connected: "connected";
361
- idle: "idle";
362
361
  streaming: "streaming";
362
+ disconnected: "disconnected";
363
363
  }>>;
364
364
  message: z.ZodOptional<z.ZodObject<{
365
365
  id: z.ZodString;
@@ -412,9 +412,9 @@ export declare const SessionUpdate: z.ZodUnion<readonly [z.ZodObject<{
412
412
  toolCallUpdate: z.ZodObject<{
413
413
  id: z.ZodString;
414
414
  status: z.ZodOptional<z.ZodEnum<{
415
- completed: "completed";
416
415
  pending: "pending";
417
416
  in_progress: "in_progress";
417
+ completed: "completed";
418
418
  failed: "failed";
419
419
  }>>;
420
420
  title: z.ZodOptional<z.ZodString>;
@@ -473,9 +473,9 @@ export declare const SessionUpdate: z.ZodUnion<readonly [z.ZodObject<{
473
473
  prettyName: z.ZodOptional<z.ZodString>;
474
474
  icon: z.ZodOptional<z.ZodString>;
475
475
  status: z.ZodEnum<{
476
- completed: "completed";
477
476
  pending: "pending";
478
477
  in_progress: "in_progress";
478
+ completed: "completed";
479
479
  failed: "failed";
480
480
  }>;
481
481
  content: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
@@ -518,9 +518,9 @@ export declare const SessionUpdate: z.ZodUnion<readonly [z.ZodObject<{
518
518
  prettyName: z.ZodOptional<z.ZodString>;
519
519
  icon: z.ZodOptional<z.ZodString>;
520
520
  status: z.ZodEnum<{
521
- completed: "completed";
522
521
  pending: "pending";
523
522
  in_progress: "in_progress";
523
+ completed: "completed";
524
524
  failed: "failed";
525
525
  }>;
526
526
  content: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
@@ -562,11 +562,11 @@ export declare const SessionUpdate: z.ZodUnion<readonly [z.ZodObject<{
562
562
  status: z.ZodOptional<z.ZodEnum<{
563
563
  error: "error";
564
564
  active: "active";
565
- disconnected: "disconnected";
565
+ idle: "idle";
566
566
  connecting: "connecting";
567
567
  connected: "connected";
568
- idle: "idle";
569
568
  streaming: "streaming";
569
+ disconnected: "disconnected";
570
570
  }>>;
571
571
  message: z.ZodOptional<z.ZodObject<{
572
572
  id: z.ZodString;
@@ -627,11 +627,11 @@ export declare const SessionUpdate: z.ZodUnion<readonly [z.ZodObject<{
627
627
  status: z.ZodOptional<z.ZodEnum<{
628
628
  error: "error";
629
629
  active: "active";
630
- disconnected: "disconnected";
630
+ idle: "idle";
631
631
  connecting: "connecting";
632
632
  connected: "connected";
633
- idle: "idle";
634
633
  streaming: "streaming";
634
+ disconnected: "disconnected";
635
635
  }>>;
636
636
  message: z.ZodOptional<z.ZodObject<{
637
637
  id: z.ZodString;
@@ -1,5 +1,5 @@
1
1
  import type { Message, MessageChunk, SessionUpdate } from "../schemas/index.js";
2
- import type { HttpTransportOptions, SessionSummary, Transport } from "./types.js";
2
+ import { type HttpTransportOptions, type SessionSummary, type Transport } from "./types.js";
3
3
  /**
4
4
  * HTTP transport implementation using ACP over HTTP + SSE
5
5
  * Uses POST /rpc for client->agent messages and GET /events (SSE) for agent->client
@@ -1,5 +1,6 @@
1
1
  import * as acp from "@agentclientprotocol/sdk";
2
2
  import { createLogger } from "@townco/core";
3
+ import { httpTransportOptionsSchema, } from "./types.js";
3
4
  const logger = createLogger("http-transport");
4
5
  /**
5
6
  * HTTP transport implementation using ACP over HTTP + SSE
@@ -24,6 +25,14 @@ export class HttpTransport {
24
25
  isInReplayMode = false; // True during session replay, ignores non-replay streaming
25
26
  agentInfo;
26
27
  constructor(options) {
28
+ // Validate options at the boundary using Zod
29
+ const validationResult = httpTransportOptionsSchema.safeParse(options);
30
+ if (!validationResult.success) {
31
+ const errorMessages = validationResult.error.issues
32
+ .map((issue) => `${issue.path.join(".")}: ${issue.message}`)
33
+ .join("; ");
34
+ throw new Error(`Invalid HTTP transport configuration: ${errorMessages}`);
35
+ }
27
36
  // Ensure baseUrl doesn't end with a slash
28
37
  this.options = { ...options, baseUrl: options.baseUrl.replace(/\/$/, "") };
29
38
  }
@@ -180,7 +180,7 @@ export class StdioTransport {
180
180
  }),
181
181
  startedAt: Date.now(),
182
182
  };
183
- const sessionUpdate = {
183
+ const _sessionUpdate = {
184
184
  type: "tool_call",
185
185
  sessionId,
186
186
  status: "active",
@@ -265,7 +265,7 @@ export class StdioTransport {
265
265
  ? Date.now()
266
266
  : undefined,
267
267
  };
268
- const sessionUpdate = {
268
+ const _sessionUpdate = {
269
269
  type: "tool_call_update",
270
270
  sessionId,
271
271
  status: "active",
@@ -1,3 +1,4 @@
1
+ import { z } from "zod";
1
2
  import type { Message, MessageChunk, SessionUpdate } from "../schemas/index.js";
2
3
  /**
3
4
  * Agent tool information exposed via initialize response
@@ -127,6 +128,16 @@ export interface StdioTransportOptions {
127
128
  workingDirectory?: string;
128
129
  timeout?: number;
129
130
  }
131
+ /**
132
+ * Zod schema for validating HTTP transport options
133
+ * Validates that baseUrl is a valid URL at the boundary
134
+ */
135
+ export declare const httpTransportOptionsSchema: z.ZodObject<{
136
+ baseUrl: z.ZodString;
137
+ apiKey: z.ZodOptional<z.ZodString>;
138
+ timeout: z.ZodOptional<z.ZodNumber>;
139
+ headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
140
+ }, z.core.$strip>;
130
141
  /**
131
142
  * HTTP transport options
132
143
  */
@@ -1 +1,28 @@
1
- export {};
1
+ import { z } from "zod";
2
+ /**
3
+ * Zod schema for validating HTTP transport options
4
+ * Validates that baseUrl is a valid URL at the boundary
5
+ */
6
+ export const httpTransportOptionsSchema = z.object({
7
+ baseUrl: z
8
+ .string()
9
+ .url("Invalid base URL. Must be a valid URL (e.g., http://localhost:3000)")
10
+ .refine((url) => {
11
+ try {
12
+ const parsed = new URL(url);
13
+ // Check for common mistakes like double ports (e.g., http://localhost:3102:3100)
14
+ if (parsed.port && parsed.hostname.includes(":")) {
15
+ return false;
16
+ }
17
+ return true;
18
+ }
19
+ catch {
20
+ return false;
21
+ }
22
+ }, {
23
+ message: "Invalid URL format. Check for typos like duplicate ports (e.g., http://localhost:3102:3100)",
24
+ }),
25
+ apiKey: z.string().optional(),
26
+ timeout: z.number().optional(),
27
+ headers: z.record(z.string(), z.string()).optional(),
28
+ });