@stigmer/react 0.0.92 → 0.0.94

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 (328) hide show
  1. package/README.md +57 -12
  2. package/agent/AgentDetailView.js +7 -7
  3. package/agent/AgentDetailView.js.map +1 -1
  4. package/agent/AgentPicker.js +3 -3
  5. package/agent/AgentPicker.js.map +1 -1
  6. package/api-key/ApiKeyCreatedAlert.js +1 -1
  7. package/api-key/ApiKeyCreatedAlert.js.map +1 -1
  8. package/api-key/ApiKeyListPanel.js +4 -4
  9. package/api-key/ApiKeyListPanel.js.map +1 -1
  10. package/api-key/CreateApiKeyForm.js +1 -1
  11. package/api-key/CreateApiKeyForm.js.map +1 -1
  12. package/attachment/AttachmentChipList.js +2 -2
  13. package/attachment/AttachmentChipList.js.map +1 -1
  14. package/color-mode.d.ts +48 -0
  15. package/color-mode.d.ts.map +1 -0
  16. package/color-mode.js +53 -0
  17. package/color-mode.js.map +1 -0
  18. package/composer/ComposerToolbar.d.ts +6 -2
  19. package/composer/ComposerToolbar.d.ts.map +1 -1
  20. package/composer/ComposerToolbar.js +5 -3
  21. package/composer/ComposerToolbar.js.map +1 -1
  22. package/composer/ConfigureMenu.js +3 -3
  23. package/composer/ConfigureMenu.js.map +1 -1
  24. package/composer/ContextChip.js +1 -1
  25. package/composer/ContextChip.js.map +1 -1
  26. package/composer/ContextPopover.js +1 -1
  27. package/composer/ContextPopover.js.map +1 -1
  28. package/composer/SessionComposer.d.ts +24 -3
  29. package/composer/SessionComposer.d.ts.map +1 -1
  30. package/composer/SessionComposer.js +5 -4
  31. package/composer/SessionComposer.js.map +1 -1
  32. package/environment/CreateEnvironmentForm.js +1 -1
  33. package/environment/CreateEnvironmentForm.js.map +1 -1
  34. package/environment/EnvVarForm.js +2 -2
  35. package/environment/EnvVarForm.js.map +1 -1
  36. package/environment/EnvironmentListPanel.js +2 -2
  37. package/environment/EnvironmentListPanel.js.map +1 -1
  38. package/environment/EnvironmentVariableEditor.js +6 -6
  39. package/environment/EnvironmentVariableEditor.js.map +1 -1
  40. package/error/ErrorMessage.js +1 -1
  41. package/error/ErrorMessage.js.map +1 -1
  42. package/execution/ApprovalCard.js +4 -4
  43. package/execution/ApprovalCard.js.map +1 -1
  44. package/execution/ArtifactCard.js +1 -1
  45. package/execution/ArtifactCard.js.map +1 -1
  46. package/execution/ArtifactPreviewModal.js +4 -4
  47. package/execution/ArtifactPreviewModal.js.map +1 -1
  48. package/execution/FollowUpInput.js +1 -1
  49. package/execution/FollowUpInput.js.map +1 -1
  50. package/execution/McpToolDetail.js +4 -4
  51. package/execution/McpToolDetail.js.map +1 -1
  52. package/execution/MessageEntry.js +1 -1
  53. package/execution/MessageEntry.js.map +1 -1
  54. package/execution/MessageThread.js +1 -1
  55. package/execution/MessageThread.js.map +1 -1
  56. package/execution/SessionVariablesInput.js +4 -4
  57. package/execution/SessionVariablesInput.js.map +1 -1
  58. package/execution/SubAgentSection.js +2 -2
  59. package/execution/SubAgentSection.js.map +1 -1
  60. package/execution/ToolCallDetail.js +2 -2
  61. package/execution/ToolCallDetail.js.map +1 -1
  62. package/execution/ToolCallGroup.js +1 -1
  63. package/execution/ToolCallGroup.js.map +1 -1
  64. package/execution/ToolCallItem.js +2 -2
  65. package/execution/ToolCallItem.js.map +1 -1
  66. package/execution/WriteBackCard.js +3 -3
  67. package/execution/WriteBackCard.js.map +1 -1
  68. package/execution/tool-rendering-primitives.js +3 -3
  69. package/execution/tool-rendering-primitives.js.map +1 -1
  70. package/github/GitHubRepoPicker.js +5 -5
  71. package/github/GitHubRepoPicker.js.map +1 -1
  72. package/iam-policy/GrantAccessForm.js +1 -1
  73. package/iam-policy/GrantAccessForm.js.map +1 -1
  74. package/iam-policy/OrgMembersPanel.js +5 -5
  75. package/iam-policy/OrgMembersPanel.js.map +1 -1
  76. package/identity-provider/CreateIdentityProviderForm.js +3 -3
  77. package/identity-provider/CreateIdentityProviderForm.js.map +1 -1
  78. package/identity-provider/IdentityProviderDetailPanel.js +5 -5
  79. package/identity-provider/IdentityProviderDetailPanel.js.map +1 -1
  80. package/identity-provider/IdentityProviderListPanel.js +3 -3
  81. package/identity-provider/IdentityProviderListPanel.js.map +1 -1
  82. package/identity-provider/IdentityProviderWizard.js +7 -7
  83. package/identity-provider/IdentityProviderWizard.js.map +1 -1
  84. package/identity-provider/ProviderPicker.js +2 -2
  85. package/identity-provider/ProviderPicker.js.map +1 -1
  86. package/index.d.ts +8 -4
  87. package/index.d.ts.map +1 -1
  88. package/index.js +7 -3
  89. package/index.js.map +1 -1
  90. package/internal/CloudFeatureNotice.js +1 -1
  91. package/internal/CloudFeatureNotice.js.map +1 -1
  92. package/internal/Tabs.js +1 -1
  93. package/internal/Tabs.js.map +1 -1
  94. package/internal/markdown-components.js +2 -2
  95. package/internal/markdown-components.js.map +1 -1
  96. package/invitation/InvitationCreatedAlert.js +1 -1
  97. package/invitation/InvitationCreatedAlert.js.map +1 -1
  98. package/invitation/InvitationManager.js +5 -5
  99. package/invitation/InvitationManager.js.map +1 -1
  100. package/invitation/InvitationRedemption.js +4 -4
  101. package/invitation/InvitationRedemption.js.map +1 -1
  102. package/library/ResourceCountCard.js +1 -1
  103. package/library/ResourceCountCard.js.map +1 -1
  104. package/library/ResourceListView.js +5 -5
  105. package/library/ResourceListView.js.map +1 -1
  106. package/mcp-server/McpServerConfigPanel.js +5 -5
  107. package/mcp-server/McpServerConfigPanel.js.map +1 -1
  108. package/mcp-server/McpServerConnectDialog.js +4 -4
  109. package/mcp-server/McpServerConnectDialog.js.map +1 -1
  110. package/mcp-server/McpServerDetailView.js +14 -14
  111. package/mcp-server/McpServerDetailView.js.map +1 -1
  112. package/mcp-server/McpServerPicker.js +4 -4
  113. package/mcp-server/McpServerPicker.js.map +1 -1
  114. package/mcp-server/McpToolSelector.js +3 -3
  115. package/mcp-server/McpToolSelector.js.map +1 -1
  116. package/mcp-server/OAuthAppForm.js +1 -1
  117. package/mcp-server/OAuthAppForm.js.map +1 -1
  118. package/models/ModelSelector.js +1 -1
  119. package/models/ModelSelector.js.map +1 -1
  120. package/oauth-app/CreateOAuthAppForm.js +1 -1
  121. package/oauth-app/CreateOAuthAppForm.js.map +1 -1
  122. package/oauth-app/OAuthAppDetailPanel.js +3 -3
  123. package/oauth-app/OAuthAppDetailPanel.js.map +1 -1
  124. package/oauth-app/OAuthAppListPanel.js +2 -2
  125. package/oauth-app/OAuthAppListPanel.js.map +1 -1
  126. package/organization/CreateOrganizationForm.js +1 -1
  127. package/organization/CreateOrganizationForm.js.map +1 -1
  128. package/organization/OrgProfilePanel.js +3 -3
  129. package/organization/OrgProfilePanel.js.map +1 -1
  130. package/package.json +4 -4
  131. package/platform-client/CreatePlatformClientForm.js +2 -2
  132. package/platform-client/CreatePlatformClientForm.js.map +1 -1
  133. package/platform-client/PlatformClientDetailPanel.js +4 -4
  134. package/platform-client/PlatformClientDetailPanel.js.map +1 -1
  135. package/platform-client/PlatformClientListPanel.js +3 -3
  136. package/platform-client/PlatformClientListPanel.js.map +1 -1
  137. package/platform-client/PlatformClientSecretAlert.js +2 -2
  138. package/platform-client/PlatformClientSecretAlert.js.map +1 -1
  139. package/provider.d.ts +39 -2
  140. package/provider.d.ts.map +1 -1
  141. package/provider.js +11 -3
  142. package/provider.js.map +1 -1
  143. package/runner/RunnerListPanel.d.ts +65 -0
  144. package/runner/RunnerListPanel.d.ts.map +1 -0
  145. package/runner/RunnerListPanel.js +237 -0
  146. package/runner/RunnerListPanel.js.map +1 -0
  147. package/runner/RunnerPicker.d.ts +54 -0
  148. package/runner/RunnerPicker.d.ts.map +1 -0
  149. package/runner/RunnerPicker.js +133 -0
  150. package/runner/RunnerPicker.js.map +1 -0
  151. package/runner/__tests__/useDeleteRunner.test.d.ts +2 -0
  152. package/runner/__tests__/useDeleteRunner.test.d.ts.map +1 -0
  153. package/runner/__tests__/useDeleteRunner.test.js +108 -0
  154. package/runner/__tests__/useDeleteRunner.test.js.map +1 -0
  155. package/runner/__tests__/useLaunchLocalRunner.test.d.ts +2 -0
  156. package/runner/__tests__/useLaunchLocalRunner.test.d.ts.map +1 -0
  157. package/runner/__tests__/useLaunchLocalRunner.test.js +143 -0
  158. package/runner/__tests__/useLaunchLocalRunner.test.js.map +1 -0
  159. package/runner/__tests__/useStopRunner.test.d.ts +2 -0
  160. package/runner/__tests__/useStopRunner.test.d.ts.map +1 -0
  161. package/runner/__tests__/useStopRunner.test.js +114 -0
  162. package/runner/__tests__/useStopRunner.test.js.map +1 -0
  163. package/runner/index.d.ts +14 -0
  164. package/runner/index.d.ts.map +1 -0
  165. package/runner/index.js +8 -0
  166. package/runner/index.js.map +1 -0
  167. package/runner/phase.d.ts +30 -0
  168. package/runner/phase.d.ts.map +1 -0
  169. package/runner/phase.js +58 -0
  170. package/runner/phase.js.map +1 -0
  171. package/runner/useDeleteRunner.d.ts +36 -0
  172. package/runner/useDeleteRunner.d.ts.map +1 -0
  173. package/runner/useDeleteRunner.js +42 -0
  174. package/runner/useDeleteRunner.js.map +1 -0
  175. package/runner/useLaunchLocalRunner.d.ts +84 -0
  176. package/runner/useLaunchLocalRunner.d.ts.map +1 -0
  177. package/runner/useLaunchLocalRunner.js +75 -0
  178. package/runner/useLaunchLocalRunner.js.map +1 -0
  179. package/runner/useRunnerList.d.ts +49 -0
  180. package/runner/useRunnerList.d.ts.map +1 -0
  181. package/runner/useRunnerList.js +70 -0
  182. package/runner/useRunnerList.js.map +1 -0
  183. package/runner/useStopRunner.d.ts +53 -0
  184. package/runner/useStopRunner.d.ts.map +1 -0
  185. package/runner/useStopRunner.js +50 -0
  186. package/runner/useStopRunner.js.map +1 -0
  187. package/session/draft.d.ts +53 -0
  188. package/session/draft.d.ts.map +1 -0
  189. package/session/draft.js +45 -0
  190. package/session/draft.js.map +1 -0
  191. package/session/index.d.ts +10 -0
  192. package/session/index.d.ts.map +1 -1
  193. package/session/index.js +5 -0
  194. package/session/index.js.map +1 -1
  195. package/session/useCreateSession.d.ts +8 -0
  196. package/session/useCreateSession.d.ts.map +1 -1
  197. package/session/useCreateSession.js +1 -0
  198. package/session/useCreateSession.js.map +1 -1
  199. package/session/useEditSessionPrep.d.ts +26 -0
  200. package/session/useEditSessionPrep.d.ts.map +1 -0
  201. package/session/useEditSessionPrep.js +83 -0
  202. package/session/useEditSessionPrep.js.map +1 -0
  203. package/session/useNewSessionFlow.d.ts +110 -0
  204. package/session/useNewSessionFlow.d.ts.map +1 -0
  205. package/session/useNewSessionFlow.js +184 -0
  206. package/session/useNewSessionFlow.js.map +1 -0
  207. package/session/usePersistedModel.d.ts +18 -0
  208. package/session/usePersistedModel.d.ts.map +1 -0
  209. package/session/usePersistedModel.js +31 -0
  210. package/session/usePersistedModel.js.map +1 -0
  211. package/session/useSessionPageFlow.d.ts +104 -0
  212. package/session/useSessionPageFlow.d.ts.map +1 -0
  213. package/session/useSessionPageFlow.js +172 -0
  214. package/session/useSessionPageFlow.js.map +1 -0
  215. package/skill/SkillDetailView.js +3 -3
  216. package/skill/SkillDetailView.js.map +1 -1
  217. package/skill/SkillPicker.js +3 -3
  218. package/skill/SkillPicker.js.map +1 -1
  219. package/src/agent/AgentDetailView.tsx +8 -8
  220. package/src/agent/AgentPicker.tsx +3 -3
  221. package/src/api-key/ApiKeyCreatedAlert.tsx +2 -2
  222. package/src/api-key/ApiKeyListPanel.tsx +6 -6
  223. package/src/api-key/CreateApiKeyForm.tsx +2 -2
  224. package/src/attachment/AttachmentChipList.tsx +3 -3
  225. package/src/color-mode.ts +75 -0
  226. package/src/composer/ComposerToolbar.tsx +29 -7
  227. package/src/composer/ConfigureMenu.tsx +6 -6
  228. package/src/composer/ContextChip.tsx +1 -1
  229. package/src/composer/ContextPopover.tsx +2 -2
  230. package/src/composer/SessionComposer.tsx +34 -5
  231. package/src/environment/CreateEnvironmentForm.tsx +3 -3
  232. package/src/environment/EnvVarForm.tsx +6 -6
  233. package/src/environment/EnvironmentListPanel.tsx +3 -3
  234. package/src/environment/EnvironmentVariableEditor.tsx +7 -7
  235. package/src/error/ErrorMessage.tsx +5 -5
  236. package/src/execution/ApprovalCard.tsx +5 -5
  237. package/src/execution/ArtifactCard.tsx +2 -2
  238. package/src/execution/ArtifactPreviewModal.tsx +4 -4
  239. package/src/execution/FollowUpInput.tsx +2 -2
  240. package/src/execution/McpToolDetail.tsx +4 -4
  241. package/src/execution/MessageEntry.tsx +1 -1
  242. package/src/execution/MessageThread.tsx +1 -1
  243. package/src/execution/SessionVariablesInput.tsx +7 -7
  244. package/src/execution/SubAgentSection.tsx +5 -5
  245. package/src/execution/ToolCallDetail.tsx +2 -2
  246. package/src/execution/ToolCallGroup.tsx +3 -3
  247. package/src/execution/ToolCallItem.tsx +4 -4
  248. package/src/execution/WriteBackCard.tsx +5 -5
  249. package/src/execution/tool-rendering-primitives.tsx +5 -5
  250. package/src/github/GitHubRepoPicker.tsx +5 -5
  251. package/src/iam-policy/GrantAccessForm.tsx +2 -2
  252. package/src/iam-policy/OrgMembersPanel.tsx +11 -11
  253. package/src/identity-provider/CreateIdentityProviderForm.tsx +4 -4
  254. package/src/identity-provider/IdentityProviderDetailPanel.tsx +7 -7
  255. package/src/identity-provider/IdentityProviderListPanel.tsx +7 -7
  256. package/src/identity-provider/IdentityProviderWizard.tsx +8 -8
  257. package/src/identity-provider/ProviderPicker.tsx +2 -2
  258. package/src/index.ts +46 -7
  259. package/src/internal/CloudFeatureNotice.tsx +1 -1
  260. package/src/internal/Tabs.tsx +1 -1
  261. package/src/internal/markdown-components.tsx +2 -2
  262. package/src/invitation/InvitationCreatedAlert.tsx +2 -2
  263. package/src/invitation/InvitationManager.tsx +9 -9
  264. package/src/invitation/InvitationRedemption.tsx +11 -11
  265. package/src/library/ResourceCountCard.tsx +1 -1
  266. package/src/library/ResourceListView.tsx +7 -7
  267. package/src/mcp-server/McpServerConfigPanel.tsx +7 -7
  268. package/src/mcp-server/McpServerConnectDialog.tsx +5 -5
  269. package/src/mcp-server/McpServerDetailView.tsx +19 -19
  270. package/src/mcp-server/McpServerPicker.tsx +6 -6
  271. package/src/mcp-server/McpToolSelector.tsx +4 -4
  272. package/src/mcp-server/OAuthAppForm.tsx +3 -3
  273. package/src/models/ModelSelector.tsx +1 -1
  274. package/src/oauth-app/CreateOAuthAppForm.tsx +3 -3
  275. package/src/oauth-app/OAuthAppDetailPanel.tsx +7 -7
  276. package/src/oauth-app/OAuthAppListPanel.tsx +3 -3
  277. package/src/organization/CreateOrganizationForm.tsx +3 -3
  278. package/src/organization/OrgProfilePanel.tsx +4 -5
  279. package/src/platform-client/CreatePlatformClientForm.tsx +6 -6
  280. package/src/platform-client/PlatformClientDetailPanel.tsx +19 -19
  281. package/src/platform-client/PlatformClientListPanel.tsx +7 -7
  282. package/src/platform-client/PlatformClientSecretAlert.tsx +2 -2
  283. package/src/provider.tsx +52 -2
  284. package/src/runner/RunnerListPanel.tsx +725 -0
  285. package/src/runner/RunnerPicker.tsx +319 -0
  286. package/src/runner/__tests__/useDeleteRunner.test.tsx +150 -0
  287. package/src/runner/__tests__/useLaunchLocalRunner.test.tsx +223 -0
  288. package/src/runner/__tests__/useStopRunner.test.tsx +154 -0
  289. package/src/runner/index.ts +34 -0
  290. package/src/runner/phase.ts +62 -0
  291. package/src/runner/useDeleteRunner.ts +67 -0
  292. package/src/runner/useLaunchLocalRunner.ts +139 -0
  293. package/src/runner/useRunnerList.ts +114 -0
  294. package/src/runner/useStopRunner.ts +92 -0
  295. package/src/session/draft.ts +82 -0
  296. package/src/session/index.ts +28 -0
  297. package/src/session/useCreateSession.ts +9 -0
  298. package/src/session/useEditSessionPrep.ts +111 -0
  299. package/src/session/useNewSessionFlow.ts +283 -0
  300. package/src/session/usePersistedModel.ts +41 -0
  301. package/src/session/useSessionPageFlow.ts +280 -0
  302. package/src/skill/SkillDetailView.tsx +5 -5
  303. package/src/skill/SkillPicker.tsx +3 -3
  304. package/src/styles.css +25 -1
  305. package/src/usage/OrgUsagePanel.tsx +4 -4
  306. package/src/workspace/WorkspaceEditor.tsx +78 -66
  307. package/src/workspace/index.ts +0 -8
  308. package/styles.css +1 -1
  309. package/usage/OrgUsagePanel.js +2 -2
  310. package/usage/OrgUsagePanel.js.map +1 -1
  311. package/workspace/WorkspaceEditor.d.ts +28 -3
  312. package/workspace/WorkspaceEditor.d.ts.map +1 -1
  313. package/workspace/WorkspaceEditor.js +24 -25
  314. package/workspace/WorkspaceEditor.js.map +1 -1
  315. package/workspace/index.d.ts +0 -4
  316. package/workspace/index.d.ts.map +1 -1
  317. package/workspace/index.js +0 -2
  318. package/workspace/index.js.map +1 -1
  319. package/src/workspace/FolderBrowser.tsx +0 -579
  320. package/src/workspace/useFolderListing.ts +0 -164
  321. package/workspace/FolderBrowser.d.ts +0 -37
  322. package/workspace/FolderBrowser.d.ts.map +0 -1
  323. package/workspace/FolderBrowser.js +0 -188
  324. package/workspace/FolderBrowser.js.map +0 -1
  325. package/workspace/useFolderListing.d.ts +0 -73
  326. package/workspace/useFolderListing.d.ts.map +0 -1
  327. package/workspace/useFolderListing.js +0 -110
  328. package/workspace/useFolderListing.js.map +0 -1
