@skaile/forge-settings 1.3.2 → 1.4.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.
Files changed (46) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +20 -0
  3. package/dist/module.json +1 -1
  4. package/dist/runtime/components/ForgeProvidersPanel.d.vue.ts +13 -0
  5. package/dist/runtime/components/ForgeProvidersPanel.vue +461 -78
  6. package/dist/runtime/components/ForgeProvidersPanel.vue.d.ts +13 -0
  7. package/dist/runtime/server/harness-settings.d.ts +50 -12
  8. package/dist/runtime/server/harness-settings.js +93 -9
  9. package/dist/runtime/server/index.d.ts +2 -0
  10. package/dist/runtime/server/index.js +2 -0
  11. package/dist/runtime/server/provider-service.d.ts +26 -2
  12. package/dist/runtime/server/provider-service.js +16 -1
  13. package/dist/runtime/server/provider-write.d.ts +25 -9
  14. package/dist/runtime/server/provider-write.js +72 -4
  15. package/dist/runtime/shared/data-zone.d.ts +66 -0
  16. package/dist/runtime/shared/data-zone.js +79 -0
  17. package/dist/runtime/shared/harness-providers.d.ts +41 -2
  18. package/dist/runtime/shared/harness-providers.js +31 -8
  19. package/dist/runtime/shared/harnesses.d.ts +7 -0
  20. package/dist/runtime/shared/harnesses.js +18 -0
  21. package/dist/runtime/shared/provider-accounts.d.ts +215 -0
  22. package/dist/runtime/shared/provider-accounts.js +255 -0
  23. package/dist/runtime/shared/provider-registry.d.ts +38 -0
  24. package/dist/runtime/shared/provider-registry.js +16 -0
  25. package/dist/runtime/shared/settings-types.d.ts +7 -0
  26. package/dist/runtime/vue/index.d.ts +2 -0
  27. package/dist/runtime/vue/index.js +2 -0
  28. package/dist/runtime/vue/providers-panel-model.d.ts +98 -5
  29. package/dist/runtime/vue/providers-panel-model.js +13 -1
  30. package/dist/runtime/vue/useProvidersPanel.d.ts +66 -5
  31. package/dist/runtime/vue/useProvidersPanel.js +417 -29
  32. package/package.json +2 -2
  33. package/src/runtime/components/ForgeProvidersPanel.vue +477 -78
  34. package/src/runtime/server/harness-settings.ts +165 -21
  35. package/src/runtime/server/index.ts +2 -0
  36. package/src/runtime/server/provider-service.ts +53 -1
  37. package/src/runtime/server/provider-write.ts +129 -15
  38. package/src/runtime/shared/data-zone.ts +172 -0
  39. package/src/runtime/shared/harness-providers.ts +77 -6
  40. package/src/runtime/shared/harnesses.ts +25 -0
  41. package/src/runtime/shared/provider-accounts.ts +526 -0
  42. package/src/runtime/shared/provider-registry.ts +58 -0
  43. package/src/runtime/shared/settings-types.ts +7 -0
  44. package/src/runtime/vue/index.ts +2 -0
  45. package/src/runtime/vue/providers-panel-model.ts +123 -5
  46. package/src/runtime/vue/useProvidersPanel.ts +586 -37
@@ -71,10 +71,54 @@
71
71
  variant="subtle"
72
72
  color="neutral"
73
73
  >{{ row.badge }}</UBadge>
74
+ <UBadge
75
+ :data-testid="`provider-zone-${row.id}`"
76
+ size="sm"
77
+ variant="outline"
78
+ :color="row.zone === 'transfer' ? 'warning' : 'neutral'"
79
+ :title="`Data zone: ${row.zoneLabel}`"
80
+ >{{ row.zoneLabel }}</UBadge>
81
+ <button
82
+ v-if="row.modelsChip && row.canFilterModels"
83
+ type="button"
84
+ class="rounded-md border border-surface-border px-1.5 py-0.5 text-[10px] text-foreground-muted hover:border-primary-500"
85
+ :data-testid="`provider-models-chip-${row.id}`"
86
+ @click="openModelsEditor(row)"
87
+ >{{ row.modelsChip }}</button>
88
+ <UBadge
89
+ v-else-if="row.modelsChip"
90
+ :data-testid="`provider-models-chip-${row.id}`"
91
+ size="sm"
92
+ variant="subtle"
93
+ color="neutral"
94
+ >{{ row.modelsChip }}</UBadge>
74
95
  <Icon v-if="row.locked" name="lucide:lock" class="text-foreground-subtle shrink-0" />
