@townco/ui 0.1.133 → 0.1.135

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.
@@ -228,7 +228,7 @@ export declare function useChatMessages(client: AcpClient | null, startSession:
228
228
  }[] | undefined;
229
229
  sources?: {
230
230
  id: string;
231
- url: string;
231
+ url?: string | undefined;
232
232
  title: string;
233
233
  snippet?: string | undefined;
234
234
  favicon?: string | undefined;
@@ -312,7 +312,7 @@ export declare const DisplayMessage: z.ZodObject<{
312
312
  }, z.core.$strip>>>;
313
313
  sources: z.ZodOptional<z.ZodArray<z.ZodObject<{
314
314
  id: z.ZodString;
315
- url: z.ZodString;
315
+ url: z.ZodOptional<z.ZodString>;
316
316
  title: z.ZodString;
317
317
  snippet: z.ZodOptional<z.ZodString>;
318
318
  favicon: z.ZodOptional<z.ZodString>;
@@ -607,7 +607,7 @@ export declare const ChatSessionState: z.ZodObject<{
607
607
  }, z.core.$strip>>>;
608
608
  sources: z.ZodOptional<z.ZodArray<z.ZodObject<{
609
609
  id: z.ZodString;
610
- url: z.ZodString;
610
+ url: z.ZodOptional<z.ZodString>;
611
611
  title: z.ZodString;
612
612
  snippet: z.ZodOptional<z.ZodString>;
613
613
  favicon: z.ZodOptional<z.ZodString>;
@@ -4,7 +4,7 @@ import { z } from "zod";
4
4
  */
5
5
  export declare const SourceSchema: z.ZodObject<{
6
6
  id: z.ZodString;
7
- url: z.ZodString;
7
+ url: z.ZodOptional<z.ZodString>;
8
8
  title: z.ZodString;
9
9
  snippet: z.ZodOptional<z.ZodString>;
10
10
  favicon: z.ZodOptional<z.ZodString>;
@@ -5,8 +5,8 @@ import { z } from "zod";
5
5
  export const SourceSchema = z.object({
6
6
  /** Unique identifier for LLM reference (e.g., "1", "2") */
7
7
  id: z.string(),
8
- /** The URL of the source */
9
- url: z.string().url(),
8
+ /** The URL of the source (optional for backward compatibility with sessions that have missing URLs) */
9
+ url: z.string().optional(),
10
10
  /** Title of the source page/article */
11
11
  title: z.string(),
12
12
  /** Optional snippet/excerpt from the source */
@@ -144,7 +144,7 @@ export function ChatView({ client, initialSessionId, error: initError, debuggerU
144
144
  byId.set(source.id, {
145
145
  id: source.id,
146
146
  title: source.title,
147
- url: source.url,
147
+ url: source.url || "",
148
148
  snippet: source.snippet || "",
149
149
  sourceName: source.sourceName || "",
150
150
  favicon: source.favicon || "",
@@ -39,7 +39,7 @@ export const CitationChip = React.forwardRef(({ sourceId, sources, className },
39
39
  if (!source) {
40
40
  return (_jsxs("span", { ref: ref, className: cn("inline-flex items-center px-1.5 py-0.5 text-xs font-medium", "bg-muted text-muted-foreground rounded-md", "cursor-default", className), children: ["[", sourceId, "]"] }));
41
41
  }
42
- const domain = source.sourceName || getDomain(source.url);
42
+ const domain = source.sourceName || (source.url ? getDomain(source.url) : "");
43
43
  return (_jsx(TooltipProvider, { delayDuration: 200, children: _jsxs(Tooltip, { children: [
44
44
  _jsx(TooltipTrigger, { asChild: true, children: _jsxs("span", { ref: ref, role: "button", tabIndex: 0, onClick: handleClick, onKeyDown: (e) => {
45
45
  if (e.key === "Enter" || e.key === " ") {
@@ -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 &&
@@ -411,7 +411,7 @@ export type HookNotificationChunk = z.infer<typeof HookNotificationChunk>;
411
411
  */
412
412
  export declare const CitationSource: z.ZodObject<{
413
413
  id: z.ZodString;
414
- url: z.ZodString;
414
+ url: z.ZodOptional<z.ZodString>;
415
415
  title: z.ZodString;
416
416
  snippet: z.ZodOptional<z.ZodString>;
417
417
  favicon: z.ZodOptional<z.ZodString>;
@@ -426,7 +426,7 @@ export declare const SourcesChunk: z.ZodObject<{
426
426
  type: z.ZodLiteral<"sources">;
427
427
  sources: z.ZodArray<z.ZodObject<{
428
428
  id: z.ZodString;
429
- url: z.ZodString;
429
+ url: z.ZodOptional<z.ZodString>;
430
430
  title: z.ZodString;
431
431
  snippet: z.ZodOptional<z.ZodString>;
432
432
  favicon: z.ZodOptional<z.ZodString>;
@@ -553,7 +553,7 @@ export declare const MessageChunk: z.ZodDiscriminatedUnion<[z.ZodObject<{
553
553
  type: z.ZodLiteral<"sources">;
554
554
  sources: z.ZodArray<z.ZodObject<{
555
555
  id: z.ZodString;
556
- url: z.ZodString;
556
+ url: z.ZodOptional<z.ZodString>;
557
557
  title: z.ZodString;
558
558
  snippet: z.ZodOptional<z.ZodString>;
559
559
  favicon: z.ZodOptional<z.ZodString>;
@@ -214,7 +214,7 @@ export const HookNotificationChunk = z.object({
214
214
  */
215
215
  export const CitationSource = z.object({
216
216
  id: z.string(),
217
- url: z.string().url(),
217
+ url: z.string().optional(), // Optional for backward compatibility with sessions that have missing URLs
218
218
  title: z.string(),
219
219
  snippet: z.string().optional(),
220
220
  favicon: z.string().optional(),
@@ -758,7 +758,7 @@ export declare const SessionUpdate: z.ZodUnion<readonly [z.ZodObject<{
758
758
  type: z.ZodLiteral<"sources">;
759
759
  sources: z.ZodArray<z.ZodObject<{
760
760
  id: z.ZodString;
761
- url: z.ZodString;
761
+ url: z.ZodOptional<z.ZodString>;
762
762
  title: z.ZodString;
763
763
  snippet: z.ZodOptional<z.ZodString>;
764
764
  favicon: z.ZodOptional<z.ZodString>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@townco/ui",
3
- "version": "0.1.133",
3
+ "version": "0.1.135",
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.111",
52
+ "@townco/core": "0.0.113",
53
53
  "@types/mdast": "^4.0.4",
54
54
  "@uiw/react-json-view": "^2.0.0-alpha.39",
55
55
  "class-variance-authority": "^0.7.1",
@@ -67,7 +67,7 @@
67
67
  "zustand": "^5.0.8"
68
68
  },
69
69
  "devDependencies": {
70
- "@townco/tsconfig": "0.1.130",
70
+ "@townco/tsconfig": "0.1.132",
71
71
  "@types/node": "^24.10.0",
72
72
  "@types/react": "^19.2.2",
73
73
  "@types/unist": "^3.0.3",