@stigmer/react 0.0.36

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 (190) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +248 -0
  3. package/agent/components/AgentCard.d.ts +9 -0
  4. package/agent/components/AgentCard.d.ts.map +1 -0
  5. package/agent/components/AgentCard.js +26 -0
  6. package/agent/components/AgentCard.js.map +1 -0
  7. package/agent/components/AgentOverview.d.ts +7 -0
  8. package/agent/components/AgentOverview.d.ts.map +1 -0
  9. package/agent/components/AgentOverview.js +36 -0
  10. package/agent/components/AgentOverview.js.map +1 -0
  11. package/agent/components/AgentPicker.d.ts +17 -0
  12. package/agent/components/AgentPicker.d.ts.map +1 -0
  13. package/agent/components/AgentPicker.js +86 -0
  14. package/agent/components/AgentPicker.js.map +1 -0
  15. package/agent/hooks/useAgentSearch.d.ts +28 -0
  16. package/agent/hooks/useAgentSearch.d.ts.map +1 -0
  17. package/agent/hooks/useAgentSearch.js +63 -0
  18. package/agent/hooks/useAgentSearch.js.map +1 -0
  19. package/agent/index.d.ts +9 -0
  20. package/agent/index.d.ts.map +1 -0
  21. package/agent/index.js +7 -0
  22. package/agent/index.js.map +1 -0
  23. package/agent-execution/components/ApprovalControls.d.ts +10 -0
  24. package/agent-execution/components/ApprovalControls.d.ts.map +1 -0
  25. package/agent-execution/components/ApprovalControls.js +19 -0
  26. package/agent-execution/components/ApprovalControls.js.map +1 -0
  27. package/agent-execution/components/ExecutionStatus.d.ts +8 -0
  28. package/agent-execution/components/ExecutionStatus.d.ts.map +1 -0
  29. package/agent-execution/components/ExecutionStatus.js +14 -0
  30. package/agent-execution/components/ExecutionStatus.js.map +1 -0
  31. package/agent-execution/components/ExecutionStream.d.ts +16 -0
  32. package/agent-execution/components/ExecutionStream.d.ts.map +1 -0
  33. package/agent-execution/components/ExecutionStream.js +39 -0
  34. package/agent-execution/components/ExecutionStream.js.map +1 -0
  35. package/agent-execution/components/MessageEntry.d.ts +17 -0
  36. package/agent-execution/components/MessageEntry.d.ts.map +1 -0
  37. package/agent-execution/components/MessageEntry.js +36 -0
  38. package/agent-execution/components/MessageEntry.js.map +1 -0
  39. package/agent-execution/components/MessageInput.d.ts +10 -0
  40. package/agent-execution/components/MessageInput.d.ts.map +1 -0
  41. package/agent-execution/components/MessageInput.js +27 -0
  42. package/agent-execution/components/MessageInput.js.map +1 -0
  43. package/agent-execution/components/OutputBlock.d.ts +9 -0
  44. package/agent-execution/components/OutputBlock.d.ts.map +1 -0
  45. package/agent-execution/components/OutputBlock.js +15 -0
  46. package/agent-execution/components/OutputBlock.js.map +1 -0
  47. package/agent-execution/components/SubAgentCard.d.ts +11 -0
  48. package/agent-execution/components/SubAgentCard.d.ts.map +1 -0
  49. package/agent-execution/components/SubAgentCard.js +19 -0
  50. package/agent-execution/components/SubAgentCard.js.map +1 -0
  51. package/agent-execution/components/ToolCallCard.d.ts +11 -0
  52. package/agent-execution/components/ToolCallCard.d.ts.map +1 -0
  53. package/agent-execution/components/ToolCallCard.js +25 -0
  54. package/agent-execution/components/ToolCallCard.js.map +1 -0
  55. package/agent-execution/helpers.d.ts +35 -0
  56. package/agent-execution/helpers.d.ts.map +1 -0
  57. package/agent-execution/helpers.js +157 -0
  58. package/agent-execution/helpers.js.map +1 -0
  59. package/agent-execution/hooks/useAgentExecution.d.ts +21 -0
  60. package/agent-execution/hooks/useAgentExecution.d.ts.map +1 -0
  61. package/agent-execution/hooks/useAgentExecution.js +99 -0
  62. package/agent-execution/hooks/useAgentExecution.js.map +1 -0
  63. package/agent-execution/hooks/useApproval.d.ts +12 -0
  64. package/agent-execution/hooks/useApproval.d.ts.map +1 -0
  65. package/agent-execution/hooks/useApproval.js +32 -0
  66. package/agent-execution/hooks/useApproval.js.map +1 -0
  67. package/agent-execution/index.d.ts +14 -0
  68. package/agent-execution/index.d.ts.map +1 -0
  69. package/agent-execution/index.js +15 -0
  70. package/agent-execution/index.js.map +1 -0
  71. package/catalog/components/ResourceSearchCard.d.ts +23 -0
  72. package/catalog/components/ResourceSearchCard.d.ts.map +1 -0
  73. package/catalog/components/ResourceSearchCard.js +36 -0
  74. package/catalog/components/ResourceSearchCard.js.map +1 -0
  75. package/catalog/index.d.ts +4 -0
  76. package/catalog/index.d.ts.map +1 -0
  77. package/catalog/index.js +5 -0
  78. package/catalog/index.js.map +1 -0
  79. package/catalog/internal/time.d.ts +13 -0
  80. package/catalog/internal/time.d.ts.map +1 -0
  81. package/catalog/internal/time.js +41 -0
  82. package/catalog/internal/time.js.map +1 -0
  83. package/context.d.ts +12 -0
  84. package/context.d.ts.map +1 -0
  85. package/context.js +13 -0
  86. package/context.js.map +1 -0
  87. package/hooks.d.ts +19 -0
  88. package/hooks.d.ts.map +1 -0
  89. package/hooks.js +28 -0
  90. package/hooks.js.map +1 -0
  91. package/index.d.ts +4 -0
  92. package/index.d.ts.map +1 -0
  93. package/index.js +6 -0
  94. package/index.js.map +1 -0
  95. package/internal/badge.d.ts +8 -0
  96. package/internal/badge.d.ts.map +1 -0
  97. package/internal/badge.js +34 -0
  98. package/internal/badge.js.map +1 -0
  99. package/internal/button.d.ts +9 -0
  100. package/internal/button.d.ts.map +1 -0
  101. package/internal/button.js +36 -0
  102. package/internal/button.js.map +1 -0
  103. package/internal/collapsible.d.ts +6 -0
  104. package/internal/collapsible.d.ts.map +1 -0
  105. package/internal/collapsible.js +14 -0
  106. package/internal/collapsible.js.map +1 -0
  107. package/internal/section.d.ts +8 -0
  108. package/internal/section.d.ts.map +1 -0
  109. package/internal/section.js +6 -0
  110. package/internal/section.js.map +1 -0
  111. package/internal/textarea.d.ts +4 -0
  112. package/internal/textarea.d.ts.map +1 -0
  113. package/internal/textarea.js +9 -0
  114. package/internal/textarea.js.map +1 -0
  115. package/mcp-server/hooks/useMcpServerSearch.d.ts +25 -0
  116. package/mcp-server/hooks/useMcpServerSearch.d.ts.map +1 -0
  117. package/mcp-server/hooks/useMcpServerSearch.js +57 -0
  118. package/mcp-server/hooks/useMcpServerSearch.js.map +1 -0
  119. package/mcp-server/index.d.ts +3 -0
  120. package/mcp-server/index.d.ts.map +1 -0
  121. package/mcp-server/index.js +3 -0
  122. package/mcp-server/index.js.map +1 -0
  123. package/package.json +75 -0
  124. package/provider.d.ts +55 -0
  125. package/provider.d.ts.map +1 -0
  126. package/provider.js +34 -0
  127. package/provider.js.map +1 -0
  128. package/session/components/AgentSessionHistory.d.ts +8 -0
  129. package/session/components/AgentSessionHistory.d.ts.map +1 -0
  130. package/session/components/AgentSessionHistory.js +11 -0
  131. package/session/components/AgentSessionHistory.js.map +1 -0
  132. package/session/components/SessionCard.d.ts +8 -0
  133. package/session/components/SessionCard.d.ts.map +1 -0
  134. package/session/components/SessionCard.js +57 -0
  135. package/session/components/SessionCard.js.map +1 -0
  136. package/session/hooks/useAgentSessionList.d.ts +21 -0
  137. package/session/hooks/useAgentSessionList.d.ts.map +1 -0
  138. package/session/hooks/useAgentSessionList.js +90 -0
  139. package/session/hooks/useAgentSessionList.js.map +1 -0
  140. package/session/index.d.ts +7 -0
  141. package/session/index.d.ts.map +1 -0
  142. package/session/index.js +6 -0
  143. package/session/index.js.map +1 -0
  144. package/skill/hooks/useSkillSearch.d.ts +25 -0
  145. package/skill/hooks/useSkillSearch.d.ts.map +1 -0
  146. package/skill/hooks/useSkillSearch.js +57 -0
  147. package/skill/hooks/useSkillSearch.js.map +1 -0
  148. package/skill/index.d.ts +3 -0
  149. package/skill/index.d.ts.map +1 -0
  150. package/skill/index.js +3 -0
  151. package/skill/index.js.map +1 -0
  152. package/src/agent/components/AgentCard.tsx +125 -0
  153. package/src/agent/components/AgentOverview.tsx +209 -0
  154. package/src/agent/components/AgentPicker.tsx +255 -0
  155. package/src/agent/hooks/useAgentSearch.ts +94 -0
  156. package/src/agent/index.ts +17 -0
  157. package/src/agent-execution/components/ApprovalControls.tsx +99 -0
  158. package/src/agent-execution/components/ExecutionStatus.tsx +33 -0
  159. package/src/agent-execution/components/ExecutionStream.tsx +148 -0
  160. package/src/agent-execution/components/MessageEntry.tsx +125 -0
  161. package/src/agent-execution/components/MessageInput.tsx +70 -0
  162. package/src/agent-execution/components/OutputBlock.tsx +43 -0
  163. package/src/agent-execution/components/SubAgentCard.tsx +138 -0
  164. package/src/agent-execution/components/ToolCallCard.tsx +153 -0
  165. package/src/agent-execution/helpers.ts +193 -0
  166. package/src/agent-execution/hooks/useAgentExecution.ts +147 -0
  167. package/src/agent-execution/hooks/useApproval.ts +56 -0
  168. package/src/agent-execution/index.ts +46 -0
  169. package/src/catalog/components/ResourceSearchCard.tsx +137 -0
  170. package/src/catalog/index.ts +6 -0
  171. package/src/catalog/internal/time.ts +40 -0
  172. package/src/context.ts +15 -0
  173. package/src/hooks.ts +32 -0
  174. package/src/index.ts +6 -0
  175. package/src/internal/badge.tsx +52 -0
  176. package/src/internal/button.tsx +60 -0
  177. package/src/internal/collapsible.tsx +21 -0
  178. package/src/internal/section.tsx +18 -0
  179. package/src/internal/textarea.tsx +23 -0
  180. package/src/mcp-server/hooks/useMcpServerSearch.ts +79 -0
  181. package/src/mcp-server/index.ts +6 -0
  182. package/src/provider.tsx +73 -0
  183. package/src/session/components/AgentSessionHistory.tsx +109 -0
  184. package/src/session/components/SessionCard.tsx +113 -0
  185. package/src/session/hooks/useAgentSessionList.ts +117 -0
  186. package/src/session/index.ts +13 -0
  187. package/src/skill/hooks/useSkillSearch.ts +79 -0
  188. package/src/skill/index.ts +6 -0
  189. package/src/styles.css +72 -0
  190. package/styles.css +2 -0
