@stigmer/react 1.0.3 → 1.0.4
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/agent/__tests__/useAgent.test.d.ts +2 -0
- package/agent/__tests__/useAgent.test.d.ts.map +1 -0
- package/agent/__tests__/useAgent.test.js +86 -0
- package/agent/__tests__/useAgent.test.js.map +1 -0
- package/agent/__tests__/useDefaultAgent.test.js +106 -223
- package/agent/__tests__/useDefaultAgent.test.js.map +1 -1
- package/agent/useDefaultAgent.d.ts +9 -0
- package/agent/useDefaultAgent.d.ts.map +1 -1
- package/agent/useDefaultAgent.js +35 -5
- package/agent/useDefaultAgent.js.map +1 -1
- package/composer/ComposerToolbar.d.ts +18 -16
- package/composer/ComposerToolbar.d.ts.map +1 -1
- package/composer/ComposerToolbar.js +10 -11
- package/composer/ComposerToolbar.js.map +1 -1
- package/composer/ConfigureMenu.d.ts.map +1 -1
- package/composer/ConfigureMenu.js +1 -1
- package/composer/ConfigureMenu.js.map +1 -1
- package/composer/ContextPopover.d.ts +1 -3
- package/composer/ContextPopover.d.ts.map +1 -1
- package/composer/ContextPopover.js +2 -2
- package/composer/ContextPopover.js.map +1 -1
- package/composer/SessionComposer.js +5 -5
- package/composer/SessionComposer.js.map +1 -1
- package/composer/icons.js +3 -3
- package/internal/withTimeout.d.ts +8 -0
- package/internal/withTimeout.d.ts.map +1 -0
- package/internal/withTimeout.js +19 -0
- package/internal/withTimeout.js.map +1 -0
- package/package.json +4 -4
- package/session/__tests__/useCreateSession.test.js +99 -191
- package/session/__tests__/useCreateSession.test.js.map +1 -1
- package/session/__tests__/useNewSessionFlow.test.js +71 -0
- package/session/__tests__/useNewSessionFlow.test.js.map +1 -1
- package/session/__tests__/useSession.test.js +71 -108
- package/session/__tests__/useSession.test.js.map +1 -1
- package/session/__tests__/useSessionList.test.d.ts +2 -0
- package/session/__tests__/useSessionList.test.d.ts.map +1 -0
- package/session/__tests__/useSessionList.test.js +63 -0
- package/session/__tests__/useSessionList.test.js.map +1 -0
- package/session/useNewSessionFlow.d.ts.map +1 -1
- package/session/useNewSessionFlow.js +13 -7
- package/session/useNewSessionFlow.js.map +1 -1
- package/src/agent/__tests__/useAgent.test.tsx +116 -0
- package/src/agent/__tests__/useDefaultAgent.test.tsx +115 -240
- package/src/agent/useDefaultAgent.ts +53 -2
- package/src/composer/ComposerToolbar.tsx +76 -96
- package/src/composer/ConfigureMenu.tsx +16 -14
- package/src/composer/ContextPopover.tsx +11 -11
- package/src/composer/SessionComposer.tsx +6 -6
- package/src/composer/icons.tsx +6 -6
- package/src/internal/withTimeout.ts +25 -0
- package/src/session/__tests__/useCreateSession.test.tsx +114 -235
- package/src/session/__tests__/useNewSessionFlow.test.tsx +96 -1
- package/src/session/__tests__/useSession.test.tsx +82 -141
- package/src/session/__tests__/useSessionList.test.tsx +86 -0
- package/src/session/useNewSessionFlow.ts +18 -9
- package/styles.css +1 -1
|
@@ -20,21 +20,31 @@ export interface ComposerToolbarProps {
|
|
|
20
20
|
readonly canSend: boolean;
|
|
21
21
|
readonly onSend: () => void;
|
|
22
22
|
|
|
23
|
-
// --
|
|
23
|
+
// -- Left group: Primary state --------------------------------------------
|
|
24
|
+
|
|
25
|
+
readonly showHarnessSelector: boolean;
|
|
26
|
+
readonly harness?: HarnessOption;
|
|
27
|
+
readonly onHarnessChange: (harness: HarnessOption) => void;
|
|
28
|
+
|
|
29
|
+
readonly showInteractionModePicker: boolean;
|
|
30
|
+
readonly interactionMode?: InteractionModeOption;
|
|
31
|
+
readonly onInteractionModeChange: (mode: InteractionModeOption) => void;
|
|
32
|
+
|
|
33
|
+
readonly showModelSelector: boolean;
|
|
34
|
+
readonly modelId?: string;
|
|
35
|
+
readonly onModelChange: (id: string) => void;
|
|
36
|
+
|
|
37
|
+
// -- Right group: Secondary actions (icon-only) ---------------------------
|
|
24
38
|
|
|
25
39
|
readonly showWorkspace: boolean;
|
|
26
40
|
readonly workspaceCount: number;
|
|
27
41
|
/** Pre-built workspace editor content for the popover. */
|
|
28
42
|
readonly workspaceContent: React.ReactNode;
|
|
29
43
|
|
|
30
|
-
// -- Tier 1: Attach -------------------------------------------------------
|
|
31
|
-
|
|
32
44
|
readonly showAttach: boolean;
|
|
33
45
|
readonly attachmentCount: number;
|
|
34
46
|
readonly onAttachClick: () => void;
|
|
35
47
|
|
|
36
|
-
// -- Tier 2: Configure menu -----------------------------------------------
|
|
37
|
-
|
|
38
48
|
readonly configureItems: readonly ConfigureMenuItem[];
|
|
39
49
|
readonly configOpen: boolean;
|
|
40
50
|
readonly onConfigOpenChange: (open: boolean) => void;
|
|
@@ -42,39 +52,23 @@ export interface ComposerToolbarProps {
|
|
|
42
52
|
readonly onConfigActivePanelChange: (panel: string | null) => void;
|
|
43
53
|
/** Render the picker content for a given configure panel id. */
|
|
44
54
|
readonly renderConfigPanel: (itemId: string) => React.ReactNode;
|
|
45
|
-
|
|
46
|
-
// -- Harness selector -----------------------------------------------------
|
|
47
|
-
|
|
48
|
-
readonly showHarnessSelector: boolean;
|
|
49
|
-
readonly harness?: HarnessOption;
|
|
50
|
-
readonly onHarnessChange: (harness: HarnessOption) => void;
|
|
51
|
-
|
|
52
|
-
// -- Interaction mode picker ------------------------------------------------
|
|
53
|
-
|
|
54
|
-
readonly showInteractionModePicker: boolean;
|
|
55
|
-
readonly interactionMode?: InteractionModeOption;
|
|
56
|
-
readonly onInteractionModeChange: (mode: InteractionModeOption) => void;
|
|
57
|
-
|
|
58
|
-
// -- Model selector -------------------------------------------------------
|
|
59
|
-
|
|
60
|
-
readonly showModelSelector: boolean;
|
|
61
|
-
readonly modelId?: string;
|
|
62
|
-
readonly onModelChange: (id: string) => void;
|
|
63
55
|
}
|
|
64
56
|
|
|
65
57
|
/**
|
|
66
58
|
* Composer toolbar — Zone 3 of the SessionComposer.
|
|
67
59
|
*
|
|
68
|
-
*
|
|
60
|
+
* Layout follows a two-group pattern inspired by Cursor's compact approach:
|
|
69
61
|
*
|
|
70
|
-
* **
|
|
71
|
-
* **
|
|
72
|
-
* **Right edge:** Runner Picker, Model Selector, Send
|
|
62
|
+
* **Left group (primary state):** Interaction Mode, Model Selector
|
|
63
|
+
* **Right group (secondary actions, icon-only):** Workspace, Attach, Configure, Send
|
|
73
64
|
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
65
|
+
* Primary state indicators retain text labels (users glance at mode and model
|
|
66
|
+
* frequently). Secondary actions use icon-only buttons with tooltips and
|
|
67
|
+
* aria-labels — they are actions triggered occasionally, not state to monitor.
|
|
76
68
|
*
|
|
77
|
-
*
|
|
69
|
+
* This separation follows Fitts's Law (related actions clustered near Send),
|
|
70
|
+
* Gestalt proximity (left = "what," right = "do"), and Nielsen H8 (minimal
|
|
71
|
+
* visual weight for secondary controls).
|
|
78
72
|
*/
|
|
79
73
|
export function ComposerToolbar({
|
|
80
74
|
disabled,
|
|
@@ -103,23 +97,50 @@ export function ComposerToolbar({
|
|
|
103
97
|
modelId,
|
|
104
98
|
onModelChange,
|
|
105
99
|
}: ComposerToolbarProps) {
|
|
106
|
-
const hasTier1 = showAttach || showWorkspace;
|
|
107
|
-
const hasTier2 = configureItems.length > 0;
|
|
108
100
|
const showHarnessSeparate = showHarnessSelector && !showModelSelector;
|
|
109
|
-
const hasExecParams = showHarnessSeparate || showInteractionModePicker || showModelSelector;
|
|
110
101
|
|
|
111
102
|
return (
|
|
112
103
|
<div className="flex items-center justify-between gap-2 border-t border-border-muted px-3 py-2">
|
|
113
|
-
|
|
114
|
-
|
|
104
|
+
{/* ---- Left group: Primary state (Mode + Model) ---- */}
|
|
105
|
+
|
|
106
|
+
<div className="flex min-w-0 items-center gap-1.5">
|
|
107
|
+
{showInteractionModePicker && (
|
|
108
|
+
<InteractionModePicker
|
|
109
|
+
value={interactionMode ?? "agent"}
|
|
110
|
+
onValueChange={onInteractionModeChange}
|
|
111
|
+
disabled={disabled}
|
|
112
|
+
/>
|
|
113
|
+
)}
|
|
114
|
+
|
|
115
|
+
{showHarnessSeparate && (
|
|
116
|
+
<HarnessSelector
|
|
117
|
+
value={harness ?? "native"}
|
|
118
|
+
onValueChange={onHarnessChange}
|
|
119
|
+
disabled={disabled}
|
|
120
|
+
/>
|
|
121
|
+
)}
|
|
122
|
+
|
|
123
|
+
{showModelSelector && (
|
|
124
|
+
<ModelSelector
|
|
125
|
+
value={modelId}
|
|
126
|
+
onValueChange={onModelChange}
|
|
127
|
+
harness={showHarnessSelector ? undefined : harness}
|
|
128
|
+
initialHarness={showHarnessSelector ? harness : undefined}
|
|
129
|
+
onHarnessChange={showHarnessSelector ? onHarnessChange : undefined}
|
|
130
|
+
disabled={disabled}
|
|
131
|
+
/>
|
|
132
|
+
)}
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
{/* ---- Right group: Secondary actions (icon-only) + Send ---- */}
|
|
115
136
|
|
|
137
|
+
<div className="flex shrink-0 items-center gap-1">
|
|
116
138
|
{showWorkspace && (
|
|
117
139
|
<ContextPopover
|
|
118
140
|
icon={<WorkspaceIcon />}
|
|
119
141
|
label="Workspace"
|
|
120
142
|
count={workspaceCount}
|
|
121
143
|
disabled={disabled}
|
|
122
|
-
hideLabel
|
|
123
144
|
>
|
|
124
145
|
{workspaceContent}
|
|
125
146
|
</ContextPopover>
|
|
@@ -130,31 +151,25 @@ export function ComposerToolbar({
|
|
|
130
151
|
type="button"
|
|
131
152
|
disabled={disabled}
|
|
132
153
|
onClick={onAttachClick}
|
|
154
|
+
title="Attach files"
|
|
133
155
|
className={cn(
|
|
134
|
-
"inline-flex items-center
|
|
156
|
+
"inline-flex h-8 w-8 items-center justify-center rounded-md text-xs transition-colors",
|
|
135
157
|
"text-muted-foreground hover:text-foreground hover:bg-accent-hover",
|
|
136
158
|
"disabled:pointer-events-none disabled:opacity-50",
|
|
137
159
|
)}
|
|
138
160
|
aria-label="Attach files"
|
|
139
161
|
>
|
|
140
|
-
<
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
162
|
+
<span className="relative">
|
|
163
|
+
<PaperclipIcon />
|
|
164
|
+
{attachmentCount > 0 && (
|
|
165
|
+
<span className="absolute -right-1.5 -top-1.5 flex h-3.5 min-w-3.5 items-center justify-center rounded-full bg-primary px-0.5 text-[0.5rem] font-medium leading-none text-primary-foreground">
|
|
166
|
+
{attachmentCount}
|
|
167
|
+
</span>
|
|
168
|
+
)}
|
|
169
|
+
</span>
|
|
147
170
|
</button>
|
|
148
171
|
)}
|
|
149
172
|
|
|
150
|
-
{/* ---- Separator between Tier 1 and Tier 2 ---- */}
|
|
151
|
-
|
|
152
|
-
{hasTier1 && hasTier2 && (
|
|
153
|
-
<div className="mx-0.5 h-4 w-px bg-border/50" aria-hidden="true" />
|
|
154
|
-
)}
|
|
155
|
-
|
|
156
|
-
{/* ---- Tier 2: Agent configuration (behind Configure menu) ---- */}
|
|
157
|
-
|
|
158
173
|
<ConfigureMenu
|
|
159
174
|
open={configOpen}
|
|
160
175
|
onOpenChange={onConfigOpenChange}
|
|
@@ -165,51 +180,16 @@ export function ComposerToolbar({
|
|
|
165
180
|
disabled={disabled}
|
|
166
181
|
/>
|
|
167
182
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
<
|
|
176
|
-
|
|
177
|
-
onValueChange={onHarnessChange}
|
|
178
|
-
disabled={disabled}
|
|
179
|
-
/>
|
|
180
|
-
)}
|
|
181
|
-
|
|
182
|
-
{showInteractionModePicker && (
|
|
183
|
-
<InteractionModePicker
|
|
184
|
-
value={interactionMode ?? "agent"}
|
|
185
|
-
onValueChange={onInteractionModeChange}
|
|
186
|
-
disabled={disabled}
|
|
187
|
-
/>
|
|
188
|
-
)}
|
|
189
|
-
|
|
190
|
-
{showModelSelector && (
|
|
191
|
-
<ModelSelector
|
|
192
|
-
value={modelId}
|
|
193
|
-
onValueChange={onModelChange}
|
|
194
|
-
harness={showHarnessSelector ? undefined : harness}
|
|
195
|
-
initialHarness={showHarnessSelector ? harness : undefined}
|
|
196
|
-
onHarnessChange={showHarnessSelector ? onHarnessChange : undefined}
|
|
197
|
-
disabled={disabled}
|
|
198
|
-
/>
|
|
199
|
-
)}
|
|
183
|
+
<button
|
|
184
|
+
type="button"
|
|
185
|
+
disabled={!canSend}
|
|
186
|
+
onClick={onSend}
|
|
187
|
+
className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-primary text-primary-foreground transition-colors hover:bg-primary-hover disabled:pointer-events-none disabled:opacity-40"
|
|
188
|
+
aria-label="Send message"
|
|
189
|
+
>
|
|
190
|
+
{isSubmitting ? <SpinnerIcon /> : <ArrowUpIcon />}
|
|
191
|
+
</button>
|
|
200
192
|
</div>
|
|
201
|
-
|
|
202
|
-
{/* ---- Send button ---- */}
|
|
203
|
-
|
|
204
|
-
<button
|
|
205
|
-
type="button"
|
|
206
|
-
disabled={!canSend}
|
|
207
|
-
onClick={onSend}
|
|
208
|
-
className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-primary text-primary-foreground transition-colors hover:bg-primary-hover disabled:pointer-events-none disabled:opacity-40"
|
|
209
|
-
aria-label="Send message"
|
|
210
|
-
>
|
|
211
|
-
{isSubmitting ? <SpinnerIcon /> : <ArrowUpIcon />}
|
|
212
|
-
</button>
|
|
213
193
|
</div>
|
|
214
194
|
);
|
|
215
195
|
}
|
|
@@ -73,26 +73,28 @@ export function ConfigureMenu({
|
|
|
73
73
|
<Popover.Root open={open} onOpenChange={handleOpenChange}>
|
|
74
74
|
<Popover.Trigger
|
|
75
75
|
disabled={disabled}
|
|
76
|
+
title="Configure"
|
|
76
77
|
className={cn(
|
|
77
|
-
"inline-flex items-center
|
|
78
|
+
"inline-flex h-8 w-8 items-center justify-center rounded-md text-xs transition-colors",
|
|
78
79
|
"text-muted-foreground hover:text-foreground hover:bg-accent-hover",
|
|
79
80
|
"disabled:pointer-events-none disabled:opacity-50",
|
|
80
81
|
)}
|
|
81
82
|
aria-label="Configure agent, tools, and skills"
|
|
82
83
|
>
|
|
83
|
-
<
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
84
|
+
<span className="relative">
|
|
85
|
+
<ConfigureIcon />
|
|
86
|
+
{totalCount > 0 && (
|
|
87
|
+
<span className="absolute -right-1.5 -top-1.5 flex h-3.5 min-w-3.5 items-center justify-center rounded-full bg-primary px-0.5 text-[0.5rem] font-medium leading-none text-primary-foreground">
|
|
88
|
+
{totalCount}
|
|
89
|
+
</span>
|
|
90
|
+
)}
|
|
91
|
+
{hasWarning && totalCount === 0 && (
|
|
92
|
+
<span
|
|
93
|
+
className="absolute -right-0.5 -top-0.5 inline-block h-2 w-2 rounded-full bg-warning"
|
|
94
|
+
aria-label="Configuration needed"
|
|
95
|
+
/>
|
|
96
|
+
)}
|
|
97
|
+
</span>
|
|
96
98
|
</Popover.Trigger>
|
|
97
99
|
<Popover.Portal container={portalContainer}>
|
|
98
100
|
<Popover.Positioner sideOffset={8} align="start">
|
|
@@ -10,7 +10,6 @@ export function ContextPopover({
|
|
|
10
10
|
disabled,
|
|
11
11
|
open,
|
|
12
12
|
onOpenChange,
|
|
13
|
-
hideLabel,
|
|
14
13
|
}: {
|
|
15
14
|
icon: React.ReactNode;
|
|
16
15
|
label: string;
|
|
@@ -19,8 +18,6 @@ export function ContextPopover({
|
|
|
19
18
|
disabled?: boolean;
|
|
20
19
|
open?: boolean;
|
|
21
20
|
onOpenChange?: (open: boolean) => void;
|
|
22
|
-
/** When true, hides the text label on small viewports (icon-only). */
|
|
23
|
-
hideLabel?: boolean;
|
|
24
21
|
}) {
|
|
25
22
|
const portalContainer = useStigmerPortalContainer();
|
|
26
23
|
|
|
@@ -28,19 +25,22 @@ export function ContextPopover({
|
|
|
28
25
|
<Popover.Root open={open} onOpenChange={onOpenChange}>
|
|
29
26
|
<Popover.Trigger
|
|
30
27
|
disabled={disabled}
|
|
28
|
+
title={label}
|
|
29
|
+
aria-label={label}
|
|
31
30
|
className={cn(
|
|
32
|
-
"inline-flex items-center
|
|
31
|
+
"inline-flex h-8 w-8 items-center justify-center rounded-md text-xs transition-colors",
|
|
33
32
|
"text-muted-foreground hover:text-foreground hover:bg-accent-hover",
|
|
34
33
|
"disabled:pointer-events-none disabled:opacity-50",
|
|
35
34
|
)}
|
|
36
35
|
>
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
36
|
+
<span className="relative">
|
|
37
|
+
{icon}
|
|
38
|
+
{count > 0 && (
|
|
39
|
+
<span className="absolute -right-1.5 -top-1.5 flex h-3.5 min-w-3.5 items-center justify-center rounded-full bg-primary px-0.5 text-[0.5rem] font-medium leading-none text-primary-foreground">
|
|
40
|
+
{count}
|
|
41
|
+
</span>
|
|
42
|
+
)}
|
|
43
|
+
</span>
|
|
44
44
|
</Popover.Trigger>
|
|
45
45
|
<Popover.Portal container={portalContainer}>
|
|
46
46
|
<Popover.Positioner sideOffset={8} align="start">
|
|
@@ -381,9 +381,9 @@ export interface SessionComposerProps {
|
|
|
381
381
|
* Combines a self-resizing textarea, model selector, and context pickers
|
|
382
382
|
* (agent, workspace, MCP servers, skills) into a single input card.
|
|
383
383
|
*
|
|
384
|
-
* The toolbar uses a two-
|
|
385
|
-
* - **
|
|
386
|
-
* - **
|
|
384
|
+
* The toolbar uses a two-group layout:
|
|
385
|
+
* - **Left (primary state):** Interaction Mode, Model Selector
|
|
386
|
+
* - **Right (secondary actions, icon-only):** Workspace, Attach, Configure (Agent, MCP, Skills, Secrets), Send
|
|
387
387
|
*
|
|
388
388
|
* Selected items render as removable chips between the textarea and toolbar.
|
|
389
389
|
*
|
|
@@ -1510,9 +1510,6 @@ const SessionComposerInner = forwardRef<SessionComposerHandle, SessionComposerPr
|
|
|
1510
1510
|
isSubmitting={isSubmitting}
|
|
1511
1511
|
canSend={canSend}
|
|
1512
1512
|
onSend={composer.submit}
|
|
1513
|
-
showAttach={showAttach}
|
|
1514
|
-
attachmentCount={attachments.entries.length}
|
|
1515
|
-
onAttachClick={() => fileInputRef.current?.click()}
|
|
1516
1513
|
showWorkspace={showWorkspace}
|
|
1517
1514
|
workspaceCount={workspaceCount}
|
|
1518
1515
|
workspaceContent={
|
|
@@ -1540,6 +1537,9 @@ const SessionComposerInner = forwardRef<SessionComposerHandle, SessionComposerPr
|
|
|
1540
1537
|
</div>
|
|
1541
1538
|
: null
|
|
1542
1539
|
}
|
|
1540
|
+
showAttach={showAttach}
|
|
1541
|
+
attachmentCount={attachments.entries.length}
|
|
1542
|
+
onAttachClick={() => fileInputRef.current?.click()}
|
|
1543
1543
|
configureItems={configureItems}
|
|
1544
1544
|
configOpen={configOpen}
|
|
1545
1545
|
onConfigOpenChange={handleConfigOpenChange}
|
package/src/composer/icons.tsx
CHANGED
|
@@ -93,8 +93,8 @@ export function XIcon() {
|
|
|
93
93
|
export function PaperclipIcon() {
|
|
94
94
|
return (
|
|
95
95
|
<svg
|
|
96
|
-
width="
|
|
97
|
-
height="
|
|
96
|
+
width="16"
|
|
97
|
+
height="16"
|
|
98
98
|
viewBox="0 0 16 16"
|
|
99
99
|
fill="none"
|
|
100
100
|
stroke="currentColor"
|
|
@@ -133,8 +133,8 @@ export function AgentIcon() {
|
|
|
133
133
|
export function WorkspaceIcon() {
|
|
134
134
|
return (
|
|
135
135
|
<svg
|
|
136
|
-
width="
|
|
137
|
-
height="
|
|
136
|
+
width="16"
|
|
137
|
+
height="16"
|
|
138
138
|
viewBox="0 0 14 14"
|
|
139
139
|
fill="none"
|
|
140
140
|
stroke="currentColor"
|
|
@@ -238,8 +238,8 @@ export function RunnerIcon() {
|
|
|
238
238
|
export function ConfigureIcon() {
|
|
239
239
|
return (
|
|
240
240
|
<svg
|
|
241
|
-
width="
|
|
242
|
-
height="
|
|
241
|
+
width="16"
|
|
242
|
+
height="16"
|
|
243
243
|
viewBox="0 0 16 16"
|
|
244
244
|
fill="none"
|
|
245
245
|
stroke="currentColor"
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Races a promise against a timer. Rejects with `message` if the timer
|
|
3
|
+
* fires before the promise settles.
|
|
4
|
+
*
|
|
5
|
+
* @internal Not part of the public `@stigmer/react` API.
|
|
6
|
+
*/
|
|
7
|
+
export function withTimeout<T>(
|
|
8
|
+
promise: Promise<T>,
|
|
9
|
+
ms: number,
|
|
10
|
+
message: string,
|
|
11
|
+
): Promise<T> {
|
|
12
|
+
return new Promise<T>((resolve, reject) => {
|
|
13
|
+
const timer = setTimeout(() => reject(new Error(message)), ms);
|
|
14
|
+
promise.then(
|
|
15
|
+
(value) => {
|
|
16
|
+
clearTimeout(timer);
|
|
17
|
+
resolve(value);
|
|
18
|
+
},
|
|
19
|
+
(reason) => {
|
|
20
|
+
clearTimeout(timer);
|
|
21
|
+
reject(reason);
|
|
22
|
+
},
|
|
23
|
+
);
|
|
24
|
+
});
|
|
25
|
+
}
|