@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,155 @@
1
+ "use client";
2
+
3
+ import { useState, useCallback } from "react";
4
+ import { Badge } from "../../internal/ui/badge";
5
+ import {
6
+ Collapsible,
7
+ CollapsibleContent,
8
+ CollapsibleTrigger,
9
+ } from "../../internal/ui/collapsible";
10
+ import { ApprovalControls } from "./ApprovalControls";
11
+ import {
12
+ toolCallStatusLabel,
13
+ toolCallStatusVariant,
14
+ qualifiedToolName,
15
+ formatDuration,
16
+ } from "../helpers";
17
+ import { ToolCallStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
18
+ import type { ApprovalAction } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
19
+ import type { ToolCall } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
20
+ import { cn } from "@stigmer/theme";
21
+ import { ChevronRight, Wrench, Loader2 } from "lucide-react";
22
+
23
+ interface ToolCallCardProps {
24
+ toolCall: ToolCall;
25
+ /** Callback for approval submission. Only required when tool is awaiting approval. */
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 ToolCallCard({
36
+ toolCall,
37
+ onApproval,
38
+ isApprovalSubmitting = false,
39
+ className,
40
+ }: ToolCallCardProps) {
41
+ const [open, setOpen] = useState(false);
42
+ const isWaitingApproval =
43
+ toolCall.status === ToolCallStatus.TOOL_CALL_WAITING_APPROVAL;
44
+ const isRunning = toolCall.status === ToolCallStatus.TOOL_CALL_RUNNING;
45
+ const duration = formatDuration(toolCall.startedAt, toolCall.completedAt);
46
+ const displayName = qualifiedToolName(toolCall.name, toolCall.mcpServerSlug);
47
+
48
+ const handleApproval = useCallback(
49
+ async (action: ApprovalAction, comment?: string) => {
50
+ await onApproval?.(toolCall.id, action, comment);
51
+ },
52
+ [onApproval, toolCall.id],
53
+ );
54
+
55
+ return (
56
+ <Collapsible open={open || isWaitingApproval} onOpenChange={setOpen}>
57
+ <div
58
+ className={cn(
59
+ "rounded-lg border bg-card text-card-foreground text-sm",
60
+ isWaitingApproval && "border-primary/40 ring-1 ring-primary/20",
61
+ className,
62
+ )}
63
+ >
64
+ <CollapsibleTrigger className="flex w-full items-center gap-2 px-3 py-2 text-left hover:bg-muted/50 transition-colors rounded-t-lg">
65
+ <ChevronRight
66
+ className={cn(
67
+ "size-3.5 shrink-0 text-muted-foreground transition-transform",
68
+ (open || isWaitingApproval) && "rotate-90",
69
+ )}
70
+ />
71
+ {isRunning ? (
72
+ <Loader2 className="size-3.5 shrink-0 animate-spin text-muted-foreground" />
73
+ ) : (
74
+ <Wrench className="size-3.5 shrink-0 text-muted-foreground" />
75
+ )}
76
+ <span className="truncate font-mono text-xs">{displayName}</span>
77
+ <Badge
78
+ variant={toolCallStatusVariant(toolCall.status)}
79
+ className="ml-auto shrink-0 text-[10px]"
80
+ >
81
+ {toolCallStatusLabel(toolCall.status)}
82
+ </Badge>
83
+ {duration && (
84
+ <span className="shrink-0 text-[10px] text-muted-foreground">
85
+ {duration}
86
+ </span>
87
+ )}
88
+ </CollapsibleTrigger>
89
+
90
+ <CollapsibleContent>
91
+ <div className="border-t px-3 py-2 space-y-2">
92
+ {toolCall.args && Object.keys(toolCall.args).length > 0 && (
93
+ <ToolCallSection label="Arguments">
94
+ <pre className="overflow-x-auto rounded bg-muted p-2 text-[11px] leading-relaxed">
95
+ {JSON.stringify(toolCall.args, null, 2)}
96
+ </pre>
97
+ </ToolCallSection>
98
+ )}
99
+
100
+ {toolCall.result && (
101
+ <ToolCallSection label="Result">
102
+ <pre className="overflow-x-auto rounded bg-muted p-2 text-[11px] leading-relaxed whitespace-pre-wrap">
103
+ {toolCall.result}
104
+ </pre>
105
+ </ToolCallSection>
106
+ )}
107
+
108
+ {toolCall.error && (
109
+ <ToolCallSection label="Error">
110
+ <p className="text-xs text-destructive">{toolCall.error}</p>
111
+ </ToolCallSection>
112
+ )}
113
+
114
+ {toolCall.isStreaming && !toolCall.result && (
115
+ <div className="flex items-center gap-1.5 text-xs text-muted-foreground">
116
+ <Loader2 className="size-3 animate-spin" />
117
+ Executing...
118
+ </div>
119
+ )}
120
+ </div>
121
+
122
+ {isWaitingApproval && onApproval && (
123
+ <div className="border-t px-3 py-2">
124
+ <ApprovalControls
125
+ approvalMessage={
126
+ toolCall.approvalMessage ||
127
+ `Execute tool: ${displayName}`
128
+ }
129
+ onSubmit={handleApproval}
130
+ isSubmitting={isApprovalSubmitting}
131
+ />
132
+ </div>
133
+ )}
134
+ </CollapsibleContent>
135
+ </div>
136
+ </Collapsible>
137
+ );
138
+ }
139
+
140
+ function ToolCallSection({
141
+ label,
142
+ children,
143
+ }: {
144
+ label: string;
145
+ children: React.ReactNode;
146
+ }) {
147
+ return (
148
+ <div>
149
+ <p className="mb-1 text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
150
+ {label}
151
+ </p>
152
+ {children}
153
+ </div>
154
+ );
155
+ }
@@ -0,0 +1,199 @@
1
+ import {
2
+ ExecutionPhase,
3
+ ToolCallStatus,
4
+ SubAgentStatus,
5
+ MessageType,
6
+ } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
7
+ import type { AgentExecution } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
8
+ import type { SubAgentExecution } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/subagent_pb";
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Execution phase utilities
12
+ // ---------------------------------------------------------------------------
13
+
14
+ const TERMINAL_PHASES = new Set<ExecutionPhase>([
15
+ ExecutionPhase.EXECUTION_COMPLETED,
16
+ ExecutionPhase.EXECUTION_FAILED,
17
+ ExecutionPhase.EXECUTION_CANCELLED,
18
+ ExecutionPhase.EXECUTION_TERMINATED,
19
+ ]);
20
+
21
+ export function isTerminalPhase(phase: ExecutionPhase): boolean {
22
+ return TERMINAL_PHASES.has(phase);
23
+ }
24
+
25
+ const PHASE_LABELS: Record<ExecutionPhase, string> = {
26
+ [ExecutionPhase.EXECUTION_PHASE_UNSPECIFIED]: "Unknown",
27
+ [ExecutionPhase.EXECUTION_PENDING]: "Pending",
28
+ [ExecutionPhase.EXECUTION_IN_PROGRESS]: "Running",
29
+ [ExecutionPhase.EXECUTION_COMPLETED]: "Completed",
30
+ [ExecutionPhase.EXECUTION_FAILED]: "Failed",
31
+ [ExecutionPhase.EXECUTION_CANCELLED]: "Cancelled",
32
+ [ExecutionPhase.EXECUTION_TERMINATED]: "Terminated",
33
+ [ExecutionPhase.EXECUTION_WAITING_FOR_APPROVAL]: "Waiting for Approval",
34
+ [ExecutionPhase.EXECUTION_PAUSED]: "Paused",
35
+ };
36
+
37
+ export function phaseLabel(phase: ExecutionPhase): string {
38
+ return PHASE_LABELS[phase] ?? "Unknown";
39
+ }
40
+
41
+ type BadgeVariant =
42
+ | "default"
43
+ | "secondary"
44
+ | "destructive"
45
+ | "outline";
46
+
47
+ const PHASE_VARIANTS: Record<ExecutionPhase, BadgeVariant> = {
48
+ [ExecutionPhase.EXECUTION_PHASE_UNSPECIFIED]: "outline",
49
+ [ExecutionPhase.EXECUTION_PENDING]: "outline",
50
+ [ExecutionPhase.EXECUTION_IN_PROGRESS]: "default",
51
+ [ExecutionPhase.EXECUTION_COMPLETED]: "secondary",
52
+ [ExecutionPhase.EXECUTION_FAILED]: "destructive",
53
+ [ExecutionPhase.EXECUTION_CANCELLED]: "outline",
54
+ [ExecutionPhase.EXECUTION_TERMINATED]: "destructive",
55
+ [ExecutionPhase.EXECUTION_WAITING_FOR_APPROVAL]: "default",
56
+ [ExecutionPhase.EXECUTION_PAUSED]: "outline",
57
+ };
58
+
59
+ export function phaseVariant(phase: ExecutionPhase): BadgeVariant {
60
+ return PHASE_VARIANTS[phase] ?? "outline";
61
+ }
62
+
63
+ // ---------------------------------------------------------------------------
64
+ // Tool call status utilities
65
+ // ---------------------------------------------------------------------------
66
+
67
+ const TOOL_CALL_STATUS_LABELS: Record<ToolCallStatus, string> = {
68
+ [ToolCallStatus.TOOL_CALL_STATUS_UNSPECIFIED]: "Unknown",
69
+ [ToolCallStatus.TOOL_CALL_PENDING]: "Pending",
70
+ [ToolCallStatus.TOOL_CALL_RUNNING]: "Running",
71
+ [ToolCallStatus.TOOL_CALL_COMPLETED]: "Completed",
72
+ [ToolCallStatus.TOOL_CALL_FAILED]: "Failed",
73
+ [ToolCallStatus.TOOL_CALL_WAITING_APPROVAL]: "Awaiting Approval",
74
+ [ToolCallStatus.TOOL_CALL_SKIPPED]: "Skipped",
75
+ };
76
+
77
+ export function toolCallStatusLabel(status: ToolCallStatus): string {
78
+ return TOOL_CALL_STATUS_LABELS[status] ?? "Unknown";
79
+ }
80
+
81
+ const TOOL_CALL_STATUS_VARIANTS: Record<ToolCallStatus, BadgeVariant> = {
82
+ [ToolCallStatus.TOOL_CALL_STATUS_UNSPECIFIED]: "outline",
83
+ [ToolCallStatus.TOOL_CALL_PENDING]: "outline",
84
+ [ToolCallStatus.TOOL_CALL_RUNNING]: "default",
85
+ [ToolCallStatus.TOOL_CALL_COMPLETED]: "secondary",
86
+ [ToolCallStatus.TOOL_CALL_FAILED]: "destructive",
87
+ [ToolCallStatus.TOOL_CALL_WAITING_APPROVAL]: "default",
88
+ [ToolCallStatus.TOOL_CALL_SKIPPED]: "outline",
89
+ };
90
+
91
+ export function toolCallStatusVariant(status: ToolCallStatus): BadgeVariant {
92
+ return TOOL_CALL_STATUS_VARIANTS[status] ?? "outline";
93
+ }
94
+
95
+ export function isToolCallTerminal(status: ToolCallStatus): boolean {
96
+ return (
97
+ status === ToolCallStatus.TOOL_CALL_COMPLETED ||
98
+ status === ToolCallStatus.TOOL_CALL_FAILED ||
99
+ status === ToolCallStatus.TOOL_CALL_SKIPPED
100
+ );
101
+ }
102
+
103
+ // ---------------------------------------------------------------------------
104
+ // Sub-agent status utilities
105
+ // ---------------------------------------------------------------------------
106
+
107
+ const SUB_AGENT_STATUS_LABELS: Record<SubAgentStatus, string> = {
108
+ [SubAgentStatus.SUB_AGENT_STATUS_UNSPECIFIED]: "Unknown",
109
+ [SubAgentStatus.SUB_AGENT_PENDING]: "Pending",
110
+ [SubAgentStatus.SUB_AGENT_IN_PROGRESS]: "Running",
111
+ [SubAgentStatus.SUB_AGENT_COMPLETED]: "Completed",
112
+ [SubAgentStatus.SUB_AGENT_FAILED]: "Failed",
113
+ [SubAgentStatus.SUB_AGENT_CANCELLED]: "Cancelled",
114
+ };
115
+
116
+ export function subAgentStatusLabel(status: SubAgentStatus): string {
117
+ return SUB_AGENT_STATUS_LABELS[status] ?? "Unknown";
118
+ }
119
+
120
+ const SUB_AGENT_STATUS_VARIANTS: Record<SubAgentStatus, BadgeVariant> = {
121
+ [SubAgentStatus.SUB_AGENT_STATUS_UNSPECIFIED]: "outline",
122
+ [SubAgentStatus.SUB_AGENT_PENDING]: "outline",
123
+ [SubAgentStatus.SUB_AGENT_IN_PROGRESS]: "default",
124
+ [SubAgentStatus.SUB_AGENT_COMPLETED]: "secondary",
125
+ [SubAgentStatus.SUB_AGENT_FAILED]: "destructive",
126
+ [SubAgentStatus.SUB_AGENT_CANCELLED]: "outline",
127
+ };
128
+
129
+ export function subAgentStatusVariant(status: SubAgentStatus): BadgeVariant {
130
+ return SUB_AGENT_STATUS_VARIANTS[status] ?? "outline";
131
+ }
132
+
133
+ // ---------------------------------------------------------------------------
134
+ // Message type utilities
135
+ // ---------------------------------------------------------------------------
136
+
137
+ export function isHumanMessage(type: MessageType): boolean {
138
+ return type === MessageType.MESSAGE_HUMAN;
139
+ }
140
+
141
+ export function isAiMessage(type: MessageType): boolean {
142
+ return type === MessageType.MESSAGE_AI;
143
+ }
144
+
145
+ export function isToolMessage(type: MessageType): boolean {
146
+ return type === MessageType.MESSAGE_TOOL;
147
+ }
148
+
149
+ export function isSystemMessage(type: MessageType): boolean {
150
+ return type === MessageType.MESSAGE_SYSTEM;
151
+ }
152
+
153
+ // ---------------------------------------------------------------------------
154
+ // Execution-level helpers
155
+ // ---------------------------------------------------------------------------
156
+
157
+ /**
158
+ * Build an index of sub-agent executions keyed by their ID.
159
+ * The sub-agent ID matches the tool call ID from the parent's "task" tool invocation,
160
+ * enabling O(1) lookup when rendering tool calls that represent sub-agent delegations.
161
+ */
162
+ export function buildSubAgentIndex(
163
+ execution: AgentExecution,
164
+ ): Map<string, SubAgentExecution> {
165
+ const subAgents = execution.status?.subAgentExecutions;
166
+ if (!subAgents || subAgents.length === 0) return new Map();
167
+ return new Map(subAgents.map((sa) => [sa.id, sa]));
168
+ }
169
+
170
+ /**
171
+ * Format a qualified tool name: "mcp-server/tool-name" when an MCP server
172
+ * slug is present, or just "tool-name" for built-in sandbox tools.
173
+ */
174
+ export function qualifiedToolName(
175
+ name: string,
176
+ mcpServerSlug: string,
177
+ ): string {
178
+ if (mcpServerSlug) return `${mcpServerSlug}/${name}`;
179
+ return name;
180
+ }
181
+
182
+ /**
183
+ * Format elapsed time between two ISO timestamps as a human-readable duration.
184
+ * Returns null if either timestamp is missing.
185
+ */
186
+ export function formatDuration(
187
+ startedAt: string,
188
+ completedAt: string,
189
+ ): string | null {
190
+ if (!startedAt || !completedAt) return null;
191
+ const ms = new Date(completedAt).getTime() - new Date(startedAt).getTime();
192
+ if (ms < 0) return null;
193
+ if (ms < 1000) return `${ms}ms`;
194
+ const seconds = ms / 1000;
195
+ if (seconds < 60) return `${seconds.toFixed(1)}s`;
196
+ const minutes = Math.floor(seconds / 60);
197
+ const remainingSeconds = Math.round(seconds % 60);
198
+ return `${minutes}m ${remainingSeconds}s`;
199
+ }
@@ -0,0 +1,126 @@
1
+ "use client";
2
+
3
+ import { useCallback, useEffect, 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 { useExecutionService } from "./useExecutionService";
7
+ import { isTerminalPhase } from "../helpers";
8
+ import type { CreateExecutionInput } from "../services/execution-service";
9
+
10
+ export type { CreateExecutionInput };
11
+
12
+ export interface UseAgentExecutionOptions {
13
+ /** Subscribe to an existing execution on mount. */
14
+ executionId?: string;
15
+ }
16
+
17
+ export interface UseAgentExecutionReturn {
18
+ /** The latest execution state from the stream. Null before subscription starts. */
19
+ execution: AgentExecution | null;
20
+ /** Convenience accessor — the current phase (defaults to UNSPECIFIED when no execution). */
21
+ phase: ExecutionPhase;
22
+ /** True while the subscription stream is open and receiving updates. */
23
+ isConnected: boolean;
24
+ /** Error message from creation, subscription, or cancellation. Null when healthy. */
25
+ error: string | null;
26
+ /** Create a new execution and auto-subscribe to its stream. */
27
+ start: (input: CreateExecutionInput) => Promise<void>;
28
+ /** Gracefully cancel the current execution. */
29
+ cancel: (reason?: string) => Promise<void>;
30
+ }
31
+
32
+ export function useAgentExecution(
33
+ options?: UseAgentExecutionOptions,
34
+ ): UseAgentExecutionReturn {
35
+ const service = useExecutionService();
36
+ const [execution, setExecution] = useState<AgentExecution | null>(null);
37
+ const [isConnected, setIsConnected] = useState(false);
38
+ const [error, setError] = useState<string | null>(null);
39
+
40
+ const activeExecutionIdRef = useRef<string | null>(
41
+ options?.executionId ?? null,
42
+ );
43
+ const abortRef = useRef<AbortController | null>(null);
44
+
45
+ const subscribe = useCallback((executionId: string) => {
46
+ abortRef.current?.abort();
47
+
48
+ const controller = new AbortController();
49
+ abortRef.current = controller;
50
+ activeExecutionIdRef.current = executionId;
51
+ setIsConnected(true);
52
+ setError(null);
53
+
54
+ (async () => {
55
+ try {
56
+ const stream = service.subscribeToExecution(executionId, controller.signal);
57
+ for await (const update of stream) {
58
+ if (controller.signal.aborted) break;
59
+ setExecution(update);
60
+
61
+ const phase = update.status?.phase ?? ExecutionPhase.EXECUTION_PHASE_UNSPECIFIED;
62
+ if (isTerminalPhase(phase)) {
63
+ setIsConnected(false);
64
+ break;
65
+ }
66
+ }
67
+ } catch (err: unknown) {
68
+ if (controller.signal.aborted) return;
69
+ const message =
70
+ err instanceof Error ? err.message : "Stream disconnected";
71
+ setError(message);
72
+ } finally {
73
+ if (!controller.signal.aborted) {
74
+ setIsConnected(false);
75
+ }
76
+ }
77
+ })();
78
+ }, [service]);
79
+
80
+ useEffect(() => {
81
+ if (options?.executionId) {
82
+ subscribe(options.executionId);
83
+ }
84
+ return () => {
85
+ abortRef.current?.abort();
86
+ };
87
+ }, [options?.executionId, subscribe]);
88
+
89
+ const start = useCallback(
90
+ async (input: CreateExecutionInput) => {
91
+ setError(null);
92
+ setExecution(null);
93
+ try {
94
+ const created = await service.createExecution(input);
95
+ const executionId = created.metadata?.id;
96
+ if (!executionId) {
97
+ throw new Error("Created execution missing metadata.id");
98
+ }
99
+ setExecution(created);
100
+ subscribe(executionId);
101
+ } catch (err: unknown) {
102
+ const message =
103
+ err instanceof Error ? err.message : "Failed to create execution";
104
+ setError(message);
105
+ }
106
+ },
107
+ [service, subscribe],
108
+ );
109
+
110
+ const cancel = useCallback(async (reason?: string) => {
111
+ const id = activeExecutionIdRef.current;
112
+ if (!id) return;
113
+ try {
114
+ await service.cancelExecution(id, reason);
115
+ } catch (err: unknown) {
116
+ const message =
117
+ err instanceof Error ? err.message : "Failed to cancel execution";
118
+ setError(message);
119
+ }
120
+ }, [service]);
121
+
122
+ const phase =
123
+ execution?.status?.phase ?? ExecutionPhase.EXECUTION_PHASE_UNSPECIFIED;
124
+
125
+ return { execution, phase, isConnected, error, start, cancel };
126
+ }
@@ -0,0 +1,55 @@
1
+ "use client";
2
+
3
+ import { useCallback, useState } from "react";
4
+ import type { ApprovalAction } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
5
+ import { useExecutionService } from "./useExecutionService";
6
+
7
+ export interface UseApprovalOptions {
8
+ executionId: string;
9
+ }
10
+
11
+ export interface UseApprovalReturn {
12
+ /** Submit an approval decision for a specific tool call. */
13
+ submit: (
14
+ toolCallId: string,
15
+ action: ApprovalAction,
16
+ comment?: string,
17
+ ) => Promise<void>;
18
+ /** True while an approval request is in flight. */
19
+ isSubmitting: boolean;
20
+ /** Error message from the last failed submission. Null when healthy. */
21
+ error: string | null;
22
+ /** Clear the current error state. */
23
+ clearError: () => void;
24
+ }
25
+
26
+ export function useApproval(options: UseApprovalOptions): UseApprovalReturn {
27
+ const service = useExecutionService();
28
+ const [isSubmitting, setIsSubmitting] = useState(false);
29
+ const [error, setError] = useState<string | null>(null);
30
+
31
+ const submit = useCallback(
32
+ async (
33
+ toolCallId: string,
34
+ action: ApprovalAction,
35
+ comment?: string,
36
+ ) => {
37
+ setIsSubmitting(true);
38
+ setError(null);
39
+ try {
40
+ await service.submitApproval(options.executionId, toolCallId, action, comment);
41
+ } catch (err: unknown) {
42
+ const message =
43
+ err instanceof Error ? err.message : "Failed to submit approval";
44
+ setError(message);
45
+ } finally {
46
+ setIsSubmitting(false);
47
+ }
48
+ },
49
+ [service, options.executionId],
50
+ );
51
+
52
+ const clearError = useCallback(() => setError(null), []);
53
+
54
+ return { submit, isSubmitting, error, clearError };
55
+ }
@@ -0,0 +1,15 @@
1
+ "use client";
2
+
3
+ import { useMemo } from "react";
4
+ import { useStigmerTransport } from "@stigmer/rpc-client";
5
+ import { createExecutionService, type ExecutionService } from "../services/execution-service";
6
+
7
+ /**
8
+ * Returns an {@link ExecutionService} bound to the transport from
9
+ * {@link StigmerTransportProvider}. The service instance is memoized
10
+ * for the lifetime of the transport.
11
+ */
12
+ export function useExecutionService(): ExecutionService {
13
+ const transport = useStigmerTransport();
14
+ return useMemo(() => createExecutionService(transport), [transport]);
15
+ }
@@ -0,0 +1,53 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Components
3
+ // ---------------------------------------------------------------------------
4
+
5
+ export { ExecutionStream } from "./components/ExecutionStream";
6
+ export { ExecutionStatus } from "./components/ExecutionStatus";
7
+ export { OutputBlock } from "./components/OutputBlock";
8
+ export { ToolCallCard } from "./components/ToolCallCard";
9
+ export { ApprovalControls } from "./components/ApprovalControls";
10
+ export { SubAgentCard } from "./components/SubAgentCard";
11
+ export { MessageInput } from "./components/MessageInput";
12
+ export { MessageEntry, HumanMessageBubble, SystemMessageBlock } from "./components/MessageEntry";
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Hooks
16
+ // ---------------------------------------------------------------------------
17
+
18
+ export { useAgentExecution } from "./hooks/useAgentExecution";
19
+ export type { UseAgentExecutionOptions, UseAgentExecutionReturn } from "./hooks/useAgentExecution";
20
+
21
+ export { useApproval } from "./hooks/useApproval";
22
+ export type { UseApprovalOptions, UseApprovalReturn } from "./hooks/useApproval";
23
+
24
+ export { useExecutionService } from "./hooks/useExecutionService";
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // Services (for non-React usage or custom wiring)
28
+ // ---------------------------------------------------------------------------
29
+
30
+ export { createExecutionService } from "./services/execution-service";
31
+ export type { ExecutionService, CreateExecutionInput, ListExecutionsBySessionOptions } from "./services/execution-service";
32
+
33
+ // ---------------------------------------------------------------------------
34
+ // Helpers
35
+ // ---------------------------------------------------------------------------
36
+
37
+ export {
38
+ isTerminalPhase,
39
+ phaseLabel,
40
+ phaseVariant,
41
+ toolCallStatusLabel,
42
+ toolCallStatusVariant,
43
+ isToolCallTerminal,
44
+ subAgentStatusLabel,
45
+ subAgentStatusVariant,
46
+ isHumanMessage,
47
+ isAiMessage,
48
+ isToolMessage,
49
+ isSystemMessage,
50
+ buildSubAgentIndex,
51
+ qualifiedToolName,
52
+ formatDuration,
53
+ } from "./helpers";