@@ -0,0 +1,209 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import type { Agent } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
5
+ import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
6
+ import {
7
+ Bot,
8
+ Globe,
9
+ Users,
10
+ Server,
11
+ FileCode2,
12
+ Wrench,
13
+ ChevronDown,
14
+ } from "lucide-react";
15
+
16
+ import { cn } from "@stigmer/theme";
17
+
18
+ import { Badge } from "../../internal/badge";
19
+ import {
20
+ Collapsible,
21
+ CollapsibleTrigger,
22
+ CollapsibleContent,
23
+ } from "../../internal/collapsible";
24
+ import { Section } from "../../internal/section";
25
+
26
+ const INSTRUCTIONS_COLLAPSE_THRESHOLD = 300;
27
+
28
+ export interface AgentOverviewProps {
29
+ agent: Agent;
30
+ className?: string;
31
+ }
32
+
33
+ export function AgentOverview({ agent, className }: AgentOverviewProps) {
34
+ const meta = agent.metadata;
35
+ const spec = agent.spec;
36
+ const isPublic = meta?.visibility === ApiResourceVisibility.visibility_public;
37
+ const qualifiedSlug = meta?.org ? `${meta.org}/${meta.slug}` : meta?.slug;
38
+ const hasLongInstructions =
39
+ (spec?.instructions?.length ?? 0) > INSTRUCTIONS_COLLAPSE_THRESHOLD;
40
+
41
+ return (
42
+ <div className={cn("space-y-8", className)}>
43
+ <div className="space-y-2">
44
+ <div className="flex items-center gap-3">
45
+ {spec?.iconUrl ? (
46
+ <img
47
+ src={spec.iconUrl}
48
+ alt=""
49
+ className="size-10 rounded-lg object-cover"
50
+ />
51
+ ) : (
52
+ <div className="bg-muted flex size-10 items-center justify-center rounded-lg">
53
+ <Bot className="text-muted-foreground size-5" />
54
+ </div>
55
+ )}
56
+ <div>
57
+ <h2 className="text-xl font-semibold">{meta?.name}</h2>
58
+ <p className="text-muted-foreground font-mono text-xs">
59
+ {qualifiedSlug}
60
+ </p>
61
+ </div>
62
+ </div>
63
+ {spec?.description && (
64
+ <p className="text-muted-foreground max-w-prose text-sm">
65
+ {spec.description}
66
+ </p>
67
+ )}
68
+ <div className="flex items-center gap-2">
69
+ {isPublic ? (
70
+ <Badge variant="outline" className="gap-1">
71
+ <Globe className="size-3" />
72
+ Public
73
+ </Badge>
74
+ ) : (
75
+ <Badge variant="secondary" className="gap-1">
76
+ <Users className="size-3" />
77
+ Private
78
+ </Badge>
79
+ )}
80
+ {meta?.tags?.map((tag: string) => (
81
+ <Badge key={tag} variant="secondary">
82
+ {tag}
83
+ </Badge>
84
+ ))}
85
+ </div>
86
+ </div>
87
+
88
+ {spec?.instructions && (
89
+ <Section title="Instructions">
90
+ {hasLongInstructions ? (
91
+ <CollapsibleInstructions instructions={spec.instructions} />
92
+ ) : (
93
+ <pre className="bg-muted rounded-lg p-4 font-mono text-xs leading-relaxed whitespace-pre-wrap">
94
+ {spec.instructions}
95
+ </pre>
96
+ )}
97
+ </Section>
98
+ )}
99
+
100
+ {spec?.mcpServerUsages && spec.mcpServerUsages.length > 0 && (
101
+ <Section title="MCP Servers">
102
+ <div className="space-y-3">
103
+ {spec.mcpServerUsages.map((usage, i) => {
104
+ const ref = usage.mcpServerRef;
105
+ const slug = ref
106
+ ? ref.org
107
+ ? `${ref.org}/${ref.slug}`
108
+ : ref.slug
109
+ : `server-${i}`;
110
+ return (
111
+ <div key={slug} className="rounded-lg border p-3 text-sm">
112
+ <div className="flex items-center gap-2">
113
+ <Server className="text-muted-foreground size-4 shrink-0" />
114
+ <span className="font-mono font-medium">{slug}</span>
115
+ </div>
116
+ {usage.enabledTools.length > 0 && (
117
+ <div className="mt-2 flex flex-wrap gap-1">
118
+ {usage.enabledTools.map((tool) => (
119
+ <Badge
120
+ key={tool}
121
+ variant="secondary"
122
+ className="text-[10px]"
123
+ >
124
+ <Wrench className="mr-0.5 size-2.5" />
125
+ {tool}
126
+ </Badge>
127
+ ))}
128
+ </div>
129
+ )}
130
+ {usage.toolApprovalOverrides.length > 0 && (
131
+ <div className="text-muted-foreground mt-2 text-xs">
132
+ {usage.toolApprovalOverrides.length} approval override
133
+ {usage.toolApprovalOverrides.length !== 1 ? "s" : ""}
134
+ </div>
135
+ )}
136
+ </div>
137
+ );
138
+ })}
139
+ </div>
140
+ </Section>
141
+ )}
142
+
143
+ {spec?.skillRefs && spec.skillRefs.length > 0 && (
144
+ <Section title="Skills">
145
+ <div className="space-y-2">
146
+ {spec.skillRefs.map((ref, i) => {
147
+ const slug = ref.org ? `${ref.org}/${ref.slug}` : ref.slug;
148
+ return (
149
+ <div
150
+ key={slug || i}
151
+ className="flex items-center gap-2 rounded-lg border p-3 text-sm"
152
+ >
153
+ <FileCode2 className="text-muted-foreground size-4 shrink-0" />
154
+ <span className="font-mono font-medium">{slug}</span>
155
+ </div>
156
+ );
157
+ })}
158
+ </div>
159
+ </Section>
160
+ )}
161
+
162
+ {spec?.subAgents && spec.subAgents.length > 0 && (
163
+ <Section title="Sub-Agents">
164
+ <div className="space-y-3">
165
+ {spec.subAgents.map((sub) => (
166
+ <div key={sub.name} className="rounded-lg border p-3 text-sm">
167
+ <div className="flex items-center gap-2">
168
+ <Bot className="text-muted-foreground size-4 shrink-0" />
169
+ <span className="font-medium">{sub.name}</span>
170
+ {sub.modelOverride && (
171
+ <Badge variant="outline" className="text-[10px]">
172
+ {sub.modelOverride}
173
+ </Badge>
174
+ )}
175
+ </div>
176
+ {sub.description && (
177
+ <p className="text-muted-foreground mt-1 text-xs">
178
+ {sub.description}
179
+ </p>
180
+ )}
181
+ </div>
182
+ ))}
183
+ </div>
184
+ </Section>
185
+ )}
186
+ </div>
187
+ );
188
+ }
189
+
190
+ function CollapsibleInstructions({ instructions }: { instructions: string }) {
191
+ const [open, setOpen] = useState(false);
192
+
193
+ return (
194
+ <Collapsible open={open} onOpenChange={setOpen}>
195
+ <pre className="bg-muted rounded-lg p-4 font-mono text-xs leading-relaxed whitespace-pre-wrap">
196
+ {open
197
+ ? instructions
198
+ : instructions.slice(0, INSTRUCTIONS_COLLAPSE_THRESHOLD) + "..."}
199
+ </pre>
200
+ <CollapsibleContent />
201
+ <CollapsibleTrigger className="text-muted-foreground hover:text-foreground mt-2 flex items-center gap-1 text-xs">
202
+ <ChevronDown
203
+ className={cn("size-3.5 transition-transform", open && "rotate-180")}
204
+ />
205
+ {open ? "Show less" : "Show full instructions"}
206
+ </CollapsibleTrigger>
207
+ </Collapsible>
208
+ );
209
+ }
@@ -0,0 +1,255 @@
1
+ "use client";
2
+
3
+ import {
4
+ useCallback,
5
+ useId,
6
+ useRef,
7
+ useState,
8
+ type KeyboardEvent,
9
+ } from "react";
10
+ import { Bot, Search, X, Loader2 } from "lucide-react";
11
+ import { cn } from "@stigmer/theme";
12
+ import { useAgentSearch, type AgentSearchResult } from "../hooks/useAgentSearch";
13
+
14
+ export interface SelectedAgent {
15
+ id: string;
16
+ name: string;
17
+ qualifiedSlug: string;
18
+ org: string;
19
+ description: string;
20
+ }
21
+
22
+ export interface AgentPickerProps {
23
+ org: string;
24
+ onSelect: (agent: SelectedAgent) => void;
25
+ onClear: () => void;
26
+ selected: SelectedAgent | null;
27
+ disabled?: boolean;
28
+ className?: string;
29
+ }
30
+
31
+ export function AgentPicker({
32
+ org,
33
+ onSelect,
34
+ onClear,
35
+ selected,
36
+ disabled = false,
37
+ className,
38
+ }: AgentPickerProps) {
39
+ const instanceId = useId();
40
+ const listboxId = `${instanceId}-listbox`;
41
+ const inputRef = useRef<HTMLInputElement>(null);
42
+
43
+ const { query, setQuery, results, isLoading, error } = useAgentSearch({ org });
44
+ const [activeIndex, setActiveIndex] = useState(-1);
45
+ const [isOpen, setIsOpen] = useState(false);
46
+
47
+ const showResults = isOpen && !selected;
48
+
49
+ const handleSelect = useCallback(
50
+ (index: number) => {
51
+ const result = results[index];
52
+ if (!result) return;
53
+ onSelect({
54
+ id: result.id,
55
+ name: result.name,
56
+ qualifiedSlug: result.qualifiedSlug,
57
+ org: result.org,
58
+ description: result.description,
59
+ });
60
+ setIsOpen(false);
61
+ setActiveIndex(-1);
62
+ },
63
+ [results, onSelect],
64
+ );
65
+
66
+ const handleClear = useCallback(() => {
67
+ onClear();
68
+ setQuery("");
69
+ setActiveIndex(-1);
70
+ setIsOpen(true);
71
+ requestAnimationFrame(() => inputRef.current?.focus());
72
+ }, [onClear, setQuery]);
73
+
74
+ const handleKeyDown = useCallback(
75
+ (e: KeyboardEvent<HTMLInputElement>) => {
76
+ if (!showResults) return;
77
+
78
+ switch (e.key) {
79
+ case "ArrowDown": {
80
+ e.preventDefault();
81
+ setActiveIndex((prev) => (prev < results.length - 1 ? prev + 1 : 0));
82
+ break;
83
+ }
84
+ case "ArrowUp": {
85
+ e.preventDefault();
86
+ setActiveIndex((prev) => (prev > 0 ? prev - 1 : results.length - 1));
87
+ break;
88
+ }
89
+ case "Enter": {
90
+ e.preventDefault();
91
+ if (activeIndex >= 0 && activeIndex < results.length) {
92
+ handleSelect(activeIndex);
93
+ }
94
+ break;
95
+ }
96
+ case "Escape": {
97
+ e.preventDefault();
98
+ setIsOpen(false);
99
+ setActiveIndex(-1);
100
+ break;
101
+ }
102
+ }
103
+ },
104
+ [showResults, results.length, activeIndex, handleSelect],
105
+ );
106
+
107
+ if (selected) {
108
+ return (
109
+ <div className={cn("stgm-agent-picker space-y-1", className)}>
110
+ <label className="text-muted-foreground text-sm font-medium">
111
+ Agent
112
+ </label>
113
+ <div className="bg-muted/50 flex items-center gap-3 rounded-lg border px-3 py-2.5">
114
+ <div className="bg-primary/10 flex size-8 shrink-0 items-center justify-center rounded-md">
115
+ <Bot className="text-primary size-4" />
116
+ </div>
117
+ <div className="min-w-0 flex-1">
118
+ <p className="truncate text-sm font-medium">{selected.name}</p>
119
+ <p className="text-muted-foreground truncate font-mono text-xs">
120
+ {selected.qualifiedSlug}
121
+ </p>
122
+ </div>
123
+ <button
124
+ type="button"
125
+ onClick={handleClear}
126
+ disabled={disabled}
127
+ aria-label="Change agent"
128
+ className={cn(
129
+ "inline-flex size-7 shrink-0 items-center justify-center rounded-md",
130
+ "text-muted-foreground hover:bg-accent hover:text-accent-foreground",
131
+ "transition-colors focus-visible:ring-ring focus-visible:ring-2 focus-visible:outline-none",
132
+ "disabled:pointer-events-none disabled:opacity-50",
133
+ )}
134
+ >
135
+ <X className="size-3.5" />
136
+ </button>
137
+ </div>
138
+ </div>
139
+ );
140
+ }
141
+
142
+ const activeOptionId =
143
+ activeIndex >= 0 ? `${instanceId}-option-${activeIndex}` : undefined;
144
+
145
+ return (
146
+ <div className={cn("stgm-agent-picker space-y-1", className)}>
147
+ <label
148
+ htmlFor={`${instanceId}-input`}
149
+ className="text-muted-foreground text-sm font-medium"
150
+ >
151
+ Agent
152
+ </label>
153
+
154
+ <div className="relative">
155
+ <div className="relative">
156
+ <Search className="text-muted-foreground pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2" />
157
+ <input
158
+ ref={inputRef}
159
+ id={`${instanceId}-input`}
160
+ type="text"
161
+ role="combobox"
162
+ aria-expanded={showResults}
163
+ aria-controls={listboxId}
164
+ aria-activedescendant={activeOptionId}
165
+ aria-autocomplete="list"
166
+ aria-label="Search agents"
167
+ value={query}
168
+ disabled={disabled}
169
+ placeholder="Search agents..."
170
+ className={cn(
171
+ "bg-background w-full rounded-lg border py-2.5 pr-9 pl-9 text-sm",
172
+ "placeholder:text-muted-foreground",
173
+ "focus-visible:ring-ring focus-visible:ring-2 focus-visible:outline-none",
174
+ "disabled:cursor-not-allowed disabled:opacity-50",
175
+ )}
176
+ onChange={(e) => {
177
+ setQuery(e.target.value);
178
+ setActiveIndex(-1);
179
+ if (!isOpen) setIsOpen(true);
180
+ }}
181
+ onFocus={() => setIsOpen(true)}
182
+ onBlur={() => {
183
+ setTimeout(() => setIsOpen(false), 150);
184
+ }}
185
+ onKeyDown={handleKeyDown}
186
+ />
187
+ {isLoading && (
188
+ <Loader2 className="text-muted-foreground absolute top-1/2 right-3 size-4 -translate-y-1/2 animate-spin" />
189
+ )}
190
+ </div>
191
+
192
+ {showResults && (
193
+ <ul
194
+ id={listboxId}
195
+ role="listbox"
196
+ aria-label="Agent search results"
197
+ className={cn(
198
+ "bg-popover absolute z-popover mt-1 max-h-64 w-full overflow-y-auto rounded-lg border shadow-md",
199
+ results.length === 0 && !isLoading && "p-3",
200
+ )}
201
+ >
202
+ {results.length === 0 && !isLoading && (
203
+ <li
204
+ className="text-muted-foreground text-center text-sm"
205
+ role="presentation"
206
+ >
207
+ {error
208
+ ? error
209
+ : query
210
+ ? "No agents found"
211
+ : "No agents available"}
212
+ </li>
213
+ )}
214
+
215
+ {results.map((result, index) => (
216
+ <li
217
+ key={result.id}
218
+ id={`${instanceId}-option-${index}`}
219
+ role="option"
220
+ aria-selected={index === activeIndex}
221
+ className={cn(
222
+ "flex cursor-pointer items-start gap-3 px-3 py-2.5",
223
+ "transition-colors",
224
+ index === activeIndex
225
+ ? "bg-accent text-accent-foreground"
226
+ : "hover:bg-accent/50",
227
+ )}
228
+ onMouseDown={(e) => {
229
+ e.preventDefault();
230
+ }}
231
+ onClick={() => handleSelect(index)}
232
+ onMouseEnter={() => setActiveIndex(index)}
233
+ >
234
+ <div className="bg-primary/10 mt-0.5 flex size-7 shrink-0 items-center justify-center rounded-md">
235
+ <Bot className="text-primary size-3.5" />
236
+ </div>
237
+ <div className="min-w-0 flex-1">
238
+ <p className="truncate text-sm font-medium">{result.name}</p>
239
+ <p className="text-muted-foreground truncate font-mono text-xs">
240
+ {result.qualifiedSlug}
241
+ </p>
242
+ {result.description && (
243
+ <p className="text-muted-foreground/70 mt-0.5 line-clamp-1 text-xs">
244
+ {result.description}
245
+ </p>
246
+ )}
247
+ </div>
248
+ </li>
249
+ ))}
250
+ </ul>
251
+ )}
252
+ </div>
253
+ </div>
254
+ );
255
+ }
@@ -0,0 +1,94 @@
1
+ "use client";
2
+
3
+ import { useCallback, useEffect, useRef, useState } from "react";
4
+ import { useStigmer } from "../../hooks";
5
+ import type { SearchResult } from "@stigmer/protos/ai/stigmer/search/v1/io_pb";
6
+
7
+ const DEBOUNCE_MS = 300;
8
+ const DEFAULT_PAGE_SIZE = 20;
9
+
10
+ export interface AgentSearchResult {
11
+ id: string;
12
+ name: string;
13
+ qualifiedSlug: string;
14
+ org: string;
15
+ description: string;
16
+ }
17
+
18
+ export interface UseAgentSearchOptions {
19
+ org: string;
20
+ debounceMs?: number;
21
+ pageSize?: number;
22
+ }
23
+
24
+ export interface UseAgentSearchReturn {
25
+ query: string;
26
+ setQuery: (query: string) => void;
27
+ results: AgentSearchResult[];
28
+ isLoading: boolean;
29
+ error: string | null;
30
+ }
31
+
32
+ function toAgentSearchResult(r: SearchResult): AgentSearchResult {
33
+ return {
34
+ id: r.id,
35
+ name: r.name,
36
+ qualifiedSlug: r.qualifiedSlug,
37
+ org: r.org,
38
+ description: r.description,
39
+ };
40
+ }
41
+
42
+ /**
43
+ * Debounced agent search scoped to a specific organization.
44
+ *
45
+ * On mount fetches all accessible agents (empty query). Typing fires at most
46
+ * one request per debounce interval. Stale responses from out-of-order
47
+ * requests are discarded.
48
+ */
49
+ export function useAgentSearch(options: UseAgentSearchOptions): UseAgentSearchReturn {
50
+ const { org, debounceMs = DEBOUNCE_MS, pageSize = DEFAULT_PAGE_SIZE } = options;
51
+ const stigmer = useStigmer();
52
+
53
+ const [query, setQuery] = useState("");
54
+ const [results, setResults] = useState<AgentSearchResult[]>([]);
55
+ const [isLoading, setIsLoading] = useState(false);
56
+ const [error, setError] = useState<string | null>(null);
57
+
58
+ const latestRequestId = useRef(0);
59
+ const debouncedQuery = useDebouncedValue(query, debounceMs);
60
+
61
+ useEffect(() => {
62
+ const requestId = ++latestRequestId.current;
63
+ setIsLoading(true);
64
+ setError(null);
65
+
66
+ stigmer.agent
67
+ .list({ query: debouncedQuery, org, page: { num: 1, size: pageSize } })
68
+ .then((response) => {
69
+ if (requestId !== latestRequestId.current) return;
70
+ setResults(response.entries.map(toAgentSearchResult));
71
+ })
72
+ .catch((err: unknown) => {
73
+ if (requestId !== latestRequestId.current) return;
74
+ setError(err instanceof Error ? err.message : "Search failed");
75
+ })
76
+ .finally(() => {
77
+ if (requestId !== latestRequestId.current) return;
78
+ setIsLoading(false);
79
+ });
80
+ }, [debouncedQuery, org, pageSize, stigmer]);
81
+
82
+ return { query, setQuery, results, isLoading, error };
83
+ }
84
+
85
+ function useDebouncedValue<T>(value: T, delayMs: number): T {
86
+ const [debounced, setDebounced] = useState(value);
87
+
88
+ useEffect(() => {
89
+ const timer = setTimeout(() => setDebounced(value), delayMs);
90
+ return () => clearTimeout(timer);
91
+ }, [value, delayMs]);
92
+
93
+ return debounced;
94
+ }
@@ -0,0 +1,17 @@
1
+ // Hooks
2
+ export { useAgentSearch } from "./hooks/useAgentSearch";
3
+ export type {
4
+ UseAgentSearchOptions,
5
+ UseAgentSearchReturn,
6
+ AgentSearchResult,
7
+ } from "./hooks/useAgentSearch";
8
+
9
+ // Components
10
+ export { AgentCard } from "./components/AgentCard";
11
+ export type { AgentCardProps } from "./components/AgentCard";
12
+
13
+ export { AgentOverview } from "./components/AgentOverview";
14
+ export type { AgentOverviewProps } from "./components/AgentOverview";
15
+
16
+ export { AgentPicker } from "./components/AgentPicker";
17
+ export type { AgentPickerProps, SelectedAgent } from "./components/AgentPicker";
@@ -0,0 +1,99 @@
1
+ "use client";
2
+
3
+ import { useCallback, useState } from "react";
4
+ import { Button } from "../../internal/button";
5
+ import { Textarea } from "../../internal/textarea";
6
+ import { ApprovalAction } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
7
+ import { cn } from "@stigmer/theme";
8
+ import { Check, X, SkipForward, Loader2 } from "lucide-react";
9
+
10
+ interface ApprovalControlsProps {
11
+ approvalMessage: string;
12
+ onSubmit: (action: ApprovalAction, comment?: string) => Promise<void>;
13
+ isSubmitting: boolean;
14
+ className?: string;
15
+ }
16
+
17
+ export function ApprovalControls({
18
+ approvalMessage,
19
+ onSubmit,
20
+ isSubmitting,
21
+ className,
22
+ }: ApprovalControlsProps) {
23
+ const [showComment, setShowComment] = useState(false);
24
+ const [comment, setComment] = useState("");
25
+
26
+ const handleAction = useCallback(
27
+ async (action: ApprovalAction) => {
28
+ await onSubmit(action, comment || undefined);
29
+ setComment("");
30
+ setShowComment(false);
31
+ },
32
+ [onSubmit, comment],
33
+ );
34
+
35
+ return (
36
+ <div
37
+ className={cn(
38
+ "border-primary/30 bg-primary/5 space-y-3 rounded-lg border-2 p-3",
39
+ className,
40
+ )}
41
+ >
42
+ <p className="text-sm font-medium">{approvalMessage}</p>
43
+
44
+ {showComment && (
45
+ <Textarea
46
+ value={comment}
47
+ onChange={(e) => setComment(e.target.value)}
48
+ placeholder="Optional comment..."
49
+ className="min-h-10 text-sm"
50
+ disabled={isSubmitting}
51
+ />
52
+ )}
53
+
54
+ <div className="flex items-center gap-2">
55
+ <Button
56
+ size="sm"
57
+ onClick={() => handleAction(ApprovalAction.APPROVE)}
58
+ disabled={isSubmitting}
59
+ >
60
+ {isSubmitting ? (
61
+ <Loader2 className="size-3.5 animate-spin" />
62
+ ) : (
63
+ <Check className="size-3.5" />
64
+ )}
65
+ Approve
66
+ </Button>
67
+ <Button
68
+ size="sm"
69
+ variant="secondary"
70
+ onClick={() => handleAction(ApprovalAction.SKIP)}
71
+ disabled={isSubmitting}
72
+ >
73
+ <SkipForward className="size-3.5" />
74
+ Skip
75
+ </Button>
76
+ <Button
77
+ size="sm"
78
+ variant="destructive"
79
+ onClick={() => handleAction(ApprovalAction.REJECT)}
80
+ disabled={isSubmitting}
81
+ >
82
+ <X className="size-3.5" />
83
+ Reject
84
+ </Button>
85
+ {!showComment && (
86
+ <Button
87
+ size="sm"
88
+ variant="ghost"
89
+ onClick={() => setShowComment(true)}
90
+ disabled={isSubmitting}
91
+ className="text-muted-foreground ml-auto text-xs"
92
+ >
93
+ Add comment
94
+ </Button>
95
+ )}
96
+ </div>
97
+ </div>
98
+ );
99
+ }