@skaile/forge-settings 1.3.1 → 1.3.2

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/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.3.2
4
+
5
+ - `<ForgeProvidersPanel>` lists providers as stacked rows instead of a six-column table: name and badge, then one muted line with id, preset and base URL, the credential picker below, and Test / Edit / Delete / Enabled on the right. It fits a settings column without clipping the actions or wrapping the base URL per character. Test ids are unchanged.
6
+
3
7
  ## 1.3.1
4
8
 
5
9
  - `<ForgeProvidersPanel>` takes an optional `note` prop: a string replaces the line above the provider list, `false` hides it (Add provider stays, right-aligned). Absent keeps today's text, so existing hosts see no change.
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@skaile/forge-settings",
3
3
  "configKey": "forgeSettings",
4
- "version": "1.3.1",
4
+ "version": "1.3.2",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.3",
7
7
  "unbuild": "3.6.1"
@@ -52,191 +52,176 @@
52
52
  This registry has no providers yet.
53
53
  </div>
54
54
 
55
- <div v-else class="overflow-x-auto rounded-lg border border-surface-border">
56
- <table class="w-full text-xs">
57
- <thead class="bg-surface-ground/50 text-foreground-muted">
58
- <tr class="text-left">
59
- <th class="px-3 py-2 font-medium">Provider</th>
60
- <th class="px-3 py-2 font-medium">Preset / surface</th>
61
- <th class="px-3 py-2 font-medium">Base URL</th>
62
- <th class="px-3 py-2 font-medium">Credential</th>
63
- <th class="px-3 py-2 font-medium">Enabled</th>
64
- <th class="px-3 py-2" />
65
- </tr>
66
- </thead>
67
- <tbody>
68
- <tr
69
- v-for="row in rows"
70
- :key="row.id"
71
- :data-testid="`provider-row-${row.id}`"
72
- class="border-t border-surface-border align-top"
73
- :class="row.enabled ? '' : 'opacity-60'"
74
- >
75
- <td class="px-3 py-2">
76
- <div class="flex items-center gap-1.5 flex-wrap">
77
- <span class="font-medium text-foreground">{{ row.name }}</span>
78
- <UBadge
79
- :data-testid="`provider-badge-${row.id}`"
80
- size="sm"
81
- variant="subtle"
82
- color="neutral"
83
- >{{ row.badge }}</UBadge>
84
- <Icon v-if="row.locked" name="lucide:lock" class="text-foreground-subtle shrink-0" />
85
- </div>
86
- <code v-if="row.name !== row.id" class="font-mono text-foreground-subtle">{{ row.id }}</code>
87
- <p v-if="row.inheritedNote" class="text-foreground-subtle italic">{{ row.inheritedNote }}</p>
88
- </td>
89
- <td class="px-3 py-2 font-mono text-foreground-muted">{{ row.origin }}</td>
90
- <td class="px-3 py-2 font-mono text-foreground-muted break-all">{{ row.baseUrl ?? "\u2014" }}</td>
91
- <td class="px-3 py-2 min-w-[15rem] space-y-1">
92
- <USelect
93
- v-if="row.needsCredential"
94
- :model-value="row.credentialValue"
95
- :items="row.credentialOptions"
96
- placeholder="Choose a credential"
97
- :disabled="!row.canEditCredential || rowBusy === row.id"
98
- :data-testid="`provider-credential-${row.id}`"
99
- class="w-full bg-surface-ground border-surface-border text-foreground text-xs font-mono"
100
- @update:model-value="onCredentialSelect(row, String($event ?? ''))"
101
- />
102
- <p :class="row.configured ? 'text-foreground-subtle' : 'text-amber-600 dark:text-amber-400'" class="italic">
103
- {{ row.status }}
104
- </p>
105
-
106
- <!-- Inline key editor -->
107
- <div
108
- v-if="keyEditing === row.id"
109
- :data-testid="`provider-credential-enter-key-${row.id}`"
110
- class="space-y-1.5 p-2 rounded-lg border border-surface-border bg-surface-ground/50"
55
+ <!-- One stacked row per provider: fits a settings column; a table squeezed the base URL and clipped the actions. -->
56
+ <ul v-else class="rounded-lg border border-surface-border divide-y divide-surface-border text-xs">
57
+ <li
58
+ v-for="row in rows"
59
+ :key="row.id"
60
+ :data-testid="`provider-row-${row.id}`"
61
+ class="p-3 space-y-2"
62
+ :class="row.enabled ? '' : 'opacity-60'"
63
+ >
64
+ <div class="flex items-start gap-3">
65
+ <div class="flex-1 min-w-0">
66
+ <div class="flex items-center gap-1.5 flex-wrap">
67
+ <span class="text-sm font-medium text-foreground">{{ row.name }}</span>
68
+ <UBadge
69
+ :data-testid="`provider-badge-${row.id}`"
70
+ size="sm"
71
+ variant="subtle"
72
+ color="neutral"
73
+ >{{ row.badge }}</UBadge>
74
+ <Icon v-if="row.locked" name="lucide:lock" class="text-foreground-subtle shrink-0" />
75
+ </div>
76
+ <p class="font-mono text-foreground-subtle truncate" :title="row.baseUrl ?? void 0">{{ rowMeta(row) }}</p>
77
+ <p v-if="row.inheritedNote" class="text-foreground-subtle italic">{{ row.inheritedNote }}</p>
78
+ </div>
79
+ <div class="flex items-center gap-1 shrink-0">
80
+ <UButton
81
+ v-if="canTest"
82
+ size="sm"
83
+ variant="ghost"
84
+ :loading="testBusy === row.id"
85
+ :disabled="testBusy !== null"
86
+ :data-testid="`provider-test-${row.id}`"
87
+ @click="testProvider(row)"
88
+ >
89
+ <Icon name="lucide:plug-zap" class="mr-1.5 w-3.5 h-3.5" />Test
90
+ </UButton>
91
+ <UButton
92
+ v-if="row.canReset"
93
+ size="sm"
94
+ variant="ghost"
95
+ :loading="rowBusy === row.id"
96
+ :data-testid="`provider-reset-${row.id}`"
97
+ @click="resetPatch(row)"
98
+ >
99
+ <Icon name="lucide:undo-2" class="mr-1.5 w-3.5 h-3.5" />Reset
100
+ </UButton>
101
+ <template v-if="row.editable">
102
+ <UButton
103
+ size="sm"
104
+ variant="ghost"
105
+ :aria-label="`Edit ${row.id}`"
106
+ :data-testid="`provider-edit-${row.id}`"
107
+ @click="openEdit(row)"
111
108
  >
