@townco/ui 0.1.126 → 0.1.128

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.
@@ -347,8 +347,13 @@ export function ChatView({ client, initialSessionId, error: initError, debuggerU
347
347
  setEditingMessageIndex(isEditing ? index : null);
348
348
  } })) : (_jsxs(_Fragment, { children: [
349
349
  _jsx(MessageContent, { message: message, thinkingDisplayStyle: "collapsible" }), (() => {
350
- const isLastAssistantMessage = index ===
351
- messages.findLastIndex((m) => m.role === "assistant");
350
+ // "Last assistant message" for actions should mean the last
351
+ // assistant message that actually has text content.
352
+ // (Tool-only assistant messages exist and should not steal the
353
+ // action row from the last text response.)
354
+ const lastTextAssistantIndex = messages.findLastIndex((m) => m.role === "assistant" &&
355
+ Boolean(m.content?.trim()));
356
+ const isLastAssistantMessage = index === lastTextAssistantIndex;
352
357
  // Only show MessageActions for the last assistant message,
353
358
  // and never while it's streaming.
354
359
  if (!isLastAssistantMessage ||
@@ -44,15 +44,7 @@ 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
- // 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;
47
+ const isSubagentCall = !!(toolCall.subagentPort && toolCall.subagentSessionId);
56
48
  // Safely access ChatLayout context - will be undefined if not within ChatLayout
57
49
  const layoutContext = React.useContext(ChatLayout.Context);
58
50
  // Click handler: toggle sidepanel for TodoWrite, subagent details for subagents, expand for others
@@ -147,7 +139,7 @@ export function ToolCall({ toolCall }) {
147
139
  if (isPreliminary) {
148
140
  return (_jsx("div", { className: "flex flex-col my-4", children: _jsxs("span", { className: "text-paragraph-sm text-muted-foreground/50", children: ["Invoking ", displayName] }) }));
149
141
  }
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 &&
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 &&
151
143
  Object.keys(toolCall.rawInput).length > 0 &&
152
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 &&
153
145
  loc.line !== undefined &&
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@townco/ui",
3
- "version": "0.1.126",
3
+ "version": "0.1.128",
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.104",
52
+ "@townco/core": "0.0.106",
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.123",
70
+ "@townco/tsconfig": "0.1.125",
71
71
  "@types/node": "^24.10.0",
72
72
  "@types/react": "^19.2.2",
73
73
  "@types/unist": "^3.0.3",
@@ -1,23 +0,0 @@
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;
@@ -1,98 +0,0 @@
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
- }