@townco/ui 0.1.51 → 0.1.53

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.
@@ -0,0 +1,121 @@
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
+ * SubAgentDetails 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 SubAgentDetails({ port, sessionId, host, parentStatus, agentName, query, isExpanded: controlledIsExpanded, onExpandChange, }) {
15
+ const [internalIsExpanded, setInternalIsExpanded] = useState(false);
16
+ // Use controlled state if provided, otherwise use internal state
17
+ const isExpanded = controlledIsExpanded ?? internalIsExpanded;
18
+ const setIsExpanded = onExpandChange ?? setInternalIsExpanded;
19
+ const [isThinkingExpanded, setIsThinkingExpanded] = useState(false);
20
+ const [isNearBottom, setIsNearBottom] = useState(true);
21
+ const thinkingContainerRef = useRef(null);
22
+ const { messages, isStreaming: hookIsStreaming, error, } = useSubagentStream({
23
+ port,
24
+ sessionId,
25
+ ...(host !== undefined ? { host } : {}),
26
+ });
27
+ // Use parent status as primary indicator, fall back to hook's streaming state
28
+ // Parent is "in_progress" means sub-agent is definitely still running
29
+ const isRunning = parentStatus === "in_progress" ||
30
+ parentStatus === "pending" ||
31
+ hookIsStreaming;
32
+ // Get the current/latest message
33
+ const currentMessage = messages[messages.length - 1];
34
+ const hasContent = currentMessage &&
35
+ (currentMessage.content ||
36
+ (currentMessage.toolCalls && currentMessage.toolCalls.length > 0) ||
37
+ (currentMessage.contentBlocks &&
38
+ currentMessage.contentBlocks.length > 0));
39
+ // Auto-collapse Thinking when completed (so Output is the primary view)
40
+ const prevIsRunningRef = useRef(isRunning);
41
+ useEffect(() => {
42
+ if (prevIsRunningRef.current && !isRunning) {
43
+ // Just completed - collapse thinking to show output
44
+ setIsThinkingExpanded(false);
45
+ }
46
+ prevIsRunningRef.current = isRunning;
47
+ }, [isRunning]);
48
+ // Check if user is near bottom of scroll area
49
+ const checkScrollPosition = useCallback(() => {
50
+ const container = thinkingContainerRef.current;
51
+ if (!container)
52
+ return;
53
+ const { scrollTop, scrollHeight, clientHeight } = container;
54
+ const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
55
+ setIsNearBottom(distanceFromBottom < SCROLL_THRESHOLD);
56
+ }, []);
57
+ // Scroll to bottom
58
+ const scrollToBottom = useCallback(() => {
59
+ const container = thinkingContainerRef.current;
60
+ if (!container)
61
+ return;
62
+ container.scrollTop = container.scrollHeight;
63
+ }, []);
64
+ // Auto-scroll when content changes and user is near bottom
65
+ useEffect(() => {
66
+ if (isNearBottom && (isRunning || hasContent)) {
67
+ scrollToBottom();
68
+ }
69
+ }, [
70
+ currentMessage?.content,
71
+ currentMessage?.toolCalls,
72
+ isNearBottom,
73
+ isRunning,
74
+ hasContent,
75
+ scrollToBottom,
76
+ ]);
77
+ // Set up scroll listener
78
+ useEffect(() => {
79
+ const container = thinkingContainerRef.current;
80
+ if (!container)
81
+ return;
82
+ const handleScroll = () => checkScrollPosition();
83
+ container.addEventListener("scroll", handleScroll, { passive: true });
84
+ checkScrollPosition(); // Check initial position
85
+ return () => container.removeEventListener("scroll", handleScroll);
86
+ }, [checkScrollPosition, isThinkingExpanded, isExpanded]);
87
+ // Get last line of streaming content for preview
88
+ const lastLine = currentMessage?.content
89
+ ? currentMessage.content.split("\n").filter(Boolean).pop() || ""
90
+ : "";
91
+ const previewText = lastLine.length > 100 ? `${lastLine.slice(0, 100)}...` : lastLine;
92
+ // Get first line of query for fallback preview
93
+ const queryFirstLine = query
94
+ ? (query.split("\n")[0] ?? "").slice(0, 100) +
95
+ (query.length > 100 ? "..." : "")
96
+ : "";
97
+ 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
+ currentMessage.contentBlocks.length > 0
99
+ ? // 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 })] }))] }))] }));
103
+ }
104
+ /**
105
+ * Simple tool call display for sub-agent tool calls
106
+ */
107
+ function SubagentToolCallItem({ toolCall }) {
108
+ const statusIcon = {
109
+ pending: "...",
110
+ in_progress: "",
111
+ completed: "",
112
+ failed: "",
113
+ }[toolCall.status];
114
+ const statusColor = {
115
+ pending: "text-muted-foreground",
116
+ in_progress: "text-blue-500",
117
+ completed: "text-green-500",
118
+ failed: "text-destructive",
119
+ }[toolCall.status];
120
+ 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" }))] }));
121
+ }
@@ -1,8 +1,9 @@
1
1
  import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