75
96
  </div>
76
97
  <p class="font-mono text-foreground-subtle truncate" :title="row.baseUrl ?? void 0">{{ rowMeta(row) }}</p>
98
+ <p v-if="row.accountLine" :data-testid="`provider-account-${row.id}`" class="text-foreground-muted">{{ row.accountLine }}</p>
77
99
  <p v-if="row.inheritedNote" class="text-foreground-subtle italic">{{ row.inheritedNote }}</p>
100
+ <p
101
+ v-if="row.blockedNote"
102
+ :data-testid="`provider-blocked-${row.id}`"
103
+ class="text-foreground-subtle italic"
104
+ >{{ row.blockedNote }}</p>
105
+ <p
106
+ v-if="row.expiry"
107
+ :data-testid="`provider-expiry-${row.id}`"
108
+ class="flex items-center gap-1"
109
+ :class="row.expiry.expired || row.expiry.days < 7 ? 'text-amber-600 dark:text-amber-400' : 'text-foreground-subtle'"
110
+ >
111
+ {{ row.expiry.text }}
112
+ <template v-if="row.canRenew">
113
+ ·
114
+ <button
115
+ type="button"
116
+ class="underline hover:text-foreground"
117
+ :data-testid="`provider-renew-${row.id}`"
118
+ @click="openSignIn(row)"
119
+ >Renew</button>
120
+ </template>
121
+ </p>
78
122
  </div>
79
123
  <div class="flex items-center gap-1 shrink-0">
80
124
  <UButton
@@ -145,6 +189,16 @@
145
189
  {{ row.status }}
146
190
  </p>
147
191
 
192
+ <USwitch
193
+ v-if="row.zone === 'eu_option'"
194
+ :model-value="row.euProcessing === true"
195
+ :disabled="!row.canSetEuProcessing || rowBusy === row.id"
196
+ label="EU processing is on for this account"
197
+ size="xs"
198
+ :data-testid="`provider-eu-processing-${row.id}`"
199
+ @update:model-value="setEuProcessing(row, Boolean($event))"
200
+ />
201
+
148
202
  <!-- Inline key editor -->
149
203
  <div
150
204
  v-if="keyEditing === row.id"
@@ -203,6 +257,67 @@
203
257
  </div>
204
258
  </div>
205
259
 
