@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.
- package/README.md +57 -12
- package/agent/AgentDetailView.js +7 -7
- package/agent/AgentDetailView.js.map +1 -1
- package/agent/AgentPicker.js +3 -3
- package/agent/AgentPicker.js.map +1 -1
- package/api-key/ApiKeyCreatedAlert.js +1 -1
- package/api-key/ApiKeyCreatedAlert.js.map +1 -1
- package/api-key/ApiKeyListPanel.js +4 -4
- package/api-key/ApiKeyListPanel.js.map +1 -1
- package/api-key/CreateApiKeyForm.js +1 -1
- package/api-key/CreateApiKeyForm.js.map +1 -1
- package/attachment/AttachmentChipList.js +2 -2
- package/attachment/AttachmentChipList.js.map +1 -1
- package/color-mode.d.ts +48 -0
- package/color-mode.d.ts.map +1 -0
- package/color-mode.js +53 -0
- package/color-mode.js.map +1 -0
- package/composer/ComposerToolbar.d.ts +6 -2
- package/composer/ComposerToolbar.d.ts.map +1 -1
- package/composer/ComposerToolbar.js +5 -3
- package/composer/ComposerToolbar.js.map +1 -1
- package/composer/ConfigureMenu.js +3 -3
- package/composer/ConfigureMenu.js.map +1 -1
- package/composer/ContextChip.js +1 -1
- package/composer/ContextChip.js.map +1 -1
- package/composer/ContextPopover.js +1 -1
- package/composer/ContextPopover.js.map +1 -1
- package/composer/SessionComposer.d.ts +24 -3
- package/composer/SessionComposer.d.ts.map +1 -1
- package/composer/SessionComposer.js +5 -4
- package/composer/SessionComposer.js.map +1 -1
- package/environment/CreateEnvironmentForm.js +1 -1
- package/environment/CreateEnvironmentForm.js.map +1 -1
- package/environment/EnvVarForm.js +2 -2
- package/environment/EnvVarForm.js.map +1 -1
- package/environment/EnvironmentListPanel.js +2 -2
- package/environment/EnvironmentListPanel.js.map +1 -1
- package/environment/EnvironmentVariableEditor.js +6 -6
- package/environment/EnvironmentVariableEditor.js.map +1 -1
- package/error/ErrorMessage.js +1 -1
- package/error/ErrorMessage.js.map +1 -1
- package/execution/ApprovalCard.js +4 -4
- package/execution/ApprovalCard.js.map +1 -1
- package/execution/ArtifactCard.js +1 -1
- package/execution/ArtifactCard.js.map +1 -1
- package/execution/ArtifactPreviewModal.js +4 -4
- package/execution/ArtifactPreviewModal.js.map +1 -1
- package/execution/FollowUpInput.js +1 -1
- package/execution/FollowUpInput.js.map +1 -1
- package/execution/McpToolDetail.js +4 -4
- package/execution/McpToolDetail.js.map +1 -1
- package/execution/MessageEntry.js +1 -1
- package/execution/MessageEntry.js.map +1 -1
- package/execution/MessageThread.js +1 -1
- package/execution/MessageThread.js.map +1 -1
- package/execution/SessionVariablesInput.js +4 -4
- package/execution/SessionVariablesInput.js.map +1 -1
- package/execution/SubAgentSection.js +2 -2
- package/execution/SubAgentSection.js.map +1 -1
- package/execution/ToolCallDetail.js +2 -2
- package/execution/ToolCallDetail.js.map +1 -1
- package/execution/ToolCallGroup.js +1 -1
- package/execution/ToolCallGroup.js.map +1 -1
- package/execution/ToolCallItem.js +2 -2
- package/execution/ToolCallItem.js.map +1 -1
- package/execution/WriteBackCard.js +3 -3
- package/execution/WriteBackCard.js.map +1 -1
- package/execution/tool-rendering-primitives.js +3 -3
- package/execution/tool-rendering-primitives.js.map +1 -1
- package/github/GitHubRepoPicker.js +5 -5
- package/github/GitHubRepoPicker.js.map +1 -1
- package/iam-policy/GrantAccessForm.js +1 -1
- package/iam-policy/GrantAccessForm.js.map +1 -1
- package/iam-policy/OrgMembersPanel.js +5 -5
- package/iam-policy/OrgMembersPanel.js.map +1 -1
- package/identity-provider/CreateIdentityProviderForm.js +3 -3
- package/identity-provider/CreateIdentityProviderForm.js.map +1 -1
- package/identity-provider/IdentityProviderDetailPanel.js +5 -5
- package/identity-provider/IdentityProviderDetailPanel.js.map +1 -1
- package/identity-provider/IdentityProviderListPanel.js +3 -3
- package/identity-provider/IdentityProviderListPanel.js.map +1 -1
- package/identity-provider/IdentityProviderWizard.js +7 -7
- package/identity-provider/IdentityProviderWizard.js.map +1 -1
- package/identity-provider/ProviderPicker.js +2 -2
- package/identity-provider/ProviderPicker.js.map +1 -1
- package/index.d.ts +8 -4
- package/index.d.ts.map +1 -1
- package/index.js +7 -3
- package/index.js.map +1 -1
- package/internal/CloudFeatureNotice.js +1 -1
- package/internal/CloudFeatureNotice.js.map +1 -1
- package/internal/Tabs.js +1 -1
- package/internal/Tabs.js.map +1 -1
- package/internal/markdown-components.js +2 -2
- package/internal/markdown-components.js.map +1 -1
- package/invitation/InvitationCreatedAlert.js +1 -1
- package/invitation/InvitationCreatedAlert.js.map +1 -1
- package/invitation/InvitationManager.js +5 -5
- package/invitation/InvitationManager.js.map +1 -1
- package/invitation/InvitationRedemption.js +4 -4
- package/invitation/InvitationRedemption.js.map +1 -1
- package/library/ResourceCountCard.js +1 -1
- package/library/ResourceCountCard.js.map +1 -1
- package/library/ResourceListView.js +5 -5
- package/library/ResourceListView.js.map +1 -1
- package/mcp-server/McpServerConfigPanel.js +5 -5
- package/mcp-server/McpServerConfigPanel.js.map +1 -1
- package/mcp-server/McpServerConnectDialog.js +4 -4
- package/mcp-server/McpServerConnectDialog.js.map +1 -1
- package/mcp-server/McpServerDetailView.js +14 -14
- package/mcp-server/McpServerDetailView.js.map +1 -1
- package/mcp-server/McpServerPicker.js +4 -4
- package/mcp-server/McpServerPicker.js.map +1 -1
- package/mcp-server/McpToolSelector.js +3 -3
- package/mcp-server/McpToolSelector.js.map +1 -1
- package/mcp-server/OAuthAppForm.js +1 -1
- package/mcp-server/OAuthAppForm.js.map +1 -1
- package/models/ModelSelector.js +1 -1
- package/models/ModelSelector.js.map +1 -1
- package/oauth-app/CreateOAuthAppForm.js +1 -1
- package/oauth-app/CreateOAuthAppForm.js.map +1 -1
- package/oauth-app/OAuthAppDetailPanel.js +3 -3
- package/oauth-app/OAuthAppDetailPanel.js.map +1 -1
- package/oauth-app/OAuthAppListPanel.js +2 -2
- package/oauth-app/OAuthAppListPanel.js.map +1 -1
- package/organization/CreateOrganizationForm.js +1 -1
- package/organization/CreateOrganizationForm.js.map +1 -1
- package/organization/OrgProfilePanel.js +3 -3
- package/organization/OrgProfilePanel.js.map +1 -1
- package/package.json +4 -4
- package/platform-client/CreatePlatformClientForm.js +2 -2
- package/platform-client/CreatePlatformClientForm.js.map +1 -1
- package/platform-client/PlatformClientDetailPanel.js +4 -4
- package/platform-client/PlatformClientDetailPanel.js.map +1 -1
- package/platform-client/PlatformClientListPanel.js +3 -3
- package/platform-client/PlatformClientListPanel.js.map +1 -1
- package/platform-client/PlatformClientSecretAlert.js +2 -2
- package/platform-client/PlatformClientSecretAlert.js.map +1 -1
- package/provider.d.ts +39 -2
- package/provider.d.ts.map +1 -1
- package/provider.js +11 -3
- package/provider.js.map +1 -1
- package/runner/RunnerListPanel.d.ts +65 -0
- package/runner/RunnerListPanel.d.ts.map +1 -0
- package/runner/RunnerListPanel.js +237 -0
- package/runner/RunnerListPanel.js.map +1 -0
- package/runner/RunnerPicker.d.ts +54 -0
- package/runner/RunnerPicker.d.ts.map +1 -0
- package/runner/RunnerPicker.js +133 -0
- package/runner/RunnerPicker.js.map +1 -0
- package/runner/__tests__/useDeleteRunner.test.d.ts +2 -0
- package/runner/__tests__/useDeleteRunner.test.d.ts.map +1 -0
- package/runner/__tests__/useDeleteRunner.test.js +108 -0
- package/runner/__tests__/useDeleteRunner.test.js.map +1 -0
- package/runner/__tests__/useLaunchLocalRunner.test.d.ts +2 -0
- package/runner/__tests__/useLaunchLocalRunner.test.d.ts.map +1 -0
- package/runner/__tests__/useLaunchLocalRunner.test.js +143 -0
- package/runner/__tests__/useLaunchLocalRunner.test.js.map +1 -0
- package/runner/__tests__/useStopRunner.test.d.ts +2 -0
- package/runner/__tests__/useStopRunner.test.d.ts.map +1 -0
- package/runner/__tests__/useStopRunner.test.js +114 -0
- package/runner/__tests__/useStopRunner.test.js.map +1 -0
- package/runner/index.d.ts +14 -0
- package/runner/index.d.ts.map +1 -0
- package/runner/index.js +8 -0
- package/runner/index.js.map +1 -0
- package/runner/phase.d.ts +30 -0
- package/runner/phase.d.ts.map +1 -0
- package/runner/phase.js +58 -0
- package/runner/phase.js.map +1 -0
- package/runner/useDeleteRunner.d.ts +36 -0
- package/runner/useDeleteRunner.d.ts.map +1 -0
- package/runner/useDeleteRunner.js +42 -0
- package/runner/useDeleteRunner.js.map +1 -0
- package/runner/useLaunchLocalRunner.d.ts +84 -0
- package/runner/useLaunchLocalRunner.d.ts.map +1 -0
- package/runner/useLaunchLocalRunner.js +75 -0
- package/runner/useLaunchLocalRunner.js.map +1 -0
- package/runner/useRunnerList.d.ts +49 -0
- package/runner/useRunnerList.d.ts.map +1 -0
- package/runner/useRunnerList.js +70 -0
- package/runner/useRunnerList.js.map +1 -0
- package/runner/useStopRunner.d.ts +53 -0
- package/runner/useStopRunner.d.ts.map +1 -0
- package/runner/useStopRunner.js +50 -0
- package/runner/useStopRunner.js.map +1 -0
- package/session/draft.d.ts +53 -0
- package/session/draft.d.ts.map +1 -0
- package/session/draft.js +45 -0
- package/session/draft.js.map +1 -0
- package/session/index.d.ts +10 -0
- package/session/index.d.ts.map +1 -1
- package/session/index.js +5 -0
- package/session/index.js.map +1 -1
- package/session/useCreateSession.d.ts +8 -0
- package/session/useCreateSession.d.ts.map +1 -1
- package/session/useCreateSession.js +1 -0
- package/session/useCreateSession.js.map +1 -1
- package/session/useEditSessionPrep.d.ts +26 -0
- package/session/useEditSessionPrep.d.ts.map +1 -0
- package/session/useEditSessionPrep.js +83 -0
- package/session/useEditSessionPrep.js.map +1 -0
- package/session/useNewSessionFlow.d.ts +110 -0
- package/session/useNewSessionFlow.d.ts.map +1 -0
- package/session/useNewSessionFlow.js +184 -0
- package/session/useNewSessionFlow.js.map +1 -0
- package/session/usePersistedModel.d.ts +18 -0
- package/session/usePersistedModel.d.ts.map +1 -0
- package/session/usePersistedModel.js +31 -0
- package/session/usePersistedModel.js.map +1 -0
- package/session/useSessionPageFlow.d.ts +104 -0
- package/session/useSessionPageFlow.d.ts.map +1 -0
- package/session/useSessionPageFlow.js +172 -0
- package/session/useSessionPageFlow.js.map +1 -0
- package/skill/SkillDetailView.js +3 -3
- package/skill/SkillDetailView.js.map +1 -1
- package/skill/SkillPicker.js +3 -3
- package/skill/SkillPicker.js.map +1 -1
- package/src/agent/AgentDetailView.tsx +8 -8
- package/src/agent/AgentPicker.tsx +3 -3
- package/src/api-key/ApiKeyCreatedAlert.tsx +2 -2
- package/src/api-key/ApiKeyListPanel.tsx +6 -6
- package/src/api-key/CreateApiKeyForm.tsx +2 -2
- package/src/attachment/AttachmentChipList.tsx +3 -3
- package/src/color-mode.ts +75 -0
- package/src/composer/ComposerToolbar.tsx +29 -7
- package/src/composer/ConfigureMenu.tsx +6 -6
- package/src/composer/ContextChip.tsx +1 -1
- package/src/composer/ContextPopover.tsx +2 -2
- package/src/composer/SessionComposer.tsx +34 -5
- package/src/environment/CreateEnvironmentForm.tsx +3 -3
- package/src/environment/EnvVarForm.tsx +6 -6
- package/src/environment/EnvironmentListPanel.tsx +3 -3
- package/src/environment/EnvironmentVariableEditor.tsx +7 -7
- package/src/error/ErrorMessage.tsx +5 -5
- package/src/execution/ApprovalCard.tsx +5 -5
- package/src/execution/ArtifactCard.tsx +2 -2
- package/src/execution/ArtifactPreviewModal.tsx +4 -4
- package/src/execution/FollowUpInput.tsx +2 -2
- package/src/execution/McpToolDetail.tsx +4 -4
- package/src/execution/MessageEntry.tsx +1 -1
- package/src/execution/MessageThread.tsx +1 -1
- package/src/execution/SessionVariablesInput.tsx +7 -7
- package/src/execution/SubAgentSection.tsx +5 -5
- package/src/execution/ToolCallDetail.tsx +2 -2
- package/src/execution/ToolCallGroup.tsx +3 -3
- package/src/execution/ToolCallItem.tsx +4 -4
- package/src/execution/WriteBackCard.tsx +5 -5
- package/src/execution/tool-rendering-primitives.tsx +5 -5
- package/src/github/GitHubRepoPicker.tsx +5 -5
- package/src/iam-policy/GrantAccessForm.tsx +2 -2
- package/src/iam-policy/OrgMembersPanel.tsx +11 -11
- package/src/identity-provider/CreateIdentityProviderForm.tsx +4 -4
- package/src/identity-provider/IdentityProviderDetailPanel.tsx +7 -7
- package/src/identity-provider/IdentityProviderListPanel.tsx +7 -7
- package/src/identity-provider/IdentityProviderWizard.tsx +8 -8
- package/src/identity-provider/ProviderPicker.tsx +2 -2
- package/src/index.ts +46 -7
- package/src/internal/CloudFeatureNotice.tsx +1 -1
- package/src/internal/Tabs.tsx +1 -1
- package/src/internal/markdown-components.tsx +2 -2
- package/src/invitation/InvitationCreatedAlert.tsx +2 -2
- package/src/invitation/InvitationManager.tsx +9 -9
- package/src/invitation/InvitationRedemption.tsx +11 -11
- package/src/library/ResourceCountCard.tsx +1 -1
- package/src/library/ResourceListView.tsx +7 -7
- package/src/mcp-server/McpServerConfigPanel.tsx +7 -7
- package/src/mcp-server/McpServerConnectDialog.tsx +5 -5
- package/src/mcp-server/McpServerDetailView.tsx +19 -19
- package/src/mcp-server/McpServerPicker.tsx +6 -6
- package/src/mcp-server/McpToolSelector.tsx +4 -4
- package/src/mcp-server/OAuthAppForm.tsx +3 -3
- package/src/models/ModelSelector.tsx +1 -1
- package/src/oauth-app/CreateOAuthAppForm.tsx +3 -3
- package/src/oauth-app/OAuthAppDetailPanel.tsx +7 -7
- package/src/oauth-app/OAuthAppListPanel.tsx +3 -3
- package/src/organization/CreateOrganizationForm.tsx +3 -3
- package/src/organization/OrgProfilePanel.tsx +4 -5
- package/src/platform-client/CreatePlatformClientForm.tsx +6 -6
- package/src/platform-client/PlatformClientDetailPanel.tsx +19 -19
- package/src/platform-client/PlatformClientListPanel.tsx +7 -7
- package/src/platform-client/PlatformClientSecretAlert.tsx +2 -2
- package/src/provider.tsx +52 -2
- package/src/runner/RunnerListPanel.tsx +725 -0
- package/src/runner/RunnerPicker.tsx +319 -0
- package/src/runner/__tests__/useDeleteRunner.test.tsx +150 -0
- package/src/runner/__tests__/useLaunchLocalRunner.test.tsx +223 -0
- package/src/runner/__tests__/useStopRunner.test.tsx +154 -0
- package/src/runner/index.ts +34 -0
- package/src/runner/phase.ts +62 -0
- package/src/runner/useDeleteRunner.ts +67 -0
- package/src/runner/useLaunchLocalRunner.ts +139 -0
- package/src/runner/useRunnerList.ts +114 -0
- package/src/runner/useStopRunner.ts +92 -0
- package/src/session/draft.ts +82 -0
- package/src/session/index.ts +28 -0
- package/src/session/useCreateSession.ts +9 -0
- package/src/session/useEditSessionPrep.ts +111 -0
- package/src/session/useNewSessionFlow.ts +283 -0
- package/src/session/usePersistedModel.ts +41 -0
- package/src/session/useSessionPageFlow.ts +280 -0
- package/src/skill/SkillDetailView.tsx +5 -5
- package/src/skill/SkillPicker.tsx +3 -3
- package/src/styles.css +25 -1
- package/src/usage/OrgUsagePanel.tsx +4 -4
- package/src/workspace/WorkspaceEditor.tsx +78 -66
- package/src/workspace/index.ts +0 -8
- package/styles.css +1 -1
- package/usage/OrgUsagePanel.js +2 -2
- package/usage/OrgUsagePanel.js.map +1 -1
- package/workspace/WorkspaceEditor.d.ts +28 -3
- package/workspace/WorkspaceEditor.d.ts.map +1 -1
- package/workspace/WorkspaceEditor.js +24 -25
- package/workspace/WorkspaceEditor.js.map +1 -1
- package/workspace/index.d.ts +0 -4
- package/workspace/index.d.ts.map +1 -1
- package/workspace/index.js +0 -2
- package/workspace/index.js.map +1 -1
- package/src/workspace/FolderBrowser.tsx +0 -579
- package/src/workspace/useFolderListing.ts +0 -164
- package/workspace/FolderBrowser.d.ts +0 -37
- package/workspace/FolderBrowser.d.ts.map +0 -1
- package/workspace/FolderBrowser.js +0 -188
- package/workspace/FolderBrowser.js.map +0 -1
- package/workspace/useFolderListing.d.ts +0 -73
- package/workspace/useFolderListing.d.ts.map +0 -1
- package/workspace/useFolderListing.js +0 -110
- 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
|
+
});
|