112
- <UInput
113
- v-model="keyForm.name"
114
- placeholder="PROVIDER_API_KEY"
115
- :data-testid="`provider-key-name-${row.id}`"
116
- class="w-full bg-surface-ground border-surface-border text-foreground text-xs font-mono"
117
- />
118
- <UInput
119
- v-model="keyForm.value"
120
- type="password"
121
- autocomplete="off"
122
- placeholder="Key"
123
- :data-testid="`provider-key-value-${row.id}`"
124
- class="w-full bg-surface-ground border-surface-border text-foreground text-xs font-mono"
125
- />
126
- <p v-if="keyError" class="text-error-500">{{ keyError }}</p>
127
- <div class="flex justify-end gap-1">
128
- <UButton size="xs" variant="ghost" :data-testid="`provider-key-cancel-${row.id}`" @click="cancelKeyEditor()">Cancel</UButton>
129
- <UButton
130
- size="xs"
131
- color="primary"
132
- :loading="rowBusy === row.id"
133
- :data-testid="`provider-key-save-${row.id}`"
134
- @click="saveKeyEditor()"
135
- >Save</UButton>
136
- </div>
137
- </div>
138
-
139
- <!-- Inline reference editor -->
140
- <div
141
- v-if="refEditing === row.id"
142
- :data-testid="`provider-credential-ref-${row.id}`"
143
- class="space-y-1.5 p-2 rounded-lg border border-surface-border bg-surface-ground/50"
109
+ <Icon name="lucide:pencil" class="w-3.5 h-3.5" />
110
+ </UButton>
111
+ <UButton
112
+ size="sm"
113
+ variant="ghost"
114
+ color="error"
115
+ :aria-label="`Delete ${row.id}`"
116
+ :data-testid="`provider-delete-${row.id}`"
117
+ @click="requestDelete(row)"
144
118
  >
145
- <UInput
146
- v-model="refForm.ref"
147
- placeholder="keyring:forge/name · cmd:… · platform:…"
148
- :data-testid="`provider-ref-input-${row.id}`"
149
- class="w-full bg-surface-ground border-surface-border text-foreground text-xs font-mono"
150
- />
151
- <p v-if="refError" class="text-error-500">{{ refError }}</p>
152
- <div class="flex justify-end gap-1">
153
- <UButton size="xs" variant="ghost" :data-testid="`provider-ref-cancel-${row.id}`" @click="cancelRefEditor()">Cancel</UButton>
154
- <UButton
155
- size="xs"
156
- color="primary"
157
- :loading="rowBusy === row.id"
158
- :data-testid="`provider-ref-save-${row.id}`"
159
- @click="saveRefEditor()"
160
- >Save</UButton>
161
- </div>
162
- </div>
119
+ <Icon name="lucide:trash-2" class="w-3.5 h-3.5" />
120
+ </UButton>
121
+ </template>
122
+ <USwitch
123
+ :model-value="row.enabled"
124
+ :disabled="!row.canToggle || rowBusy === row.id"
125
+ :aria-label="`Enable ${row.id}`"
126
+ :data-testid="`provider-enabled-${row.id}`"
127
+ class="ml-1"
128
+ @update:model-value="setEnabled(row, Boolean($event))"
129
+ />
130
+ </div>
131
+ </div>
163
132
 