260
+ <!-- Models allowed here -->
261
+ <div
262
+ v-if="modelsEditing === row.id"
263
+ :data-testid="`provider-models-${row.id}`"
264
+ class="space-y-1.5 p-2 rounded-lg border border-surface-border bg-surface-ground/50"
265
+ >
266
+ <p class="font-medium text-foreground">Models allowed here</p>
267
+ <URadioGroup
268
+ :model-value="modelsForm.mode"
269
+ :items="MODELS_MODE_ITEMS"
270
+ orientation="horizontal"
271
+ size="xs"
272
+ :data-testid="`provider-models-mode-${row.id}`"
273
+ @update:model-value="setModelsMode($event)"
274
+ />
275
+ <template v-if="modelsForm.mode === 'only'">
276
+ <UInput
277
+ v-model="modelsForm.query"
278
+ placeholder="Search models"
279
+ size="xs"
280
+ :data-testid="`provider-models-search-${row.id}`"
281
+ class="w-full bg-surface-ground border-surface-border text-foreground text-xs"
282
+ />
283
+ <p v-if="modelsLoading" class="text-foreground-subtle italic">Listing models…</p>
284
+ <p
285
+ v-if="modelsListMessage"
286
+ :data-testid="`provider-models-message-${row.id}`"
287
+ class="text-foreground-subtle italic"
288
+ >{{ modelsListMessage }}</p>
289
+ <div class="max-h-48 overflow-y-auto space-y-1">
290
+ <UCheckbox
291
+ v-for="modelId in modelChoices"
292
+ :key="modelId"
293
+ :model-value="modelsForm.selected.includes(modelId)"
294
+ :label="modelId"
295
+ size="xs"
296
+ :data-testid="`provider-model-${row.id}-${modelId}`"
297
+ @update:model-value="toggleModel(modelId, Boolean($event))"
298
+ />
299
+ </div>
300
+ <p class="text-foreground-subtle">
301
+ {{ modelsForm.selected.length === 0 ? "No model ticked: this provider will run no model." : `${modelsForm.selected.length} ticked.` }}
302
+ </p>
303
+ </template>
304
+ <p
305
+ v-if="modelsError"
306
+ :data-testid="`provider-models-error-${row.id}`"
307
+ class="text-error-500"
308
+ >{{ modelsError }}</p>
309
+ <div class="flex justify-end gap-1">
310
+ <UButton size="xs" variant="ghost" :data-testid="`provider-models-cancel-${row.id}`" @click="cancelModelsEditor()">Cancel</UButton>
311
+ <UButton
312
+ size="xs"
313
+ color="primary"
314
+ :loading="rowBusy === row.id"
315
+ :data-testid="`provider-models-save-${row.id}`"
316
+ @click="saveModelsEditor()"
317
+ >Save</UButton>
318
+ </div>
319
+ </div>
320
+
206
321
  <p
207
322
  v-for="notice in row.overrideNotices"
208
323
  :key="notice"
@@ -228,7 +343,7 @@
228
343
  :open="dialogVisible"
229
344
  :title="dialogTitle"
230
345
  :ui="{
231
- content: 'bg-surface-card border border-surface-border sm:max-w-[460px]',
346
+ content: 'bg-surface-card border border-surface-border sm:max-w-[520px]',
232
347
  header: 'bg-surface-card border-b border-surface-border text-foreground',
233
348
  body: 'bg-surface-card p-5',
234
349
  footer: 'bg-surface-ground/50 border-t border-surface-border p-3 justify-end'
@@ -237,104 +352,323 @@
237
352
  >
238
353
  <template #body>
239
354
  <div data-testid="provider-dialog" class="space-y-4">
240
- <div class="flex flex-col gap-1">
241
- <label class="text-xs text-foreground-muted font-medium">
242
- Preset <span class="text-error-500">*</span>
243
- </label>
244
- <USelect
245
- :model-value="form.preset"
246
- :items="presetOptions"
247
- placeholder="Choose a preset"
248
- data-testid="provider-preset"
249
- class="w-full bg-surface-ground border-surface-border text-foreground text-sm"
250
- @update:model-value="selectPreset(String($event ?? ''))"
251
- />
355
+ <!-- Step 1: library -->
356
+ <div v-if="dialogStep === 'library'" data-testid="provider-library" class="space-y-3">
357
+ <p class="text-xs text-foreground-muted">Which provider do you want to add?</p>
358
+ <button
359
+ type="button"
360
+ data-testid="provider-library-azure"
361
+ class="w-full text-left p-3 rounded-lg border border-surface-border hover:border-primary-500 hover:bg-primary-500/5 transition-colors"
362
+ @click="chooseLibrary(AZURE_ACCOUNT)"
363
+ >
364
+ <span class="flex items-center gap-2">
365
+ <Icon name="lucide:cloud" class="text-primary-500 shrink-0" />
366
+ <span class="text-sm font-medium text-foreground">Microsoft Azure</span>
367
+ </span>
368
+ <span class="block text-xs text-foreground-muted mt-1">
369
+ Azure OpenAI and Claude on Foundry from one Azure resource, with Entra ID or an API key.
370
+ </span>
371
+ </button>
372
+ <div class="grid grid-cols-2 gap-2 max-h-72 overflow-y-auto">
373
+ <button
374
+ v-for="preset in libraryPresets"
375
+ :key="preset.id"
376
+ type="button"
377
+ :data-testid="`provider-library-${preset.id}`"
378
+ class="text-left p-2 rounded-lg border border-surface-border hover:border-primary-500 hover:bg-primary-500/5 transition-colors"
379
+ @click="chooseLibrary(preset.id)"
380
+ >
381
+ <span class="block text-xs font-medium text-foreground truncate">{{ preset.label }}</span>
382
+ <span class="block text-[10px] text-foreground-subtle">{{ libraryHint(preset) }}</span>
383
+ </button>
384
+ <button
385
+ type="button"
386
+ data-testid="provider-library-custom"
387
+ class="text-left p-2 rounded-lg border border-dashed border-surface-border hover:border-primary-500 transition-colors"
388
+ @click="chooseLibrary(CUSTOM_PRESET)"
389
+ >
390
+ <span class="block text-xs font-medium text-foreground">Custom</span>
391
+ <span class="block text-[10px] text-foreground-subtle">Any endpoint, by base URL</span>
392
+ </button>
393
+ </div>
252
394
  </div>
