@desplega.ai/agent-swarm 1.86.0 → 1.88.0
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 +2 -1
- package/openapi.json +84 -1
- package/package.json +7 -5
- package/src/be/db-queries/tracker.ts +21 -0
- package/src/be/db.ts +284 -21
- package/src/be/migrations/079_task_followup_config.sql +1 -0
- package/src/be/migrations/080_skill_system_defaults.sql +8 -0
- package/src/be/modelsdev-cache.json +77652 -73973
- package/src/be/seed/registry.ts +3 -2
- package/src/be/seed-skills/index.ts +172 -0
- package/src/cli.tsx +55 -0
- package/src/commands/context-preamble.ts +272 -0
- package/src/commands/e2b-stack-wizard.tsx +394 -0
- package/src/commands/e2b.ts +2027 -0
- package/src/commands/onboard/dashboard-url.ts +29 -0
- package/src/commands/onboard/steps/post-dashboard.tsx +3 -1
- package/src/commands/onboard.tsx +3 -1
- package/src/commands/resume-session.ts +35 -78
- package/src/commands/runner.ts +126 -13
- package/src/e2b/dispatch.ts +645 -0
- package/src/e2b/env.ts +206 -0
- package/src/heartbeat/heartbeat.ts +145 -30
- package/src/heartbeat/templates.ts +11 -7
- package/src/http/memory.ts +13 -1
- package/src/http/session-data.ts +8 -1
- package/src/http/skills.ts +53 -0
- package/src/http/tasks.ts +152 -3
- package/src/http/webhooks.ts +75 -0
- package/src/integrations/kapso/client.ts +82 -0
- package/src/jira/sync.ts +4 -4
- package/src/linear/sync.ts +6 -5
- package/src/memory/automatic-task-gate.ts +47 -0
- package/src/prompts/base-prompt.ts +16 -1
- package/src/prompts/session-templates.ts +51 -0
- package/src/providers/claude-adapter.ts +29 -76
- package/src/providers/claude-managed-adapter.ts +61 -75
- package/src/providers/codex-adapter.ts +37 -18
- package/src/providers/codex-oauth/auth-json.ts +18 -1
- package/src/providers/codex-oauth/flow.ts +24 -1
- package/src/providers/ctx-mode-env.ts +10 -0
- package/src/providers/opencode-adapter.ts +50 -1
- package/src/providers/types.ts +6 -0
- package/src/slack/blocks.ts +12 -4
- package/src/slack/watcher.ts +3 -3
- package/src/tasks/worker-follow-up.ts +162 -2
- package/src/telemetry.ts +25 -2
- package/src/templates.d.ts +4 -0
- package/src/tests/base-prompt.test.ts +41 -0
- package/src/tests/claude-adapter.test.ts +87 -24
- package/src/tests/claude-managed-adapter.test.ts +38 -52
- package/src/tests/codex-adapter.test.ts +95 -31
- package/src/tests/codex-oauth.test.ts +149 -3
- package/src/tests/codex-pool.test.ts +14 -3
- package/src/tests/e2b-dispatch.test.ts +922 -0
- package/src/tests/heartbeat-supersede-resume.test.ts +285 -0
- package/src/tests/heartbeat.test.ts +26 -16
- package/src/tests/http-api-integration.test.ts +113 -0
- package/src/tests/kapso-client.test.ts +74 -1
- package/src/tests/kapso-inbound.test.ts +60 -2
- package/src/tests/opencode-adapter.test.ts +95 -0
- package/src/tests/prompt-template-remaining.test.ts +4 -0
- package/src/tests/prompt-template-session.test.ts +4 -2
- package/src/tests/resume-session.test.ts +42 -50
- package/src/tests/self-improvement.test.ts +89 -0
- package/src/tests/skill-update-scope.test.ts +88 -1
- package/src/tests/slack-blocks.test.ts +15 -0
- package/src/tests/structured-output.test.ts +69 -0
- package/src/tests/system-default-skills.test.ts +119 -0
- package/src/tests/task-completion-idempotency.test.ts +185 -2
- package/src/tests/task-supersede-resume.test.ts +722 -0
- package/src/tests/telemetry-init.test.ts +155 -0
- package/src/tests/vcs-tracking.test.ts +39 -0
- package/src/tools/send-task.ts +12 -1
- package/src/tools/skills/skill-delete.ts +14 -0
- package/src/tools/skills/skill-update.ts +14 -0
- package/src/tools/store-progress.ts +21 -7
- package/src/tools/templates.ts +14 -2
- package/src/types.ts +47 -1
- package/src/workflows/executors/agent-task.ts +3 -0
- package/templates/skills/artifacts/config.json +1 -0
- package/templates/skills/kv-storage/config.json +1 -0
- package/templates/skills/pages/config.json +1 -0
- package/templates/skills/scheduled-task-resilience/config.json +1 -0
- package/templates/skills/swarm-scripts/SKILL.md +91 -0
- package/templates/skills/swarm-scripts/config.json +14 -0
- package/templates/skills/swarm-scripts/content.md +86 -0
- package/templates/skills/workflow-iterate/config.json +1 -0
- package/templates/skills/workflow-structured-output/config.json +1 -0
- package/tsconfig.json +2 -1
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
import { Select, TextInput } from "@inkjs/ui";
|
|
2
|
+
import { Box, Text, useApp } from "ink";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Interactive wizard for `e2b start-stack`. Collects the handful of decisions an
|
|
7
|
+
* operator makes when launching a swarm (name → slug, worker count, provider,
|
|
8
|
+
* TTL, env files, integrations), skipping any step whose flag was already
|
|
9
|
+
* provided on the command line. On finish it resolves a {@link StackWizardResult}
|
|
10
|
+
* back to the caller, which translates it into flags and (optionally) echoes the
|
|
11
|
+
* equivalent headless `--yes` command.
|
|
12
|
+
*
|
|
13
|
+
* Consistency with `onboard*.tsx`: Ink + `@inkjs/ui` `Select`/`TextInput`,
|
|
14
|
+
* one logical question per render, `useApp().exit()` to tear down on completion.
|
|
15
|
+
*
|
|
16
|
+
* Headless detection lives in the caller (`e2b.ts`) — this component is only
|
|
17
|
+
* ever rendered when interactive, so it always reads from stdin.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** Integrations the wizard can toggle. Each maps to an API-side `*_DISABLE` env. */
|
|
21
|
+
export const STACK_INTEGRATIONS = ["slack", "github", "jira", "linear"] as const;
|
|
22
|
+
export type StackIntegration = (typeof STACK_INTEGRATIONS)[number];
|
|
23
|
+
|
|
24
|
+
/** Provider choices surfaced in the wizard picker (mirrors HARNESS_PROVIDER). */
|
|
25
|
+
export const STACK_PROVIDERS = ["claude", "codex", "pi", "devin"] as const;
|
|
26
|
+
|
|
27
|
+
export const DEFAULT_STACK_WORKERS = 1;
|
|
28
|
+
export const DEFAULT_STACK_TIMEOUT_SEC = 3600;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The shape the wizard resolves. `undefined` fields mean "the operator did not
|
|
32
|
+
* change the prebaked flag/default" — the caller already has the flag value, so
|
|
33
|
+
* it only overrides with wizard answers that were actually collected.
|
|
34
|
+
*/
|
|
35
|
+
export type StackWizardResult = {
|
|
36
|
+
swarmSlug: string;
|
|
37
|
+
workers: number;
|
|
38
|
+
provider: string;
|
|
39
|
+
timeoutSec: number;
|
|
40
|
+
/** Shared --env-file paths the operator typed (comma/space separated → array). */
|
|
41
|
+
envFiles: string[];
|
|
42
|
+
/** Map of integration → enabled. A disabled integration becomes `*_DISABLE=true`. */
|
|
43
|
+
integrations: Record<StackIntegration, boolean>;
|
|
44
|
+
noLead: boolean;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/** Which wizard steps to skip because their value already came from a flag. */
|
|
48
|
+
export type StackWizardSkips = {
|
|
49
|
+
swarm?: boolean;
|
|
50
|
+
workers?: boolean;
|
|
51
|
+
provider?: boolean;
|
|
52
|
+
timeout?: boolean;
|
|
53
|
+
envFiles?: boolean;
|
|
54
|
+
integrations?: boolean;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export type StackWizardDefaults = {
|
|
58
|
+
swarmSlug?: string;
|
|
59
|
+
workers: number;
|
|
60
|
+
provider: string;
|
|
61
|
+
timeoutSec: number;
|
|
62
|
+
envFiles: string[];
|
|
63
|
+
integrations: Record<StackIntegration, boolean>;
|
|
64
|
+
noLead: boolean;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export type StackWizardProps = {
|
|
68
|
+
defaults: StackWizardDefaults;
|
|
69
|
+
skips: StackWizardSkips;
|
|
70
|
+
onComplete: (result: StackWizardResult) => void;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/** Lowercase, dash-separated slug suitable for a swarm name / metadata value. */
|
|
74
|
+
export function slugify(input: string): string {
|
|
75
|
+
return (
|
|
76
|
+
input
|
|
77
|
+
.trim()
|
|
78
|
+
.toLowerCase()
|
|
79
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
80
|
+
.replace(/^-+|-+$/g, "")
|
|
81
|
+
.slice(0, 48) || "swarm"
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Human preview of when a TTL of `seconds` from now would expire. */
|
|
86
|
+
function expiryPreview(seconds: number): string {
|
|
87
|
+
const expires = new Date(Date.now() + seconds * 1000);
|
|
88
|
+
const hours = Math.floor(seconds / 3600);
|
|
89
|
+
const minutes = Math.floor((seconds % 3600) / 60);
|
|
90
|
+
const parts: string[] = [];
|
|
91
|
+
if (hours > 0) parts.push(`${hours}h`);
|
|
92
|
+
if (minutes > 0 || hours === 0) parts.push(`${minutes}m`);
|
|
93
|
+
return `${parts.join(" ")} from now (~${expires.toLocaleTimeString()})`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
type WizardStep =
|
|
97
|
+
| "mode"
|
|
98
|
+
| "swarm"
|
|
99
|
+
| "workers"
|
|
100
|
+
| "provider"
|
|
101
|
+
| "timeout"
|
|
102
|
+
| "env_files"
|
|
103
|
+
| "integrations"
|
|
104
|
+
| "done";
|
|
105
|
+
|
|
106
|
+
export function StackWizard({ defaults, skips, onComplete }: StackWizardProps) {
|
|
107
|
+
const { exit } = useApp();
|
|
108
|
+
|
|
109
|
+
// Accumulated answers, seeded from the flag-provided defaults.
|
|
110
|
+
const [result, setResult] = useState<StackWizardResult>({
|
|
111
|
+
swarmSlug: defaults.swarmSlug ?? "",
|
|
112
|
+
workers: defaults.workers,
|
|
113
|
+
provider: defaults.provider,
|
|
114
|
+
timeoutSec: defaults.timeoutSec,
|
|
115
|
+
envFiles: defaults.envFiles,
|
|
116
|
+
integrations: { ...defaults.integrations },
|
|
117
|
+
noLead: defaults.noLead,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Determine the ordered list of steps, skipping any that are flag-satisfied.
|
|
121
|
+
const steps: WizardStep[] = ["mode"];
|
|
122
|
+
if (!skips.swarm) steps.push("swarm");
|
|
123
|
+
if (!skips.workers) steps.push("workers");
|
|
124
|
+
if (!skips.provider) steps.push("provider");
|
|
125
|
+
if (!skips.timeout) steps.push("timeout");
|
|
126
|
+
if (!skips.envFiles) steps.push("env_files");
|
|
127
|
+
if (!skips.integrations) steps.push("integrations");
|
|
128
|
+
steps.push("done");
|
|
129
|
+
|
|
130
|
+
const [stepIdx, setStepIdx] = useState(0);
|
|
131
|
+
const step = steps[stepIdx] ?? "done";
|
|
132
|
+
const [error, setError] = useState("");
|
|
133
|
+
// Toggle scratch state for the integrations multi-step.
|
|
134
|
+
const [integrationToggles, setIntegrationToggles] = useState<Record<StackIntegration, boolean>>({
|
|
135
|
+
...defaults.integrations,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const advance = (partial?: Partial<StackWizardResult>) => {
|
|
139
|
+
setError("");
|
|
140
|
+
if (partial) setResult((r) => ({ ...r, ...partial }));
|
|
141
|
+
const nextIdx = stepIdx + 1;
|
|
142
|
+
setStepIdx(nextIdx);
|
|
143
|
+
if ((steps[nextIdx] ?? "done") === "done") {
|
|
144
|
+
// Resolve on the freshest result; merge any pending partial.
|
|
145
|
+
setResult((r) => {
|
|
146
|
+
const finalResult = { ...r, ...partial };
|
|
147
|
+
// Defer the callback so React finishes its commit before we exit.
|
|
148
|
+
queueMicrotask(() => {
|
|
149
|
+
onComplete(finalResult);
|
|
150
|
+
exit();
|
|
151
|
+
});
|
|
152
|
+
return finalResult;
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
if (step === "done") {
|
|
158
|
+
return (
|
|
159
|
+
<Box flexDirection="column" padding={1}>
|
|
160
|
+
<Text color="green">Configuration captured — launching…</Text>
|
|
161
|
+
</Box>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return (
|
|
166
|
+
<Box flexDirection="column" padding={1}>
|
|
167
|
+
<Text color="cyan" bold>
|
|
168
|
+
Agent Swarm — E2B stack launcher
|
|
169
|
+
</Text>
|
|
170
|
+
{error && (
|
|
171
|
+
<Box marginTop={1}>
|
|
172
|
+
<Text color="red">{error}</Text>
|
|
173
|
+
</Box>
|
|
174
|
+
)}
|
|
175
|
+
|
|
176
|
+
{step === "mode" && (
|
|
177
|
+
<Box marginTop={1} flexDirection="column">
|
|
178
|
+
<Text bold>Create a new swarm or add to an existing one?</Text>
|
|
179
|
+
<Box marginTop={1}>
|
|
180
|
+
<Select
|
|
181
|
+
options={[
|
|
182
|
+
{ label: "Create a new swarm", value: "create" },
|
|
183
|
+
// Phase 4: the add-to-existing flow lives in the standalone
|
|
184
|
+
// `e2b swarms add` command, which has its own TTY swarm picker,
|
|
185
|
+
// `--workers`/`--add-lead` flags, and TTL re-sync to the group's
|
|
186
|
+
// end. Rather than fork that whole flow into this wizard (which
|
|
187
|
+
// is scoped to *creating* a stack), we point the operator at it.
|
|
188
|
+
// This keeps a single, fully-working add path instead of a
|
|
189
|
+
// half-duplicated one.
|
|
190
|
+
{ label: "Add to an existing swarm (run: e2b swarms add <slug>)", value: "add" },
|
|
191
|
+
]}
|
|
192
|
+
onChange={(value) => {
|
|
193
|
+
if (value === "add") {
|
|
194
|
+
setError(
|
|
195
|
+
"To add to an existing swarm, exit and run: e2b swarms add <slug> " +
|
|
196
|
+
"(no slug → it lists your swarms to pick from). Choose 'Create a new swarm' to continue here.",
|
|
197
|
+
);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
advance();
|
|
201
|
+
}}
|
|
202
|
+
/>
|
|
203
|
+
</Box>
|
|
204
|
+
</Box>
|
|
205
|
+
)}
|
|
206
|
+
|
|
207
|
+
{step === "swarm" && (
|
|
208
|
+
<Box marginTop={1} flexDirection="column">
|
|
209
|
+
<Text bold>Swarm name:</Text>
|
|
210
|
+
<Text dimColor>Used as the group slug and dashboard name.</Text>
|
|
211
|
+
<Box marginTop={1}>
|
|
212
|
+
<TextInput
|
|
213
|
+
key="swarm-name"
|
|
214
|
+
placeholder="my-swarm"
|
|
215
|
+
defaultValue={result.swarmSlug}
|
|
216
|
+
onSubmit={(raw) => {
|
|
217
|
+
const slug = slugify(raw || "swarm");
|
|
218
|
+
advance({ swarmSlug: slug });
|
|
219
|
+
}}
|
|
220
|
+
/>
|
|
221
|
+
</Box>
|
|
222
|
+
</Box>
|
|
223
|
+
)}
|
|
224
|
+
|
|
225
|
+
{step === "workers" && (
|
|
226
|
+
<Box marginTop={1} flexDirection="column">
|
|
227
|
+
<Text bold>How many workers?</Text>
|
|
228
|
+
<Box marginTop={1}>
|
|
229
|
+
<TextInput
|
|
230
|
+
key="workers"
|
|
231
|
+
placeholder={String(result.workers)}
|
|
232
|
+
defaultValue={String(result.workers)}
|
|
233
|
+
onSubmit={(raw) => {
|
|
234
|
+
const trimmed = raw.trim();
|
|
235
|
+
const parsed = trimmed ? Number.parseInt(trimmed, 10) : result.workers;
|
|
236
|
+
// Keep this in lock-step with the headless `integerFlag("workers")`
|
|
237
|
+
// contract, which requires a positive integer.
|
|
238
|
+
if (!Number.isFinite(parsed) || parsed < 1) {
|
|
239
|
+
setError("Enter a positive integer (at least 1 worker).");
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
advance({ workers: parsed });
|
|
243
|
+
}}
|
|
244
|
+
/>
|
|
245
|
+
</Box>
|
|
246
|
+
</Box>
|
|
247
|
+
)}
|
|
248
|
+
|
|
249
|
+
{step === "provider" && (
|
|
250
|
+
<Box marginTop={1} flexDirection="column">
|
|
251
|
+
<Text bold>Harness provider:</Text>
|
|
252
|
+
<Box marginTop={1}>
|
|
253
|
+
<Select
|
|
254
|
+
options={STACK_PROVIDERS.map((p) => ({ label: p, value: p }))}
|
|
255
|
+
onChange={(value) => advance({ provider: value })}
|
|
256
|
+
/>
|
|
257
|
+
</Box>
|
|
258
|
+
</Box>
|
|
259
|
+
)}
|
|
260
|
+
|
|
261
|
+
{step === "timeout" && (
|
|
262
|
+
<Box marginTop={1} flexDirection="column">
|
|
263
|
+
<Text bold>Sandbox TTL (seconds):</Text>
|
|
264
|
+
<Text dimColor>
|
|
265
|
+
Default {result.timeoutSec}s — {expiryPreview(result.timeoutSec)}.
|
|
266
|
+
</Text>
|
|
267
|
+
<Box marginTop={1}>
|
|
268
|
+
<TextInput
|
|
269
|
+
key="timeout"
|
|
270
|
+
placeholder={String(result.timeoutSec)}
|
|
271
|
+
defaultValue={String(result.timeoutSec)}
|
|
272
|
+
onSubmit={(raw) => {
|
|
273
|
+
const trimmed = raw.trim();
|
|
274
|
+
const parsed = trimmed ? Number.parseInt(trimmed, 10) : result.timeoutSec;
|
|
275
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
276
|
+
setError("Enter a positive integer (seconds).");
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
advance({ timeoutSec: parsed });
|
|
280
|
+
}}
|
|
281
|
+
/>
|
|
282
|
+
</Box>
|
|
283
|
+
</Box>
|
|
284
|
+
)}
|
|
285
|
+
|
|
286
|
+
{step === "env_files" && (
|
|
287
|
+
<Box marginTop={1} flexDirection="column">
|
|
288
|
+
<Text bold>Shared env file(s):</Text>
|
|
289
|
+
<Text dimColor>Comma-separated paths applied to all roles, or leave blank.</Text>
|
|
290
|
+
<Box marginTop={1}>
|
|
291
|
+
<TextInput
|
|
292
|
+
key="env-files"
|
|
293
|
+
placeholder=".env"
|
|
294
|
+
defaultValue={result.envFiles.join(",")}
|
|
295
|
+
onSubmit={(raw) => {
|
|
296
|
+
const files = raw
|
|
297
|
+
.split(",")
|
|
298
|
+
.map((p) => p.trim())
|
|
299
|
+
.filter(Boolean);
|
|
300
|
+
advance({ envFiles: files });
|
|
301
|
+
}}
|
|
302
|
+
/>
|
|
303
|
+
</Box>
|
|
304
|
+
</Box>
|
|
305
|
+
)}
|
|
306
|
+
|
|
307
|
+
{step === "integrations" && (
|
|
308
|
+
<IntegrationToggleStep
|
|
309
|
+
toggles={integrationToggles}
|
|
310
|
+
setToggles={setIntegrationToggles}
|
|
311
|
+
onContinue={() => advance({ integrations: { ...integrationToggles } })}
|
|
312
|
+
/>
|
|
313
|
+
)}
|
|
314
|
+
</Box>
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* One-shot picker over existing swarm slugs, used by `e2b swarms add` when no
|
|
320
|
+
* slug was passed on an interactive TTY. Resolves the chosen slug back to the
|
|
321
|
+
* caller via `onSelect`, then exits. Consistent with the wizard's Ink + Select.
|
|
322
|
+
*/
|
|
323
|
+
export type SwarmPickerOption = { slug: string; label: string };
|
|
324
|
+
|
|
325
|
+
export function SwarmPicker({
|
|
326
|
+
slugs,
|
|
327
|
+
onSelect,
|
|
328
|
+
}: {
|
|
329
|
+
slugs: SwarmPickerOption[];
|
|
330
|
+
onSelect: (slug: string) => void;
|
|
331
|
+
}) {
|
|
332
|
+
const { exit } = useApp();
|
|
333
|
+
return (
|
|
334
|
+
<Box flexDirection="column" padding={1}>
|
|
335
|
+
<Text color="cyan" bold>
|
|
336
|
+
Add to which swarm?
|
|
337
|
+
</Text>
|
|
338
|
+
<Box marginTop={1}>
|
|
339
|
+
<Select
|
|
340
|
+
options={slugs.map((s) => ({ label: s.label, value: s.slug }))}
|
|
341
|
+
onChange={(value) => {
|
|
342
|
+
// Defer so React commits before we tear down the Ink instance.
|
|
343
|
+
queueMicrotask(() => {
|
|
344
|
+
onSelect(value);
|
|
345
|
+
exit();
|
|
346
|
+
});
|
|
347
|
+
}}
|
|
348
|
+
/>
|
|
349
|
+
</Box>
|
|
350
|
+
</Box>
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const CONTINUE_VALUE = "__continue__";
|
|
355
|
+
|
|
356
|
+
function IntegrationToggleStep({
|
|
357
|
+
toggles,
|
|
358
|
+
setToggles,
|
|
359
|
+
onContinue,
|
|
360
|
+
}: {
|
|
361
|
+
toggles: Record<StackIntegration, boolean>;
|
|
362
|
+
setToggles: (
|
|
363
|
+
update: (prev: Record<StackIntegration, boolean>) => Record<StackIntegration, boolean>,
|
|
364
|
+
) => void;
|
|
365
|
+
onContinue: () => void;
|
|
366
|
+
}) {
|
|
367
|
+
const options = [
|
|
368
|
+
...STACK_INTEGRATIONS.map((key) => ({
|
|
369
|
+
label: `${toggles[key] ? "[x]" : "[ ]"} ${key}`,
|
|
370
|
+
value: key as string,
|
|
371
|
+
})),
|
|
372
|
+
{ label: "Continue →", value: CONTINUE_VALUE },
|
|
373
|
+
];
|
|
374
|
+
|
|
375
|
+
return (
|
|
376
|
+
<Box marginTop={1} flexDirection="column">
|
|
377
|
+
<Text bold>Integrations (enabled = on):</Text>
|
|
378
|
+
<Text dimColor>Disabled integrations set the matching *_DISABLE env on the API.</Text>
|
|
379
|
+
<Box marginTop={1}>
|
|
380
|
+
<Select
|
|
381
|
+
options={options}
|
|
382
|
+
onChange={(value) => {
|
|
383
|
+
if (value === CONTINUE_VALUE) {
|
|
384
|
+
onContinue();
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
const key = value as StackIntegration;
|
|
388
|
+
setToggles((prev) => ({ ...prev, [key]: !prev[key] }));
|
|
389
|
+
}}
|
|
390
|
+
/>
|
|
391
|
+
</Box>
|
|
392
|
+
</Box>
|
|
393
|
+
);
|
|
394
|
+
}
|