@lazyingart/agintiflow 0.20.14 → 0.20.16
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 +4 -0
- package/demos/README.md +6 -0
- package/demos/agintiflow-cli-launch.jpg +0 -0
- package/docs/model-selection.md +1 -1
- package/package.json +2 -1
- package/public/index.html +6 -3
- package/public/styles.css +62 -2
- package/references/model-routing-provider-design.md +2 -0
- package/src/interactive-cli.js +133 -97
package/README.md
CHANGED
|
@@ -21,6 +21,10 @@ AgInTiFlow is AgInTi's web-first agent platform for controlled website automatio
|
|
|
21
21
|
|
|
22
22
|
It is designed for workflows where an AI agent should act, but every tool, log, and session state should remain inspectable.
|
|
23
23
|
|
|
24
|
+
<p align="center">
|
|
25
|
+
<img src="./demos/agintiflow-cli-launch.jpg" alt="AgInTiFlow interactive CLI launch screen with colorful terminal banner, Docker workspace status, and chat input panel" width="960" />
|
|
26
|
+
</p>
|
|
27
|
+
|
|
24
28
|
## Product Snapshot
|
|
25
29
|
|
|
26
30
|
| Area | Direction |
|
package/demos/README.md
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# AgInTiFlow Demos
|
|
2
|
+
|
|
3
|
+
This folder stores shareable screenshots and visual demos used by the repository README.
|
|
4
|
+
|
|
5
|
+
- `agintiflow-cli-launch.png`: interactive CLI launch screen with the AgInTi Flow terminal banner, Docker workspace status, and input panel.
|
|
6
|
+
- `agintiflow-cli-launch.jpg`: optimized README/web preview derived from the same screenshot.
|
|
Binary file
|
package/docs/model-selection.md
CHANGED
|
@@ -46,7 +46,7 @@ Interactive commands:
|
|
|
46
46
|
/auxiliary model grsai/nano-banana-2
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
-
In the interactive CLI, `/provider`, `/route`, `/model`, `/spare`, and `/auxiliary model` without arguments open selectors. Use Up/Down/Left/Right to move through choices
|
|
49
|
+
In the interactive CLI, `/provider`, `/route`, `/model`, `/spare`, and `/auxiliary model` without arguments open selectors. Use Up/Down/Left/Right to move through choices and Enter to confirm. Esc cancels at the top level; inside multi-level selectors it goes back one level. Slash-command hints use the same arrow selection behavior: type a prefix such as `/mo`, use arrows to choose `/model` or `/models`, then press Enter or Tab.
|
|
50
50
|
|
|
51
51
|
`/route`, `/model`, and `/spare` intentionally share the same text-model selector so users do not need to learn three different catalogs. The selector is hierarchical:
|
|
52
52
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lazyingart/agintiflow",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.16",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "AgInTiFlow is a web-first coding agent and CLI with DeepSeek routing, sandboxed tools, model providers, canvas artifacts, and optional wrappers.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"bin/",
|
|
38
38
|
"docs/",
|
|
39
39
|
"docker/",
|
|
40
|
+
"demos/agintiflow-cli-launch.jpg",
|
|
40
41
|
"i18n/",
|
|
41
42
|
"logos/banner-opaque.png",
|
|
42
43
|
"logos/logo.png",
|
package/public/index.html
CHANGED
|
@@ -36,10 +36,13 @@
|
|
|
36
36
|
<p class="subtle" data-i18n="intro">
|
|
37
37
|
Browser agent with provider selection, resumable runs, persisted settings, and an optional guarded shell tool with Docker sandbox support.
|
|
38
38
|
</p>
|
|
39
|
-
<
|
|
40
|
-
<
|
|
39
|
+
<details class="project-card">
|
|
40
|
+
<summary>
|
|
41
|
+
<strong data-i18n="projectStatusTitle">Project folder</strong>
|
|
42
|
+
<span class="project-card-toggle" aria-hidden="true"></span>
|
|
43
|
+
</summary>
|
|
41
44
|
<p id="project-status" class="subtle">Loading project context...</p>
|
|
42
|
-
</
|
|
45
|
+
</details>
|
|
43
46
|
|
|
44
47
|
<section id="setup-card" class="setup-card" hidden>
|
|
45
48
|
<div>
|
package/public/styles.css
CHANGED
|
@@ -290,9 +290,7 @@ h1 {
|
|
|
290
290
|
|
|
291
291
|
.project-card,
|
|
292
292
|
.setup-card {
|
|
293
|
-
display: grid;
|
|
294
293
|
min-width: 0;
|
|
295
|
-
gap: 10px;
|
|
296
294
|
margin: 14px 0;
|
|
297
295
|
padding: 13px;
|
|
298
296
|
border: 1px solid rgba(15, 118, 110, 0.18);
|
|
@@ -300,11 +298,73 @@ h1 {
|
|
|
300
298
|
background: linear-gradient(135deg, rgba(204, 251, 241, 0.44), rgba(255, 247, 237, 0.9));
|
|
301
299
|
}
|
|
302
300
|
|
|
301
|
+
.setup-card {
|
|
302
|
+
display: grid;
|
|
303
|
+
gap: 10px;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.project-card {
|
|
307
|
+
display: block;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.project-card summary {
|
|
311
|
+
display: flex;
|
|
312
|
+
align-items: center;
|
|
313
|
+
justify-content: space-between;
|
|
314
|
+
gap: 12px;
|
|
315
|
+
min-width: 0;
|
|
316
|
+
list-style: none;
|
|
317
|
+
cursor: pointer;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
.project-card summary::-webkit-details-marker {
|
|
321
|
+
display: none;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.project-card summary strong {
|
|
325
|
+
min-width: 0;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.project-card summary:focus-visible {
|
|
329
|
+
border-radius: 10px;
|
|
330
|
+
outline: 3px solid rgba(15, 118, 110, 0.22);
|
|
331
|
+
outline-offset: 4px;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.project-card-toggle {
|
|
335
|
+
display: inline-grid;
|
|
336
|
+
width: 30px;
|
|
337
|
+
height: 30px;
|
|
338
|
+
flex: 0 0 auto;
|
|
339
|
+
place-items: center;
|
|
340
|
+
border: 1px solid rgba(15, 118, 110, 0.22);
|
|
341
|
+
border-radius: 999px;
|
|
342
|
+
background: rgba(255, 253, 250, 0.78);
|
|
343
|
+
color: var(--accent);
|
|
344
|
+
font-size: 0.88rem;
|
|
345
|
+
font-weight: 900;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
.project-card-toggle::before {
|
|
349
|
+
content: "⌄";
|
|
350
|
+
transform: rotate(-90deg);
|
|
351
|
+
transition: transform 160ms ease;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
.project-card[open] .project-card-toggle::before {
|
|
355
|
+
transform: rotate(0deg);
|
|
356
|
+
}
|
|
357
|
+
|
|
303
358
|
.project-card p,
|
|
304
359
|
.setup-card p {
|
|
305
360
|
overflow-wrap: anywhere;
|
|
306
361
|
}
|
|
307
362
|
|
|
363
|
+
.project-card p {
|
|
364
|
+
margin: 10px 0 0;
|
|
365
|
+
line-height: 1.55;
|
|
366
|
+
}
|
|
367
|
+
|
|
308
368
|
.setup-card .key-links {
|
|
309
369
|
display: flex;
|
|
310
370
|
flex-wrap: wrap;
|
|
@@ -55,6 +55,8 @@ Interactive equivalents:
|
|
|
55
55
|
|
|
56
56
|
`/route`, `/model`, and `/spare` share a single text-model selector. The selector is grouped as DeepSeek, Venice Uncensored, Venice GPT/Claude/Gemma/Qwen, OpenAI, Qwen, and Mock so the user sees the same union of route/main/spare-capable models in every role.
|
|
57
57
|
|
|
58
|
+
Interactive selectors are hierarchical. Enter moves forward or confirms. Esc cancels only at the first level; at model/reasoning levels it returns to the previous selector instead of dropping the whole flow.
|
|
59
|
+
|
|
58
60
|
`/venice` opens a route/main selector for Venice text models. The selector includes `venice/venice-uncensored-1-2` (Venice 1.2), `venice/venice-uncensored` (Venice 1.1), `venice/gemma-4-uncensored` (Gemma 4), and a Disable Venice option. In non-interactive shells, `/venice` keeps script compatibility by selecting `venice/venice-uncensored-1-2` for both roles. `/venice 1.2 gemma` sets route to Venice 1.2 and main to Gemma 4; `/venice off` or the Disable Venice selector option restores `deepseek/deepseek-v4-flash` for route and `deepseek/deepseek-v4-pro` for main.
|
|
59
61
|
|
|
60
62
|
The web UI should expose model names as dropdowns, not free-text fields. The left panel should stay focused on common daily controls, while model-role editing and less-used switches live in an Advanced settings modal. The terminal-like capability panels belong after the runtime log so the left control panel remains short.
|
package/src/interactive-cli.js
CHANGED
|
@@ -2079,7 +2079,13 @@ function renderSelector({ title, subtitle, options, selectedIndex, lineCount = 0
|
|
|
2079
2079
|
return rows.length;
|
|
2080
2080
|
}
|
|
2081
2081
|
|
|
2082
|
-
|
|
2082
|
+
const SELECTOR_BACK = Symbol("selector-back");
|
|
2083
|
+
|
|
2084
|
+
function isSelectorBack(value) {
|
|
2085
|
+
return value === SELECTOR_BACK;
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
function selectModelChoice({ title, subtitle, options, initialIndex = 0, escapeValue = null }) {
|
|
2083
2089
|
if (!input.isTTY || !output.isTTY || typeof input.setRawMode !== "function") return Promise.resolve(null);
|
|
2084
2090
|
return new Promise((resolve) => {
|
|
2085
2091
|
emitKeypressEvents(input);
|
|
@@ -2107,7 +2113,7 @@ function selectModelChoice({ title, subtitle, options, initialIndex = 0 }) {
|
|
|
2107
2113
|
return;
|
|
2108
2114
|
}
|
|
2109
2115
|
if (key.name === "escape") {
|
|
2110
|
-
finish(
|
|
2116
|
+
finish(escapeValue);
|
|
2111
2117
|
return;
|
|
2112
2118
|
}
|
|
2113
2119
|
if (key.name === "up" || key.name === "left") {
|
|
@@ -2157,73 +2163,95 @@ async function pickModelRole(role, state) {
|
|
|
2157
2163
|
? modelRoleChoices("auxiliary").find((item) => item.provider === currentProvider && item.model === currentModel)
|
|
2158
2164
|
: modelRoleChoices(role).find((item) => item.provider === currentProvider && item.model === currentModel);
|
|
2159
2165
|
const currentGroup = currentModelEntry?.group || currentProvider;
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
: role === "spare"
|
|
2165
|
-
? "Select spare model family"
|
|
2166
|
-
: role === "auxiliary"
|
|
2167
|
-
? "Select auxiliary provider"
|
|
2168
|
-
: "Select main model family",
|
|
2169
|
-
subtitle: "First choose a provider family. Up/Down/Left/Right selects, Enter confirms, Esc cancels.",
|
|
2170
|
-
options: groupOptions,
|
|
2171
|
-
initialIndex: Math.max(
|
|
2172
|
-
groupOptions.findIndex((item) => item.groupId === currentGroup),
|
|
2173
|
-
0
|
|
2174
|
-
),
|
|
2175
|
-
});
|
|
2176
|
-
if (!group) return false;
|
|
2177
|
-
|
|
2178
|
-
const options = role === "auxiliary" ? auxiliaryModelRoleChoices(group.groupId) : textModelRoleChoices(group.groupId);
|
|
2179
|
-
const initialIndex = Math.max(
|
|
2180
|
-
options.findIndex((item) => item.provider === currentProvider && item.model === currentModel),
|
|
2166
|
+
let selected = null;
|
|
2167
|
+
let selectedReasoning = "";
|
|
2168
|
+
let initialGroupIndex = Math.max(
|
|
2169
|
+
groupOptions.findIndex((item) => item.groupId === currentGroup),
|
|
2181
2170
|
0
|
|
2182
2171
|
);
|
|
2183
|
-
const selected = await selectModelChoice({
|
|
2184
|
-
title:
|
|
2185
|
-
role === "route"
|
|
2186
|
-
? `Select route model: ${group.label}`
|
|
2187
|
-
: role === "spare"
|
|
2188
|
-
? `Select spare model: ${group.label}`
|
|
2189
|
-
: role === "auxiliary"
|
|
2190
|
-
? `Select auxiliary model: ${group.label}`
|
|
2191
|
-
: `Select main model: ${group.label}`,
|
|
2192
|
-
subtitle:
|
|
2193
|
-
role === "auxiliary"
|
|
2194
|
-
? "Choose the image/edit auxiliary model. Enter confirms, Esc cancels."
|
|
2195
|
-
: "Choose the text model. OpenAI choices ask for reasoning next.",
|
|
2196
|
-
options,
|
|
2197
|
-
initialIndex,
|
|
2198
|
-
});
|
|
2199
|
-
if (!selected) return false;
|
|
2200
2172
|
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
}));
|
|
2215
|
-
const reasoning = await selectModelChoice({
|
|
2216
|
-
title: `Select reasoning: ${selected.label}`,
|
|
2217
|
-
subtitle: "Up/Down selects reasoning effort. Enter confirms, Esc cancels.",
|
|
2218
|
-
options: reasoningOptions,
|
|
2219
|
-
initialIndex: Math.max(
|
|
2220
|
-
reasoningOptions.findIndex((item) => item.value === currentReasoning),
|
|
2221
|
-
reasoningOptions.findIndex((item) => item.value === selectedReasoning),
|
|
2222
|
-
0
|
|
2223
|
-
),
|
|
2173
|
+
while (!selected) {
|
|
2174
|
+
const group = await selectModelChoice({
|
|
2175
|
+
title:
|
|
2176
|
+
role === "route"
|
|
2177
|
+
? "Select route model family"
|
|
2178
|
+
: role === "spare"
|
|
2179
|
+
? "Select spare model family"
|
|
2180
|
+
: role === "auxiliary"
|
|
2181
|
+
? "Select auxiliary provider"
|
|
2182
|
+
: "Select main model family",
|
|
2183
|
+
subtitle: "First choose a provider family. Up/Down/Left/Right selects, Enter confirms, Esc cancels.",
|
|
2184
|
+
options: groupOptions,
|
|
2185
|
+
initialIndex: initialGroupIndex,
|
|
2224
2186
|
});
|
|
2225
|
-
if (!
|
|
2226
|
-
|
|
2187
|
+
if (!group) return false;
|
|
2188
|
+
initialGroupIndex = Math.max(
|
|
2189
|
+
groupOptions.findIndex((item) => item.groupId === group.groupId),
|
|
2190
|
+
0
|
|
2191
|
+
);
|
|
2192
|
+
|
|
2193
|
+
const options = role === "auxiliary" ? auxiliaryModelRoleChoices(group.groupId) : textModelRoleChoices(group.groupId);
|
|
2194
|
+
let initialModelIndex = Math.max(
|
|
2195
|
+
options.findIndex((item) => item.provider === currentProvider && item.model === currentModel),
|
|
2196
|
+
0
|
|
2197
|
+
);
|
|
2198
|
+
|
|
2199
|
+
while (!selected) {
|
|
2200
|
+
const modelSelection = await selectModelChoice({
|
|
2201
|
+
title:
|
|
2202
|
+
role === "route"
|
|
2203
|
+
? `Select route model: ${group.label}`
|
|
2204
|
+
: role === "spare"
|
|
2205
|
+
? `Select spare model: ${group.label}`
|
|
2206
|
+
: role === "auxiliary"
|
|
2207
|
+
? `Select auxiliary model: ${group.label}`
|
|
2208
|
+
: `Select main model: ${group.label}`,
|
|
2209
|
+
subtitle:
|
|
2210
|
+
role === "auxiliary"
|
|
2211
|
+
? "Choose the image/edit auxiliary model. Enter confirms, Esc goes back."
|
|
2212
|
+
: "Choose the text model. OpenAI choices ask for reasoning next. Esc goes back.",
|
|
2213
|
+
options,
|
|
2214
|
+
initialIndex: initialModelIndex,
|
|
2215
|
+
escapeValue: SELECTOR_BACK,
|
|
2216
|
+
});
|
|
2217
|
+
if (isSelectorBack(modelSelection)) break;
|
|
2218
|
+
if (!modelSelection) return false;
|
|
2219
|
+
initialModelIndex = Math.max(
|
|
2220
|
+
options.findIndex((item) => item.provider === modelSelection.provider && item.model === modelSelection.model),
|
|
2221
|
+
0
|
|
2222
|
+
);
|
|
2223
|
+
|
|
2224
|
+
selectedReasoning = modelSelection.reasoningDefault || "";
|
|
2225
|
+
if (Array.isArray(modelSelection.reasoningOptions) && modelSelection.reasoningOptions.length > 0) {
|
|
2226
|
+
const currentReasoning =
|
|
2227
|
+
role === "route"
|
|
2228
|
+
? state.routeReasoning || selectedReasoning || "medium"
|
|
2229
|
+
: role === "spare"
|
|
2230
|
+
? state.spareReasoning || selectedReasoning || "medium"
|
|
2231
|
+
: state.mainReasoning || selectedReasoning || "medium";
|
|
2232
|
+
const reasoningOptions = modelSelection.reasoningOptions.map((level) => ({
|
|
2233
|
+
action: "reasoning",
|
|
2234
|
+
label: level,
|
|
2235
|
+
value: level,
|
|
2236
|
+
description: `${modelSelection.label} with ${level} reasoning`,
|
|
2237
|
+
}));
|
|
2238
|
+
const reasoning = await selectModelChoice({
|
|
2239
|
+
title: `Select reasoning: ${modelSelection.label}`,
|
|
2240
|
+
subtitle: "Up/Down selects reasoning effort. Enter confirms, Esc goes back.",
|
|
2241
|
+
options: reasoningOptions,
|
|
2242
|
+
initialIndex: Math.max(
|
|
2243
|
+
reasoningOptions.findIndex((item) => item.value === currentReasoning),
|
|
2244
|
+
reasoningOptions.findIndex((item) => item.value === selectedReasoning),
|
|
2245
|
+
0
|
|
2246
|
+
),
|
|
2247
|
+
escapeValue: SELECTOR_BACK,
|
|
2248
|
+
});
|
|
2249
|
+
if (isSelectorBack(reasoning)) continue;
|
|
2250
|
+
if (!reasoning) return false;
|
|
2251
|
+
selectedReasoning = reasoning.value;
|
|
2252
|
+
}
|
|
2253
|
+
selected = modelSelection;
|
|
2254
|
+
}
|
|
2227
2255
|
}
|
|
2228
2256
|
|
|
2229
2257
|
state.routingMode = "smart";
|
|
@@ -2255,44 +2283,52 @@ async function pickModelRole(role, state) {
|
|
|
2255
2283
|
|
|
2256
2284
|
async function pickVeniceRouteAndMain(state) {
|
|
2257
2285
|
const options = veniceTextModelChoices();
|
|
2258
|
-
|
|
2286
|
+
let routeIndex = Math.max(
|
|
2259
2287
|
options.findIndex((item) => item.provider === state.routeProvider && item.model === state.routeModel),
|
|
2260
2288
|
0
|
|
2261
2289
|
);
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2290
|
+
while (true) {
|
|
2291
|
+
const route = await selectModelChoice({
|
|
2292
|
+
title: "Select Venice route model",
|
|
2293
|
+
subtitle: "Route handles planning and short turns. Up/Down selects, Enter confirms, Esc cancels.",
|
|
2294
|
+
options,
|
|
2295
|
+
initialIndex: routeIndex,
|
|
2296
|
+
});
|
|
2297
|
+
if (!route) return false;
|
|
2298
|
+
routeIndex = Math.max(
|
|
2299
|
+
options.findIndex((item) => item.provider === route.provider && item.model === route.model),
|
|
2300
|
+
0
|
|
2301
|
+
);
|
|
2302
|
+
if (route.action === "disable") {
|
|
2303
|
+
useDeepSeekDefaults(state);
|
|
2304
|
+
printSystemLine("venice=off routing=smart route=deepseek/deepseek-v4-flash main=deepseek/deepseek-v4-pro");
|
|
2305
|
+
return true;
|
|
2306
|
+
}
|
|
2274
2307
|
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2308
|
+
const mainIndex = Math.max(
|
|
2309
|
+
options.findIndex((item) => item.action !== "disable" && item.provider === state.mainProvider && item.model === state.mainModel),
|
|
2310
|
+
options.findIndex((item) => item.action !== "disable" && item.model === route.model),
|
|
2311
|
+
0
|
|
2312
|
+
);
|
|
2313
|
+
const main = await selectModelChoice({
|
|
2314
|
+
title: "Select Venice main model",
|
|
2315
|
+
subtitle: "Main handles complex work. Up/Down selects, Enter confirms, Esc goes back.",
|
|
2316
|
+
options,
|
|
2317
|
+
initialIndex: mainIndex,
|
|
2318
|
+
escapeValue: SELECTOR_BACK,
|
|
2319
|
+
});
|
|
2320
|
+
if (isSelectorBack(main)) continue;
|
|
2321
|
+
if (!main) return false;
|
|
2322
|
+
if (main.action === "disable") {
|
|
2323
|
+
useDeepSeekDefaults(state);
|
|
2324
|
+
printSystemLine("venice=off routing=smart route=deepseek/deepseek-v4-flash main=deepseek/deepseek-v4-pro");
|
|
2325
|
+
return true;
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2328
|
+
useVeniceModels(state, route.model, main.model);
|
|
2329
|
+
printSystemLine(`venice=on routing=smart route=venice/${state.routeModel} main=venice/${state.mainModel}`);
|
|
2290
2330
|
return true;
|
|
2291
2331
|
}
|
|
2292
|
-
|
|
2293
|
-
useVeniceModels(state, route.model, main.model);
|
|
2294
|
-
printSystemLine(`venice=on routing=smart route=venice/${state.routeModel} main=venice/${state.mainModel}`);
|
|
2295
|
-
return true;
|
|
2296
2332
|
}
|
|
2297
2333
|
|
|
2298
2334
|
async function pickProvider(state) {
|