253
395
 
254
- <div v-if="showSurface" class="flex flex-col gap-1">
255
- <label class="text-xs text-foreground-muted font-medium">
256
- Surface <span class="text-error-500">*</span>
257
- </label>
258
- <USelect
259
- v-model="form.surface"
260
- :items="surfaceOptions"
261
- placeholder="Wire protocol"
262
- data-testid="provider-surface"
263
- class="w-full bg-surface-ground border-surface-border text-foreground text-sm"
264
- />
265
- </div>
396
+ <!-- Step 2: account -->
397
+ <template v-else-if="dialogStep === 'account'">
398
+ <!-- Microsoft Azure account -->
399
+ <template v-if="isAzureAccount">
400
+ <div class="flex flex-col gap-1">
401
+ <label class="text-xs text-foreground-muted font-medium">
402
+ Resource name <span class="text-error-500">*</span>
403
+ </label>
404
+ <UInput
405
+ v-model="azureForm.resource"
406
+ placeholder="contoso-ai"
407
+ data-testid="provider-azure-resource"
408
+ class="w-full bg-surface-ground border-surface-border text-foreground text-sm font-mono"
409
+ />
410
+ <p class="text-[10px] text-foreground-subtle">
411
+ The <code class="font-mono">{{ azureForm.resource.trim() || "name" }}</code> of
412
+ <code class="font-mono">{{ azureForm.resource.trim() || "name" }}.openai.azure.com</code>; shared by every entry this account creates.
413
+ </p>
414
+ </div>
266
415
 
