@townco/ui 0.1.77 → 0.1.79

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 (49) hide show
  1. package/dist/core/hooks/use-chat-messages.d.ts +6 -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 +5 -3
  7. package/dist/core/hooks/use-tool-calls.js +1 -1
  8. package/dist/core/schemas/chat.d.ts +14 -10
  9. package/dist/core/schemas/tool-call.d.ts +21 -8
  10. package/dist/core/schemas/tool-call.js +15 -0
  11. package/dist/core/store/chat-store.js +1 -0
  12. package/dist/core/utils/tool-summary.js +8 -3
  13. package/dist/core/utils/tool-verbiage.js +1 -1
  14. package/dist/gui/components/AppSidebar.d.ts +1 -1
  15. package/dist/gui/components/AppSidebar.js +4 -3
  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 +117 -81
  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/session.d.ts +37 -24
  44. package/dist/sdk/transports/http.d.ts +1 -1
  45. package/dist/sdk/transports/http.js +99 -1
  46. package/dist/sdk/transports/stdio.js +2 -2
  47. package/dist/sdk/transports/types.d.ts +11 -0
  48. package/dist/sdk/transports/types.js +28 -1
  49. package/package.json +3 -5
@@ -1,16 +1,14 @@
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
- import { AlertCircle, BrainCircuit, CheckSquare, ChevronDown, ChevronRight, CircleDot, Cloud, Edit, FileText, Globe, Image, Link, ListVideo, Search, Wrench, } from "lucide-react";
3
+ import { AlertCircle, BrainCircuit, CheckSquare, ChevronDown, ChevronRight, CircleDot, Cloud, Edit, FileText, FoldVertical, Globe, Image, Link, ListVideo, ScissorsLineDashed, 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";
13
10
  import { TodoSubline } from "./TodoSubline.js";
11
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "./Tooltip.js";
14
12
  /**
15
13
  * Map of icon names to Lucide components
16
14
  */
@@ -27,6 +25,17 @@ const ICON_MAP = {
27
25
  BrainCircuit: BrainCircuit,
28
26
  CircleDot: CircleDot,
29
27
  };
28
+ /**
29
+ * CompactionDetails component - shows detailed stats when tool response was compacted
30
+ */
31
+ function CompactionDetails({ compactionAction, originalTokens, finalTokens, originalContentPath, }) {
32
+ // Calculate stats
33
+ const tokensSaved = originalTokens && finalTokens ? originalTokens - finalTokens : undefined;
34
+ const reductionPercent = originalTokens && finalTokens
35
+ ? Math.round((1 - finalTokens / originalTokens) * 100)
36
+ : undefined;
37
+ return (_jsxs("div", { className: "p-3 border-b border-border", children: [_jsxs("div", { className: "flex items-center gap-2 mb-2", children: [compactionAction === "compacted" ? (_jsx(FoldVertical, { className: "w-3.5 h-3.5 text-text-secondary" })) : (_jsx(ScissorsLineDashed, { className: "w-3.5 h-3.5 text-destructive" })), _jsxs("span", { className: "text-[10px] font-bold text-text-secondary uppercase tracking-wider font-sans", children: ["Response", " ", compactionAction === "compacted" ? "Compacted" : "Truncated"] })] }), _jsxs("div", { className: "grid grid-cols-4 gap-3 mb-3", children: [originalTokens !== undefined && (_jsxs("div", { children: [_jsx("div", { className: "text-[9px] text-text-secondary uppercase tracking-wide font-sans mb-0.5", children: "Original" }), _jsxs("div", { className: "text-[12px] font-medium text-foreground font-sans", children: [originalTokens.toLocaleString(), " tokens"] })] })), finalTokens !== undefined && (_jsxs("div", { children: [_jsx("div", { className: "text-[9px] text-text-secondary uppercase tracking-wide font-sans mb-0.5", children: "Compacted" }), _jsxs("div", { className: "text-[12px] font-medium text-foreground font-sans", children: [finalTokens.toLocaleString(), " tokens"] })] })), tokensSaved !== undefined && (_jsxs("div", { children: [_jsx("div", { className: "text-[9px] text-text-secondary uppercase tracking-wide font-sans mb-0.5", children: "Saved" }), _jsxs("div", { className: "text-[12px] font-medium text-foreground font-sans", children: [tokensSaved.toLocaleString(), " tokens"] })] })), reductionPercent !== undefined && (_jsxs("div", { children: [_jsx("div", { className: "text-[9px] text-text-secondary uppercase tracking-wide font-sans mb-0.5", children: "Reduction" }), _jsxs("div", { className: "text-[12px] font-medium text-foreground font-sans", children: [reductionPercent, "%"] })] }))] }), originalContentPath && (_jsxs("div", { className: "text-[10px] text-text-secondary font-sans", children: [_jsx("span", { className: "font-medium", children: "Original saved to:" }), " ", _jsx("code", { className: "bg-muted px-1 py-0.5 rounded text-[9px]", children: originalContentPath })] }))] }));
38
+ }
30
39
  /**
31
40
  * ToolOperation component - unified display for tool calls
32
41
  * Handles both individual and grouped tool calls with smooth transitions
@@ -35,8 +44,6 @@ export function ToolOperation({ toolCalls, isGrouped = false, autoMinimize = tru
35
44
  const [isExpanded, setIsExpanded] = useState(false);
36
45
  const [isMinimized, setIsMinimized] = useState(false);
37
46
  const [userInteracted, setUserInteracted] = useState(false);
38
- const { resolvedTheme } = useTheme();
39
- const shouldReduceMotion = useReducedMotion();
40
47
  // For single tool call, extract it
41
48
  const singleToolCall = toolCalls.length === 1 ? toolCalls[0] : null;
42
49
  const isTodoWrite = singleToolCall?.title === "todo_write";
@@ -44,6 +51,31 @@ export function ToolOperation({ toolCalls, isGrouped = false, autoMinimize = tru
44
51
  const toolHookNotification = singleToolCall
45
52
  ? hookNotifications.find((n) => n.toolCallId === singleToolCall.id)
46
53
  : undefined;
54
+ // Detect if compaction was applied (from hook notification or persisted _meta)
55
+ const hasCompaction = !!((toolHookNotification?.status === "completed" &&
56
+ toolHookNotification.metadata?.action &&
57
+ toolHookNotification.metadata.action !== "no_action_needed" &&
58
+ toolHookNotification.metadata.action !== "none") ||
59
+ singleToolCall?._meta?.compactionAction);
60
+ // Detect if truncation was used (as opposed to intelligent compaction)
61
+ const isTruncation = !!(toolHookNotification?.metadata?.action === "truncated" ||
62
+ toolHookNotification?.metadata?.action === "compacted_then_truncated" ||
63
+ singleToolCall?._meta?.compactionAction === "truncated");
64
+ // For grouped tool calls, check if any have compaction
65
+ const groupHasCompaction = isGrouped
66
+ ? toolCalls.some((tc) => tc._meta?.compactionAction ||
67
+ hookNotifications.some((n) => n.toolCallId === tc.id &&
68
+ n.status === "completed" &&
69
+ n.metadata?.action &&
70
+ n.metadata.action !== "no_action_needed" &&
71
+ n.metadata.action !== "none"))
72
+ : false;
73
+ const groupHasTruncation = isGrouped
74
+ ? toolCalls.some((tc) => tc._meta?.compactionAction === "truncated" ||
75
+ hookNotifications.some((n) => n.toolCallId === tc.id &&
76
+ (n.metadata?.action === "truncated" ||
77
+ n.metadata?.action === "compacted_then_truncated")))
78
+ : false;
47
79
  // Detect subagent calls
48
80
  const hasLiveSubagent = !!(singleToolCall?.subagentPort && singleToolCall?.subagentSessionId);
49
81
  const hasStoredSubagent = !!(singleToolCall?.subagentMessages &&
@@ -63,7 +95,6 @@ export function ToolOperation({ toolCalls, isGrouped = false, autoMinimize = tru
63
95
  const isCompleted = displayState === "completed";
64
96
  const isFailed = displayState === "failed";
65
97
  const isSelecting = displayState === "selecting";
66
- const isActive = displayState === "executing";
67
98
  // Auto-minimize when completed (only if user hasn't manually interacted)
68
99
  useEffect(() => {
69
100
  if (autoMinimize && isCompleted && !isMinimized && !userInteracted) {
@@ -136,64 +167,31 @@ export function ToolOperation({ toolCalls, isGrouped = false, autoMinimize = tru
136
167
  const displayText = getDisplayText();
137
168
  // For preliminary/selecting states, show simple non-expandable text
138
169
  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 }) }));
170
+ 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
171
  }
149
172
  // If it's a grouped preliminary (selecting) state
150
173
  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 })] }));
174
+ 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
175
  }
161
176
  // 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 &&
177
+ 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("span", { title: isGrouped
178
+ ? `${toolCalls.filter((tc) => tc.status === "failed").length} of ${toolCalls.length} operations failed`
179
+ : singleToolCall?.error || "Operation failed", children: _jsx(AlertCircle, { className: "h-3 w-3 text-destructive" }) })), isGrouped && groupHasCompaction && (_jsx(TooltipProvider, { delayDuration: 0, children: _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("span", { children: groupHasTruncation ? (_jsx(ScissorsLineDashed, { className: "h-3 w-3 text-destructive" })) : (_jsx(FoldVertical, { className: "h-3 w-3 text-text-secondary/70" })) }) }), _jsx(TooltipContent, { children: groupHasTruncation
180
+ ? "Some responses were truncated"
181
+ : "Some responses were compacted" })] }) })), !isGrouped && hasCompaction && (_jsx(TooltipProvider, { delayDuration: 0, children: _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("span", { children: isTruncation ? (_jsx(ScissorsLineDashed, { className: "h-3 w-3 text-destructive" })) : (_jsx(FoldVertical, { className: "h-3 w-3 text-text-secondary/70" })) }) }), _jsx(TooltipContent, { children: (() => {
182
+ const meta = singleToolCall?._meta;
183
+ const percentage = meta?.originalTokens && meta?.finalTokens
184
+ ? Math.round((1 - meta.finalTokens / meta.originalTokens) * 100)
185
+ : null;
186
+ if (isTruncation) {
187
+ return percentage
188
+ ? `Response truncated (${percentage}% reduction)`
189
+ : "Response was truncated";
190
+ }
191
+ return percentage
192
+ ? `Response compacted (${percentage}% reduction)`
193
+ : "Response was compacted";
194
+ })() })] }) })), 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
195
  singleToolCall &&
198
196
  (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
197
  ? "Compacting response..."
@@ -218,37 +216,70 @@ export function ToolOperation({ toolCalls, isGrouped = false, autoMinimize = tru
218
216
  return (_jsxs("span", { className: "text-paragraph-sm text-text-secondary/70 pl-4.5", children: [compactedCount, " response", compactedCount > 1 ? "s" : "", " ", "compacted"] }));
219
217
  }
220
218
  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")) })] }));
219
+ })()] }), !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 ? (
220
+ // Render individual tool calls in group
221
+ _jsx("div", { className: "flex flex-col gap-4 mt-2", children: toolCalls.map((toolCall) => {
222
+ const hookNotification = hookNotifications.find((n) => n.toolCallId === toolCall.id);
223
+ return (_jsx(GroupedToolCallItem, { toolCall: toolCall, ...(hookNotification ? { hookNotification } : {}) }, toolCall.id));
224
+ }) })) : (
225
+ // Render single tool call details
226
+ singleToolCall && (_jsx(ToolOperationDetails, { toolCall: singleToolCall, ...(toolHookNotification
227
+ ? { hookNotification: toolHookNotification }
228
+ : {}) }))) }))] }));
234
229
  }
235
230
  /**
236
231
  * Component to render a single tool call within a grouped parallel operation
237
- * Handles subagent calls with their own expansion state
232
+ * Each tool call is individually expandable and collapsed by default
238
233
  */
239
234
  function GroupedToolCallItem({ toolCall, hookNotification, }) {
240
- const [isSubagentExpanded, setIsSubagentExpanded] = useState(false);
235
+ const [isExpanded, setIsExpanded] = useState(false);
241
236
  // Detect subagent calls
242
237
  const hasLiveSubagent = !!(toolCall.subagentPort && toolCall.subagentSessionId);
243
238
  const hasStoredSubagent = !!(toolCall.subagentMessages && toolCall.subagentMessages.length > 0);
244
239
  const isSubagentCall = hasLiveSubagent || hasStoredSubagent;
245
240
  const isReplaySubagent = hasStoredSubagent;
241
+ // Detect compaction for this individual tool call
242
+ const hasCompaction = !!((hookNotification?.status === "completed" &&
243
+ hookNotification.metadata?.action &&
244
+ hookNotification.metadata.action !== "no_action_needed" &&
245
+ hookNotification.metadata.action !== "none") ||
246
+ toolCall._meta?.compactionAction);
247
+ const isTruncation = !!(hookNotification?.metadata?.action === "truncated" ||
248
+ hookNotification?.metadata?.action === "compacted_then_truncated" ||
249
+ toolCall._meta?.compactionAction === "truncated");
250
+ const isFailed = toolCall.status === "failed";
246
251
  if (isSubagentCall) {
247
252
  // Render subagent with clickable header and SubAgentDetails component
248
- return (_jsxs("div", { className: "flex flex-col ml-5", children: [_jsxs("button", { type: "button", className: "flex items-center gap-1.5 cursor-pointer bg-transparent border-none p-0 text-left group w-fit", onClick: () => setIsSubagentExpanded(!isSubagentExpanded), "aria-expanded": isSubagentExpanded, children: [_jsx("div", { className: "text-text-secondary/70 group-hover:text-text-secondary transition-colors", children: _jsx(CircleDot, { className: "h-3 w-3" }) }), _jsx("span", { className: "text-paragraph-sm text-text-secondary/70 group-hover:text-text-secondary transition-colors", children: toolCall.rawInput?.agentName || "Subagent" }), _jsx(ChevronDown, { className: `h-3 w-3 text-text-secondary/70 group-hover:text-text-secondary transition-colors transition-transform duration-200 ${isSubagentExpanded ? "rotate-180" : ""}` })] }), _jsx("div", { className: "pl-4.5", children: _jsx(SubAgentDetails, { port: toolCall.subagentPort, sessionId: toolCall.subagentSessionId, parentStatus: toolCall.status, agentName: toolCall.rawInput?.agentName, query: toolCall.rawInput?.query, isExpanded: isSubagentExpanded, onExpandChange: setIsSubagentExpanded, storedMessages: toolCall.subagentMessages, isReplay: isReplaySubagent }) })] }));
253
+ return (_jsxs("div", { className: "flex flex-col ml-5", children: [_jsxs("button", { type: "button", className: "flex items-center gap-1.5 cursor-pointer bg-transparent border-none p-0 text-left group w-fit", onClick: () => setIsExpanded(!isExpanded), "aria-expanded": isExpanded, children: [_jsx("div", { className: "text-text-secondary/70 group-hover:text-text-secondary transition-colors", children: _jsx(CircleDot, { className: "h-3 w-3" }) }), _jsx("span", { className: "text-paragraph-sm text-text-secondary/70 group-hover:text-text-secondary transition-colors", children: toolCall.rawInput?.agentName || "Subagent" }), isFailed && (_jsx("span", { title: toolCall.error || "Operation failed", children: _jsx(AlertCircle, { className: "h-3 w-3 text-destructive" }) })), hasCompaction && (_jsx(TooltipProvider, { delayDuration: 0, children: _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("span", { children: isTruncation ? (_jsx(ScissorsLineDashed, { className: "h-3 w-3 text-destructive" })) : (_jsx(FoldVertical, { className: "h-3 w-3 text-text-secondary/70" })) }) }), _jsx(TooltipContent, { children: (() => {
254
+ const meta = toolCall._meta;
255
+ const percentage = meta?.originalTokens && meta?.finalTokens
256
+ ? Math.round((1 - meta.finalTokens / meta.originalTokens) * 100)
257
+ : null;
258
+ if (isTruncation) {
259
+ return percentage
260
+ ? `Response truncated (${percentage}% reduction)`
261
+ : "Response was truncated";
262
+ }
263
+ return percentage
264
+ ? `Response compacted (${percentage}% reduction)`
265
+ : "Response was compacted";
266
+ })() })] }) })), _jsx(ChevronDown, { className: `h-3 w-3 text-text-secondary/70 group-hover:text-text-secondary transition-colors transition-transform duration-200 ${isExpanded ? "rotate-180" : ""}` })] }), _jsx("div", { className: "pl-4.5", children: _jsx(SubAgentDetails, { port: toolCall.subagentPort, sessionId: toolCall.subagentSessionId, parentStatus: toolCall.status, agentName: toolCall.rawInput?.agentName, query: toolCall.rawInput?.query, isExpanded: isExpanded, onExpandChange: setIsExpanded, storedMessages: toolCall.subagentMessages, isReplay: isReplaySubagent }) })] }));
249
267
  }