@@ -0,0 +1,319 @@
1
+ "use client";
2
+
3
+ import { useMemo } from "react";
4
+ import { Select } from "@base-ui/react/select";
5
+ import { RunnerPhase } from "@stigmer/protos/ai/stigmer/agentic/runner/v1/enum_pb";
6
+ import type { Runner } from "@stigmer/protos/ai/stigmer/agentic/runner/v1/api_pb";
7
+ import { useRunnerList } from "./useRunnerList";
8
+ import {
9
+ isActivePhase,
10
+ phaseLabel,
11
+ phaseDotColor,
12
+ PHASE_SORT_ORDER,
13
+ } from "./phase";
14
+
15
+ /**
16
+ * Sentinel value representing "Auto" (let the backend decide).
17
+ *
18
+ * Base UI Select requires a non-null string value for every item,
19
+ * so we use this internal sentinel and map to/from `null` at the
20
+ * component boundary.
21
+ */
22
+ const AUTO_VALUE = "__auto__";
23
+
24
+ /** Props for {@link RunnerPicker}. */
25
+ export interface RunnerPickerProps {
26
+ /** Organization slug to scope the runner list. */
27
+ readonly org: string;
28
+ /**
29
+ * Currently selected runner ID, or `null` for "Auto".
30
+ *
31
+ * `null` means the backend decides which runner to use — session
32
+ * auto-bind in OSS, cloud auto-provisioning in Cloud.
33
+ */
34
+ readonly value: string | null;
35
+ /** Called when the user picks a different runner. `null` = "Auto". */
36
+ readonly onChange: (runnerId: string | null) => void;
37
+ /**
38
+ * Show the "Auto" option as the first item in the dropdown.
39
+ * @default true
40
+ */
41
+ readonly showAutoOption?: boolean;
42
+ /** Additional CSS class names for the trigger button. */
43
+ readonly className?: string;
44
+ /** When true, disables the selector. */
45
+ readonly disabled?: boolean;
46
+ }
47
+
48
+ /**
49
+ * Theme-able runner picker built on `@base-ui/react` Select.
50
+ *
51
+ * Fetches available runners via {@link useRunnerList} and renders
52
+ * them in a dropdown grouped by operational phase. READY runners
53
+ * appear first, then BUSY (selectable), then inactive runners
54
+ * (STOPPED/PENDING/FAILED — visible but disabled).
55
+ *
56
+ * Includes an "Auto" option (default) that lets the backend decide
57
+ * which runner to use. Platform builders who manage runner assignment
58
+ * programmatically can disable this via `showAutoOption={false}`.
59
+ *
60
+ * All visual properties flow through `--stgm-*` tokens — no
61
+ * hardcoded colors or sizes.
62
+ *
63
+ * Platform builders who need different rendering use
64
+ * {@link useRunnerList} directly.
65
+ *
66
+ * @example
67
+ * ```tsx
68
+ * const [runnerId, setRunnerId] = useState<string | null>(null);
69
+ *
70
+ * <RunnerPicker
71
+ * org="acme"
72
+ * value={runnerId}
73
+ * onChange={setRunnerId}
74
+ * />
75
+ * ```
76
+ */
77
+ export function RunnerPicker({
78
+ org,
79
+ value,
80
+ onChange,
81
+ showAutoOption = true,
82
+ className,
83
+ disabled,
84
+ }: RunnerPickerProps) {
85
+ const { runners, isLoading } = useRunnerList(org);
86
+
87
+ const { active, inactive } = useMemo(() => {
88
+ const act: Runner[] = [];
89
+ const inact: Runner[] = [];
90
+
91
+ for (const r of runners) {
92
+ if (isActivePhase(r.status?.phase ?? RunnerPhase.UNSPECIFIED)) {
93
+ act.push(r);
94
+ } else {
95
+ inact.push(r);
96
+ }
97
+ }
98
+
99
+ act.sort(phaseThenName);
100
+ inact.sort(phaseThenName);
101
+
102
+ return { active: act, inactive: inact };
103
+ }, [runners]);
104
+
105
+ const selectValue = value ?? AUTO_VALUE;
106
+
107
+ const handleChange = (v: string | null) => {
108
+ if (v === null) return;
109
+ onChange(v === AUTO_VALUE ? null : v);
110
+ };
111
+
112
+ const triggerLabel = useMemo(() => {
113
+ if (!value) return "Auto";
114
+ const runner = runners.find((r) => r.metadata?.id === value);
115
+ return runner?.metadata?.name ?? "Runner";
116
+ }, [value, runners]);
117
+
118
+ return (
119
+ <Select.Root
120
+ value={selectValue}
121
+ onValueChange={handleChange}
122
+ disabled={disabled || isLoading}
123
+ >
124
+ <Select.Trigger
125
+ className={[
126
+ "inline-flex items-center gap-1.5 rounded-md border border-border",
127
+ "bg-background px-2.5 py-1.5 text-xs text-foreground",
128
+ "hover:bg-accent-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
129
+ "disabled:pointer-events-none disabled:opacity-50",
130
+ "transition-colors",
131
+ className,
132
+ ]
133
+ .filter(Boolean)
134
+ .join(" ")}
135
+ >
136
+ <RunnerIcon />
137
+ <span className="max-w-[10rem] truncate">{triggerLabel}</span>
138
+ <ChevronIcon />
139
+ </Select.Trigger>
140
+
141
+ <Select.Portal>
142
+ <Select.Positioner sideOffset={4}>
143
+ <Select.Popup
144
+ className={[
145
+ "z-popover max-h-72 min-w-[var(--anchor-width)] overflow-auto",
146
+ "rounded-lg border border-border bg-popover p-1 shadow-md",
147
+ "text-popover-foreground",
148
+ ].join(" ")}
149
+ >
150
+ {showAutoOption && (
151
+ <Select.Item
152
+ value={AUTO_VALUE}
153
+ className={[
154
+ "flex cursor-pointer items-center gap-2",
155
+ "rounded-md px-2 py-1.5 text-xs outline-none",
156
+ "data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground",
157
+ "data-[selected]:font-medium",
158
+ ].join(" ")}
159
+ >
160
+ <Select.ItemText>Auto</Select.ItemText>
161
+ <span className="text-[0.6rem] text-muted-foreground">
162
+ default
163
+ </span>
164
+ </Select.Item>
165
+ )}
166
+
167
+ {(showAutoOption && (active.length > 0 || inactive.length > 0)) && (
168
+ <div className="my-1 h-px bg-border/50" role="separator" />
169
+ )}
170
+
171
+ {active.length > 0 && (
172
+ <Select.Group>
173
+ <Select.GroupLabel className="px-2 py-1.5 text-[0.65rem] font-medium uppercase tracking-wider text-muted-foreground">
174
+ Available
175
+ </Select.GroupLabel>
176
+ {active.map((r) => (
177
+ <RunnerItem key={r.metadata!.id} runner={r} />
178
+ ))}
179
+ </Select.Group>
180
+ )}
181
+
182
+ {inactive.length > 0 && (
183
+ <Select.Group>
184
+ <Select.GroupLabel className="px-2 py-1.5 text-[0.65rem] font-medium uppercase tracking-wider text-muted-foreground">
185
+ Offline
186
+ </Select.GroupLabel>
187
+ {inactive.map((r) => (
188
+ <RunnerItem key={r.metadata!.id} runner={r} disabled />
189
+ ))}
190
+ </Select.Group>
191
+ )}
192
+
193
+ {runners.length === 0 && !isLoading && (
194
+ <div className="px-2 py-3 text-center text-xs text-muted-foreground">
195
+ No runners found
196
+ </div>
197
+ )}
198
+ </Select.Popup>
199
+ </Select.Positioner>
200
+ </Select.Portal>
201
+ </Select.Root>
202
+ );
203
+ }
204
+
205
+ // ---------------------------------------------------------------------------
206
+ // Internal components
207
+ // ---------------------------------------------------------------------------
208
+
209
+ function RunnerItem({
210
+ runner,
211
+ disabled,
212
+ }: {
213
+ runner: Runner;
214
+ disabled?: boolean;
215
+ }) {
216
+ const id = runner.metadata!.id;
217
+ const name = runner.metadata?.name ?? "Unnamed";
218
+ const phase = runner.status?.phase ?? RunnerPhase.UNSPECIFIED;
219
+ const hostname = runner.status?.connectionInfo?.hostname;
220
+
221
+ return (
222
+ <Select.Item
223
+ value={id}
224
+ disabled={disabled}
225
+ className={[
226
+ "flex cursor-pointer items-center gap-2",
227
+ "rounded-md px-2 py-1.5 text-xs outline-none",
228
+ "data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground",
229
+ "data-[selected]:font-medium",
230
+ "data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50",
231
+ ].join(" ")}
232
+ >
233
+ <PhaseDot phase={phase} />
234
+ <div className="flex min-w-0 flex-col">
235
+ <Select.ItemText>
236
+ <span className="truncate">{name}</span>
237
+ </Select.ItemText>
238
+ {hostname && (
239
+ <span className="truncate text-[0.6rem] leading-tight text-muted-foreground">
240
+ {hostname}
241
+ </span>
242
+ )}
243
+ </div>
244
+ <span className="ml-auto shrink-0 text-[0.6rem] lowercase text-muted-foreground">
245
+ {phaseLabel(phase)}
246
+ </span>
247
+ </Select.Item>
248
+ );
249
+ }
250
+
251
+ function PhaseDot({ phase }: { phase: RunnerPhase }) {
252
+ return (
253
+ <span
254
+ className={`inline-block h-1.5 w-1.5 shrink-0 rounded-full ${phaseDotColor(phase)}`}
255
+ aria-hidden="true"
256
+ />
257
+ );
258
+ }
259
+
260
+ // ---------------------------------------------------------------------------
261
+ // Utilities
262
+ // ---------------------------------------------------------------------------
263
+
264
+ function phaseThenName(a: Runner, b: Runner): number {
265
+ const pa = a.status?.phase ?? RunnerPhase.UNSPECIFIED;
266
+ const pb = b.status?.phase ?? RunnerPhase.UNSPECIFIED;
267
+ const phaseOrder = PHASE_SORT_ORDER[pa] - PHASE_SORT_ORDER[pb];
268
+ if (phaseOrder !== 0) return phaseOrder;
269
+ return (a.metadata?.name ?? "").localeCompare(b.metadata?.name ?? "");
270
+ }
271
+
272
+ // ---------------------------------------------------------------------------
273
+ // Icons
274
+ // ---------------------------------------------------------------------------
275
+
276
+ function RunnerIcon() {
277
+ return (
278
+ <svg
279
+ width="12"
280
+ height="12"
281
+ viewBox="0 0 24 24"
282
+ fill="none"
283
+ stroke="currentColor"
284
+ strokeWidth="2"
285
+ strokeLinecap="round"
286
+ strokeLinejoin="round"
287
+ className="text-muted-foreground"
288
+ >
289
+ <rect x="4" y="4" width="16" height="16" rx="2" />
290
+ <rect x="9" y="9" width="6" height="6" />
291
+ <path d="M15 2v2" />
292
+ <path d="M15 20v2" />
293
+ <path d="M2 15h2" />
294
+ <path d="M2 9h2" />
295
+ <path d="M20 15h2" />
296
+ <path d="M20 9h2" />
297
+ <path d="M9 2v2" />
298
+ <path d="M9 20v2" />
299
+ </svg>
300
+ );
301
+ }
302
+
303
+ function ChevronIcon() {
304
+ return (
305
+ <svg
306
+ width="10"
307
+ height="10"
308
+ viewBox="0 0 10 10"
309
+ fill="none"
310
+ stroke="currentColor"
311
+ strokeWidth="1.5"
312
+ strokeLinecap="round"
313
+ strokeLinejoin="round"
314
+ className="text-muted-foreground"
315
+ >
316
+ <path d="M2.5 3.75L5 6.25L7.5 3.75" />
317
+ </svg>
318
+ );
319
+ }
@@ -0,0 +1,150 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { renderHook, act } from "@testing-library/react";
3
+ import type { ReactNode } from "react";
4
+ import { create } from "@bufbuild/protobuf";
5
+ import {
6
+ RunnerSchema,
7
+ RunnerStatusSchema,
8
+ type Runner,
9
+ } from "@stigmer/protos/ai/stigmer/agentic/runner/v1/api_pb";
10
+ import { RunnerPhase } from "@stigmer/protos/ai/stigmer/agentic/runner/v1/enum_pb";
11
+ import { ApiResourceMetadataSchema } from "@stigmer/protos/ai/stigmer/commons/apiresource/metadata_pb";
12
+ import type { Stigmer } from "@stigmer/sdk";
13
+ import { StigmerContext } from "../../context";
14
+ import { useDeleteRunner } from "../useDeleteRunner";
15
+
16
+ function makeRunner(id: string, phase: RunnerPhase): Runner {
17
+ const runner = create(RunnerSchema);
18
+ runner.metadata = create(ApiResourceMetadataSchema);
19
+ runner.metadata.id = id;
20
+ runner.status = create(RunnerStatusSchema);
21
+ runner.status.phase = phase;
22
+ return runner;
23
+ }
24
+
25
+ function buildMockClient(overrides: {
26
+ delete?: ReturnType<typeof vi.fn>;
27
+ } = {}) {
28
+ return {
29
+ runner: {
30
+ delete: overrides.delete ?? vi.fn(),
31
+ },
32
+ } as unknown as Stigmer;
33
+ }
34
+
35
+ function makeWrapper(client: Stigmer) {
36
+ return ({ children }: { children: ReactNode }) => (
37
+ <StigmerContext.Provider value={client}>
38
+ {children}
39
+ </StigmerContext.Provider>
40
+ );
41
+ }
42
+
43
+ describe("useDeleteRunner", () => {
44
+ let deleteMock: ReturnType<typeof vi.fn>;
45
+ let client: Stigmer;
46
+
47
+ beforeEach(() => {
48
+ deleteMock = vi.fn();
49
+ client = buildMockClient({ delete: deleteMock });
50
+ });
51
+
52
+ it("calls runner.delete with the ID and returns the deleted runner", async () => {
53
+ const deletedRunner = makeRunner("rnr_del1", RunnerPhase.STOPPED);
54
+ deleteMock.mockResolvedValueOnce(deletedRunner);
55
+
56
+ const { result } = renderHook(() => useDeleteRunner(), {
57
+ wrapper: makeWrapper(client),
58
+ });
59
+
60
+ expect(result.current.isDeleting).toBe(false);
61
+ expect(result.current.error).toBeNull();
62
+
63
+ let returned: Runner;
64
+ await act(async () => {
65
+ returned = await result.current.deleteRunner("rnr_del1");
66
+ });
67
+
68
+ expect(deleteMock).toHaveBeenCalledOnce();
69
+ expect(deleteMock).toHaveBeenCalledWith("rnr_del1");
70
+
71
+ expect(returned!).toBe(deletedRunner);
72
+ expect(result.current.isDeleting).toBe(false);
73
+ expect(result.current.error).toBeNull();
74
+ });
75
+
76
+ it("sets error and rethrows on failure", async () => {
77
+ const rpcError = new Error("permission denied");
78
+ deleteMock.mockRejectedValueOnce(rpcError);
79
+
80
+ const { result } = renderHook(() => useDeleteRunner(), {
81
+ wrapper: makeWrapper(client),
82
+ });
83
+
84
+ await act(async () => {
85
+ await expect(
86
+ result.current.deleteRunner("rnr_nope"),
87
+ ).rejects.toThrow("permission denied");
88
+ });
89
+
90
+ expect(result.current.error).toBeInstanceOf(Error);
91
+ expect(result.current.error!.message).toBe("permission denied");
92
+ expect(result.current.isDeleting).toBe(false);
93
+ });
94
+
95
+ it("handles non-Error rejection values", async () => {
96
+ deleteMock.mockRejectedValueOnce("raw string error");
97
+
98
+ const { result } = renderHook(() => useDeleteRunner(), {
99
+ wrapper: makeWrapper(client),
100
+ });
101
+
102
+ await act(async () => {
103
+ await expect(
104
+ result.current.deleteRunner("rnr_x"),
105
+ ).rejects.toBe("raw string error");
106
+ });
107
+
108
+ expect(result.current.error).toBeInstanceOf(Error);
109
+ expect(result.current.error!.message).toBe("raw string error");
110
+ });
111
+
112
+ it("clears error via clearError", async () => {
113
+ deleteMock.mockRejectedValueOnce(new Error("fail"));
114
+
115
+ const { result } = renderHook(() => useDeleteRunner(), {
116
+ wrapper: makeWrapper(client),
117
+ });
118
+
119
+ await act(async () => {
120
+ await result.current.deleteRunner("rnr_1").catch(() => {});
121
+ });
122
+ expect(result.current.error).not.toBeNull();
123
+
124
+ act(() => {
125
+ result.current.clearError();
126
+ });
127
+ expect(result.current.error).toBeNull();
128
+ });
129
+
130
+ it("resets previous error on a new successful delete", async () => {
131
+ deleteMock.mockRejectedValueOnce(new Error("first fail"));
132
+
133
+ const { result } = renderHook(() => useDeleteRunner(), {
134
+ wrapper: makeWrapper(client),
135
+ });
136
+
137
+ await act(async () => {
138
+ await result.current.deleteRunner("rnr_1").catch(() => {});
139
+ });
140
+ expect(result.current.error).not.toBeNull();
141
+
142
+ const deletedRunner = makeRunner("rnr_1", RunnerPhase.STOPPED);
143
+ deleteMock.mockResolvedValueOnce(deletedRunner);
144
+
145
+ await act(async () => {
146
+ await result.current.deleteRunner("rnr_1");
147
+ });
148
+ expect(result.current.error).toBeNull();
149
+ });
150
+ });
@@ -0,0 +1,223 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { renderHook, act } from "@testing-library/react";
3
+ import type { ReactNode } from "react";
4
+ import { create } from "@bufbuild/protobuf";
5
+ import { timestampFromDate } from "@bufbuild/protobuf/wkt";
6
+ import { CreateLaunchTokenResponseSchema } from "@stigmer/protos/ai/stigmer/agentic/runner/v1/io_pb";
7
+ import type { Stigmer } from "@stigmer/sdk";
8
+ import { StigmerContext } from "../../context";
9
+ import { useLaunchLocalRunner } from "../useLaunchLocalRunner";
10
+
11
+ function buildMockClient(overrides: {
12
+ createLaunchToken?: ReturnType<typeof vi.fn>;
13
+ } = {}) {
14
+ return {
15
+ runner: {
16
+ createLaunchToken:
17
+ overrides.createLaunchToken ?? vi.fn(),
18
+ },
19
+ } as unknown as Stigmer;
20
+ }
21
+
22
+ function makeWrapper(client: Stigmer) {
23
+ return ({ children }: { children: ReactNode }) => (
24
+ <StigmerContext.Provider value={client}>
25
+ {children}
26
+ </StigmerContext.Provider>
27
+ );
28
+ }
29
+
30
+ describe("useLaunchLocalRunner", () => {
31
+ const ORG = "test-org";
32
+
33
+ let createLaunchToken: ReturnType<typeof vi.fn>;
34
+ let client: Stigmer;
35
+
36
+ beforeEach(() => {
37
+ createLaunchToken = vi.fn();
38
+ client = buildMockClient({ createLaunchToken });
39
+ });
40
+
41
+ it("creates a token and opens the stigmer:// URL on success", async () => {
42
+ const response = create(CreateLaunchTokenResponseSchema, {
43
+ token: "tok_abc123",
44
+ expiresAt: timestampFromDate(new Date()),
45
+ });
46
+ createLaunchToken.mockResolvedValueOnce(response);
47
+
48
+ const openUrl = vi.fn();
49
+ const { result } = renderHook(
50
+ () => useLaunchLocalRunner({ openUrl }),
51
+ { wrapper: makeWrapper(client) },
52
+ );
53
+
54
+ expect(result.current.isLaunching).toBe(false);
55
+ expect(result.current.error).toBeNull();
56
+
57
+ let launchResult: Awaited<ReturnType<typeof result.current.launch>>;
58
+ await act(async () => {
59
+ launchResult = await result.current.launch({ org: ORG });
60
+ });
61
+
62
+ expect(createLaunchToken).toHaveBeenCalledOnce();
63
+ expect(openUrl).toHaveBeenCalledOnce();
64
+ expect(openUrl).toHaveBeenCalledWith(
65
+ `stigmer://launch-runner?token=${encodeURIComponent("tok_abc123")}`,
66
+ );
67
+
68
+ expect(launchResult!.url).toBe(
69
+ `stigmer://launch-runner?token=${encodeURIComponent("tok_abc123")}`,
70
+ );
71
+ expect(launchResult!.expiresAt).toBeInstanceOf(Date);
72
+
73
+ expect(result.current.isLaunching).toBe(false);
74
+ expect(result.current.error).toBeNull();
75
+ });
76
+
77
+ it("URL-encodes tokens with special characters", async () => {
78
+ const response = create(CreateLaunchTokenResponseSchema, {
79
+ token: "tok/with+special=chars&more",
80
+ });
81
+ createLaunchToken.mockResolvedValueOnce(response);
82
+
83
+ const openUrl = vi.fn();
84
+ const { result } = renderHook(
85
+ () => useLaunchLocalRunner({ openUrl }),
86
+ { wrapper: makeWrapper(client) },
87
+ );
88
+
89
+ await act(async () => {
90
+ await result.current.launch({ org: ORG });
91
+ });
92
+
93
+ const url = openUrl.mock.calls[0][0] as string;
94
+ expect(url).toContain(encodeURIComponent("tok/with+special=chars&more"));
95
+ expect(url).not.toContain("&more");
96
+ });
97
+
98
+ it("uses the custom openUrl callback", async () => {
99
+ const response = create(CreateLaunchTokenResponseSchema, {
100
+ token: "tok_custom",
101
+ });
102
+ createLaunchToken.mockResolvedValueOnce(response);
103
+
104
+ const customOpen = vi.fn();
105
+ const { result } = renderHook(
106
+ () => useLaunchLocalRunner({ openUrl: customOpen }),
107
+ { wrapper: makeWrapper(client) },
108
+ );
109
+
110
+ await act(async () => {
111
+ await result.current.launch({ org: ORG });
112
+ });
113
+
114
+ expect(customOpen).toHaveBeenCalledOnce();
115
+ expect(customOpen.mock.calls[0][0]).toMatch(/^stigmer:\/\//);
116
+ });
117
+
118
+ it("returns undefined expiresAt when the server omits it", async () => {
119
+ const response = create(CreateLaunchTokenResponseSchema, {
120
+ token: "tok_no_expiry",
121
+ });
122
+ createLaunchToken.mockResolvedValueOnce(response);
123
+
124
+ const openUrl = vi.fn();
125
+ const { result } = renderHook(
126
+ () => useLaunchLocalRunner({ openUrl }),
127
+ { wrapper: makeWrapper(client) },
128
+ );
129
+
130
+ let launchResult: Awaited<ReturnType<typeof result.current.launch>>;
131
+ await act(async () => {
132
+ launchResult = await result.current.launch({ org: ORG });
133
+ });
134
+
135
+ expect(launchResult!.expiresAt).toBeUndefined();
136
+ });
137
+
138
+ it("sets error and rethrows when createLaunchToken fails", async () => {
139
+ const rpcError = new Error("token creation failed");
140
+ createLaunchToken.mockRejectedValueOnce(rpcError);
141
+
142
+ const openUrl = vi.fn();
143
+ const { result } = renderHook(
144
+ () => useLaunchLocalRunner({ openUrl }),
145
+ { wrapper: makeWrapper(client) },
146
+ );
147
+
148
+ await act(async () => {
149
+ await expect(
150
+ result.current.launch({ org: ORG }),
151
+ ).rejects.toThrow("token creation failed");
152
+ });
153
+
154
+ expect(result.current.error).toBeInstanceOf(Error);
155
+ expect(result.current.error!.message).toBe("token creation failed");
156
+ expect(result.current.isLaunching).toBe(false);
157
+ expect(openUrl).not.toHaveBeenCalled();
158
+ });
159
+
160
+ it("handles non-Error rejection values", async () => {
161
+ createLaunchToken.mockRejectedValueOnce("string error");
162
+
163
+ const openUrl = vi.fn();
164
+ const { result } = renderHook(
165
+ () => useLaunchLocalRunner({ openUrl }),
166
+ { wrapper: makeWrapper(client) },
167
+ );
168
+
169
+ await act(async () => {
170
+ await expect(result.current.launch({ org: ORG })).rejects.toBe(
171
+ "string error",
172
+ );
173
+ });
174
+
175
+ expect(result.current.error).toBeInstanceOf(Error);
176
+ expect(result.current.error!.message).toBe("string error");
177
+ });
178
+
179
+ it("clears error via clearError", async () => {
180
+ createLaunchToken.mockRejectedValueOnce(new Error("fail"));
181
+
182
+ const openUrl = vi.fn();
183
+ const { result } = renderHook(
184
+ () => useLaunchLocalRunner({ openUrl }),
185
+ { wrapper: makeWrapper(client) },
186
+ );
187
+
188
+ await act(async () => {
189
+ await result.current.launch({ org: ORG }).catch(() => {});
190
+ });
191
+ expect(result.current.error).not.toBeNull();
192
+
193
+ act(() => {
194
+ result.current.clearError();
195
+ });
196
+ expect(result.current.error).toBeNull();
197
+ });
198
+
199
+ it("resets previous error on a new successful launch", async () => {
200
+ createLaunchToken.mockRejectedValueOnce(new Error("first fail"));
201
+
202
+ const openUrl = vi.fn();
203
+ const { result } = renderHook(
204
+ () => useLaunchLocalRunner({ openUrl }),
205
+ { wrapper: makeWrapper(client) },
206
+ );
207
+
208
+ await act(async () => {
209
+ await result.current.launch({ org: ORG }).catch(() => {});
210
+ });
211
+ expect(result.current.error).not.toBeNull();
212
+
213
+ const response = create(CreateLaunchTokenResponseSchema, {
214
+ token: "tok_retry",
215
+ });
216
+ createLaunchToken.mockResolvedValueOnce(response);
217
+
218
+ await act(async () => {
219
+ await result.current.launch({ org: ORG });
220
+ });
221
+ expect(result.current.error).toBeNull();
222
+ });
223
+ });