267
- <div class="flex flex-col gap-1">
268
- <label class="text-xs text-foreground-muted font-medium">
269
- ID <span class="text-error-500">*</span>
270
- </label>
271
- <UInput
272
- v-model="form.id"
273
- :disabled="editing !== null"
274
- placeholder="openrouter"
275
- data-testid="provider-id"
276
- class="w-full bg-surface-ground border-surface-border text-foreground text-sm font-mono"
277
- :class="editing !== null ? 'opacity-60 cursor-not-allowed' : ''"
278
- />
279
- <p v-if="idError" class="text-xs text-error-500">{{ idError }}</p>
280
- <p v-else class="text-[10px] text-foreground-subtle">The left half of a model selector: <code class="font-mono">&lt;id&gt;/&lt;model&gt;</code>.</p>
281
- </div>
416
+ <div class="space-y-2">
417
+ <p class="text-xs text-foreground-muted font-medium">What do you want to use?</p>
418
+ <UCheckbox
419
+ v-model="azureForm.useOpenAI"
420
+ label="Azure OpenAI"
421
+ description="GPT models you deployed on the resource."
422
+ data-testid="provider-azure-openai"
423
+ />
424
+ <div v-if="azureForm.useOpenAI" class="ml-6 space-y-1.5" data-testid="provider-azure-deployments">
425
+ <p class="text-[10px] text-foreground-subtle">Deployment name → model id the pickers show (blank: the deployment name).</p>
426
+ <div
427
+ v-for="(row, index) in azureForm.deployments"
428
+ :key="index"
429
+ class="flex items-center gap-1.5"
430
+ >
431
+ <UInput
432
+ v-model="row.deployment"
433
+ placeholder="gpt-5-eu"
434
+ :aria-label="`Deployment ${index + 1}`"
435
+ :data-testid="`provider-azure-deployment-${index}`"
436
+ class="flex-1 bg-surface-ground border-surface-border text-foreground text-xs font-mono"
437
+ />
438
+ <Icon name="lucide:arrow-right" class="text-foreground-subtle shrink-0" />
439
+ <UInput
440
+ v-model="row.model"
441
+ placeholder="gpt-5"
442
+ :aria-label="`Model id ${index + 1}`"
443
+ :data-testid="`provider-azure-model-${index}`"
444
+ class="flex-1 bg-surface-ground border-surface-border text-foreground text-xs font-mono"
445
+ />
446
+ <UButton
447
+ size="xs"
448
+ variant="ghost"
449
+ :aria-label="`Remove deployment ${index + 1}`"
450
+ :data-testid="`provider-azure-deployment-remove-${index}`"
451
+ @click="removeDeploymentRow(index)"
452
+ >
453
+ <Icon name="lucide:x" class="w-3.5 h-3.5" />
454
+ </UButton>
455
+ </div>
456
+ <UButton size="xs" variant="ghost" data-testid="provider-azure-deployment-add" @click="addDeploymentRow()">
457
+ <Icon name="lucide:plus" class="mr-1.5" />Add deployment
458
+ </UButton>
459
+ <div class="flex flex-col gap-1">
460
+ <label class="text-xs text-foreground-muted font-medium">API version</label>
461
+ <UInput
462
+ v-model="azureForm.apiVersion"
463
+ :disabled="!props.expert"
464
+ data-testid="provider-azure-api-version"
465
+ class="w-full bg-surface-ground border-surface-border text-foreground text-xs font-mono"
466
+ />
467
+ <p v-if="!props.expert" class="text-[10px] text-foreground-subtle">Editable in expert mode.</p>
468
+ </div>
469
+ </div>
470
+ <UCheckbox
471
+ v-model="azureForm.useFoundry"
472
+ label="Claude on Foundry"
473
+ description="Anthropic models deployed through Azure AI Foundry on the same resource."
474
+ data-testid="provider-azure-foundry"
475
+ />
476
+ </div>
477
+ </template>
282
478
 
283
- <div class="flex flex-col gap-1">
284
- <label class="text-xs text-foreground-muted font-medium">
285
- Base URL <span v-if="isCustom" class="text-error-500">*</span>
286
- </label>
287
- <UInput
288
- v-model="form.baseUrl"
289
- :placeholder="baseUrlPlaceholder"
290
- data-testid="provider-base-url"
291
- class="w-full bg-surface-ground border-surface-border text-foreground text-sm font-mono"
292
- />
293
- </div>
479
+ <template v-else>
480
+ <div class="flex flex-col gap-1">
481
+ <label class="text-xs text-foreground-muted font-medium">
482
+ Preset <span class="text-error-500">*</span>
483
+ </label>
484
+ <USelect
485
+ :model-value="form.preset"
486
+ :items="presetOptions"
487
+ placeholder="Choose a preset"
488
+ data-testid="provider-preset"
489
+ class="w-full bg-surface-ground border-surface-border text-foreground text-sm"
490
+ @update:model-value="selectPreset(String($event ?? ''))"
491
+ />
492
+ </div>
294
493
 
