@townco/ui 0.1.106 → 0.1.108

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.
@@ -160,6 +160,7 @@ export declare function useChatMessages(client: AcpClient | null, startSession:
160
160
  isStreaming?: boolean | undefined;
161
161
  }[] | undefined;
162
162
  subagentStreaming?: boolean | undefined;
163
+ subagentCompleted?: boolean | undefined;
163
164
  }[] | undefined;
164
165
  hookNotifications?: {
165
166
  id: string;
@@ -76,12 +76,28 @@ export function useChatSession(client, initialSessionId) {
76
76
  // Check if we should append to the last assistant message or create a new one
77
77
  const messages = useChatStore.getState().messages;
78
78
  const lastMessage = messages[messages.length - 1];
79
+ // Check if this is a replay chunk (not live streaming)
80
+ const isReplayChunk = updateMeta?.isReplay === true;
79
81
  if (update.message.role === "assistant" &&
82
+ lastMessage?.role === "assistant" &&
83
+ textContent &&
84
+ isReplayChunk) {
85
+ // During replay, always append to the last assistant message
86
+ // This allows multi-part messages (text, tools, text) to accumulate correctly
87
+ logger.debug("Appending replay text to existing assistant message", {
88
+ existingLength: lastMessage.content.length,
89
+ appendingLength: textContent.length,
90
+ });
91
+ useChatStore.getState().updateMessage(lastMessage.id, {
92
+ content: lastMessage.content + textContent,
93
+ });
94
+ }
95
+ else if (update.message.role === "assistant" &&
80
96
  lastMessage?.role === "assistant" &&
81
97
  lastMessage.content === "" &&
82
98
  textContent) {
83
- // Append text to existing empty assistant message (created by tool call)
84
- logger.debug("Appending text to existing assistant message");
99
+ // Non-replay: append text to existing empty assistant message (created by tool call)
100
+ logger.debug("Appending text to existing empty assistant message");
85
101
  useChatStore.getState().updateMessage(lastMessage.id, {
86
102
  content: lastMessage.content + textContent,
87
103
  });
@@ -157,6 +157,7 @@ export declare function useToolCalls(client: AcpClient | null): {
157
157
  isStreaming?: boolean | undefined;
158
158
  }[] | undefined;
159
159
  subagentStreaming?: boolean | undefined;
160
+ subagentCompleted?: boolean | undefined;
160
161
  }[]>;
161
162
  getToolCallsForSession: (sessionId: string) => {
162
163
  id: string;
@@ -307,5 +308,6 @@ export declare function useToolCalls(client: AcpClient | null): {
307
308
  isStreaming?: boolean | undefined;
308
309
  }[] | undefined;
309
310
  subagentStreaming?: boolean | undefined;
311
+ subagentCompleted?: boolean | undefined;
310
312
  }[];
311
313
  };
@@ -28,22 +28,10 @@ export function useToolCalls(client) {
28
28
  addToolCallToCurrentMessage(update.toolCall);
29
29
  }
30
30
  else if (update.type === "tool_call_update") {
31
- // Tool call update notification
32
- _logger.info("[SUBAGENT] Frontend received tool_call_update", {
33
- sessionId: update.sessionId,
34
- toolCallId: update.toolCallUpdate.id,
35
- status: update.toolCallUpdate.status,
36
- hasSubagentMessages: !!update.toolCallUpdate.subagentMessages,
37
- subagentMessageCount: update.toolCallUpdate.subagentMessages?.length || 0,
38
- });
39
31
  // Update session-level tool calls (for sidebar)
40
32
  updateToolCall(update.sessionId, update.toolCallUpdate);
41
33
  // Also update in current assistant message (for inline display)
42
34
  updateToolCallInCurrentMessage(update.toolCallUpdate);
43
- _logger.info("[SUBAGENT] Successfully updated tool call state", {
44
- sessionId: update.sessionId,
45
- toolCallId: update.toolCallUpdate.id,
46
- });
47
35
  }
48
36
  });
49
37
  return () => {
@@ -239,6 +239,7 @@ export declare const DisplayMessage: z.ZodObject<{
239
239
  isStreaming: z.ZodOptional<z.ZodBoolean>;
240
240
  }, z.core.$strip>>>;
241
241
  subagentStreaming: z.ZodOptional<z.ZodBoolean>;
242
+ subagentCompleted: z.ZodOptional<z.ZodBoolean>;
242
243
  }, z.core.$strip>>>;
