@stigmer/react-ui 0.0.34

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.
Files changed (100) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +70 -0
  3. package/execution/components/ApprovalControls.d.ts +10 -0
  4. package/execution/components/ApprovalControls.d.ts.map +1 -0
  5. package/execution/components/ApprovalControls.js +19 -0
  6. package/execution/components/ApprovalControls.js.map +1 -0
  7. package/execution/components/ExecutionStatus.d.ts +8 -0
  8. package/execution/components/ExecutionStatus.d.ts.map +1 -0
  9. package/execution/components/ExecutionStatus.js +13 -0
  10. package/execution/components/ExecutionStatus.js.map +1 -0
  11. package/execution/components/ExecutionStream.d.ts +16 -0
  12. package/execution/components/ExecutionStream.d.ts.map +1 -0
  13. package/execution/components/ExecutionStream.js +47 -0
  14. package/execution/components/ExecutionStream.js.map +1 -0
  15. package/execution/components/MessageEntry.d.ts +17 -0
  16. package/execution/components/MessageEntry.d.ts.map +1 -0
  17. package/execution/components/MessageEntry.js +44 -0
  18. package/execution/components/MessageEntry.js.map +1 -0
  19. package/execution/components/MessageInput.d.ts +10 -0
  20. package/execution/components/MessageInput.d.ts.map +1 -0
  21. package/execution/components/MessageInput.js +28 -0
  22. package/execution/components/MessageInput.js.map +1 -0
  23. package/execution/components/OutputBlock.d.ts +9 -0
  24. package/execution/components/OutputBlock.d.ts.map +1 -0
  25. package/execution/components/OutputBlock.js +15 -0
  26. package/execution/components/OutputBlock.js.map +1 -0
  27. package/execution/components/SubAgentCard.d.ts +11 -0
  28. package/execution/components/SubAgentCard.d.ts.map +1 -0
  29. package/execution/components/SubAgentCard.js +19 -0
  30. package/execution/components/SubAgentCard.js.map +1 -0
  31. package/execution/components/ToolCallCard.d.ts +12 -0
  32. package/execution/components/ToolCallCard.d.ts.map +1 -0
  33. package/execution/components/ToolCallCard.js +26 -0
  34. package/execution/components/ToolCallCard.js.map +1 -0
  35. package/execution/helpers.d.ts +34 -0
  36. package/execution/helpers.d.ts.map +1 -0
  37. package/execution/helpers.js +163 -0
  38. package/execution/helpers.js.map +1 -0
  39. package/execution/hooks/useAgentExecution.d.ts +24 -0
  40. package/execution/hooks/useAgentExecution.d.ts.map +1 -0
  41. package/execution/hooks/useAgentExecution.js +112 -0
  42. package/execution/hooks/useAgentExecution.js.map +1 -0
  43. package/execution/hooks/useApproval.d.ts +16 -0
  44. package/execution/hooks/useApproval.d.ts.map +1 -0
  45. package/execution/hooks/useApproval.js +25 -0
  46. package/execution/hooks/useApproval.js.map +1 -0
  47. package/execution/hooks/useExecutionService.d.ts +8 -0
  48. package/execution/hooks/useExecutionService.d.ts.map +1 -0
  49. package/execution/hooks/useExecutionService.js +14 -0
  50. package/execution/hooks/useExecutionService.js.map +1 -0
  51. package/execution/index.d.ts +17 -0
  52. package/execution/index.d.ts.map +1 -0
  53. package/execution/index.js +26 -0
  54. package/execution/index.js.map +1 -0
  55. package/execution/services/execution-service.d.ts +23 -0
  56. package/execution/services/execution-service.d.ts.map +1 -0
  57. package/execution/services/execution-service.js +70 -0
  58. package/execution/services/execution-service.js.map +1 -0
  59. package/index.d.ts +2 -0
  60. package/index.d.ts.map +1 -0
  61. package/index.js +2 -0
  62. package/index.js.map +1 -0
  63. package/internal/ui/badge.d.ts +8 -0
  64. package/internal/ui/badge.d.ts.map +1 -0
  65. package/internal/ui/badge.js +46 -0
  66. package/internal/ui/badge.js.map +1 -0
  67. package/internal/ui/button.d.ts +9 -0
  68. package/internal/ui/button.d.ts.map +1 -0
  69. package/internal/ui/button.js +48 -0
  70. package/internal/ui/button.js.map +1 -0
  71. package/internal/ui/collapsible.d.ts +6 -0
  72. package/internal/ui/collapsible.d.ts.map +1 -0
  73. package/internal/ui/collapsible.js +28 -0
  74. package/internal/ui/collapsible.js.map +1 -0
  75. package/internal/ui/textarea.d.ts +4 -0
  76. package/internal/ui/textarea.d.ts.map +1 -0
  77. package/internal/ui/textarea.js +19 -0
  78. package/internal/ui/textarea.js.map +1 -0
  79. package/package.json +54 -0
  80. package/src/execution/components/ApprovalControls.tsx +99 -0
  81. package/src/execution/components/ExecutionStatus.tsx +36 -0
  82. package/src/execution/components/ExecutionStream.tsx +167 -0
  83. package/src/execution/components/MessageEntry.tsx +143 -0
  84. package/src/execution/components/MessageInput.tsx +70 -0
  85. package/src/execution/components/OutputBlock.tsx +47 -0
  86. package/src/execution/components/SubAgentCard.tsx +135 -0
  87. package/src/execution/components/ToolCallCard.tsx +155 -0
  88. package/src/execution/helpers.ts +199 -0
  89. package/src/execution/hooks/useAgentExecution.ts +126 -0
  90. package/src/execution/hooks/useApproval.ts +55 -0
  91. package/src/execution/hooks/useExecutionService.ts +15 -0
  92. package/src/execution/index.ts +53 -0
  93. package/src/execution/services/execution-service.ts +125 -0
  94. package/src/index.ts +1 -0
  95. package/src/internal/ui/badge.tsx +52 -0
  96. package/src/internal/ui/button.tsx +60 -0
  97. package/src/internal/ui/collapsible.tsx +21 -0
  98. package/src/internal/ui/textarea.tsx +18 -0
  99. package/src/styles.css +43 -0
  100. package/styles.css +2 -0