295
- <template v-if="showCredentialFields">
296
- <div class="flex flex-col gap-1">
297
- <label class="text-xs text-foreground-muted font-medium">Credential environment variable</label>
298
- <UInput
299
- v-model="form.envName"
300
- placeholder="PROVIDER_API_KEY"
301
- data-testid="provider-credential-env"
302
- class="w-full bg-surface-ground border-surface-border text-foreground text-sm font-mono"
303
- />
304
- <p class="text-[10px] text-foreground-subtle">
305
- Stored as <code class="font-mono">env:{{ form.envName.trim() || "NAME" }}</code> — the key itself stays in the environment.
306
- </p>
494
+ <div v-if="showSurface" class="flex flex-col gap-1">
495
+ <label class="text-xs text-foreground-muted font-medium">
496
+ Surface <span class="text-error-500">*</span>
497
+ </label>
498
+ <USelect
499
+ v-model="form.surface"
500
+ :items="surfaceOptions"
501
+ placeholder="Wire protocol"
502
+ data-testid="provider-surface"
503
+ class="w-full bg-surface-ground border-surface-border text-foreground text-sm"
504
+ />
505
+ </div>
506
+
507
+ <div class="flex flex-col gap-1">
508
+ <label class="text-xs text-foreground-muted font-medium">
509
+ ID <span class="text-error-500">*</span>
510
+ </label>
511
+ <UInput
512
+ v-model="form.id"
513
+ :disabled="editing !== null"
514
+ placeholder="openrouter"
515
+ data-testid="provider-id"
516
+ class="w-full bg-surface-ground border-surface-border text-foreground text-sm font-mono"
517
+ :class="editing !== null ? 'opacity-60 cursor-not-allowed' : ''"
518
+ />
519
+ <p v-if="idError" class="text-xs text-error-500">{{ idError }}</p>
520
+ <p v-else class="text-[10px] text-foreground-subtle">The left half of a model selector: <code class="font-mono">&lt;id&gt;/&lt;model&gt;</code>.</p>
521
+ </div>
522
+
523
+ <div class="flex flex-col gap-1">
524
+ <label class="text-xs text-foreground-muted font-medium">
525
+ Base URL <span v-if="isCustom" class="text-error-500">*</span>
526
+ </label>
527
+ <UInput
528
+ v-model="form.baseUrl"
529
+ :placeholder="baseUrlPlaceholder"
530
+ data-testid="provider-base-url"
531
+ class="w-full bg-surface-ground border-surface-border text-foreground text-sm font-mono"
532
+ />
533
+ </div>
534
+
535
+ <div v-for="field in paramFields" :key="field.key" class="flex flex-col gap-1">
536
+ <label class="text-xs text-foreground-muted font-medium">
537
+ {{ field.label }} <span v-if="field.required" class="text-error-500">*</span>
538
+ </label>
539
+ <UInput
540
+ v-model="form.params[field.key]"
541
+ :disabled="!field.editable"
542
+ :data-testid="`provider-param-${field.key}`"
543
+ class="w-full bg-surface-ground border-surface-border text-foreground text-sm font-mono"
544
+ />
545
+ <p v-if="!field.editable" class="text-[10px] text-foreground-subtle">Editable in expert mode.</p>
546
+ </div>
547
+ </template>
548
+
549
+ <!-- How do you sign in? -->
550
+ <div v-if="showSignIn" data-testid="provider-signin" class="space-y-1.5">
551
+ <p class="text-xs text-foreground-muted font-medium">How do you sign in?</p>
552
+ <div class="flex flex-wrap gap-1.5" role="radiogroup" aria-label="How do you sign in?">
553
+ <UButton
554
+ v-for="option in signInOptions"
555
+ :key="option.id"
556
+ size="xs"
557
+ role="radio"
558
+ :aria-checked="form.signIn === option.id"
559
+ :variant="form.signIn === option.id ? 'solid' : 'outline'"
560
+ :color="form.signIn === option.id ? 'primary' : 'neutral'"
561
+ :data-testid="`provider-signin-${option.id}`"
562
+ @click="selectSignIn(option.id)"
563
+ >{{ option.label }}</UButton>
564
+ </div>
565
+ <p v-if="signInOption" class="text-[10px] text-foreground-subtle leading-relaxed">{{ signInOption.description }}</p>
307
566
  </div>
308
567
 