250
- // Regular tool call - show details
251
- return (_jsx("div", { className: "flex items-start gap-1.5", children: _jsx("div", { className: "flex-1 ml-5", children: _jsx(ToolOperationDetails, { toolCall: toolCall, ...(hookNotification ? { hookNotification } : {}) }) }) }));
268
+ // Regular tool call - collapsible with clickable header
269
+ return (_jsxs("div", { className: "flex flex-col ml-5", children: [_jsxs("button", { type: "button", className: "flex items-center gap-1.5 cursor-pointer bg-transparent border-none p-0 text-left group w-fit", onClick: () => setIsExpanded(!isExpanded), "aria-expanded": isExpanded, children: [_jsx("span", { className: "text-paragraph-sm text-text-secondary/70 group-hover:text-text-secondary transition-colors", children: toolCall.prettyName || toolCall.title }), isFailed && (_jsx("span", { title: toolCall.error || "Operation failed", children: _jsx(AlertCircle, { className: "h-3 w-3 text-destructive" }) })), hasCompaction && (_jsx(TooltipProvider, { delayDuration: 0, children: _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("span", { children: isTruncation ? (_jsx(ScissorsLineDashed, { className: "h-3 w-3 text-destructive" })) : (_jsx(FoldVertical, { className: "h-3 w-3 text-text-secondary/70" })) }) }), _jsx(TooltipContent, { children: (() => {
270
+ const meta = toolCall._meta;
271
+ const percentage = meta?.originalTokens && meta?.finalTokens
272
+ ? Math.round((1 - meta.finalTokens / meta.originalTokens) * 100)
273
+ : null;
274
+ if (isTruncation) {
275
+ return percentage
276
+ ? `Response truncated (${percentage}% reduction)`
277
+ : "Response was truncated";
278
+ }
279
+ return percentage
280
+ ? `Response compacted (${percentage}% reduction)`
281
+ : "Response was compacted";
282
+ })() })] }) })), _jsx(ChevronDown, { className: `h-3 w-3 text-text-secondary/70 group-hover:text-text-secondary transition-colors transition-transform duration-200 ${isExpanded ? "rotate-180" : ""}` })] }), isExpanded && (_jsx("div", { className: "mt-1", children: _jsx(ToolOperationDetails, { toolCall: toolCall, ...(hookNotification ? { hookNotification } : {}) }) }))] }));
252
283
  }