243
244
  hookNotifications: z.ZodOptional<z.ZodArray<z.ZodObject<{
244
245
  id: z.ZodString;
@@ -503,6 +504,7 @@ export declare const ChatSessionState: z.ZodObject<{
503
504
  isStreaming: z.ZodOptional<z.ZodBoolean>;
504
505
  }, z.core.$strip>>>;
505
506
  subagentStreaming: z.ZodOptional<z.ZodBoolean>;
507
+ subagentCompleted: z.ZodOptional<z.ZodBoolean>;
506
508
  }, z.core.$strip>>>;
507
509
  hookNotifications: z.ZodOptional<z.ZodArray<z.ZodObject<{
508
510
  id: z.ZodString;
@@ -445,6 +445,7 @@ export declare const ToolCallSchema: z.ZodObject<{
445
445
  isStreaming: z.ZodOptional<z.ZodBoolean>;
446
446
  }, z.core.$strip>>>;
447
447
  subagentStreaming: z.ZodOptional<z.ZodBoolean>;
448
+ subagentCompleted: z.ZodOptional<z.ZodBoolean>;
448
449
  }, z.core.$strip>;
449
450
  export type ToolCall = z.infer<typeof ToolCallSchema>;
450
451
  /**
@@ -596,6 +597,7 @@ export declare const ToolCallUpdateSchema: z.ZodObject<{
596
597
  }, z.core.$strip>], "type">>>;
597
598
  isStreaming: z.ZodOptional<z.ZodBoolean>;
598
599
  }, z.core.$strip>>>;
600
+ subagentCompleted: z.ZodOptional<z.ZodBoolean>;
599
601
  _meta: z.ZodOptional<z.ZodObject<{
600
602
  truncationWarning: z.ZodOptional<z.ZodString>;
601
603
  compactionAction: z.ZodOptional<z.ZodEnum<{
@@ -183,6 +183,8 @@ export const ToolCallSchema = z.object({
183
183
  subagentMessages: z.array(SubagentMessageSchema).optional(),
184
184
  /** Whether the sub-agent is currently streaming */
185
185
  subagentStreaming: z.boolean().optional(),
186
+ /** Whether the sub-agent has completed */
187
+ subagentCompleted: z.boolean().optional(),
186
188
  });
187
189
  /**
188
190
  * Partial update for an existing tool call
@@ -207,6 +209,8 @@ export const ToolCallUpdateSchema = z.object({
207
209
  subagentSessionId: z.string().optional(),
208
210
  /** Sub-agent messages for replay */
209
211
  subagentMessages: z.array(SubagentMessageSchema).optional(),
212
+ /** Whether the sub-agent has completed */
213
+ subagentCompleted: z.boolean().optional(),
210
214
  /** Internal metadata (e.g., compaction info) */
211
215
  _meta: z