309
- <div v-if="canSetValue" class="flex flex-col gap-1">
310
- <label class="text-xs text-foreground-muted font-medium">Set value now</label>
568
+ <template v-if="showCredentialFields">
569
+ <div class="flex flex-col gap-1">
570
+ <label class="text-xs text-foreground-muted font-medium">Credential environment variable</label>
571
+ <UInput
572
+ v-model="form.envName"
573
+ :placeholder="defaultEnvPlaceholder"
574
+ data-testid="provider-credential-env"
575
+ class="w-full bg-surface-ground border-surface-border text-foreground text-sm font-mono"
576
+ />
577
+ <p class="text-[10px] text-foreground-subtle">
578
+ Stored as <code class="font-mono">env:{{ form.envName.trim() || defaultEnvPlaceholder }}</code> — the key itself stays in the environment.
579
+ </p>
580
+ </div>
581
+
582
+ <div v-if="canSetValue" class="flex flex-col gap-1">
583
+ <label class="text-xs text-foreground-muted font-medium">Set value now</label>
584
+ <UInput
585
+ v-model="form.secret"
586
+ type="password"
587
+ autocomplete="off"
588
+ placeholder="Optional — written to this scope's .env"
589
+ data-testid="provider-set-value"
590
+ class="w-full bg-surface-ground border-surface-border text-foreground text-sm font-mono"
591
+ />
592
+ </div>
593
+ </template>
594
+
595
+ <div v-if="showReferenceField" class="flex flex-col gap-1">
596
+ <label class="text-xs text-foreground-muted font-medium">
597
+ Credential reference <span class="text-error-500">*</span>
598
+ </label>
311
599
  <UInput
312
- v-model="form.secret"
313
- type="password"
314
- autocomplete="off"
315
- placeholder="Optional — written to this scope's .env"
316
- data-testid="provider-set-value"
600
+ v-model="form.credentialRef"
601
+ placeholder="env:NAME · keyring:forge/name · cmd:… · platform:…"
602
+ data-testid="provider-dialog-credential-ref"
317
603
  class="w-full bg-surface-ground border-surface-border text-foreground text-sm font-mono"
318
604
  />
319
605
  </div>
320
606
  </template>
321
607
 
608
+ <!-- Step 3: done, the created entries tested -->
609
+ <div v-else data-testid="provider-done" class="space-y-2">
610
+ <p class="text-xs text-foreground-muted">
611
+ {{ createdIds.length === 1 ? "Added" : `Added ${createdIds.length} providers` }}. Testing the sign-in:
612
+ </p>
613
+ <div
614
+ v-for="id in createdIds"
615
+ :key="id"
616
+ :data-testid="`provider-created-${id}`"
617
+ class="flex items-start gap-2 p-2 rounded-lg border border-surface-border text-xs"
618
+ >
619
+ <span class="font-mono text-foreground shrink-0">{{ id }}</span>
620
+ <span v-if="testBusy === id" class="text-foreground-subtle italic">testing…</span>
621
+ <span
622
+ v-else-if="testResults[id]"
623
+ :data-testid="`provider-created-result-${id}`"
624
+ :class="testResults[id]?.ok ? 'text-success-500' : 'text-error-500'"
625
+ >{{ testResults[id]?.message }}</span>
626
+ <UButton
627
+ v-if="testBusy !== id"
628
+ size="xs"
629
+ variant="ghost"
630
+ class="ml-auto"
631
+ :disabled="testBusy !== null"
632
+ :data-testid="`provider-created-test-${id}`"
633
+ @click="testProvider({ id })"
634
+ >Test again</UButton>
635
+ </div>
636
+ </div>
637
+
322
638
  <p v-if="dialogError" data-testid="provider-dialog-error" class="text-sm text-error-500">{{ dialogError }}</p>
323
639
  </div>
324
640
  </template>
325
641
 
326
642
  <template #footer>
327
643
  <div class="flex gap-2 justify-end w-full">
328
- <UButton size="sm" variant="ghost" @click="dialogVisible = false">Cancel</UButton>
329
644
  <UButton
645
+ v-if="!editing && dialogStep === 'account'"
330
646
  size="sm"
331
- color="primary"
332
- data-testid="provider-save"
333
- :loading="saving"
334
- @click="saveProvider"
647
+ variant="ghost"
648
+ class="mr-auto"
649
+ data-testid="provider-back"
650
+ @click="backToLibrary()"
335
651
  >
336
- <Icon name="lucide:check" class="mr-1.5" />{{ editing ? "Save" : "Add" }}
652
+ <Icon name="lucide:arrow-left" class="mr-1.5" />Back
337
653
  </UButton>
