@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.
- package/dist/core/hooks/use-chat-messages.d.ts +1 -1
- package/dist/core/hooks/use-chat-session.d.ts +1 -1
- package/dist/core/hooks/use-tool-calls.d.ts +1 -1
- package/dist/core/hooks/use-tool-calls.js +2 -1
- package/dist/core/schemas/tool-call.d.ts +91 -0
- package/dist/core/schemas/tool-call.js +4 -0
- package/dist/gui/components/Button.d.ts +1 -1
- package/dist/gui/components/MessageContent.js +49 -11
- package/dist/gui/components/SubAgentDetails.d.ts +13 -6
- package/dist/gui/components/SubAgentDetails.js +29 -14
- package/dist/gui/components/SubagentStream.d.ts +23 -0
- package/dist/gui/components/SubagentStream.js +98 -0
- package/dist/gui/components/ToolCall.js +10 -2
- package/dist/sdk/schemas/session.d.ts +91 -0
- package/dist/sdk/transports/http.js +105 -37
- package/package.json +3 -3
|
@@ -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: "
|
|
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: "
|
|
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: "
|
|
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?: "
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
137
|
-
|
|
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
|
-
//
|
|
211
|
+
// Different title or switching from batchId to title grouping
|
|
175
212
|
flushBatch();
|
|
176
|
-
|
|
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
|
|
4
|
-
/** Sub-agent session ID */
|
|
5
|
-
sessionId
|
|
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,
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
101
|
-
: // Fallback to legacy content
|
|
102
|
-
currentMessage.content && (_jsx("div", { className: "text-[11px] text-foreground
|
|
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
|
-
|
|
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
|
-
//
|
|
767
|
-
const
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
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
|
-
|
|
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("
|
|
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
|
-
//
|
|
999
|
-
|
|
1000
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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",
|