164
- <p
165
- v-for="notice in row.overrideNotices"
166
- :key="notice"
167
- :data-testid="`provider-override-notice-${row.id}`"
168
- class="text-foreground-subtle"
169
- >{{ notice }}</p>
170
- <p
171
- v-if="testResults[row.id]"
172
- :data-testid="`provider-test-result-${row.id}`"
173
- :class="testResults[row.id]?.ok ? 'text-success-500' : 'text-error-500'"
174
- >{{ testResults[row.id]?.message }}</p>
175
- <p
176
- v-if="rowError?.id === row.id"
177
- :data-testid="`provider-row-error-${row.id}`"
178
- class="text-error-500"
179
- >{{ rowError.message }}</p>
180
- </td>
181
- <td class="px-3 py-2">
182
- <USwitch
183
- :model-value="row.enabled"
184
- :disabled="!row.canToggle || rowBusy === row.id"
185
- :aria-label="`Enable ${row.id}`"
186
- :data-testid="`provider-enabled-${row.id}`"
187
- @update:model-value="setEnabled(row, Boolean($event))"
188
- />
189
- </td>
190
- <td class="px-3 py-2">
191
- <div class="flex items-center justify-end gap-1">
192
- <UButton
193
- v-if="canTest"
194
- size="sm"
195
- variant="ghost"
196
- :loading="testBusy === row.id"
197
- :disabled="testBusy !== null"
198
- :data-testid="`provider-test-${row.id}`"
199
- @click="testProvider(row)"
200
- >
201
- <Icon name="lucide:plug-zap" class="mr-1.5 w-3.5 h-3.5" />Test
202
- </UButton>
203
- <UButton
204
- v-if="row.canReset"
205
- size="sm"
206
- variant="ghost"
207
- :loading="rowBusy === row.id"
208
- :data-testid="`provider-reset-${row.id}`"
209
- @click="resetPatch(row)"
210
- >
211
- <Icon name="lucide:undo-2" class="mr-1.5 w-3.5 h-3.5" />Reset
212
- </UButton>
213
- <template v-if="row.editable">
214
- <UButton
215
- size="sm"
216
- variant="ghost"
217
- :aria-label="`Edit ${row.id}`"
218
- :data-testid="`provider-edit-${row.id}`"
219
- @click="openEdit(row)"
220
- >
221
- <Icon name="lucide:pencil" class="w-3.5 h-3.5" />
222
- </UButton>
223
- <UButton
224
- size="sm"
225
- variant="ghost"
226
- color="error"
227
- :aria-label="`Delete ${row.id}`"
228
- :data-testid="`provider-delete-${row.id}`"
229
- @click="requestDelete(row)"
230
- >
231
- <Icon name="lucide:trash-2" class="w-3.5 h-3.5" />
232
- </UButton>
233
- </template>
234
- </div>
235
- </td>
236
- </tr>
237
- </tbody>
238
- </table>
239
- </div>
133
+ <div class="space-y-1 max-w-md">
134
+ <USelect
135
+ v-if="row.needsCredential"
136
+ :model-value="row.credentialValue"
137
+ :items="row.credentialOptions"
138
+ placeholder="Choose a credential"
139
+ :disabled="!row.canEditCredential || rowBusy === row.id"
140
+ :data-testid="`provider-credential-${row.id}`"
141
+ class="w-full bg-surface-ground border-surface-border text-foreground text-xs font-mono"
142
+ @update:model-value="onCredentialSelect(row, String($event ?? ''))"
143
+ />
144
+ <p :class="row.configured ? 'text-foreground-subtle' : 'text-amber-600 dark:text-amber-400'" class="italic">
145
+ {{ row.status }}
146
+ </p>
147
+
148
+ <!-- Inline key editor -->
149
+ <div
150
+ v-if="keyEditing === row.id"
151
+ :data-testid="`provider-credential-enter-key-${row.id}`"
152
+ class="space-y-1.5 p-2 rounded-lg border border-surface-border bg-surface-ground/50"
153
+ >
154
+ <UInput
155
+ v-model="keyForm.name"
156
+ placeholder="PROVIDER_API_KEY"
157
+ :data-testid="`provider-key-name-${row.id}`"
158
+ class="w-full bg-surface-ground border-surface-border text-foreground text-xs font-mono"
159
+ />
160
+ <UInput
161
+ v-model="keyForm.value"
162
+ type="password"
163
+ autocomplete="off"
164
+ placeholder="Key"
165
+ :data-testid="`provider-key-value-${row.id}`"
166
+ class="w-full bg-surface-ground border-surface-border text-foreground text-xs font-mono"
167
+ />
168
+ <p v-if="keyError" class="text-error-500">{{ keyError }}</p>
169
+ <div class="flex justify-end gap-1">
170
+ <UButton size="xs" variant="ghost" :data-testid="`provider-key-cancel-${row.id}`" @click="cancelKeyEditor()">Cancel</UButton>
171
+ <UButton
172
+ size="xs"
173
+ color="primary"
174
+ :loading="rowBusy === row.id"
175
+ :data-testid="`provider-key-save-${row.id}`"
176
+ @click="saveKeyEditor()"
177
+ >Save</UButton>
178
+ </div>
179
+ </div>
180
+
181
+ <!-- Inline reference editor -->
182
+ <div
183
+ v-if="refEditing === row.id"
184
+ :data-testid="`provider-credential-ref-${row.id}`"
185
+ class="space-y-1.5 p-2 rounded-lg border border-surface-border bg-surface-ground/50"
186
+ >
187
+ <UInput
188
+ v-model="refForm.ref"
189
+ placeholder="keyring:forge/name · cmd:… · platform:…"
190
+ :data-testid="`provider-ref-input-${row.id}`"
191
+ class="w-full bg-surface-ground border-surface-border text-foreground text-xs font-mono"
192
+ />
193
+ <p v-if="refError" class="text-error-500">{{ refError }}</p>
194
+ <div class="flex justify-end gap-1">
195
+ <UButton size="xs" variant="ghost" :data-testid="`provider-ref-cancel-${row.id}`" @click="cancelRefEditor()">Cancel</UButton>
196
+ <UButton
197
+ size="xs"
198
+ color="primary"
199
+ :loading="rowBusy === row.id"
200
+ :data-testid="`provider-ref-save-${row.id}`"
201
+ @click="saveRefEditor()"
202
+ >Save</UButton>
203
+ </div>
204
+ </div>
205
+
206
+ <p
207
+ v-for="notice in row.overrideNotices"
208
+ :key="notice"
209
+ :data-testid="`provider-override-notice-${row.id}`"
210
+ class="text-foreground-subtle"
211
+ >{{ notice }}</p>
212
+ <p
213
+ v-if="testResults[row.id]"
214
+ :data-testid="`provider-test-result-${row.id}`"
215
+ :class="testResults[row.id]?.ok ? 'text-success-500' : 'text-error-500'"
216
+ >{{ testResults[row.id]?.message }}</p>
217
+ <p
218
+ v-if="rowError?.id === row.id"
219
+ :data-testid="`provider-row-error-${row.id}`"
220
+ class="text-error-500"
221
+ >{{ rowError.message }}</p>
222
+ </div>
223
+ </li>
224
+ </ul>
240
225
 