212
216
  .object({
@@ -245,6 +249,8 @@ export function mergeToolCallUpdate(existing, update) {
245
249
  subagentSessionId: update.subagentSessionId ?? existing.subagentSessionId,
246
250
  // Sub-agent messages for replay
247
251
  subagentMessages: update.subagentMessages ?? existing.subagentMessages,
252
+ // Sub-agent completion status (once true, stays true)
253
+ subagentCompleted: update.subagentCompleted ?? existing.subagentCompleted,
248
254
  // Internal metadata (compaction info)
249
255
  _meta: update._meta ?? existing._meta,
250
256
  };
@@ -265,10 +265,12 @@ export const useChatStore = create((set) => ({
265
265
  if (!lastAssistantMsg)
266
266
  return state;
267
267
  // Track the content position where this tool call was invoked
268
- const contentPosition = lastAssistantMsg.content.length;
268
+ // Set contentPosition to where the tool call was invoked in the content stream
269
+ // This ensures tool calls appear inline at the correct position, even during streaming
270
+ const currentContentLength = lastAssistantMsg.content.length;
269
271
  const toolCallWithPosition = {
270
272
  ...toolCall,
271
- contentPosition,
273
+ contentPosition: currentContentLength,
272
274
  };
273
275
  messages[lastAssistantIndex] = {
274
276
  ...lastAssistantMsg,
@@ -300,7 +302,8 @@ export const useChatStore = create((set) => ({
300
302
  if (!existing)
301
303
  return state;
302
304
  const updatedToolCalls = [...toolCalls];
303
- updatedToolCalls[existingIndex] = mergeToolCallUpdate(existing, update);
305
+ const merged = mergeToolCallUpdate(existing, update);
306
+ updatedToolCalls[existingIndex] = merged;
304
307
  messages[lastAssistantIndex] = {
305
308
  ...lastAssistantMsg,
306
309
  toolCalls: updatedToolCalls,
@@ -14,7 +14,14 @@ export function getToolCallDisplayState(toolCall) {
14
14
  if (toolCall.status === "failed" || toolCall.error) {
15
15
  return "failed";
16
16
  }
17
- // Completed state
17
+ // Check if this is a subagent call
18
+ const hasSubagent = !!(toolCall.subagentMessages && toolCall.subagentMessages.length > 0);
19
+ // For subagent calls: completed when subagentCompleted is true (regardless of status)
20
+ // This handles the case where status is "in_progress" but subagent has finished
21
+ if (hasSubagent && toolCall.subagentCompleted) {
22
+ return "completed";
23
+ }
24
+ // Completed state (for non-subagent calls)
18
25
  if (toolCall.status === "completed") {
19
26
  return "completed";
20
27
  }
@@ -2,7 +2,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
2
2
  import { cva } from "class-variance-authority";
3
3
  import * as React from "react";
4
4
  import { useChatStore } from "../../core/store/chat-store.js";
5
- import { isPreliminaryToolCall } from "../../core/utils/tool-call-state.js";
5
+ import { getToolCallDisplayState, isPreliminaryToolCall, } from "../../core/utils/tool-call-state.js";
6
6
  import { cn } from "../lib/utils.js";
7
7
  import { HookNotification } from "./HookNotification.js";
8
8
  import { Reasoning } from "./Reasoning.js";
@@ -140,6 +140,19 @@ export const MessageContent = React.forwardRef(({ role: roleProp, variant, isStr
140
140
  };
141
141
  // Get tool_response hook notifications to associate with tool calls
142
142
  const toolResponseHooks = (message.hookNotifications || []).filter((n) => n.hookType === "tool_response");
143
+ // Helper to check if all non-preliminary tool calls are completed
144
+ const areAllToolCallsCompleted = () => {
145
+ const nonPreliminary = sortedToolCalls.filter((tc) => !isPreliminaryToolCall(tc));
146
+ if (nonPreliminary.length === 0)
147
+ return false;
148
+ return nonPreliminary.every((tc) => getToolCallDisplayState(tc) === "completed");
149
+ };
150
+ // Check if we should show the thinking indicator
151
+ // Show when: all tool calls are complete, message is streaming, and there's minimal content after tool calls
152
+ const shouldShowThinkingIndicator = message.isStreaming &&
153
+ sortedToolCalls.length > 0 &&
154
+ areAllToolCallsCompleted() &&
155
+ message.content.trim().length < 50; // Only show if there's very little text content
143
156
  // Helper to render a tool call or group
144
157
  const renderToolCallOrGroup = (item, index) => {
145
158
  // Batch group (parallel operations)
@@ -175,9 +188,8 @@ export const MessageContent = React.forwardRef(({ role: roleProp, variant, isStr
175
188
  // Check if hook notifications have positions for inline rendering
176
189
  const hasHookPositions = hookNotifications.some((n) => n.contentPosition !== undefined);
177
190
  if (!hasHookPositions) {
178
- // No positions - render hooks at top, then tool calls, then content
179
- return (_jsxs(_Fragment, { children: [hookNotifications.length > 0 && (_jsx("div", { className: "flex flex-col gap-2 mb-1", children: hookNotifications.map((notification) => (_jsx(HookNotification, { notification: notification }, notification.id))) })), groupedToolCalls.length > 0 && (_jsx("div", { className: "flex flex-col gap-2 mb-1", children: groupedToolCalls.map((item, index) => renderToolCallOrGroup(item, index)) })), _jsx("div", { children: _jsx(Response, { content: message.content, isStreaming: message.isStreaming, showEmpty: false, sources: message.sources ?? [] }) })
180
- ] }));
191
+ // No positions - render hooks at top, then content, then tool calls
192
+ return (_jsxs(_Fragment, { children: [hookNotifications.length > 0 && (_jsx("div", { className: "flex flex-col gap-2 mb-1", children: hookNotifications.map((notification) => (_jsx(HookNotification, { notification: notification }, notification.id))) })), _jsx("div", { children: _jsx(Response, { content: message.content, isStreaming: message.isStreaming, showEmpty: false, sources: message.sources ?? [] }) }), groupedToolCalls.length > 0 && (_jsx("div", { className: "flex flex-col gap-2 mb-1", children: groupedToolCalls.map((item, index) => renderToolCallOrGroup(item, index)) })), shouldShowThinkingIndicator && (_jsx("div", { className: "flex flex-col my-4 rounded-md px-1 -mx-1 w-fit", children: _jsx("div", { className: "flex items-center gap-1.5", children: _jsx("span", { className: "text-paragraph-sm text-text-secondary/70", children: "Thinking..." }) }) }))] }));
181
193
  }
182
194
  // Hooks have positions - render them inline with content
183
195
  const elements = [];
@@ -336,6 +348,10 @@ export const MessageContent = React.forwardRef(({ role: roleProp, variant, isStr
336
348
  toolCalls: [preliminaryToolCalls[0]], isGrouped: false }) }, `tool-${preliminaryToolCalls[0]?.id}`));
337
349
  }
338
350
  }
351
+ // Add thinking indicator if all tool calls are complete but message is still streaming
352
+ if (shouldShowThinkingIndicator) {
353
+ elements.push(_jsx("div", { className: "flex flex-col my-4 rounded-md px-1 -mx-1 w-fit", children: _jsx("div", { className: "flex items-center gap-1.5", children: _jsx("span", { className: "text-paragraph-sm text-text-secondary/70", children: "Thinking..." }) }) }, "thinking-indicator"));
354
+ }
339
355
  return _jsx(_Fragment, { children: elements });
340
356
  })()) : (_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, imageIndex) => (_jsx("img", { src: `data:${image.mimeType};base64,${image.data}`, alt: `Attachment ${imageIndex + 1}`, className: "max-w-[200px] max-h-[200px] rounded-lg object-cover" }, `image-${image.mimeType}-${image.data.slice(0, 20)}`))) })), message.content && (_jsx("div", { className: "whitespace-pre-wrap", children: message.content }))] }))] }));