654
+ <template v-if="dialogStep === 'done'">
655
+ <UButton size="sm" color="primary" data-testid="provider-done-close" @click="finishDialog()">
656
+ <Icon name="lucide:check" class="mr-1.5" />Done
657
+ </UButton>
658
+ </template>
659
+ <template v-else>
660
+ <UButton size="sm" variant="ghost" @click="dialogVisible = false">Cancel</UButton>
661
+ <UButton
662
+ v-if="dialogStep === 'account'"
663
+ size="sm"
664
+ color="primary"
665
+ data-testid="provider-save"
666
+ :loading="saving"
667
+ @click="saveProvider"
668
+ >
669
+ <Icon name="lucide:check" class="mr-1.5" />{{ editing ? "Save" : isAzureAccount ? "Add account" : "Add" }}
670
+ </UButton>
671
+ </template>
338
672
  </div>
339
673
  </template>
340
674
  </UModal>
@@ -375,7 +709,12 @@
375
709
 
376
710
  <script setup>
377
711
  import { computed } from "vue";
378
- import { providersPanelNote, providersPanelRowMeta as rowMeta } from "../vue/providers-panel-model";
712
+ import {
713
+ AZURE_ACCOUNT,
714
+ CUSTOM_PRESET,
715
+ providersPanelNote,
716
+ providersPanelRowMeta as rowMeta
717
+ } from "../vue/providers-panel-model";
379
718
  import { useProvidersPanel } from "../vue/useProvidersPanel";
380
719
  const props = defineProps({
381
720
  client: { type: Object, required: false },
@@ -386,9 +725,23 @@ const props = defineProps({
386
725
  environmentEnabled: { type: Boolean, required: false, default: void 0 },
387
726
  layer: { type: String, required: true },
388
727
  readOnly: { type: Boolean, required: false },
389
- note: { type: [String, Boolean], required: false, default: void 0 }
728
+ note: { type: [String, Boolean], required: false, default: void 0 },
729
+ expert: { type: Boolean, required: false, default: false },
730
+ dataZoneRule: { type: null, required: false }
390
731
  });
391
732
  const noteText = computed(() => providersPanelNote(props.note, props.layer));
733
+ const MODELS_MODE_ITEMS = [
734
+ { label: "All models", value: "all" },
735
+ { label: "Only these", value: "only" }
736
+ ];
737
+ function libraryHint(preset) {
738
+ if (preset.credential.kind === "none") return "Local, no sign-in";
739
+ if (preset.kind === "stt") return "Speech to text";
740
+ if (preset.kind === "tts") return "Text to speech";
741
+ if (preset.kind === "embed") return "Embeddings";
742
+ return "Chat models";
743
+ }
744
+ const defaultEnvPlaceholder = computed(() => defaultEnvName.value ?? "PROVIDER_API_KEY");
392
745
  const emit = defineEmits(["changed"]);
393
746
  const {
394
747
  rows,
@@ -407,6 +760,24 @@ const {
407
760
  baseUrlPlaceholder,
408
761
  canSetValue,
409
762
  showCredentialFields,
763
+ defaultEnvName,
764
+ dialogStep,
765
+ libraryPresets,
766
+ chooseLibrary,
767
+ backToLibrary,
768
+ isAzureAccount,
769
+ signInOptions,
770
+ signInOption,
771
+ selectSignIn,
772
+ showSignIn,
773
+ showReferenceField,
774
+ paramFields,
775
+ azureForm,
776
+ addDeploymentRow,
777
+ removeDeploymentRow,
778
+ createdIds,
779
+ finishDialog,
780
+ openSignIn,
410
781
  rowBusy,
411
782
  rowError,
412
783
  canTest,
@@ -416,6 +787,18 @@ const {
416
787
  onCredentialSelect,
417
788
  setEnabled,
418
789
  resetPatch,
790
+ setEuProcessing,
791
+ modelsEditing,
792
+ modelsForm,
793
+ modelsLoading,
794
+ modelsListMessage,
795
+ modelChoices,
796
+ modelsError,
797
+ openModelsEditor,
798
+ setModelsMode,
799
+ toggleModel,
800
+ cancelModelsEditor,
801
+ saveModelsEditor,
419
802
  keyEditing,
420
803
  keyForm,
421
804
  keyError,