@townco/ui 0.1.66 → 0.1.68

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.
@@ -14,7 +14,7 @@ export declare function useChatMessages(client: AcpClient | null, startSession:
14
14
  toolCalls?: {
15
15
  id: string;
16
16
  title: string;
17
- kind: "read" | "edit" | "delete" | "move" | "search" | "execute" | "think" | "fetch" | "switch_mode" | "other";
17
+ kind: "search" | "read" | "edit" | "delete" | "move" | "execute" | "think" | "fetch" | "switch_mode" | "other";
18
18
  status: "pending" | "in_progress" | "completed" | "failed";
19
19
  batchId?: string | undefined;
20
20
  prettyName?: string | undefined;
@@ -3,7 +3,7 @@ import type { AcpClient } from "../../sdk/client/index.js";
3
3
  * Hook for managing chat session lifecycle
4
4
  */
5
5
  export declare function useChatSession(client: AcpClient | null, initialSessionId?: string | null): {
6
- connectionStatus: "error" | "connecting" | "connected" | "disconnected";
6
+ connectionStatus: "disconnected" | "connecting" | "connected" | "error";
7
7
  sessionId: string | null;
8
8
  connect: () => Promise<void>;
9
9
  loadSession: (sessionIdToLoad: string) => Promise<void>;
@@ -12,7 +12,7 @@ export declare function useToolCalls(client: AcpClient | null): {
12
12
  toolCalls: Record<string, {
13
13
  id: string;
14
14
  title: string;
15
- kind: "read" | "edit" | "delete" | "move" | "search" | "execute" | "think" | "fetch" | "switch_mode" | "other";
15
+ kind: "search" | "read" | "edit" | "delete" | "move" | "execute" | "think" | "fetch" | "switch_mode" | "other";
16
16
  status: "pending" | "in_progress" | "completed" | "failed";
17
17
  batchId?: string | undefined;
18
18
  prettyName?: string | undefined;
@@ -1,5 +1,7 @@
1
+ import { createLogger } from "@townco/core";
1
2
  import { useEffect } from "react";
2
3
  import { useChatStore } from "../store/chat-store.js";
4
+ const logger = createLogger("use-tool-calls", "debug");
3
5
  /**
4
6
  * Hook to track and manage tool calls from ACP sessions
5
7
  *
@@ -20,7 +22,6 @@ export function useToolCalls(client) {
20
22
  // Subscribe to session updates for tool calls
21
23
  const unsubscribe = client.onSessionUpdate((update) => {
22
24
  if (update.type === "tool_call") {
23
- // Initial tool call notification
24
25
  // Add to session-level tool calls (for sidebar)
25
26
  addToolCall(update.sessionId, update.toolCall);
26
27
  // Also add to current assistant message (for inline display)
@@ -498,6 +498,97 @@ export declare const ToolCallUpdateSchema: z.ZodObject<{
498
498
  }, z.core.$strip>>;
499
499
  subagentPort: z.ZodOptional<z.ZodNumber>;
500
500
  subagentSessionId: z.ZodOptional<z.ZodString>;
501
+ subagentMessages: z.ZodOptional<z.ZodArray<z.ZodObject<{
502
+ id: z.ZodString;
503
+ content: z.ZodString;
504
+ toolCalls: z.ZodOptional<z.ZodArray<z.ZodObject<{
505
+ id: z.ZodString;
506
+ title: z.ZodString;
507
+ prettyName: z.ZodOptional<z.ZodString>;
508
+ icon: z.ZodOptional<z.ZodString>;
509
+ status: z.ZodEnum<{
510
+ pending: "pending";
511
+ in_progress: "in_progress";
512
+ completed: "completed";
513
+ failed: "failed";
514
+ }>;
515
+ content: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
516
+ type: z.ZodLiteral<"content">;
517
+ content: z.ZodObject<{
518
+ type: z.ZodLiteral<"text">;
519
+ text: z.ZodString;
520
+ }, z.core.$strip>;
521
+ }, z.core.$strip>, z.ZodObject<{
522
+ type: z.ZodLiteral<"text">;
523
+ text: z.ZodString;
524
+ }, z.core.$strip>, z.ZodObject<{
525
+ type: z.ZodLiteral<"image">;
526
+ data: z.ZodString;
527
+ mimeType: z.ZodOptional<z.ZodString>;
528
+ alt: z.ZodOptional<z.ZodString>;
529
+ }, z.core.$strip>, z.ZodObject<{
530
+ type: z.ZodLiteral<"image">;
531
+ url: z.ZodString;
532
+ alt: z.ZodOptional<z.ZodString>;
533
+ }, z.core.$strip>, z.ZodObject<{
534
+ type: z.ZodLiteral<"diff">;
535
+ path: z.ZodString;
536
+ oldText: z.ZodString;
537
+ newText: z.ZodString;
538
+ line: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
539
+ }, z.core.$strip>, z.ZodObject<{
540
+ type: z.ZodLiteral<"terminal">;
541
+ terminalId: z.ZodString;
542
+ }, z.core.$strip>], "type">>>;
543
+ }, z.core.$strip>>>;
544
+ contentBlocks: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
545
+ type: z.ZodLiteral<"text">;
546
+ text: z.ZodString;
547
+ }, z.core.$strip>, z.ZodObject<{
548
+ type: z.ZodLiteral<"tool_call">;
549
+ toolCall: z.ZodObject<{
550
+ id: z.ZodString;
551
+ title: z.ZodString;
552
+ prettyName: z.ZodOptional<z.ZodString>;
553
+ icon: z.ZodOptional<z.ZodString>;
554
+ status: z.ZodEnum<{
555
+ pending: "pending";
556
+ in_progress: "in_progress";
557
+ completed: "completed";
558
+ failed: "failed";
559
+ }>;
560
+ content: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
561
+ type: z.ZodLiteral<"content">;
562
+ content: z.ZodObject<{
563
+ type: z.ZodLiteral<"text">;
564
+ text: z.ZodString;
565
+ }, z.core.$strip>;
566
+ }, z.core.$strip>, z.ZodObject<{
567
+ type: z.ZodLiteral<"text">;
568
+ text: z.ZodString;
569
+ }, z.core.$strip>, z.ZodObject<{
570
+ type: z.ZodLiteral<"image">;
571
+ data: z.ZodString;
572
+ mimeType: z.ZodOptional<z.ZodString>;
573
+ alt: z.ZodOptional<z.ZodString>;
574
+ }, z.core.$strip>, z.ZodObject<{
575
+ type: z.ZodLiteral<"image">;
576
+ url: z.ZodString;
577
+ alt: z.ZodOptional<z.ZodString>;
578
+ }, z.core.$strip>, z.ZodObject<{
579
+ type: z.ZodLiteral<"diff">;
580
+ path: z.ZodString;
581
+ oldText: z.ZodString;
582
+ newText: z.ZodString;
583
+ line: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
584
+ }, z.core.$strip>, z.ZodObject<{
585
+ type: z.ZodLiteral<"terminal">;
586
+ terminalId: z.ZodString;
587
+ }, z.core.$strip>], "type">>>;
588
+ }, z.core.$strip>;
589
+ }, z.core.$strip>], "type">>>;
590
+ isStreaming: z.ZodOptional<z.ZodBoolean>;
591
+ }, z.core.$strip>>>;
501
592
  }, z.core.$strip>;
502
593
  export type ToolCallUpdate = z.infer<typeof ToolCallUpdateSchema>;
503
594
  /**
@@ -195,6 +195,8 @@ export const ToolCallUpdateSchema = z.object({
195
195
  subagentPort: z.number().optional(),
196
196
  /** Sub-agent session ID for SSE connection */
197
197
  subagentSessionId: z.string().optional(),
198
+ /** Sub-agent messages for replay */
199
+ subagentMessages: z.array(SubagentMessageSchema).optional(),
198
200
  });
199
201
  /**
200
202
  * Helper to merge a tool call update into an existing tool call
@@ -220,6 +222,8 @@ export function mergeToolCallUpdate(existing, update) {
220
222
  // Sub-agent connection info
221
223
  subagentPort: update.subagentPort ?? existing.subagentPort,
222
224
  subagentSessionId: update.subagentSessionId ?? existing.subagentSessionId,
225
+ // Sub-agent messages for replay
226
+ subagentMessages: update.subagentMessages ?? existing.subagentMessages,
223
227
  };
224
228
  return merged;
225
229
  }
@@ -1,7 +1,7 @@
1
1
  import { type VariantProps } from "class-variance-authority";
2
2
  import * as React from "react";
3
3
  declare const buttonVariants: (props?: ({
4
- variant?: "default" | "link" | "destructive" | "outline" | "secondary" | "ghost" | null | undefined;
4
+ variant?: "link" | "default" | "destructive" | "outline" | "secondary" | "ghost" | null | undefined;
5
5
  size?: "default" | "icon" | "sm" | "lg" | null | undefined;
6
6
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
7
7
  export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
@@ -58,11 +58,13 @@ export const MessageContent = React.forwardRef(({ role: roleProp, variant, isStr
58
58
  // Helper to check if a tool call is preliminary (invoking)
59
59
  const isPreliminary = (tc) => tc.status === "pending" &&
60
60
  (!tc.rawInput || Object.keys(tc.rawInput).length === 0);
61
- // Helper to group tool calls by batchId and consecutive preliminary calls
61
+ // Helper to group tool calls by batchId, consecutive same-title calls, or consecutive preliminary calls
62
62
  const groupToolCalls = (toolCalls) => {
63
63
  const result = [];
64
64
  const batchGroups = new Map();
65
65
  let currentInvokingGroup = [];
66
+ let currentConsecutiveGroup = [];
67
+ let currentConsecutiveTitle = null;
66
68
  const flushInvokingGroup = () => {
67
69
  if (currentInvokingGroup.length > 1) {
68
70
  result.push({
@@ -75,10 +77,25 @@ export const MessageContent = React.forwardRef(({ role: roleProp, variant, isStr
75
77
  }
76
78
  currentInvokingGroup = [];
77
79
  };
80
+ const flushConsecutiveGroup = () => {
81
+ if (currentConsecutiveGroup.length > 1) {
82
+ // Multiple consecutive same-title calls - group them
83
+ result.push({
84
+ type: "batch",
85
+ toolCalls: currentConsecutiveGroup,
86
+ });
87
+ }
88
+ else if (currentConsecutiveGroup.length === 1) {
89
+ result.push(currentConsecutiveGroup[0]);
90
+ }
91
+ currentConsecutiveGroup = [];
92
+ currentConsecutiveTitle = null;
93
+ };
78
94
  for (const tc of toolCalls) {
79
- // Handle batch groups
95
+ // Handle batch groups (explicit batchId)
80
96
  if (tc.batchId) {
81
97
  flushInvokingGroup();
98
+ flushConsecutiveGroup();
82
99
  const existing = batchGroups.get(tc.batchId);
83
100
  if (existing) {
84
101
  existing.push(tc);
@@ -91,16 +108,27 @@ export const MessageContent = React.forwardRef(({ role: roleProp, variant, isStr
91
108
  }
92
109
  // Handle consecutive preliminary (invoking) tool calls
93
110
  else if (isPreliminary(tc)) {
111
+ flushConsecutiveGroup();
94
112
  currentInvokingGroup.push(tc);
95
113
  }
96
- // Regular tool call
114
+ // Regular tool call - group consecutive same-title calls (e.g., subagent)
97
115
  else {
98
116
  flushInvokingGroup();
99
- result.push(tc);
117
+ // Check if this continues a consecutive group
118
+ if (currentConsecutiveTitle === tc.title) {
119
+ currentConsecutiveGroup.push(tc);
120
+ }
121
+ else {
122
+ // Different title - flush previous group and start new one
123
+ flushConsecutiveGroup();
124
+ currentConsecutiveGroup = [tc];
125
+ currentConsecutiveTitle = tc.title;
126
+ }
100
127
  }
101
128
  }
102
- // Flush any remaining invoking group
129
+ // Flush any remaining groups
103
130
  flushInvokingGroup();
131
+ flushConsecutiveGroup();
104
132
  return result;
105
133
  };
106
134
  // Helper to render a tool call or group
@@ -127,20 +155,23 @@ export const MessageContent = React.forwardRef(({ role: roleProp, variant, isStr
127
155
  return (_jsxs(_Fragment, { children: [groupedToolCalls.length > 0 && (_jsx("div", { className: "flex flex-col gap-2 mb-1", children: groupedToolCalls.map((item, index) => renderToolCallOrGroup(item, index)) })), _jsx(Response, { content: message.content, isStreaming: message.isStreaming, showEmpty: false })] }));
128
156
  }
129
157
  // Render content interleaved with tool calls
130
- // Group consecutive tool calls with the same batchId
158
+ // Group consecutive tool calls with the same batchId or same title
131
159
  const elements = [];
132
160
  let currentPosition = 0;
133
161
  let currentBatch = [];
134
162
  let currentBatchId;
163
+ let currentBatchTitle;
135
164
  const flushBatch = () => {
136
- if (currentBatch.length > 1 && currentBatchId) {
137
- elements.push(_jsx(ToolCallGroup, { toolCalls: currentBatch }, `group-${currentBatchId}`));
165
+ if (currentBatch.length > 1) {
166
+ // Group multiple consecutive calls (by batchId or same title)
167
+ elements.push(_jsx(ToolCallGroup, { toolCalls: currentBatch }, `group-${currentBatchId || currentBatchTitle}-${currentBatch[0].id}`));
138
168
  }
139
169
  else if (currentBatch.length === 1) {
140
170
  elements.push(_jsx("div", { children: _jsx(ToolCall, { toolCall: currentBatch[0] }) }, `tool-${currentBatch[0].id}`));
141
171
  }
142
172
  currentBatch = [];
143
173
  currentBatchId = undefined;
174
+ currentBatchTitle = undefined;
144
175
  };
145
176
  // Separate preliminary tool calls - they should render at the end, not break text
146
177
  const preliminaryToolCalls = sortedToolCalls.filter(isPreliminary);
@@ -157,7 +188,7 @@ export const MessageContent = React.forwardRef(({ role: roleProp, variant, isStr
157
188
  elements.push(_jsx(Response, { content: textChunk, isStreaming: false, showEmpty: false }, `text-before-${toolCall.id}`));
158
189
  }
159
190
  }
160
- // Check if this tool call should be batched
191
+ // Check if this tool call should be batched (by batchId or consecutive same title)
161
192
  if (toolCall.batchId) {
162
193
  if (currentBatchId === toolCall.batchId) {
163
194
  // Same batch, add to current batch
@@ -167,13 +198,20 @@ export const MessageContent = React.forwardRef(({ role: roleProp, variant, isStr
167
198
  // Different batch, flush previous and start new
168
199
  flushBatch();
169
200
  currentBatchId = toolCall.batchId;
201
+ currentBatchTitle = toolCall.title;
170
202
  currentBatch = [toolCall];
171
203
  }
172
204
  }
205
+ else if (currentBatchTitle === toolCall.title &&
206
+ !currentBatchId) {
207
+ // Same title as previous (no batchId), continue grouping
208
+ currentBatch.push(toolCall);
209
+ }
173
210
  else {
174
- // No batch ID, flush previous and render individually
211
+ // Different title or switching from batchId to title grouping
175
212
  flushBatch();
176
- elements.push(_jsx("div", { children: _jsx(ToolCall, { toolCall: toolCall }) }, `tool-${toolCall.id}`));
213
+ currentBatchTitle = toolCall.title;
214
+ currentBatch = [toolCall];
177
215
  }
178
216
  currentPosition = position;
179
217
  // Flush batch at the end
@@ -1,8 +1,9 @@
1
+ import type { ToolCall as ToolCallType } from "../../core/schemas/tool-call.js";
1
2
  export interface SubAgentDetailsProps {
2
- /** Sub-agent HTTP port */
3
- port: number;
4
- /** Sub-agent session ID */
5
- sessionId: string;
3
+ /** Sub-agent HTTP port (required for live streaming, not for replay) */
4
+ port?: number | undefined;
5
+ /** Sub-agent session ID (required for live streaming, not for replay) */
6
+ sessionId?: string | undefined;
6
7
  /** Optional host (defaults to localhost) */
7
8
  host?: string;
8
9
  /** Parent tool call status - use this to determine if sub-agent is running */
@@ -15,13 +16,19 @@ export interface SubAgentDetailsProps {
15
16
  isExpanded?: boolean;
16
17
  /** Callback when expand state changes */
17
18
  onExpandChange?: (expanded: boolean) => void;
19
+ /** Stored messages for replay (if provided, SSE streaming is skipped) */
20
+ storedMessages?: ToolCallType["subagentMessages"];
21
+ /** Whether this is a replay (skips SSE connection) */
22
+ isReplay?: boolean | undefined;
18
23
  }
19
24
  /**
20
25
  * SubAgentDetails component - displays streaming content from a sub-agent.
21
26
  *
22
27
  * This component:
23
- * - Connects directly to the sub-agent's SSE endpoint
28
+ * - Connects directly to the sub-agent's SSE endpoint (live mode)
29
+ * - Or displays stored messages (replay mode)
24
30
  * - Displays streaming text and tool calls
31
+ * - Renders content as markdown
25
32
  * - Renders in a collapsible section (collapsed by default)
26
33
  */
27
- export declare function SubAgentDetails({ port, sessionId, host, parentStatus, agentName, query, isExpanded: controlledIsExpanded, onExpandChange, }: SubAgentDetailsProps): import("react/jsx-runtime").JSX.Element;
34
+ export declare function SubAgentDetails({ port, sessionId, host, parentStatus, agentName, query, isExpanded: controlledIsExpanded, onExpandChange, storedMessages, isReplay, }: SubAgentDetailsProps): import("react/jsx-runtime").JSX.Element;
@@ -1,17 +1,20 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { ChevronDown, CircleDot, Loader2 } from "lucide-react";
2
+ import { ChevronDown, Loader2 } from "lucide-react";
3
3
  import React, { useCallback, useEffect, useRef, useState } from "react";
4
4
  import { useSubagentStream } from "../../core/hooks/use-subagent-stream.js";
5
+ import { MarkdownRenderer } from "./MarkdownRenderer.js";
5
6
  const SCROLL_THRESHOLD = 50; // px from bottom to consider "at bottom"
6
7
  /**
7
8
  * SubAgentDetails component - displays streaming content from a sub-agent.
8
9
  *
9
10
  * This component:
10
- * - Connects directly to the sub-agent's SSE endpoint
11
+ * - Connects directly to the sub-agent's SSE endpoint (live mode)
12
+ * - Or displays stored messages (replay mode)
11
13
  * - Displays streaming text and tool calls
14
+ * - Renders content as markdown
12
15
  * - Renders in a collapsible section (collapsed by default)
13
16
  */
14
- export function SubAgentDetails({ port, sessionId, host, parentStatus, agentName, query, isExpanded: controlledIsExpanded, onExpandChange, }) {
17
+ export function SubAgentDetails({ port, sessionId, host, parentStatus, agentName, query, isExpanded: controlledIsExpanded, onExpandChange, storedMessages, isReplay = false, }) {
15
18
  const [internalIsExpanded, setInternalIsExpanded] = useState(false);
16
19
  // Use controlled state if provided, otherwise use internal state
17
20
  const isExpanded = controlledIsExpanded ?? internalIsExpanded;
@@ -19,16 +22,28 @@ export function SubAgentDetails({ port, sessionId, host, parentStatus, agentName
19
22
  const [isThinkingExpanded, setIsThinkingExpanded] = useState(false);
20
23
  const [isNearBottom, setIsNearBottom] = useState(true);
21
24
  const thinkingContainerRef = useRef(null);
22
- const { messages, isStreaming: hookIsStreaming, error, } = useSubagentStream({
23
- port,
24
- sessionId,
25
- ...(host !== undefined ? { host } : {}),
26
- });
25
+ // Only use SSE streaming if not in replay mode and port/sessionId provided
26
+ const shouldStream = !isReplay && port !== undefined && sessionId !== undefined;
27
+ const streamOptions = shouldStream
28
+ ? {
29
+ port: port,
30
+ sessionId: sessionId,
31
+ ...(host !== undefined ? { host } : {}),
32
+ }
33
+ : null;
34
+ const { messages: streamedMessages, isStreaming: hookIsStreaming, error, } = useSubagentStream(streamOptions);
35
+ // Use stored messages if available, otherwise use streamed messages
36
+ const messages = storedMessages && storedMessages.length > 0
37
+ ? storedMessages
38
+ : streamedMessages;
27
39
  // Use parent status as primary indicator, fall back to hook's streaming state
28
40
  // Parent is "in_progress" means sub-agent is definitely still running
29
- const isRunning = parentStatus === "in_progress" ||
30
- parentStatus === "pending" ||
31
- hookIsStreaming;
41
+ // In replay mode, we're never running
42
+ const isRunning = isReplay
43
+ ? false
44
+ : parentStatus === "in_progress" ||
45
+ parentStatus === "pending" ||
46
+ hookIsStreaming;
32
47
  // Get the current/latest message
33
48
  const currentMessage = messages[messages.length - 1];
34
49
  const hasContent = currentMessage &&
@@ -97,9 +112,9 @@ export function SubAgentDetails({ port, sessionId, host, parentStatus, agentName
97
112
  return (_jsxs("div", { children: [!isExpanded && (_jsx("div", { className: "w-full max-w-md", children: previewText ? (_jsx("p", { className: "text-paragraph-sm text-muted-foreground/70 truncate", children: previewText })) : queryFirstLine ? (_jsx("p", { className: "text-paragraph-sm text-muted-foreground/50 truncate", children: queryFirstLine })) : null })), isExpanded && (_jsxs("div", { className: "space-y-3", children: [(agentName || query) && (_jsxs("div", { children: [_jsx("div", { className: "text-[10px] font-bold text-muted-foreground uppercase tracking-wider mb-1.5 font-sans", children: "Input" }), _jsxs("div", { className: "text-[11px] font-mono space-y-1", children: [agentName && (_jsxs("div", { children: [_jsx("span", { className: "text-muted-foreground", children: "agentName: " }), _jsx("span", { className: "text-foreground", children: agentName })] })), query && (_jsxs("div", { children: [_jsx("span", { className: "text-muted-foreground", children: "query: " }), _jsx("span", { className: "text-foreground", children: query })] }))] })] })), _jsxs("div", { children: [_jsxs("button", { type: "button", onClick: () => setIsThinkingExpanded(!isThinkingExpanded), className: "flex items-center gap-2 cursor-pointer bg-transparent border-none p-0 text-left group", children: [_jsx("div", { className: "text-[10px] font-bold text-muted-foreground uppercase tracking-wider font-sans", children: "Thinking" }), _jsx(ChevronDown, { className: `h-3 w-3 text-muted-foreground/70 transition-transform duration-200 ${isThinkingExpanded ? "rotate-180" : ""}` })] }), isThinkingExpanded && (_jsxs("div", { ref: thinkingContainerRef, className: "mt-2 rounded-md overflow-hidden bg-muted/30 border border-border/50 max-h-[200px] overflow-y-auto", children: [error && (_jsxs("div", { className: "px-2 py-2 text-[11px] text-destructive", children: ["Error: ", error] })), !error && !hasContent && isRunning && (_jsx("div", { className: "px-2 py-2 text-[11px] text-muted-foreground", children: "Waiting for sub-agent response..." })), currentMessage && (_jsxs("div", { className: "px-2 py-2 space-y-2", children: [currentMessage.contentBlocks &&
98
113
  currentMessage.contentBlocks.length > 0
99
114
  ? // Render interleaved content blocks
100
- currentMessage.contentBlocks.map((block, idx) => block.type === "text" ? (_jsx("div", { className: "text-[11px] text-foreground whitespace-pre-wrap font-mono", children: block.text }, `text-${idx}`)) : (_jsx(SubagentToolCallItem, { toolCall: block.toolCall }, block.toolCall.id)))
101
- : // Fallback to legacy content
102
- currentMessage.content && (_jsx("div", { className: "text-[11px] text-foreground whitespace-pre-wrap font-mono", children: currentMessage.content })), currentMessage.isStreaming && (_jsx("span", { className: "inline-block w-1.5 h-3 bg-primary/70 ml-0.5 animate-pulse" }))] }))] }))] }), !isRunning && currentMessage?.content && (_jsxs("div", { children: [_jsx("div", { className: "text-[10px] font-bold text-muted-foreground uppercase tracking-wider mb-1.5 font-sans", children: "Output" }), _jsx("div", { className: "text-[11px] text-foreground whitespace-pre-wrap font-mono max-h-[200px] overflow-y-auto rounded-md bg-muted/30 border border-border/50 px-2 py-2", children: currentMessage.content })] }))] }))] }));
115
+ currentMessage.contentBlocks.map((block, idx) => block.type === "text" ? (_jsx("div", { className: "text-[11px] text-foreground prose prose-sm dark:prose-invert max-w-none prose-p:my-1 prose-pre:my-1 prose-code:text-[10px]", children: _jsx(MarkdownRenderer, { content: block.text }) }, `text-${idx}`)) : (_jsx(SubagentToolCallItem, { toolCall: block.toolCall }, block.toolCall.id)))
116
+ : // Fallback to legacy content with markdown
117
+ currentMessage.content && (_jsx("div", { className: "text-[11px] text-foreground prose prose-sm dark:prose-invert max-w-none prose-p:my-1 prose-pre:my-1 prose-code:text-[10px]", children: _jsx(MarkdownRenderer, { content: currentMessage.content }) })), currentMessage.isStreaming && (_jsx("span", { className: "inline-block w-1.5 h-3 bg-primary/70 ml-0.5 animate-pulse" }))] }))] }))] }), !isRunning && currentMessage?.content && (_jsxs("div", { children: [_jsx("div", { className: "text-[10px] font-bold text-muted-foreground uppercase tracking-wider mb-1.5 font-sans", children: "Output" }), _jsx("div", { className: "text-[11px] text-foreground max-h-[200px] overflow-y-auto rounded-md bg-muted/30 border border-border/50 px-2 py-2 prose prose-sm dark:prose-invert max-w-none prose-p:my-1 prose-pre:my-1 prose-code:text-[10px]", children: _jsx(MarkdownRenderer, { content: currentMessage.content }) })] }))] }))] }));
103
118
  }