2
  import JsonView from "@uiw/react-json-view";
3
- import { AlertCircle, BrainCircuit, CheckSquare, ChevronDown, ChevronRight, CircleDot, Cloud, Edit, FileText, Globe, Image, Link, Search, Wrench, } from "lucide-react";
3
+ import { AlertCircle, CheckSquare, ChevronDown, ChevronRight, CircleDot, Cloud, Edit, FileText, Globe, Image, Link, Search, Wrench, } from "lucide-react";
4
4
  import React, { useState } from "react";
5
5
  import { ChatLayout } from "./index.js";
6
+ import { SubAgentDetails } from "./SubAgentDetails.js";
6
7
  import { useTheme } from "./ThemeProvider.js";
7
8
  /**
8
9
  * Map of icon names to Lucide components
@@ -17,7 +18,6 @@ const ICON_MAP = {
17
18
  FileText: FileText,
18
19
  Edit: Edit,
19
20
  Wrench: Wrench,
20
- BrainCircuit: BrainCircuit,
21
21
  CircleDot: CircleDot,
22
22
  };
23
23
  /**
@@ -40,12 +40,14 @@ const _kindIcons = {
40
40
  */
41
41
  export function ToolCall({ toolCall }) {
42
42
  const [isExpanded, setIsExpanded] = useState(false);
43
+ const [isSubagentExpanded, setIsSubagentExpanded] = useState(false);
43
44
  const { resolvedTheme } = useTheme();
44
- // Detect TodoWrite tool
45
+ // Detect TodoWrite tool and subagent
45
46
  const isTodoWrite = toolCall.title === "todo_write";
47
+ const isSubagentCall = !!(toolCall.subagentPort && toolCall.subagentSessionId);
46
48
  // Safely access ChatLayout context - will be undefined if not within ChatLayout
47
49
  const layoutContext = React.useContext(ChatLayout.Context);
48
- // Click handler: toggle sidepanel for TodoWrite, expand for others
50
+ // Click handler: toggle sidepanel for TodoWrite, subagent details for subagents, expand for others
49
51
  const handleHeaderClick = React.useCallback(() => {
50
52
  if (isTodoWrite && layoutContext) {
51
53
  // Toggle sidepanel - close if already open on todo tab, otherwise open
@@ -58,17 +60,45 @@ export function ToolCall({ toolCall }) {
58
60
  layoutContext.setActiveTab("todo");
59
61
  }
60
62
  }
63
+ else if (isSubagentCall) {
64
+ // Toggle subagent details
65
+ setIsSubagentExpanded(!isSubagentExpanded);
66
+ }
61
67
  else {
62
68
  // Normal expand/collapse
63
69
  setIsExpanded(!isExpanded);
64
70
  }
65
- }, [isTodoWrite, layoutContext, isExpanded]);
71
+ }, [
72
+ isTodoWrite,
73
+ layoutContext,
74
+ isExpanded,
75
+ isSubagentCall,
76
+ isSubagentExpanded,
77
+ ]);
66
78
  // Determine which icon to show
67
79
  const IconComponent = toolCall.icon && ICON_MAP[toolCall.icon]
68
80
  ? ICON_MAP[toolCall.icon]
69
81
  : CircleDot;
70
82
  // Determine display name
71
83
  const displayName = toolCall.prettyName || toolCall.title;