@@ -0,0 +1,167 @@
1
+ "use client";
2
+
3
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
+ import type { AgentExecution } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
5
+ import { ExecutionPhase } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
6
+ import type { ApprovalAction } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
7
+ import { ExecutionStatus } from "./ExecutionStatus";
8
+ import { MessageEntry } from "./MessageEntry";
9
+ import { HumanMessageBubble } from "./MessageEntry";
10
+ import { MessageInput } from "./MessageInput";
11
+ import {
12
+ buildSubAgentIndex,
13
+ isTerminalPhase,
14
+ } from "../helpers";
15
+ import { cn } from "@stigmer/theme";
16
+ import { Button } from "../../internal/ui/button";
17
+ import { ArrowDown, AlertCircle } from "lucide-react";
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Props
21
+ // ---------------------------------------------------------------------------
22
+
23
+ interface ExecutionStreamProps {
24
+ execution: AgentExecution | null;
25
+ phase: ExecutionPhase;
26
+ isConnected: boolean;
27
+ error: string | null;
28
+ onApproval?: (
29
+ toolCallId: string,
30
+ action: ApprovalAction,
31
+ comment?: string,
32
+ ) => Promise<void>;
33
+ isApprovalSubmitting?: boolean;
34
+ onSendMessage?: (message: string) => void;
35
+ className?: string;
36
+ }
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // Component
40
+ // ---------------------------------------------------------------------------
41
+
42
+ export function ExecutionStream(props: ExecutionStreamProps) {
43
+ const {
44
+ execution,
45
+ phase,
46
+ error,
47
+ onApproval,
48
+ isApprovalSubmitting = false,
49
+ onSendMessage,
50
+ className,
51
+ } = props;
52
+ const scrollRef = useRef<HTMLDivElement>(null);
53
+ const bottomRef = useRef<HTMLDivElement>(null);
54
+ const [isAtBottom, setIsAtBottom] = useState(true);
55
+
56
+ const messages = execution?.status?.messages ?? [];
57
+ const subAgentIndex = useMemo(
58
+ () => (execution ? buildSubAgentIndex(execution) : new Map()),
59
+ [execution],
60
+ );
61
+
62
+ // ── Scroll-lock: auto-scroll when at bottom ──
63
+ useEffect(() => {
64
+ if (isAtBottom) {
65
+ bottomRef.current?.scrollIntoView({ behavior: "smooth" });
66
+ }
67
+ }, [messages.length, isAtBottom]);
68
+
69
+ const handleScroll = useCallback(() => {
70
+ const el = scrollRef.current;
71
+ if (!el) return;
72
+ const threshold = 48;
73
+ const atBottom =
74
+ el.scrollHeight - el.scrollTop - el.clientHeight < threshold;
75
+ setIsAtBottom(atBottom);
76
+ }, []);
77
+
78
+ const scrollToBottom = useCallback(() => {
79
+ bottomRef.current?.scrollIntoView({ behavior: "smooth" });
80
+ setIsAtBottom(true);
81
+ }, []);
82
+
83
+ const isTerminal = isTerminalPhase(phase);
84
+ const canSendMessage = isTerminal && !!onSendMessage;
85
+
86
+ return (
87
+ <div className={cn("flex flex-col", className)}>
88
+ {/* ── Header ── */}
89
+ <div className="flex items-center justify-between border-b px-4 py-2">
90
+ <ExecutionStatus phase={phase} />
91
+ {execution?.status?.startedAt && (
92
+ <time
93
+ dateTime={execution.status.startedAt}
94
+ className="text-xs text-muted-foreground"
95
+ >
96
+ {new Date(execution.status.startedAt).toLocaleTimeString()}
97
+ </time>
98
+ )}
99
+ </div>
100
+
101
+ {/* ── Stream content ── */}
102
+ <div
103
+ ref={scrollRef}
104
+ onScroll={handleScroll}
105
+ className="relative flex-1 overflow-y-auto"
106
+ >
107
+ <div className="space-y-4 p-4">
108
+ {/* Show spec.message as the initial user message if no HUMAN message exists yet */}
109
+ {messages.length === 0 && execution?.spec?.message && (
110
+ <HumanMessageBubble content={execution.spec.message} />
111
+ )}
112
+
113
+ {messages.map((msg, index) => (
114
+ <MessageEntry
115
+ key={index}
116
+ message={msg}
117
+ subAgentIndex={subAgentIndex}
118
+ onApproval={onApproval}
119
+ isApprovalSubmitting={isApprovalSubmitting}
120
+ />
121
+ ))}
122
+
123
+ {/* Error banner */}
124
+ {error && (
125
+ <div className="flex items-start gap-2 rounded-lg border border-destructive/30 bg-destructive/5 p-3 text-sm text-destructive">
126
+ <AlertCircle className="mt-0.5 size-4 shrink-0" />
127
+ <p>{error}</p>
128
+ </div>
129
+ )}
130
+
131
+ {/* Execution error from status */}
132
+ {execution?.status?.error && isTerminal && (
133
+ <div className="flex items-start gap-2 rounded-lg border border-destructive/30 bg-destructive/5 p-3 text-sm text-destructive">
134
+ <AlertCircle className="mt-0.5 size-4 shrink-0" />
135
+ <p>{execution.status.error}</p>
136
+ </div>
137
+ )}
138
+
139
+ <div ref={bottomRef} />
140
+ </div>
141
+
142
+ {/* Scroll-to-bottom FAB */}
143
+ {!isAtBottom && (
144
+ <Button
145
+ size="icon"
146
+ variant="secondary"
147
+ onClick={scrollToBottom}
148
+ className="absolute bottom-4 right-4 z-10 rounded-full shadow-md"
149
+ aria-label="Scroll to bottom"
150
+ >
151
+ <ArrowDown className="size-4" />
152
+ </Button>
153
+ )}
154
+ </div>
155
+
156
+ {/* ── Message input ── */}
157
+ {canSendMessage && (
158
+ <div className="border-t p-4">
159
+ <MessageInput
160
+ onSend={onSendMessage}
161
+ placeholder="Send a follow-up message..."
162
+ />
163
+ </div>
164
+ )}
165
+ </div>
166
+ );
167
+ }
@@ -0,0 +1,143 @@
1
+ import type { AgentMessage } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
2
+ import type { SubAgentExecution } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/subagent_pb";
3
+ import type { ApprovalAction } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
4
+ import { OutputBlock } from "./OutputBlock";
5
+ import { ToolCallCard } from "./ToolCallCard";
6
+ import { SubAgentCard } from "./SubAgentCard";
7
+ import {
8
+ isHumanMessage,
9
+ isAiMessage,
10
+ isSystemMessage,
11
+ } from "../helpers";
12
+ import { User, BotMessageSquare, Info } from "lucide-react";
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Props
16
+ // ---------------------------------------------------------------------------
17
+
18
+ export interface MessageEntryProps {
19
+ message: AgentMessage;
20
+ subAgentIndex: Map<string, SubAgentExecution>;
21
+ onApproval?: (
22
+ toolCallId: string,
23
+ action: ApprovalAction,
24
+ comment?: string,
25
+ ) => Promise<void>;
26
+ isApprovalSubmitting?: boolean;
27
+ }
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // Message dispatcher
31
+ // ---------------------------------------------------------------------------
32
+
33
+ export function MessageEntry({
34
+ message,
35
+ subAgentIndex,
36
+ onApproval,
37
+ isApprovalSubmitting,
38
+ }: MessageEntryProps) {
39
+ if (isHumanMessage(message.type)) {
40
+ return <HumanMessageBubble content={message.content} />;
41
+ }
42
+
43
+ if (isAiMessage(message.type)) {
44
+ return (
45
+ <AiMessageBlock
46
+ message={message}
47
+ subAgentIndex={subAgentIndex}
48
+ onApproval={onApproval}
49
+ isApprovalSubmitting={isApprovalSubmitting}
50
+ />
51
+ );
52
+ }
53
+
54
+ if (isSystemMessage(message.type)) {
55
+ return <SystemMessageBlock content={message.content} />;
56
+ }
57
+
58
+ // MESSAGE_TOOL: tool results are rendered inline with the tool call card,
59
+ // so we don't render a separate block for them.
60
+ return null;
61
+ }
62
+
63
+ // ---------------------------------------------------------------------------
64
+ // Message type blocks
65
+ // ---------------------------------------------------------------------------
66
+
67
+ export function HumanMessageBubble({ content }: { content: string }) {
68
+ return (
69
+ <div className="flex items-start gap-3">
70
+ <div className="flex size-7 shrink-0 items-center justify-center rounded-full bg-muted">
71
+ <User className="size-3.5 text-muted-foreground" />
72
+ </div>
73
+ <div className="min-w-0 flex-1 rounded-lg bg-muted px-3 py-2 text-sm">
74
+ <p className="whitespace-pre-wrap">{content}</p>
75
+ </div>
76
+ </div>
77
+ );
78
+ }
79
+
80
+ function AiMessageBlock({
81
+ message,
82
+ subAgentIndex,
83
+ onApproval,
84
+ isApprovalSubmitting,
85
+ }: {
86
+ message: AgentMessage;
87
+ subAgentIndex: Map<string, SubAgentExecution>;
88
+ onApproval?: (
89
+ toolCallId: string,
90
+ action: ApprovalAction,
91
+ comment?: string,
92
+ ) => Promise<void>;
93
+ isApprovalSubmitting?: boolean;
94
+ }) {
95
+ return (
96
+ <div className="flex items-start gap-3">
97
+ <div className="flex size-7 shrink-0 items-center justify-center rounded-full bg-primary/10">
98
+ <BotMessageSquare className="size-3.5 text-primary" />
99
+ </div>
100
+ <div className="min-w-0 flex-1 space-y-2">
101
+ {message.content && (
102
+ <OutputBlock
103
+ content={message.content}
104
+ isStreaming={message.isStreaming}
105
+ model={message.model}
106
+ />
107
+ )}
108
+
109
+ {message.toolCalls.map((tc) => {
110
+ const subAgent = subAgentIndex.get(tc.id);
111
+ if (subAgent) {
112
+ return (
113
+ <SubAgentCard
114
+ key={tc.id}
115
+ subAgent={subAgent}
116
+ onApproval={onApproval}
117
+ isApprovalSubmitting={isApprovalSubmitting}
118
+ />
119
+ );
120
+ }
121
+ return (
122
+ <ToolCallCard
123
+ key={tc.id}
124
+ toolCall={tc}
125
+ onApproval={onApproval}
126
+ isApprovalSubmitting={isApprovalSubmitting}
127
+ />
128
+ );
129
+ })}
130
+ </div>
131
+ </div>
132
+ );
133
+ }
134
+
135
+ export function SystemMessageBlock({ content }: { content: string }) {
136
+ if (!content) return null;
137
+ return (
138
+ <div className="flex items-center gap-2 text-xs text-muted-foreground">
139
+ <Info className="size-3 shrink-0" />
140
+ <p>{content}</p>
141
+ </div>
142
+ );
143
+ }
@@ -0,0 +1,70 @@
1
+ "use client";
2
+
3
+ import { useCallback, useRef, type KeyboardEvent } from "react";
4
+ import { Textarea } from "../../internal/ui/textarea";
5
+ import { Button } from "../../internal/ui/button";
6
+ import { cn } from "@stigmer/theme";
7
+ import { SendHorizontal, Loader2 } from "lucide-react";
8
+
9
+ interface MessageInputProps {
10
+ onSend: (message: string) => void;
11
+ disabled?: boolean;
12
+ isLoading?: boolean;
13
+ placeholder?: string;
14
+ className?: string;
15
+ }
16
+
17
+ export function MessageInput({
18
+ onSend,
19
+ disabled = false,
20
+ isLoading = false,
21
+ placeholder = "Send a message...",
22
+ className,
23
+ }: MessageInputProps) {
24
+ const textareaRef = useRef<HTMLTextAreaElement>(null);
25
+
26
+ const handleSend = useCallback(() => {
27
+ const value = textareaRef.current?.value.trim();
28
+ if (!value || disabled || isLoading) return;
29
+ onSend(value);
30
+ if (textareaRef.current) {
31
+ textareaRef.current.value = "";
32
+ }
33
+ }, [onSend, disabled, isLoading]);
34
+
35
+ const handleKeyDown = useCallback(
36
+ (e: KeyboardEvent<HTMLTextAreaElement>) => {
37
+ if (e.key === "Enter" && !e.shiftKey) {
38
+ e.preventDefault();
39
+ handleSend();
40
+ }
41
+ },
42
+ [handleSend],
43
+ );
44
+
45
+ return (
46
+ <div className={cn("flex items-end gap-2", className)}>
47
+ <Textarea
48
+ ref={textareaRef}
49
+ placeholder={placeholder}
50
+ disabled={disabled || isLoading}
51
+ onKeyDown={handleKeyDown}
52
+ className="min-h-10 resize-none"
53
+ rows={1}
54
+ />
55
+ <Button
56
+ size="icon"
57
+ onClick={handleSend}
58
+ disabled={disabled || isLoading}
59
+ className="shrink-0"
60
+ aria-label="Send message"
61
+ >
62
+ {isLoading ? (
63
+ <Loader2 className="size-4 animate-spin" />
64
+ ) : (
65
+ <SendHorizontal className="size-4" />
66
+ )}
67
+ </Button>
68
+ </div>
69
+ );
70
+ }
@@ -0,0 +1,47 @@
1
+ "use client";
2
+
3
+ import { memo } from "react";
4
+ import ReactMarkdown from "react-markdown";
5
+ import remarkGfm from "remark-gfm";
6
+ import { cn } from "@stigmer/theme";
7
+
8
+ interface OutputBlockProps {
9
+ content: string;
10
+ isStreaming?: boolean;
11
+ model?: string;
12
+ className?: string;
13
+ }
14
+
15
+ export const OutputBlock = memo(function OutputBlock({
16
+ content,
17
+ isStreaming = false,
18
+ model,
19
+ className,
20
+ }: OutputBlockProps) {
21
+ if (!content && !isStreaming) return null;
22
+
23
+ return (
24
+ <div className={cn("relative", className)}>
25
+ <div className="prose prose-sm dark:prose-invert max-w-none break-words [&_pre]:overflow-x-auto [&_pre]:rounded-lg [&_pre]:bg-muted [&_pre]:p-3 [&_pre]:text-xs [&_code]:rounded [&_code]:bg-muted [&_code]:px-1.5 [&_code]:py-0.5 [&_code]:text-xs [&_code]:before:content-none [&_code]:after:content-none [&_table]:text-xs [&_th]:px-2 [&_th]:py-1 [&_td]:px-2 [&_td]:py-1">
26
+ <ReactMarkdown remarkPlugins={[remarkGfm]}>
27
+ {content}
28
+ </ReactMarkdown>
29
+ {isStreaming && <StreamingCursor />}
30
+ </div>
31
+ {model && !isStreaming && (
32
+ <p className="mt-1.5 text-[10px] text-muted-foreground/60">
33
+ {model}
34
+ </p>
35
+ )}
36
+ </div>
37
+ );
38
+ });
39
+
40
+ function StreamingCursor() {
41
+ return (
42
+ <span
43
+ className="ml-0.5 inline-block h-4 w-0.5 animate-pulse bg-foreground align-text-bottom"
44
+ aria-label="Generating..."
45
+ />
46
+ );
47
+ }
@@ -0,0 +1,135 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { Badge } from "../../internal/ui/badge";
5
+ import {
6
+ Collapsible,
7
+ CollapsibleContent,
8
+ CollapsibleTrigger,
9
+ } from "../../internal/ui/collapsible";
10
+ import { OutputBlock } from "./OutputBlock";
11
+ import { ToolCallCard } from "./ToolCallCard";
12
+ import {
13
+ subAgentStatusLabel,
14
+ subAgentStatusVariant,
15
+ formatDuration,
16
+ isAiMessage,
17
+ } from "../helpers";
18
+ import type { SubAgentExecution } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/subagent_pb";
19
+ import type { ApprovalAction } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
20
+ import { SubAgentStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
21
+ import { cn } from "@stigmer/theme";
22
+ import { ChevronRight, Bot, Loader2 } from "lucide-react";
23
+
24
+ interface SubAgentCardProps {
25
+ subAgent: SubAgentExecution;
26
+ onApproval?: (
27
+ toolCallId: string,
28
+ action: ApprovalAction,
29
+ comment?: string,
30
+ ) => Promise<void>;
31
+ isApprovalSubmitting?: boolean;
32
+ className?: string;
33
+ }
34
+
35
+ export function SubAgentCard({
36
+ subAgent,
37
+ onApproval,
38
+ isApprovalSubmitting = false,
39
+ className,
40
+ }: SubAgentCardProps) {
41
+ const [open, setOpen] = useState(false);
42
+ const isActive = subAgent.status === SubAgentStatus.SUB_AGENT_IN_PROGRESS;
43
+ const duration = formatDuration(subAgent.startedAt, subAgent.completedAt);
44
+ const aiMessages = subAgent.messages.filter((m) => isAiMessage(m.type));
45
+
46
+ return (
47
+ <Collapsible open={open} onOpenChange={setOpen}>
48
+ <div
49
+ className={cn(
50
+ "rounded-lg border border-dashed bg-card text-card-foreground text-sm",
51
+ className,
52
+ )}
53
+ >
54
+ <CollapsibleTrigger className="flex w-full items-center gap-2 px-3 py-2 text-left hover:bg-muted/50 transition-colors rounded-t-lg">
55
+ <ChevronRight
56
+ className={cn(
57
+ "size-3.5 shrink-0 text-muted-foreground transition-transform",
58
+ open && "rotate-90",
59
+ )}
60
+ />
61
+ {isActive ? (
62
+ <Loader2 className="size-3.5 shrink-0 animate-spin text-muted-foreground" />
63
+ ) : (
64
+ <Bot className="size-3.5 shrink-0 text-muted-foreground" />
65
+ )}
66
+ <span className="truncate font-medium text-xs">
67
+ {subAgent.subject || subAgent.name}
68
+ </span>
69
+ <Badge
70
+ variant={subAgentStatusVariant(subAgent.status)}
71
+ className="ml-auto shrink-0 text-[10px]"
72
+ >
73
+ {subAgentStatusLabel(subAgent.status)}
74
+ </Badge>
75
+ {duration && (
76
+ <span className="shrink-0 text-[10px] text-muted-foreground">
77
+ {duration}
78
+ </span>
79
+ )}
80
+ {subAgent.toolCalls.length > 0 && (
81
+ <span className="shrink-0 text-[10px] text-muted-foreground">
82
+ {subAgent.toolCalls.length} tool{subAgent.toolCalls.length !== 1 && "s"}
83
+ </span>
84
+ )}
85
+ </CollapsibleTrigger>
86
+
87
+ <CollapsibleContent>
88
+ <div className="border-t px-3 py-2 space-y-3">
89
+ {subAgent.input && (
90
+ <div>
91
+ <p className="mb-1 text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
92
+ Task
93
+ </p>
94
+ <p className="text-xs text-muted-foreground">{subAgent.input}</p>
95
+ </div>
96
+ )}
97
+
98
+ {aiMessages.map((msg, i) => (
99
+ <OutputBlock
100
+ key={i}
101
+ content={msg.content}
102
+ isStreaming={msg.isStreaming}
103
+ model={msg.model}
104
+ />
105
+ ))}
106
+
107
+ {subAgent.toolCalls.map((tc) => (
108
+ <ToolCallCard
109
+ key={tc.id}
110
+ toolCall={tc}
111
+ onApproval={onApproval}
112
+ isApprovalSubmitting={isApprovalSubmitting}
113
+ />
114
+ ))}
115
+
116
+ {subAgent.error && (
117
+ <p className="text-xs text-destructive">{subAgent.error}</p>
118
+ )}
119
+
120
+ {subAgent.output && (
121
+ <div>
122
+ <p className="mb-1 text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
123
+ Result
124
+ </p>
125
+ <pre className="overflow-x-auto rounded bg-muted p-2 text-[11px] leading-relaxed whitespace-pre-wrap">
126
+ {subAgent.output}
127
+ </pre>
128
+ </div>
129
+ )}
130
+ </div>
131
+ </CollapsibleContent>
132
+ </div>
133
+ </Collapsible>
134
+ );
135
+ }