341
357
  }
@@ -23,18 +23,6 @@ export function SubAgentDetails({ parentStatus, agentName, query, isExpanded: co
23
23
  const thinkingContainerRef = useRef(null);
24
24
  // Use messages from storedMessages prop (populated by parent via tool_call_update)
25
25
  const messages = storedMessages || [];
26
- // Log when messages are received/updated
27
- useEffect(() => {
28
- console.log("[SUBAGENT] SubAgentDetails received messages update", {
29
- agentName,
30
- messageCount: messages.length,
31
- parentStatus,
32
- hasMessages: messages.length > 0,
33
- contentPreview: messages[0]?.content?.substring(0, 100),
34
- toolCallCount: messages[0]?.toolCalls?.length || 0,
35
- contentBlockCount: messages[0]?.contentBlocks?.length || 0,
36
- });
37
- }, [messages, agentName, parentStatus]);
38
26
  // Determine if subagent is still running based on parent status
39
27
  const isRunning = parentStatus === "in_progress" || parentStatus === "pending";
40
28
  // Get the current/latest message
@@ -44,13 +32,6 @@ export function SubAgentDetails({ parentStatus, agentName, query, isExpanded: co
44
32
  (currentMessage.toolCalls && currentMessage.toolCalls.length > 0) ||
45
33
  (currentMessage.contentBlocks &&
46
34
  currentMessage.contentBlocks.length > 0));
47
- console.log("[SUBAGENT] SubAgentDetails render state", {
48
- agentName,
49
- messageCount: messages.length,
50
- hasContent,
51
- isRunning,
52
- parentStatus,
53
- });
54
35
  // Auto-collapse Thinking when completed (so Output is the primary view)
55
36
  const prevIsRunningRef = useRef(isRunning);
56
37
  useEffect(() => {
@@ -71,8 +71,8 @@ function formatElapsedTime(seconds) {
71
71
  /**
72
72
  * Component to display running duration for a tool call
73
73
  */
74
- function RunningDuration({ startTime }) {
75
- const elapsed = useElapsedTime(startTime, true);
74
+ function RunningDuration({ startTime, isRunning = true, }) {
75
+ const elapsed = useElapsedTime(startTime, isRunning);
76
76
  return (_jsx("span", { className: "text-xs text-text-secondary/70 tabular-nums ml-1", children: formatElapsedTime(elapsed) }));
77
77
  }
78
78
  /**
@@ -227,7 +227,7 @@ export function ToolOperation({ toolCalls, isGrouped = false, autoMinimize = tru
227
227
  // For preliminary/selecting states, show simple non-expandable text
228
228
  if (isSelecting && !isGrouped) {
229
229
  return (_jsx("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: [
230
- _jsx("div", { className: "text-text-secondary/70", children: _jsx(IconComponent, { className: "h-3 w-3" }) }), _jsxs("span", { className: "text-paragraph-xs text-text-muted", children: [displayText, singleToolCall?.startedAt && (_jsx(RunningDuration, { startTime: singleToolCall.startedAt }))] })
230
+ _jsx("div", { className: "text-text-secondary/70", children: _jsx(IconComponent, { className: "h-3 w-3" }) }), _jsxs("span", { className: "text-paragraph-xs text-text-muted", children: [displayText, singleToolCall?.startedAt && (_jsx(RunningDuration, { startTime: singleToolCall.startedAt, isRunning: !singleToolCall.subagentCompleted }))] })
231
231
  ] }) }));
232
232
  }
233
233
  // If it's a grouped preliminary (selecting) state
@@ -241,9 +241,11 @@ export function ToolOperation({ toolCalls, isGrouped = false, autoMinimize = tru
241
241
  return null;
242
242
  return Math.min(...startTimes);
243
243
  })();
244
+ // Check if all tool calls in the group are completed
245
+ const allCompleted = toolCalls.every((tc) => tc.subagentCompleted);
244
246
  return (_jsxs("div", { className: "flex flex-col my-4 rounded-md px-1 -mx-1 w-fit", children: [
245
247
  _jsxs("div", { className: "flex items-center gap-1.5", children: [
246
- _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" }), selectingStartTime && (_jsx(RunningDuration, { startTime: selectingStartTime })), _jsx("span", { className: "text-[10px] bg-muted px-1.5 py-0.5 rounded text-text-secondary/70", children: toolCalls.length })
248
+ _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" }), selectingStartTime && (_jsx(RunningDuration, { startTime: selectingStartTime, isRunning: !allCompleted })), _jsx("span", { className: "text-[10px] bg-muted px-1.5 py-0.5 rounded text-text-secondary/70", children: toolCalls.length })
247
249
  ] }), _jsx("span", { className: "text-paragraph-sm text-text-secondary/70 pl-4.5", children: displayText })
248
250
  ] }));
249
251
  }
@@ -253,7 +255,7 @@ export function ToolOperation({ toolCalls, isGrouped = false, autoMinimize = tru
253
255
  _jsxs("div", { className: "flex items-center gap-1.5", children: [
254
256
  _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 &&
255
257
  singleToolCall?.startedAt &&
256
- displayState === "executing" && (_jsx(RunningDuration, { startTime: singleToolCall.startedAt })), isGrouped &&
258
+ displayState === "executing" && (_jsx(RunningDuration, { startTime: singleToolCall.startedAt, isRunning: !singleToolCall.subagentCompleted })), isGrouped &&
257
259
  displayState === "executing" &&
258
260
  (() => {
259
261
  const startTimes = toolCalls
@@ -262,7 +264,10 @@ export function ToolOperation({ toolCalls, isGrouped = false, autoMinimize = tru
262
264
  if (startTimes.length === 0)
263
265
  return null;
264
266
  const earliestStart = Math.min(...startTimes);
265
- return _jsx(RunningDuration, { startTime: earliestStart });
267
+ // Check if all tool calls are completed
268
+ const allCompleted = toolCalls.every((tc) => tc.status === "completed" &&
269
+ (!tc.subagentMessages || tc.subagentCompleted));
270
+ return (_jsx(RunningDuration, { startTime: earliestStart, isRunning: !allCompleted }));
266
271
  })(), 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
267
272
  ? `${toolCalls.filter((tc) => tc.status === "failed").length} of ${toolCalls.length} operations failed`
268
273
  : singleToolCall?.error || "Operation failed", children: _jsx(AlertCircle, { className: "h-3 w-3 text-destructive" }) })), isGrouped && groupHasCompaction && (_jsx(TooltipProvider, { delayDuration: 0, children: _jsxs(Tooltip, { children: [
@@ -338,7 +343,8 @@ function GroupedToolCallItem({ toolCall, hookNotification, }) {
338
343
  hookNotification?.metadata?.action === "compacted_then_truncated" ||
339
344
  toolCall._meta?.compactionAction === "truncated");
340
345
  const isFailed = toolCall.status === "failed";
341
- const isRunning = toolCall.status === "pending" || toolCall.status === "in_progress";
346
+ const isRunning = (toolCall.status === "pending" || toolCall.status === "in_progress") &&
347
+ !toolCall.subagentCompleted;
342
348
  if (isSubagentCall) {
343
349
  // Render subagent with clickable header and SubAgentDetails component
344
350
  return (_jsxs("div", { className: "flex flex-col ml-5", children: [
@@ -350,6 +350,7 @@ export declare const SessionUpdate: z.ZodUnion<readonly [z.ZodObject<{
350
350
  isStreaming: z.ZodOptional<z.ZodBoolean>;
351
351
  }, z.core.$strip>>>;
352
352
  subagentStreaming: z.ZodOptional<z.ZodBoolean>;
353
+ subagentCompleted: z.ZodOptional<z.ZodBoolean>;
353
354
  }, z.core.$strip>;
354
355
  messageId: z.ZodOptional<z.ZodString>;
355
356
  }, z.core.$strip>, z.ZodObject<{
@@ -557,6 +558,7 @@ export declare const SessionUpdate: z.ZodUnion<readonly [z.ZodObject<{
557
558
  }, z.core.$strip>], "type">>>;
558
559
  isStreaming: z.ZodOptional<z.ZodBoolean>;
559
560
  }, z.core.$strip>>>;
561
+ subagentCompleted: z.ZodOptional<z.ZodBoolean>;
560
562
  _meta: z.ZodOptional<z.ZodObject<{
561
563
  truncationWarning: z.ZodOptional<z.ZodString>;
562
564
  compactionAction: z.ZodOptional<z.ZodEnum<{
@@ -1169,6 +1169,17 @@ export class HttpTransport {
1169
1169
  typeof update._meta.originalContentPath === "string"
1170
1170
  ? update._meta.originalContentPath
1171
1171
  : undefined;
1172
+ // Check for subagentCompleted at top level first (sent by adapter),
1173
+ // then fallback to _meta (for backward compatibility)
1174
+ const subagentCompleted = "subagentCompleted" in update &&
1175
+ typeof update.subagentCompleted === "boolean"
1176
+ ? update.subagentCompleted
1177
+ : update._meta &&
1178
+ typeof update._meta === "object" &&
1179
+ "subagentCompleted" in update._meta &&
1180
+ typeof update._meta.subagentCompleted === "boolean"
1181
+ ? update._meta.subagentCompleted
1182
+ : undefined;
1172
1183
  // Tool call update notification
1173
1184
  const toolCallUpdate = {
1174
1185
  id: update.toolCallId ?? "",
@@ -1181,6 +1192,7 @@ export class HttpTransport {
1181
1192
  locations: update.locations,
1182
1193
  rawOutput: update.rawOutput,
1183
1194
  tokenUsage: update.tokenUsage,
1195
+ subagentCompleted,
1184
1196
  content: update.content?.map((c) => {
1185
1197
  // Type guard to safely check properties
1186
1198
  if (typeof c !== "object" || c === null) {
@@ -1492,8 +1504,9 @@ export class HttpTransport {
1492
1504
  isComplete: false,
1493
1505
  };
1494
1506
  }
1495
- // Only queue chunks for live streaming, not replay
1496
- if (chunk && !isReplay) {
1507
+ // Queue chunks for both live streaming AND replay
1508
+ // Replay chunks need to be accumulated just like live chunks
1509
+ if (chunk) {
1497
1510
  // Resolve any waiting receive() calls immediately
1498
1511
  const resolver = this.chunkResolvers.shift();
1499
1512
  if (resolver) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@townco/ui",
3
- "version": "0.1.106",
3
+ "version": "0.1.108",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -49,7 +49,7 @@
49
49
  "@radix-ui/react-slot": "^1.2.4",
50
50
  "@radix-ui/react-tabs": "^1.1.13",
51
51
  "@radix-ui/react-tooltip": "^1.2.8",
52
- "@townco/core": "0.0.84",
52
+ "@townco/core": "0.0.86",
53
53
  "@types/mdast": "^4.0.4",
54
54
  "@uiw/react-json-view": "^2.0.0-alpha.39",
55
55
  "class-variance-authority": "^0.7.1",
@@ -67,7 +67,7 @@
67
67
  "zustand": "^5.0.8"
68
68
  },
69
69
  "devDependencies": {
70
- "@townco/tsconfig": "0.1.103",
70
+ "@townco/tsconfig": "0.1.105",
71
71
  "@types/node": "^24.10.0",
72
72
  "@types/react": "^19.2.2",
73
73
  "@types/unist": "^3.0.3",