241
226
  <!-- Add / edit dialog -->
242
227
  <UModal
@@ -390,7 +375,7 @@
390
375
 
391
376
  <script setup>
392
377
  import { computed } from "vue";
393
- import { providersPanelNote } from "../vue/providers-panel-model";
378
+ import { providersPanelNote, providersPanelRowMeta as rowMeta } from "../vue/providers-panel-model";
394
379
  import { useProvidersPanel } from "../vue/useProvidersPanel";
395
380
  const props = defineProps({
396
381
  client: { type: Object, required: false },
@@ -149,6 +149,16 @@ export declare const PROVIDER_ID_PATTERN: RegExp;
149
149
  export declare const DEFAULT_PRESETS_URL = "/api/providers/presets";
150
150
  /** The line `ForgeProvidersPanel` shows above its list; `null` = none. */
151
151
  export declare function providersPanelNote(note: string | false | undefined, layer: ProvidersPanelLayer): string | null;
152
+ /**
153
+ * The muted line under a provider's name: its id (when the name differs), its
154
+ * preset or surface and its base URL, without repeating a value.
155
+ */
156
+ export declare function providersPanelRowMeta(row: {
157
+ id: string;
158
+ name: string;
159
+ origin: string;
160
+ baseUrl?: string | null;
161
+ }): string;
152
162
  /** An `env:NAME` credential reference; group 1 is the variable name. */
153
163
  export declare const ENV_REF_PATTERN: RegExp;
154
164
  /** The credential kind a written reference gets when the row states none. */
@@ -8,6 +8,10 @@ export function providersPanelNote(note, layer) {
8
8
  if (note !== void 0) return note;
9
9
  return layer === "workspace" ? "Entries of this workspace's providers.yml can be changed here; instance entries are inherited. Keys stay in the environment; an entry only references them." : "The instance registry, $FORGE_HOME/.skaile/providers.yml. Keys stay in the environment; an entry only references them.";
10
10
  }
11
+ export function providersPanelRowMeta(row) {
12
+ const parts = [row.name !== row.id ? row.id : null, row.origin, row.baseUrl ?? null];
13
+ return [...new Set(parts.filter((part) => !!part))].join(" \xB7 ");
14
+ }
11
15
  export const ENV_REF_PATTERN = /^env:(.+)$/;
12
16
  export const FALLBACK_CREDENTIAL_KIND = "api_key_header";
13
17
  export const ENV_SOURCE_LABEL = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skaile/forge-settings",
3
- "version": "1.3.1",
3
+ "version": "1.3.2",
4
4
  "description": "Provider and settings operations for Skaile forge apps: provider registry layers, credential store, env layers, model discovery, the providers panel, and editor / AI SDK model adapters.",
5
5
  "type": "module",
6
6
  "packageManager": "bun@1.3.9",
@@ -52,191 +52,176 @@
52
52
  This registry has no providers yet.
53
53
  </div>
54
54
 
55
- <div v-else class="overflow-x-auto rounded-lg border border-surface-border">
56
- <table class="w-full text-xs">
57
- <thead class="bg-surface-ground/50 text-foreground-muted">
58
- <tr class="text-left">
59
- <th class="px-3 py-2 font-medium">Provider</th>
60
- <th class="px-3 py-2 font-medium">Preset / surface</th>
61
- <th class="px-3 py-2 font-medium">Base URL</th>
62
- <th class="px-3 py-2 font-medium">Credential</th>
63
- <th class="px-3 py-2 font-medium">Enabled</th>
64
- <th class="px-3 py-2" />
65
- </tr>
66
- </thead>
67
- <tbody>
68
- <tr
69
- v-for="row in rows"
70
- :key="row.id"
71
- :data-testid="`provider-row-${row.id}`"
72
- class="border-t border-surface-border align-top"
73
- :class="row.enabled ? '' : 'opacity-60'"
74
- >
75
- <td class="px-3 py-2">
76
- <div class="flex items-center gap-1.5 flex-wrap">
77
- <span class="font-medium text-foreground">{{ row.name }}</span>
78
- <UBadge
79
- :data-testid="`provider-badge-${row.id}`"
80
- size="sm"
81
- variant="subtle"
82
- color="neutral"
83
- >{{ row.badge }}</UBadge>
84
- <Icon v-if="row.locked" name="lucide:lock" class="text-foreground-subtle shrink-0" />
85
- </div>
86
- <code v-if="row.name !== row.id" class="font-mono text-foreground-subtle">{{ row.id }}</code>
87
- <p v-if="row.inheritedNote" class="text-foreground-subtle italic">{{ row.inheritedNote }}</p>
88
- </td>
89
- <td class="px-3 py-2 font-mono text-foreground-muted">{{ row.origin }}</td>
90
- <td class="px-3 py-2 font-mono text-foreground-muted break-all">{{ row.baseUrl ?? '—' }}</td>
91
- <td class="px-3 py-2 min-w-[15rem] space-y-1">
92
- <USelect
93
- v-if="row.needsCredential"
94
- :model-value="row.credentialValue"
95
- :items="row.credentialOptions"
96
- placeholder="Choose a credential"
97
- :disabled="!row.canEditCredential || rowBusy === row.id"
98
- :data-testid="`provider-credential-${row.id}`"
99
- class="w-full bg-surface-ground border-surface-border text-foreground text-xs font-mono"
100
- @update:model-value="onCredentialSelect(row, String($event ?? ''))"
101
- />
102
- <p :class="row.configured ? 'text-foreground-subtle' : 'text-amber-600 dark:text-amber-400'" class="italic">
103
- {{ row.status }}
104
- </p>
105
-
106
- <!-- Inline key editor -->
107
- <div
108
- v-if="keyEditing === row.id"
109
- :data-testid="`provider-credential-enter-key-${row.id}`"
110
- class="space-y-1.5 p-2 rounded-lg border border-surface-border bg-surface-ground/50"
55
+ <!-- One stacked row per provider: fits a settings column; a table squeezed the base URL and clipped the actions. -->
56
+ <ul v-else class="rounded-lg border border-surface-border divide-y divide-surface-border text-xs">
57
+ <li
58
+ v-for="row in rows"
59
+ :key="row.id"
60
+ :data-testid="`provider-row-${row.id}`"
61
+ class="p-3 space-y-2"
62
+ :class="row.enabled ? '' : 'opacity-60'"
63
+ >
64
+ <div class="flex items-start gap-3">
65
+ <div class="flex-1 min-w-0">
66
+ <div class="flex items-center gap-1.5 flex-wrap">
67
+ <span class="text-sm font-medium text-foreground">{{ row.name }}</span>
68
+ <UBadge
69
+ :data-testid="`provider-badge-${row.id}`"
70
+ size="sm"
71
+ variant="subtle"
72
+ color="neutral"
73
+ >{{ row.badge }}</UBadge>
74
+ <Icon v-if="row.locked" name="lucide:lock" class="text-foreground-subtle shrink-0" />
75
+ </div>
76
+ <p class="font-mono text-foreground-subtle truncate" :title="row.baseUrl ?? undefined">{{ rowMeta(row) }}</p>
77
+ <p v-if="row.inheritedNote" class="text-foreground-subtle italic">{{ row.inheritedNote }}</p>
78
+ </div>
79
+ <div class="flex items-center gap-1 shrink-0">
80
+ <UButton
81
+ v-if="canTest"
82
+ size="sm"
83
+ variant="ghost"
84
+ :loading="testBusy === row.id"
85
+ :disabled="testBusy !== null"
86
+ :data-testid="`provider-test-${row.id}`"
87
+ @click="testProvider(row)"
88
+ >
89
+ <Icon name="lucide:plug-zap" class="mr-1.5 w-3.5 h-3.5" />Test
90
+ </UButton>
91
+ <UButton
92
+ v-if="row.canReset"
93
+ size="sm"
94
+ variant="ghost"
95
+ :loading="rowBusy === row.id"
96
+ :data-testid="`provider-reset-${row.id}`"
97
+ @click="resetPatch(row)"
98
+ >
99
+ <Icon name="lucide:undo-2" class="mr-1.5 w-3.5 h-3.5" />Reset
100
+ </UButton>
101
+ <template v-if="row.editable">
102
+ <UButton
103
+ size="sm"
104
+ variant="ghost"
105
+ :aria-label="`Edit ${row.id}`"
106
+ :data-testid="`provider-edit-${row.id}`"
107
+ @click="openEdit(row)"
111
108
  >
112
- <UInput
113
- v-model="keyForm.name"
114
- placeholder="PROVIDER_API_KEY"
115
- :data-testid="`provider-key-name-${row.id}`"
116
- class="w-full bg-surface-ground border-surface-border text-foreground text-xs font-mono"
117
- />
118
- <UInput
119
- v-model="keyForm.value"
120
- type="password"
121
- autocomplete="off"
122
- placeholder="Key"
123
- :data-testid="`provider-key-value-${row.id}`"
124
- class="w-full bg-surface-ground border-surface-border text-foreground text-xs font-mono"
125
- />
126
- <p v-if="keyError" class="text-error-500">{{ keyError }}</p>
127
- <div class="flex justify-end gap-1">
128
- <UButton size="xs" variant="ghost" :data-testid="`provider-key-cancel-${row.id}`" @click="cancelKeyEditor()">Cancel</UButton>
129
- <UButton
130
- size="xs"
131
- color="primary"
132
- :loading="rowBusy === row.id"
133
- :data-testid="`provider-key-save-${row.id}`"
134
- @click="saveKeyEditor()"
135
- >Save</UButton>
136
- </div>
137
- </div>
138
-
139
- <!-- Inline reference editor -->
140
- <div
141
- v-if="refEditing === row.id"
142
- :data-testid="`provider-credential-ref-${row.id}`"
143
- class="space-y-1.5 p-2 rounded-lg border border-surface-border bg-surface-ground/50"
109
+ <Icon name="lucide:pencil" class="w-3.5 h-3.5" />
110
+ </UButton>
111
+ <UButton
112
+ size="sm"
113
+ variant="ghost"
114
+ color="error"
115
+ :aria-label="`Delete ${row.id}`"
116
+ :data-testid="`provider-delete-${row.id}`"
117
+ @click="requestDelete(row)"
144
118
  >
145
- <UInput
146
- v-model="refForm.ref"
147
- placeholder="keyring:forge/name · cmd:… · platform:…"
148
- :data-testid="`provider-ref-input-${row.id}`"
149
- class="w-full bg-surface-ground border-surface-border text-foreground text-xs font-mono"
150
- />
151
- <p v-if="refError" class="text-error-500">{{ refError }}</p>
152
- <div class="flex justify-end gap-1">
153
- <UButton size="xs" variant="ghost" :data-testid="`provider-ref-cancel-${row.id}`" @click="cancelRefEditor()">Cancel</UButton>
154
- <UButton
155
- size="xs"
156
- color="primary"
157
- :loading="rowBusy === row.id"
158
- :data-testid="`provider-ref-save-${row.id}`"
159
- @click="saveRefEditor()"
160
- >Save</UButton>
161
- </div>
162
- </div>
119
+ <Icon name="lucide:trash-2" class="w-3.5 h-3.5" />
120
+ </UButton>
121
+ </template>
122
+ <USwitch
123
+ :model-value="row.enabled"
124
+ :disabled="!row.canToggle || rowBusy === row.id"
125
+ :aria-label="`Enable ${row.id}`"
126
+ :data-testid="`provider-enabled-${row.id}`"
127
+ class="ml-1"
128
+ @update:model-value="setEnabled(row, Boolean($event))"
129
+ />
130
+ </div>
131
+ </div>
163
132
 
164
- <p
165
- v-for="notice in row.overrideNotices"
166
- :key="notice"
167
- :data-testid="`provider-override-notice-${row.id}`"
168
- class="text-foreground-subtle"
169
- >{{ notice }}</p>
170
- <p
171
- v-if="testResults[row.id]"
172
- :data-testid="`provider-test-result-${row.id}`"
173
- :class="testResults[row.id]?.ok ? 'text-success-500' : 'text-error-500'"
174
- >{{ testResults[row.id]?.message }}</p>
175
- <p
176
- v-if="rowError?.id === row.id"
177
- :data-testid="`provider-row-error-${row.id}`"
178
- class="text-error-500"
179
- >{{ rowError.message }}</p>
180
- </td>
181
- <td class="px-3 py-2">
182
- <USwitch
183
- :model-value="row.enabled"
184
- :disabled="!row.canToggle || rowBusy === row.id"
185
- :aria-label="`Enable ${row.id}`"
186
- :data-testid="`provider-enabled-${row.id}`"
187
- @update:model-value="setEnabled(row, Boolean($event))"
188
- />
189
- </td>
190
- <td class="px-3 py-2">
191
- <div class="flex items-center justify-end gap-1">
192
- <UButton
193
- v-if="canTest"
194
- size="sm"
195
- variant="ghost"
196
- :loading="testBusy === row.id"
197
- :disabled="testBusy !== null"
198
- :data-testid="`provider-test-${row.id}`"
199
- @click="testProvider(row)"
200
- >
201
- <Icon name="lucide:plug-zap" class="mr-1.5 w-3.5 h-3.5" />Test
202
- </UButton>
203
- <UButton
204
- v-if="row.canReset"
205
- size="sm"
206
- variant="ghost"
207
- :loading="rowBusy === row.id"
208
- :data-testid="`provider-reset-${row.id}`"
209
- @click="resetPatch(row)"
210
- >
211
- <Icon name="lucide:undo-2" class="mr-1.5 w-3.5 h-3.5" />Reset
212
- </UButton>
213
- <template v-if="row.editable">
214
- <UButton
215
- size="sm"
216
- variant="ghost"
217
- :aria-label="`Edit ${row.id}`"
218
- :data-testid="`provider-edit-${row.id}`"
219
- @click="openEdit(row)"
220
- >
221
- <Icon name="lucide:pencil" class="w-3.5 h-3.5" />
222
- </UButton>
223
- <UButton
224
- size="sm"
225
- variant="ghost"
226
- color="error"
227
- :aria-label="`Delete ${row.id}`"
228
- :data-testid="`provider-delete-${row.id}`"
229
- @click="requestDelete(row)"
230
- >
231
- <Icon name="lucide:trash-2" class="w-3.5 h-3.5" />
232
- </UButton>
233
- </template>
234
- </div>
235
- </td>
236
- </tr>
237
- </tbody>
238
- </table>
239
- </div>
133
+ <div class="space-y-1 max-w-md">
134
+ <USelect
135
+ v-if="row.needsCredential"
136
+ :model-value="row.credentialValue"
137
+ :items="row.credentialOptions"
138
+ placeholder="Choose a credential"
139
+ :disabled="!row.canEditCredential || rowBusy === row.id"
140
+ :data-testid="`provider-credential-${row.id}`"
141
+ class="w-full bg-surface-ground border-surface-border text-foreground text-xs font-mono"
142
+ @update:model-value="onCredentialSelect(row, String($event ?? ''))"
143
+ />
144
+ <p :class="row.configured ? 'text-foreground-subtle' : 'text-amber-600 dark:text-amber-400'" class="italic">
145
+ {{ row.status }}
146
+ </p>
147
+
148
+ <!-- Inline key editor -->
149
+ <div
150
+ v-if="keyEditing === row.id"
151
+ :data-testid="`provider-credential-enter-key-${row.id}`"
152
+ class="space-y-1.5 p-2 rounded-lg border border-surface-border bg-surface-ground/50"
153
+ >
154
+ <UInput
155
+ v-model="keyForm.name"
156
+ placeholder="PROVIDER_API_KEY"
157
+ :data-testid="`provider-key-name-${row.id}`"
158
+ class="w-full bg-surface-ground border-surface-border text-foreground text-xs font-mono"
159
+ />
160
+ <UInput
161
+ v-model="keyForm.value"
162
+ type="password"
163
+ autocomplete="off"
164
+ placeholder="Key"
165
+ :data-testid="`provider-key-value-${row.id}`"
166
+ class="w-full bg-surface-ground border-surface-border text-foreground text-xs font-mono"
167
+ />
168
+ <p v-if="keyError" class="text-error-500">{{ keyError }}</p>
169
+ <div class="flex justify-end gap-1">
170
+ <UButton size="xs" variant="ghost" :data-testid="`provider-key-cancel-${row.id}`" @click="cancelKeyEditor()">Cancel</UButton>
171
+ <UButton
172
+ size="xs"
173
+ color="primary"
174
+ :loading="rowBusy === row.id"
175
+ :data-testid="`provider-key-save-${row.id}`"
176
+ @click="saveKeyEditor()"
177
+ >Save</UButton>
178
+ </div>
179
+ </div>
180
+
181
+ <!-- Inline reference editor -->
182
+ <div
183
+ v-if="refEditing === row.id"
184
+ :data-testid="`provider-credential-ref-${row.id}`"
185
+ class="space-y-1.5 p-2 rounded-lg border border-surface-border bg-surface-ground/50"
186
+ >
187
+ <UInput
188
+ v-model="refForm.ref"
189
+ placeholder="keyring:forge/name · cmd:… · platform:…"
190
+ :data-testid="`provider-ref-input-${row.id}`"
191
+ class="w-full bg-surface-ground border-surface-border text-foreground text-xs font-mono"
192
+ />
193
+ <p v-if="refError" class="text-error-500">{{ refError }}</p>
194
+ <div class="flex justify-end gap-1">
195
+ <UButton size="xs" variant="ghost" :data-testid="`provider-ref-cancel-${row.id}`" @click="cancelRefEditor()">Cancel</UButton>
196
+ <UButton
197
+ size="xs"
198
+ color="primary"
199
+ :loading="rowBusy === row.id"
200
+ :data-testid="`provider-ref-save-${row.id}`"
201
+ @click="saveRefEditor()"
202
+ >Save</UButton>
203
+ </div>
204
+ </div>
205
+
206
+ <p
207
+ v-for="notice in row.overrideNotices"
208
+ :key="notice"
209
+ :data-testid="`provider-override-notice-${row.id}`"
210
+ class="text-foreground-subtle"
211
+ >{{ notice }}</p>
212
+ <p
213
+ v-if="testResults[row.id]"
214
+ :data-testid="`provider-test-result-${row.id}`"
215
+ :class="testResults[row.id]?.ok ? 'text-success-500' : 'text-error-500'"
216
+ >{{ testResults[row.id]?.message }}</p>
217
+ <p
218
+ v-if="rowError?.id === row.id"
219
+ :data-testid="`provider-row-error-${row.id}`"
220
+ class="text-error-500"
221
+ >{{ rowError.message }}</p>
222
+ </div>
223
+ </li>
224
+ </ul>
240
225
 
241
226
  <!-- Add / edit dialog -->
242
227
  <UModal
@@ -391,7 +376,7 @@
391
376
  <script setup lang="ts">
392
377
  import { computed } from "vue";
393
378
  import type { ProvidersPanelClient } from "../vue/providers-client";
394
- import { providersPanelNote } from "../vue/providers-panel-model";
379
+ import { providersPanelNote, providersPanelRowMeta as rowMeta } from "../vue/providers-panel-model";
395
380
  import { type ProvidersPanelLayer, useProvidersPanel } from "../vue/useProvidersPanel";
396
381
 
397
382
  const props = withDefaults(
@@ -178,6 +178,20 @@ export function providersPanelNote(
178
178
  : "The instance registry, $FORGE_HOME/.skaile/providers.yml. Keys stay in the environment; an entry only references them.";
179
179
  }
180
180
 
181
+ /**
182
+ * The muted line under a provider's name: its id (when the name differs), its
183
+ * preset or surface and its base URL, without repeating a value.
184
+ */
185
+ export function providersPanelRowMeta(row: {
186
+ id: string;
187
+ name: string;
188
+ origin: string;
189
+ baseUrl?: string | null;
190
+ }): string {
191
+ const parts = [row.name !== row.id ? row.id : null, row.origin, row.baseUrl ?? null];
192
+ return [...new Set(parts.filter((part): part is string => !!part))].join(" · ");
193
+ }
194
+
181
195
  /** An `env:NAME` credential reference; group 1 is the variable name. */
182
196
  export const ENV_REF_PATTERN = /^env:(.+)$/;
183
197