253
284
  /**
254
285
  * Component to display detailed tool call information
@@ -290,7 +321,7 @@ function ToolOperationDetails({ toolCall, hookNotification, }) {
290
321
  "--w-rjv-type-nan-color": resolvedTheme === "dark" ? "#ef4444" : "#dc2626",
291
322
  "--w-rjv-type-undefined-color": resolvedTheme === "dark" ? "#ef4444" : "#dc2626",
292
323
  };
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) ||
324
+ 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._meta?.compactionAction && (_jsx(CompactionDetails, { compactionAction: toolCall._meta.compactionAction, originalTokens: toolCall._meta.originalTokens, finalTokens: toolCall._meta.finalTokens, originalContentPath: toolCall._meta.originalContentPath })), ((toolCall.content && toolCall.content.length > 0) ||
294
325
  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
326
  // Generate a stable key based on content
296
327
  const getBlockKey = () => {
@@ -314,7 +345,12 @@ function ToolOperationDetails({ toolCall, hookNotification, }) {
314
345
  try {
315
346
  const parsed = JSON.parse(text);
316
347
  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));
348
+ // Filter out internal metadata fields from display
349
+ const displayValue = { ...parsed };
350
+ if ("_compactionMeta" in displayValue) {
351
+ delete displayValue._compactionMeta;
352
+ }
353
+ return (_jsx("div", { className: "text-[11px]", children: _jsx(JsonView, { value: displayValue, collapsed: false, displayDataTypes: false, displayObjectSize: false, enableClipboard: true, shortenTextAfterLength: 80, style: jsonStyle }) }, key));
318
354
  }
319
355
  }
320
356
  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
+ };