104
119
  /**
105
120
  * Simple tool call display for sub-agent tool calls
@@ -0,0 +1,23 @@
1
+ export interface SubagentStreamProps {
2
+ /** Sub-agent HTTP port */
3
+ port: number;
4
+ /** Sub-agent session ID */
5
+ sessionId: string;
6
+ /** Optional host (defaults to localhost) */
7
+ host?: string;
8
+ /** Parent tool call status - use this to determine if sub-agent is running */
9
+ parentStatus?: "pending" | "in_progress" | "completed" | "failed";
10
+ /** Sub-agent name (for display) */
11
+ agentName?: string | undefined;
12
+ /** Query sent to the sub-agent */
13
+ query?: string | undefined;
14
+ }
15
+ /**
16
+ * SubagentStream component - displays streaming content from a sub-agent.
17
+ *
18
+ * This component:
19
+ * - Connects directly to the sub-agent's SSE endpoint
20
+ * - Displays streaming text and tool calls
21
+ * - Renders in a collapsible section (collapsed by default)
22
+ */
23
+ export declare function SubagentStream({ port, sessionId, host, parentStatus, agentName, query, }: SubagentStreamProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,98 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { ChevronDown, CircleDot, Loader2 } from "lucide-react";
3
+ import React, { useCallback, useEffect, useRef, useState } from "react";
4
+ import { useSubagentStream } from "../../core/hooks/use-subagent-stream.js";
5
+ const SCROLL_THRESHOLD = 50; // px from bottom to consider "at bottom"
6
+ /**
7
+ * SubagentStream component - displays streaming content from a sub-agent.
8
+ *
9
+ * This component:
10
+ * - Connects directly to the sub-agent's SSE endpoint
11
+ * - Displays streaming text and tool calls
12
+ * - Renders in a collapsible section (collapsed by default)
13
+ */
14
+ export function SubagentStream({ port, sessionId, host, parentStatus, agentName, query, }) {
15
+ const [isExpanded, setIsExpanded] = useState(false); // Start collapsed for parallel ops
16
+ const [isThinkingExpanded, setIsThinkingExpanded] = useState(true);
17
+ const [isNearBottom, setIsNearBottom] = useState(true);
18
+ const thinkingContainerRef = useRef(null);
19
+ const { messages, isStreaming: hookIsStreaming, error } = useSubagentStream({
20
+ port,
21
+ sessionId,
22
+ ...(host !== undefined ? { host } : {}),
23
+ });
24
+ // Use parent status as primary indicator, fall back to hook's streaming state
25
+ // Parent is "in_progress" means sub-agent is definitely still running
26
+ const isRunning = parentStatus === "in_progress" || parentStatus === "pending" || hookIsStreaming;
27
+ // Get the current/latest message
28
+ const currentMessage = messages[messages.length - 1];
29
+ const hasContent = currentMessage &&
30
+ (currentMessage.content ||
31
+ (currentMessage.toolCalls && currentMessage.toolCalls.length > 0));
32
+ // Auto-collapse Thinking when completed (so Output is the primary view)
33
+ const prevIsRunningRef = useRef(isRunning);
34
+ useEffect(() => {
35
+ if (prevIsRunningRef.current && !isRunning) {
36
+ // Just completed - collapse thinking to show output
37
+ setIsThinkingExpanded(false);
38
+ }
39
+ prevIsRunningRef.current = isRunning;
40
+ }, [isRunning]);
41
+ // Check if user is near bottom of scroll area
42
+ const checkScrollPosition = useCallback(() => {
43
+ const container = thinkingContainerRef.current;
44
+ if (!container)
45
+ return;
46
+ const { scrollTop, scrollHeight, clientHeight } = container;
47
+ const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
48
+ setIsNearBottom(distanceFromBottom < SCROLL_THRESHOLD);
49
+ }, []);
50
+ // Scroll to bottom
51
+ const scrollToBottom = useCallback(() => {
52
+ const container = thinkingContainerRef.current;
53
+ if (!container)
54
+ return;
55
+ container.scrollTop = container.scrollHeight;
56
+ }, []);
57
+ // Auto-scroll when content changes and user is near bottom
58
+ useEffect(() => {
59
+ if (isNearBottom && (isRunning || hasContent)) {
60
+ scrollToBottom();
61
+ }
62
+ }, [currentMessage?.content, currentMessage?.toolCalls, isNearBottom, isRunning, hasContent, scrollToBottom]);
63
+ // Set up scroll listener
64
+ useEffect(() => {
65
+ const container = thinkingContainerRef.current;
66
+ if (!container)
67
+ return;
68
+ const handleScroll = () => checkScrollPosition();
69
+ container.addEventListener("scroll", handleScroll, { passive: true });
70
+ checkScrollPosition(); // Check initial position
71
+ return () => container.removeEventListener("scroll", handleScroll);
72
+ }, [checkScrollPosition, isThinkingExpanded, isExpanded]);
73
+ // Get last line of streaming content for preview
74
+ const lastLine = currentMessage?.content
75
+ ? currentMessage.content.split("\n").filter(Boolean).pop() || ""
76
+ : "";
77
+ const previewText = lastLine.length > 100 ? `${lastLine.slice(0, 100)}...` : lastLine;
78
+ return (_jsxs("div", { children: [!isExpanded && (_jsx("button", { type: "button", onClick: () => setIsExpanded(true), className: "w-full max-w-md text-left cursor-pointer bg-transparent border-none p-0", children: previewText ? (_jsx("p", { className: `text-paragraph-sm text-muted-foreground truncate ${isRunning ? "animate-pulse" : ""}`, children: previewText })) : isRunning ? (_jsx("p", { className: "text-paragraph-sm text-muted-foreground/50 italic animate-pulse", children: "Waiting for response..." })) : null })), isExpanded && (_jsxs("div", { className: "space-y-3", children: [(agentName || query) && (_jsxs("div", { children: [_jsx("div", { className: "text-[10px] font-bold text-muted-foreground uppercase tracking-wider mb-1.5 font-sans", children: "Input" }), _jsxs("div", { className: "text-[11px] font-mono space-y-1", children: [agentName && (_jsxs("div", { children: [_jsx("span", { className: "text-muted-foreground", children: "agentName: " }), _jsx("span", { className: "text-foreground", children: agentName })] })), query && (_jsxs("div", { children: [_jsx("span", { className: "text-muted-foreground", children: "query: " }), _jsx("span", { className: "text-foreground", children: query })] }))] })] })), _jsxs("div", { children: [_jsxs("button", { type: "button", onClick: () => setIsThinkingExpanded(!isThinkingExpanded), className: "flex items-center gap-2 cursor-pointer bg-transparent border-none p-0 text-left group", children: [_jsx("div", { className: "text-[10px] font-bold text-muted-foreground uppercase tracking-wider font-sans", children: "Thinking" }), _jsx(ChevronDown, { className: `h-3 w-3 text-muted-foreground/70 transition-transform duration-200 ${isThinkingExpanded ? "rotate-180" : ""}` })] }), isThinkingExpanded && (_jsxs("div", { ref: thinkingContainerRef, className: "mt-2 rounded-md overflow-hidden bg-muted/30 border border-border/50 max-h-[200px] overflow-y-auto", children: [error && (_jsxs("div", { className: "px-2 py-2 text-[11px] text-destructive", children: ["Error: ", error] })), !error && !hasContent && isRunning && (_jsx("div", { className: "px-2 py-2 text-[11px] text-muted-foreground", children: "Waiting for sub-agent response..." })), currentMessage && (_jsxs("div", { className: "px-2 py-2 space-y-2", children: [currentMessage.toolCalls &&
79
+ currentMessage.toolCalls.length > 0 && (_jsx("div", { className: "space-y-1", children: currentMessage.toolCalls.map((tc) => (_jsx(SubagentToolCallItem, { toolCall: tc }, tc.id))) })), currentMessage.content && (_jsxs("div", { className: "text-[11px] text-foreground whitespace-pre-wrap font-mono", children: [currentMessage.content, currentMessage.isStreaming && (_jsx("span", { className: "inline-block w-1.5 h-3 bg-primary/70 ml-0.5 animate-pulse" }))] }))] }))] }))] }), !isRunning && currentMessage?.content && (_jsxs("div", { children: [_jsx("div", { className: "text-[10px] font-bold text-muted-foreground uppercase tracking-wider mb-1.5 font-sans", children: "Output" }), _jsx("div", { className: "text-[11px] text-foreground whitespace-pre-wrap font-mono max-h-[200px] overflow-y-auto rounded-md bg-muted/30 border border-border/50 px-2 py-2", children: currentMessage.content })] }))] }))] }));
80
+ }
81
+ /**
82
+ * Simple tool call display for sub-agent tool calls
83
+ */
84
+ function SubagentToolCallItem({ toolCall }) {
85
+ const statusIcon = {
86
+ pending: "...",
87
+ in_progress: "",
88
+ completed: "",
89
+ failed: "",
90
+ }[toolCall.status];
91
+ const statusColor = {
92
+ pending: "text-muted-foreground",
93
+ in_progress: "text-blue-500",
94
+ completed: "text-green-500",
95
+ failed: "text-destructive",
96
+ }[toolCall.status];
97
+ return (_jsxs("div", { className: "flex items-center gap-2 text-[10px] text-muted-foreground", children: [_jsx("span", { className: statusColor, children: statusIcon }), _jsx("span", { className: "font-medium", children: toolCall.prettyName || toolCall.title }), toolCall.status === "in_progress" && (_jsx(Loader2, { className: "h-2.5 w-2.5 animate-spin" }))] }));
98
+ }
@@ -44,7 +44,15 @@ export function ToolCall({ toolCall }) {
44
44
  const { resolvedTheme } = useTheme();
45
45
  // Detect TodoWrite tool and subagent
46
46
  const isTodoWrite = toolCall.title === "todo_write";
47
- const isSubagentCall = !!(toolCall.subagentPort && toolCall.subagentSessionId);
47
+ // A subagent call can be detected by:
48
+ // - Live: has port and sessionId (but no stored messages yet)
49
+ // - Replay: has stored subagentMessages
50
+ const hasLiveSubagent = !!(toolCall.subagentPort && toolCall.subagentSessionId);
51
+ const hasStoredSubagent = !!(toolCall.subagentMessages && toolCall.subagentMessages.length > 0);
52
+ const isSubagentCall = hasLiveSubagent || hasStoredSubagent;
53
+ // Use replay mode if we have stored messages - they should take precedence
54
+ // over trying to connect to SSE (which won't work for replayed sessions)
55
+ const isReplaySubagent = hasStoredSubagent;
48
56
  // Safely access ChatLayout context - will be undefined if not within ChatLayout
49
57
  const layoutContext = React.useContext(ChatLayout.Context);
50
58
  // Click handler: toggle sidepanel for TodoWrite, subagent details for subagents, expand for others
@@ -139,7 +147,7 @@ export function ToolCall({ toolCall }) {
139
147
  if (isPreliminary) {
140
148
  return (_jsx("div", { className: "flex flex-col my-4", children: _jsxs("span", { className: "text-paragraph-sm text-muted-foreground/50", children: ["Invoking ", displayName] }) }));
141
149
  }
142
- 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", onClick: handleHeaderClick, "aria-expanded": isTodoWrite ? undefined : isExpanded, children: [_jsxs("div", { className: "flex items-center gap-1.5 text-[11px] font-medium text-muted-foreground", children: [_jsx("div", { className: iconColorClass, title: statusTooltip, children: _jsx(IconComponent, { className: "h-3 w-3" }) }), _jsx("span", { className: "text-paragraph-sm text-muted-foreground", children: displayName }), hasError && _jsx(AlertCircle, { className: "h-3 w-3 text-destructive" }), isTodoWrite ? (_jsx(ChevronRight, { className: "h-3 w-3 text-muted-foreground/70" })) : (_jsx(ChevronDown, { className: `h-3 w-3 text-muted-foreground/70 transition-transform duration-200 ${isExpanded ? "rotate-180" : ""}` }))] }), toolCall.subline && (_jsx("span", { className: "text-paragraph-sm text-muted-foreground/70 pl-4.5", children: toolCall.subline }))] }), !isTodoWrite && isSubagentCall && (_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 }) })), !isTodoWrite && !isSubagentCall && isExpanded && (_jsxs("div", { className: "mt-2 text-sm border border-border rounded-lg bg-card overflow-hidden w-full", children: [toolCall.rawInput &&
150
+ 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", onClick: handleHeaderClick, "aria-expanded": isTodoWrite ? undefined : isExpanded, children: [_jsxs("div", { className: "flex items-center gap-1.5 text-[11px] font-medium text-muted-foreground", children: [_jsx("div", { className: iconColorClass, title: statusTooltip, children: _jsx(IconComponent, { className: "h-3 w-3" }) }), _jsx("span", { className: "text-paragraph-sm text-muted-foreground", children: displayName }), hasError && _jsx(AlertCircle, { className: "h-3 w-3 text-destructive" }), isTodoWrite ? (_jsx(ChevronRight, { className: "h-3 w-3 text-muted-foreground/70" })) : (_jsx(ChevronDown, { className: `h-3 w-3 text-muted-foreground/70 transition-transform duration-200 ${isExpanded ? "rotate-180" : ""}` }))] }), toolCall.subline && (_jsx("span", { className: "text-paragraph-sm text-muted-foreground/70 pl-4.5", children: toolCall.subline }))] }), !isTodoWrite && isSubagentCall && (_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 }) })), !isTodoWrite && !isSubagentCall && isExpanded && (_jsxs("div", { className: "mt-2 text-sm border border-border rounded-lg bg-card overflow-hidden w-full", children: [toolCall.rawInput &&
143
151
  Object.keys(toolCall.rawInput).length > 0 &&
144
152
  !toolCall.subagentPort && (_jsxs("div", { className: "p-3 border-b border-border", children: [_jsx("div", { className: "text-[10px] font-bold text-muted-foreground uppercase tracking-wider mb-1.5 font-sans", children: "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.locations && toolCall.locations.length > 0 && (_jsxs("div", { className: "p-3 border-b border-border", children: [_jsx("div", { className: "text-[10px] font-bold text-muted-foreground uppercase tracking-wider mb-1.5 font-sans", children: "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 &&
145
153
  loc.line !== undefined &&
@@ -459,6 +459,97 @@ export declare const SessionUpdate: z.ZodUnion<readonly [z.ZodObject<{
459
459
  }, z.core.$strip>>;
460
460
  subagentPort: z.ZodOptional<z.ZodNumber>;
461
461
  subagentSessionId: z.ZodOptional<z.ZodString>;
462
+ subagentMessages: z.ZodOptional<z.ZodArray<z.ZodObject<{
463
+ id: z.ZodString;
464
+ content: z.ZodString;
465
+ toolCalls: z.ZodOptional<z.ZodArray<z.ZodObject<{
466
+ id: z.ZodString;
467
+ title: z.ZodString;
468
+ prettyName: z.ZodOptional<z.ZodString>;
469
+ icon: z.ZodOptional<z.ZodString>;
470
+ status: z.ZodEnum<{
471
+ pending: "pending";
472
+ in_progress: "in_progress";
473
+ completed: "completed";
474
+ failed: "failed";
475
+ }>;
476
+ content: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
477
+ type: z.ZodLiteral<"content">;
478
+ content: z.ZodObject<{
479
+ type: z.ZodLiteral<"text">;
480
+ text: z.ZodString;
481
+ }, z.core.$strip>;
482
+ }, z.core.$strip>, z.ZodObject<{
483
+ type: z.ZodLiteral<"text">;
484
+ text: z.ZodString;
485
+ }, z.core.$strip>, z.ZodObject<{
486
+ type: z.ZodLiteral<"image">;
487
+ data: z.ZodString;
488
+ mimeType: z.ZodOptional<z.ZodString>;
489
+ alt: z.ZodOptional<z.ZodString>;
490
+ }, z.core.$strip>, z.ZodObject<{
491
+ type: z.ZodLiteral<"image">;
492
+ url: z.ZodString;
493
+ alt: z.ZodOptional<z.ZodString>;
494
+ }, z.core.$strip>, z.ZodObject<{
495
+ type: z.ZodLiteral<"diff">;
496
+ path: z.ZodString;
497
+ oldText: z.ZodString;
498
+ newText: z.ZodString;
499
+ line: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
500
+ }, z.core.$strip>, z.ZodObject<{
501
+ type: z.ZodLiteral<"terminal">;
502
+ terminalId: z.ZodString;
503
+ }, z.core.$strip>], "type">>>;
504
+ }, z.core.$strip>>>;
505
+ contentBlocks: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
506
+ type: z.ZodLiteral<"text">;
507
+ text: z.ZodString;
508
+ }, z.core.$strip>, z.ZodObject<{
509
+ type: z.ZodLiteral<"tool_call">;
510
+ toolCall: z.ZodObject<{
511
+ id: z.ZodString;
512
+ title: z.ZodString;
513
+ prettyName: z.ZodOptional<z.ZodString>;
514
+ icon: z.ZodOptional<z.ZodString>;
515
+ status: z.ZodEnum<{
516
+ pending: "pending";
517
+ in_progress: "in_progress";
518
+ completed: "completed";
519
+ failed: "failed";
520
+ }>;
521
+ content: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
522
+ type: z.ZodLiteral<"content">;
523
+ content: z.ZodObject<{
524
+ type: z.ZodLiteral<"text">;
525
+ text: z.ZodString;
526
+ }, z.core.$strip>;
527
+ }, z.core.$strip>, z.ZodObject<{
528
+ type: z.ZodLiteral<"text">;
529
+ text: z.ZodString;
530
+ }, z.core.$strip>, z.ZodObject<{
531
+ type: z.ZodLiteral<"image">;
532
+ data: z.ZodString;
533
+ mimeType: z.ZodOptional<z.ZodString>;
534
+ alt: z.ZodOptional<z.ZodString>;
535
+ }, z.core.$strip>, z.ZodObject<{
536
+ type: z.ZodLiteral<"image">;
537
+ url: z.ZodString;
538
+ alt: z.ZodOptional<z.ZodString>;
539
+ }, z.core.$strip>, z.ZodObject<{
540
+ type: z.ZodLiteral<"diff">;
541
+ path: z.ZodString;
542
+ oldText: z.ZodString;
543
+ newText: z.ZodString;
544
+ line: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
545
+ }, z.core.$strip>, z.ZodObject<{
546
+ type: z.ZodLiteral<"terminal">;
547
+ terminalId: z.ZodString;
548
+ }, z.core.$strip>], "type">>>;
549
+ }, z.core.$strip>;
550
+ }, z.core.$strip>], "type">>>;
551
+ isStreaming: z.ZodOptional<z.ZodBoolean>;
552
+ }, z.core.$strip>>>;
462
553
  }, z.core.$strip>;
463
554
  messageId: z.ZodOptional<z.ZodString>;
464
555
  }, z.core.$strip>, z.ZodObject<{
@@ -695,6 +695,25 @@ export class HttpTransport {
695
695
  typeof update._meta.batchId === "string"
696
696
  ? update._meta.batchId
697
697
  : undefined;
698
+ // Extract subagent connection info for replay
699
+ const subagentPort = update._meta &&
700
+ typeof update._meta === "object" &&
701
+ "subagentPort" in update._meta &&
702
+ typeof update._meta.subagentPort === "number"
703
+ ? update._meta.subagentPort
704
+ : undefined;
705
+ const subagentSessionId = update._meta &&
706
+ typeof update._meta === "object" &&
707
+ "subagentSessionId" in update._meta &&
708
+ typeof update._meta.subagentSessionId === "string"
709
+ ? update._meta.subagentSessionId
710
+ : undefined;
711
+ const subagentMessages = update._meta &&
712
+ typeof update._meta === "object" &&
713
+ "subagentMessages" in update._meta &&
714
+ Array.isArray(update._meta.subagentMessages)
715
+ ? update._meta.subagentMessages
716
+ : undefined;
698
717
  // Initial tool call notification
699
718
  const toolCall = {
700
719
  id: update.toolCallId ?? "",
@@ -755,6 +774,10 @@ export class HttpTransport {
755
774
  return { type: "text", text: "" };
756
775
  }),
757
776
  startedAt: Date.now(),
777
+ // Sub-agent connection info and messages for replay
778
+ subagentPort,
779
+ subagentSessionId,
780
+ subagentMessages,
758
781
  };
759
782
  const sessionUpdate = {
760
783
  type: "tool_call",
@@ -763,19 +786,41 @@ export class HttpTransport {
763
786
  toolCall: toolCall,
764
787
  messageId,
765
788
  };
766
- // Queue tool call as a chunk for ordered processing
767
- const toolCallChunk = {
768
- type: "tool_call",
769
- id: sessionId,
770
- toolCall: toolCall,
771
- messageId,
772
- };
773
- const resolver = this.chunkResolvers.shift();
774
- if (resolver) {
775
- resolver(toolCallChunk);
789
+ // Check if this is replay
790
+ const isReplay = update._meta &&
791
+ typeof update._meta === "object" &&
792
+ "isReplay" in update._meta &&
793
+ update._meta.isReplay === true;
794
+ // Debug: log tool call creation
795
+ logger.info("Creating tool_call session update", {
796
+ toolCallId: toolCall.id,
797
+ title: toolCall.title,
798
+ hasSubagentPort: !!subagentPort,
799
+ hasSubagentSessionId: !!subagentSessionId,
800
+ hasSubagentMessages: !!subagentMessages,
801
+ subagentMessagesCount: subagentMessages?.length,
802
+ isReplay,
803
+ isInReplayMode: this.isInReplayMode,
804
+ });
805
+ // During replay, notify directly since there's no active receive() consumer
806
+ if (isReplay || this.isInReplayMode) {
807
+ this.notifySessionUpdate(sessionUpdate);
776
808
  }
777
809
  else {
778
- this.messageQueue.push(toolCallChunk);
810
+ // Queue tool call as a chunk for ordered processing during live streaming
811
+ const toolCallChunk = {
812
+ type: "tool_call",
813
+ id: sessionId,
814
+ toolCall: toolCall,
815
+ messageId,
816
+ };
817
+ const resolver = this.chunkResolvers.shift();
818
+ if (resolver) {
819
+ resolver(toolCallChunk);
820
+ }
821
+ else {
822
+ this.messageQueue.push(toolCallChunk);
823
+ }
779
824
  }
780
825
  }
781
826
  else if (update?.sessionUpdate === "tool_call_update") {
@@ -816,13 +861,19 @@ export class HttpTransport {
816
861
  typeof update._meta.subagentSessionId === "string"
817
862
  ? update._meta.subagentSessionId
818
863
  : undefined;
864
+ const subagentMessages = update._meta &&
865
+ typeof update._meta === "object" &&
866
+ "subagentMessages" in update._meta &&
867
+ Array.isArray(update._meta.subagentMessages)
868
+ ? update._meta.subagentMessages
869
+ : undefined;
819
870
  // Debug logging for subagent connection info
820
- if (subagentPort || subagentSessionId) {
821
- logger.info("Extracted subagent connection info from tool_call_update", {
871
+ if (subagentPort || subagentSessionId || subagentMessages) {
872
+ logger.info("Extracted subagent info from tool_call_update", {
822
873
  toolCallId: update.toolCallId,
823
874
  subagentPort,
824
875
  subagentSessionId,
825
- _meta: update._meta,
876
+ subagentMessagesCount: subagentMessages?.length,
826
877
  });
827
878
  }
828
879
  // Tool call update notification
@@ -890,6 +941,8 @@ export class HttpTransport {
890
941
  // Sub-agent connection info for direct SSE streaming
891
942
  subagentPort,
892
943
  subagentSessionId,
944
+ // Sub-agent messages for replay
945
+ subagentMessages,
893
946
  };
894
947
  const sessionUpdate = {
895
948
  type: "tool_call_update",
@@ -898,22 +951,31 @@ export class HttpTransport {
898
951
  toolCallUpdate: toolCallUpdate,
899
952
  messageId,
900
953
  };
901
- // Queue tool call update as a chunk for ordered processing
902
- const toolCallUpdateChunk = {
903
- type: "tool_call_update",
904
- id: sessionId,
905
- toolCallUpdate: toolCallUpdate,
906
- messageId,
907
- };
908
- const resolver = this.chunkResolvers.shift();
909
- if (resolver) {
910
- resolver(toolCallUpdateChunk);
954
+ // Check if this is replay (tool_call_update doesn't have isReplay in _meta,
955
+ // but we can check if we're in replay mode)
956
+ if (this.isInReplayMode) {
957
+ // During replay, notify directly since there's no active receive() consumer
958
+ this.notifySessionUpdate(sessionUpdate);
911
959
  }
912
960
  else {
913
- this.messageQueue.push(toolCallUpdateChunk);
961
+ // Queue tool call update as a chunk for ordered processing
962
+ const toolCallUpdateChunk = {
963
+ type: "tool_call_update",
964
+ id: sessionId,
965
+ toolCallUpdate: toolCallUpdate,
966
+ messageId,
967
+ };
968
+ const resolver = this.chunkResolvers.shift();
969
+ if (resolver) {
970
+ resolver(toolCallUpdateChunk);
971
+ }
972
+ else {
973
+ this.messageQueue.push(toolCallUpdateChunk);
974
+ }
914
975
  }
915
- logger.debug("Queued tool_call_update chunk", {
976
+ logger.debug("Processed tool_call_update", {
916
977
  sessionUpdate,
978
+ isReplay: this.isInReplayMode,
917
979
  });
918
980
  }
919
981
  else if (update &&
@@ -995,19 +1057,25 @@ export class HttpTransport {
995
1057
  toolCallUpdate: toolOutput,
996
1058
  messageId,
997
1059
  };
998
- // Queue tool output as a chunk for ordered processing
999
- const toolCallUpdateChunk = {
1000
- type: "tool_call_update",
1001
- id: sessionId,
1002
- toolCallUpdate: toolOutput,
1003
- messageId,
1004
- };
1005
- const resolver = this.chunkResolvers.shift();
1006
- if (resolver) {
1007
- resolver(toolCallUpdateChunk);
1060
+ // During replay, notify directly; otherwise queue for ordered processing
1061
+ if (this.isInReplayMode) {
1062
+ this.notifySessionUpdate(sessionUpdate);
1008
1063
  }
1009
1064
  else {
1010
- this.messageQueue.push(toolCallUpdateChunk);
1065
+ // Queue tool output as a chunk for ordered processing
1066
+ const toolCallUpdateChunk = {
1067
+ type: "tool_call_update",
1068
+ id: sessionId,
1069
+ toolCallUpdate: toolOutput,
1070
+ messageId,
1071
+ };
1072
+ const resolver = this.chunkResolvers.shift();
1073
+ if (resolver) {
1074
+ resolver(toolCallUpdateChunk);
1075
+ }
1076
+ else {
1077
+ this.messageQueue.push(toolCallUpdateChunk);
1078
+ }
1011
1079
  }
1012
1080
  logger.debug("Queued tool_output as tool_call_update chunk", {
1013
1081
  sessionUpdate,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@townco/ui",
3
- "version": "0.1.66",
3
+ "version": "0.1.68",
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.44",
52
+ "@townco/core": "0.0.46",
53
53
  "@uiw/react-json-view": "^2.0.0-alpha.39",
54
54
  "bun": "^1.3.1",
55
55
  "class-variance-authority": "^0.7.1",
@@ -66,7 +66,7 @@
66
66
  },
67
67
  "devDependencies": {
68
68
  "@tailwindcss/postcss": "^4.1.17",
69
- "@townco/tsconfig": "0.1.63",
69
+ "@townco/tsconfig": "0.1.65",
70
70
  "@types/node": "^24.10.0",
71
71
  "@types/react": "^19.2.2",
72
72
  "ink": "^6.4.0",