84
+ // Determine icon color based on status (especially for subagents)
85
+ const isSubagentRunning = isSubagentCall &&
86
+ (toolCall.status === "in_progress" || toolCall.status === "pending");
87
+ const isSubagentFailed = isSubagentCall && toolCall.status === "failed";
88
+ const iconColorClass = isSubagentCall
89
+ ? isSubagentFailed
90
+ ? "text-destructive"
91
+ : isSubagentRunning
92
+ ? "text-foreground animate-pulse"
93
+ : "text-green-500"
94
+ : "text-muted-foreground";
95
+ const statusTooltip = isSubagentCall
96
+ ? isSubagentFailed
97
+ ? "Sub-agent failed"
98
+ : isSubagentRunning
99
+ ? "Sub-agent running"
100
+ : "Sub-agent completed"
101
+ : undefined;
72
102
  // Check if there's an error
73
103
  const hasError = toolCall.status === "failed" || !!toolCall.error;
74
104
  // Check if this is a preliminary (pending) tool call without full details yet
@@ -109,9 +139,11 @@ export function ToolCall({ toolCall }) {
109
139
  if (isPreliminary) {
110
140
  return (_jsx("div", { className: "flex flex-col my-4", children: _jsxs("span", { className: "text-paragraph-sm text-muted-foreground/50", children: ["Invoking ", displayName] }) }));
111
141
  }
112
- 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: "text-muted-foreground", 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 && isExpanded && (_jsxs("div", { className: "mt-2 text-sm border border-border rounded-lg bg-card overflow-hidden w-full", children: [toolCall.locations && toolCall.locations.length > 0 && (_jsxs("div", { className: "p-3 border-b border-border", children: [_jsx("div", { className: "text-[10px] font-bold text-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 &&
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 &&
143
+ Object.keys(toolCall.rawInput).length > 0 &&
144
+ !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 &&
113
145
  loc.line !== undefined &&
114
- `:${loc.line}`] }, `${loc.path}:${loc.line ?? ""}`))) })] })), toolCall.rawInput && Object.keys(toolCall.rawInput).length > 0 && (_jsxs("div", { className: "p-3 border-b border-border", children: [_jsx("div", { className: "text-[10px] font-bold text-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.content && toolCall.content.length > 0) ||
146
+ `:${loc.line}`] }, `${loc.path}:${loc.line ?? ""}`))) })] })), (toolCall.content && toolCall.content.length > 0) ||
115
147
  toolCall.error ? (_jsxs("div", { className: "p-3 border-b border-border last:border-0", children: [_jsx("div", { className: "text-[10px] font-bold text-muted-foreground uppercase tracking-wider mb-1.5 font-sans", children: "Output" }), _jsxs("div", { className: "space-y-2 text-[11px] text-foreground", children: [toolCall.content?.map((block, idx) => {
116
148
  // Generate a stable key based on content
117
149
  const getBlockKey = () => {
@@ -34,6 +34,7 @@ export { Response, type ResponseProps } from "./Response.js";
34
34
  export { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, } from "./Select.js";
35
35
  export { Toaster } from "./Sonner.js";
36
36
  export { type SourceItem, SourceListItem, type SourceListItemProps, } from "./SourceListItem.js";
37
+ export { SubAgentDetails, type SubAgentDetailsProps, } from "./SubAgentDetails.js";
37
38
  export { Tabs, TabsContent, TabsList, TabsTrigger } from "./Tabs.js";
38
39
  export { Task, type TaskItem, TaskList, type TaskListProps, type TaskProps, } from "./Task.js";
39
40
  export { Textarea, type TextareaProps, textareaVariants } from "./Textarea.js";
@@ -40,6 +40,7 @@ export { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScro
40
40
  export { Toaster } from "./Sonner.js";
41
41
  // Toast components
42
42
  export { SourceListItem, } from "./SourceListItem.js";
43
+ export { SubAgentDetails, } from "./SubAgentDetails.js";
43
44
  export { Tabs, TabsContent, TabsList, TabsTrigger } from "./Tabs.js";
44
45
  // Task/Todo components
45
46
  export { Task, TaskList, } from "./Task.js";
@@ -93,6 +93,7 @@ export declare class AcpClient {
93
93
  * - tools: List of tools available to the agent
94
94
  * - mcps: List of MCP servers connected to the agent
95
95
  * - subagents: List of subagents available via Task tool
96
+ * - uiConfig: UI configuration for interface appearance
96
97
  */
97
98
  getAgentInfo(): {
98
99
  name?: string;
@@ -100,6 +101,9 @@ export declare class AcpClient {
100
101
  version?: string;
101
102
  description?: string;
102
103
  suggestedPrompts?: string[];
104
+ uiConfig?: {
105
+ hideTopBar?: boolean;
106
+ };
103
107
  tools?: Array<{
104
108
  name: string;
105
109
  description?: string;
@@ -245,6 +245,7 @@ export class AcpClient {
245
245
  * - tools: List of tools available to the agent
246
246
  * - mcps: List of MCP servers connected to the agent
247
247
  * - subagents: List of subagents available via Task tool
248
+ * - uiConfig: UI configuration for interface appearance
248
249
  */
249
250
  getAgentInfo() {
250
251
  return this.transport.getAgentInfo?.() || {};
@@ -249,6 +249,100 @@ export declare const SessionUpdate: z.ZodUnion<readonly [z.ZodObject<{
249
249
  originalTokens: z.ZodOptional<z.ZodNumber>;
250
250
  finalTokens: z.ZodOptional<z.ZodNumber>;
251
251
  }, z.core.$strip>>;
252
+ subagentPort: z.ZodOptional<z.ZodNumber>;
253
+ subagentSessionId: z.ZodOptional<z.ZodString>;
254
+ subagentMessages: z.ZodOptional<z.ZodArray<z.ZodObject<{
255
+ id: z.ZodString;
256
+ content: z.ZodString;
257
+ toolCalls: z.ZodOptional<z.ZodArray<z.ZodObject<{
258
+ id: z.ZodString;
259
+ title: z.ZodString;
260
+ prettyName: z.ZodOptional<z.ZodString>;
261
+ icon: z.ZodOptional<z.ZodString>;
262
+ status: z.ZodEnum<{
263
+ pending: "pending";
264
+ in_progress: "in_progress";
265
+ completed: "completed";
266
+ failed: "failed";
267
+ }>;
268
+ content: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
269
+ type: z.ZodLiteral<"content">;
270
+ content: z.ZodObject<{
271
+ type: z.ZodLiteral<"text">;
272
+ text: z.ZodString;
273
+ }, z.core.$strip>;
274
+ }, z.core.$strip>, z.ZodObject<{
275
+ type: z.ZodLiteral<"text">;
276
+ text: z.ZodString;
277
+ }, z.core.$strip>, z.ZodObject<{
278
+ type: z.ZodLiteral<"image">;
279
+ data: z.ZodString;
280
+ mimeType: z.ZodOptional<z.ZodString>;
281
+ alt: z.ZodOptional<z.ZodString>;
282
+ }, z.core.$strip>, z.ZodObject<{
283
+ type: z.ZodLiteral<"image">;
284
+ url: z.ZodString;
285
+ alt: z.ZodOptional<z.ZodString>;
286
+ }, z.core.$strip>, z.ZodObject<{
287
+ type: z.ZodLiteral<"diff">;
288
+ path: z.ZodString;
289
+ oldText: z.ZodString;
290
+ newText: z.ZodString;
291
+ line: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
292
+ }, z.core.$strip>, z.ZodObject<{
293
+ type: z.ZodLiteral<"terminal">;
294
+ terminalId: z.ZodString;
295
+ }, z.core.$strip>], "type">>>;
296
+ }, z.core.$strip>>>;
297
+ contentBlocks: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
298
+ type: z.ZodLiteral<"text">;
299
+ text: z.ZodString;
300
+ }, z.core.$strip>, z.ZodObject<{
301
+ type: z.ZodLiteral<"tool_call">;
302
+ toolCall: z.ZodObject<{
303
+ id: z.ZodString;
304
+ title: z.ZodString;
305
+ prettyName: z.ZodOptional<z.ZodString>;
306
+ icon: z.ZodOptional<z.ZodString>;
307
+ status: z.ZodEnum<{
308
+ pending: "pending";
309
+ in_progress: "in_progress";
310
+ completed: "completed";
311
+ failed: "failed";
312
+ }>;
313
+ content: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
314
+ type: z.ZodLiteral<"content">;
315
+ content: z.ZodObject<{
316
+ type: z.ZodLiteral<"text">;
317
+ text: z.ZodString;
318
+ }, z.core.$strip>;
319
+ }, z.core.$strip>, z.ZodObject<{
320
+ type: z.ZodLiteral<"text">;
321
+ text: z.ZodString;
322
+ }, z.core.$strip>, z.ZodObject<{
323
+ type: z.ZodLiteral<"image">;
324
+ data: z.ZodString;
325
+ mimeType: z.ZodOptional<z.ZodString>;
326
+ alt: z.ZodOptional<z.ZodString>;
327
+ }, z.core.$strip>, z.ZodObject<{
328
+ type: z.ZodLiteral<"image">;
329
+ url: z.ZodString;
330
+ alt: z.ZodOptional<z.ZodString>;
331
+ }, z.core.$strip>, z.ZodObject<{
332
+ type: z.ZodLiteral<"diff">;
333
+ path: z.ZodString;
334
+ oldText: z.ZodString;
335
+ newText: z.ZodString;
336
+ line: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
337
+ }, z.core.$strip>, z.ZodObject<{
338
+ type: z.ZodLiteral<"terminal">;
339
+ terminalId: z.ZodString;
340
+ }, z.core.$strip>], "type">>>;
341
+ }, z.core.$strip>;
342
+ }, z.core.$strip>], "type">>>;
343
+ isStreaming: z.ZodOptional<z.ZodBoolean>;
344
+ }, z.core.$strip>>>;
345
+ subagentStreaming: z.ZodOptional<z.ZodBoolean>;
252
346
  }, z.core.$strip>;
253
347
  messageId: z.ZodOptional<z.ZodString>;
254
348
  }, z.core.$strip>, z.ZodObject<{
@@ -363,6 +457,8 @@ export declare const SessionUpdate: z.ZodUnion<readonly [z.ZodObject<{
363
457
  outputTokens: z.ZodOptional<z.ZodNumber>;
364
458
  totalTokens: z.ZodOptional<z.ZodNumber>;
365
459
  }, z.core.$strip>>;
460
+ subagentPort: z.ZodOptional<z.ZodNumber>;
461
+ subagentSessionId: z.ZodOptional<z.ZodString>;
366
462
  }, z.core.$strip>;
367
463
  messageId: z.ZodOptional<z.ZodString>;
368
464
  }, z.core.$strip>, z.ZodObject<{
@@ -49,6 +49,9 @@ export declare class HttpTransport implements Transport {
49
49
  enabled: boolean;
50
50
  content: string;
51
51
  };
52
+ uiConfig?: {
53
+ hideTopBar?: boolean;
54
+ };
52
55
  tools?: Array<{
53
56
  name: string;
54
57
  description?: string;
@@ -71,6 +71,12 @@ export class HttpTransport {
71
71
  typeof meta.initialMessage === "object"
72
72
  ? meta.initialMessage
73
73
  : undefined;
74
+ const uiConfig = metaIsObject &&
75
+ "uiConfig" in meta &&
76
+ meta.uiConfig &&
77
+ typeof meta.uiConfig === "object"
78
+ ? meta.uiConfig
79
+ : undefined;
74
80
  this.agentInfo = {
75
81
  name: initResponse.agentInfo.name,
76
82
  // title is the ACP field for human-readable display name
@@ -81,6 +87,7 @@ export class HttpTransport {
81
87
  ...(description ? { description } : {}),
82
88
  ...(suggestedPrompts ? { suggestedPrompts } : {}),
83
89
  ...(initialMessage ? { initialMessage } : {}),
90
+ ...(uiConfig ? { uiConfig } : {}),
84
91
  ...(tools ? { tools } : {}),
85
92
  ...(mcps ? { mcps } : {}),
86
93
  ...(subagents ? { subagents } : {}),
@@ -159,6 +166,12 @@ export class HttpTransport {
159
166
  typeof meta.initialMessage === "object"
160
167
  ? meta.initialMessage
161
168
  : undefined;
169
+ const uiConfig = metaIsObject &&
170
+ "uiConfig" in meta &&
171
+ meta.uiConfig &&
172
+ typeof meta.uiConfig === "object"
173
+ ? meta.uiConfig
174
+ : undefined;
162
175
  this.agentInfo = {
163
176
  name: initResponse.agentInfo.name,
164
177
  // title is the ACP field for human-readable display name
@@ -169,6 +182,7 @@ export class HttpTransport {
169
182
  ...(description ? { description } : {}),
170
183
  ...(suggestedPrompts ? { suggestedPrompts } : {}),
171
184
  ...(initialMessage ? { initialMessage } : {}),
185
+ ...(uiConfig ? { uiConfig } : {}),
172
186
  ...(tools ? { tools } : {}),
173
187
  ...(mcps ? { mcps } : {}),
174
188
  ...(subagents ? { subagents } : {}),
@@ -773,6 +787,28 @@ export class HttpTransport {
773
787
  typeof update._meta.batchId === "string"
774
788
  ? update._meta.batchId
775
789
  : undefined;
790
+ // Extract subagent connection info from _meta
791
+ const subagentPort = update._meta &&
792
+ typeof update._meta === "object" &&
793
+ "subagentPort" in update._meta &&
794
+ typeof update._meta.subagentPort === "number"
795
+ ? update._meta.subagentPort
796
+ : undefined;
797
+ const subagentSessionId = update._meta &&
798
+ typeof update._meta === "object" &&
799
+ "subagentSessionId" in update._meta &&
800
+ typeof update._meta.subagentSessionId === "string"
801
+ ? update._meta.subagentSessionId
802
+ : undefined;
803
+ // Debug logging for subagent connection info
804
+ if (subagentPort || subagentSessionId) {
805
+ logger.info("Extracted subagent connection info from tool_call_update", {
806
+ toolCallId: update.toolCallId,
807
+ subagentPort,
808
+ subagentSessionId,
809
+ _meta: update._meta,
810
+ });
811
+ }
776
812
  // Tool call update notification
777
813
  const toolCallUpdate = {
778
814
  id: update.toolCallId ?? "",
@@ -835,6 +871,9 @@ export class HttpTransport {
835
871
  completedAt: update.status === "completed" || update.status === "failed"
836
872
  ? Date.now()
837
873
  : undefined,
874
+ // Sub-agent connection info for direct SSE streaming
875
+ subagentPort,
876
+ subagentSessionId,
838
877
  };
839
878
  const sessionUpdate = {
840
879
  type: "tool_call_update",
@@ -31,6 +31,9 @@ export declare class StdioTransport implements Transport {
31
31
  version?: string;
32
32
  description?: string;
33
33
  suggestedPrompts?: string[];
34
+ uiConfig?: {
35
+ hideTopBar?: boolean;
36
+ };
34
37
  tools?: Array<{
35
38
  name: string;
36
39
  description?: string;
@@ -31,6 +31,13 @@ export interface AgentInitialMessage {
31
31
  /** The content of the initial message */
32
32
  content: string;
33
33
  }
34
+ /**
35
+ * UI configuration for agents
36
+ */
37
+ export interface AgentUiConfig {
38
+ /** Whether to hide the top bar (session switcher, debugger link, settings) */
39
+ hideTopBar?: boolean;
40
+ }
34
41
  /**
35
42
  * Session summary for listing
36
43
  */
@@ -90,6 +97,7 @@ export interface Transport {
90
97
  * - mcps: List of MCP servers connected to the agent
91
98
  * - subagents: List of subagents available via Task tool
92
99
  * - initialMessage: Configuration for agent's initial message on session start
100
+ * - uiConfig: UI configuration for controlling interface appearance
93
101
  */
94
102
  getAgentInfo?(): {
95
103
  name?: string;
@@ -98,6 +106,7 @@ export interface Transport {
98
106
  description?: string;
99
107
  suggestedPrompts?: string[];
100
108
  initialMessage?: AgentInitialMessage;
109
+ uiConfig?: AgentUiConfig;
101
110
  tools?: AgentToolInfo[];
102
111
  mcps?: AgentMcpInfo[];
103
112
  subagents?: AgentSubagentInfo[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@townco/ui",
3
- "version": "0.1.51",
3
+ "version": "0.1.53",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -44,7 +44,7 @@
44
44
  },
45
45
  "dependencies": {
46
46
  "@agentclientprotocol/sdk": "^0.5.1",
47
- "@townco/core": "0.0.29",
47
+ "@townco/core": "0.0.31",
48
48
  "@radix-ui/react-dialog": "^1.1.15",
49
49
  "@radix-ui/react-dropdown-menu": "^2.1.16",
50
50
  "@radix-ui/react-label": "^2.1.8",
@@ -67,7 +67,7 @@
67
67
  },
68
68
  "devDependencies": {
69
69
  "@tailwindcss/postcss": "^4.1.17",
70
- "@townco/tsconfig": "0.1.48",
70
+ "@townco/tsconfig": "0.1.50",
71
71
  "@types/node": "^24.10.0",
72
72
  "@types/react": "^19.2.2",
73
73
  "ink": "^6.4.0",