@toddzheng024/dscode-bundle 0.7.12 → 0.7.13

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 (152) hide show
  1. package/THIRD_PARTY_NOTICES.md +3 -3
  2. package/cordis.patch.yml +1 -1
  3. package/package.json +5 -4
  4. package/plugins/session-metrics/view.mjs +10 -2
  5. package/vendor/tui/lib/app.mjs +6674 -0
  6. package/vendor/tui/lib/approval.mjs +122 -0
  7. package/vendor/tui/lib/attachments.mjs +218 -0
  8. package/vendor/tui/lib/authorization-panel.mjs +242 -0
  9. package/vendor/tui/lib/authorization.mjs +111 -0
  10. package/vendor/tui/lib/commands.mjs +123 -0
  11. package/vendor/tui/lib/dscode/chat.mjs +64 -0
  12. package/vendor/tui/lib/dscode/flags.mjs +32 -0
  13. package/vendor/tui/lib/dscode/model-search.mjs +68 -0
  14. package/vendor/tui/lib/dscode/paste.mjs +87 -0
  15. package/vendor/tui/lib/dscode/telemetry.mjs +82 -0
  16. package/vendor/tui/lib/dscode/welcome.mjs +100 -0
  17. package/vendor/tui/lib/editor-keys.mjs +346 -0
  18. package/vendor/tui/lib/editor.mjs +50 -0
  19. package/vendor/tui/lib/fork.mjs +24 -0
  20. package/vendor/tui/lib/git-workflow.mjs +278 -0
  21. package/vendor/tui/{types/history.d.ts → lib/history.mjs} +69 -26
  22. package/vendor/tui/{types/i18n.d.ts → lib/i18n.mjs} +23 -15
  23. package/vendor/tui/lib/index.mjs +2198 -0
  24. package/vendor/tui/lib/input-split.mjs +170 -0
  25. package/vendor/tui/lib/internals.mjs +90 -0
  26. package/vendor/tui/lib/invariant.mjs +22 -0
  27. package/vendor/tui/lib/kernel-panels.mjs +1265 -0
  28. package/vendor/tui/lib/keyboard.mjs +283 -0
  29. package/vendor/tui/lib/language-panel.mjs +41 -0
  30. package/vendor/tui/lib/locales/en.mjs +495 -0
  31. package/vendor/tui/lib/locales/zh.mjs +495 -0
  32. package/vendor/tui/lib/mentions.mjs +150 -0
  33. package/vendor/tui/lib/model-capabilities.mjs +245 -0
  34. package/vendor/tui/lib/models.mjs +215 -0
  35. package/vendor/tui/{types/panel-accent.d.ts → lib/panel-accent.mjs} +11 -7
  36. package/vendor/tui/lib/permissions.mjs +45 -0
  37. package/vendor/tui/lib/plugin-inventory.mjs +27 -0
  38. package/vendor/tui/lib/presets.mjs +46 -0
  39. package/vendor/tui/lib/provider-settings.mjs +600 -0
  40. package/vendor/tui/lib/questions.mjs +116 -0
  41. package/vendor/tui/lib/rainbow.mjs +181 -0
  42. package/vendor/tui/lib/render/animations.mjs +665 -0
  43. package/vendor/tui/lib/render/editor.mjs +437 -0
  44. package/vendor/tui/lib/render/export.mjs +119 -0
  45. package/vendor/tui/lib/render/fuzzy.mjs +79 -0
  46. package/vendor/tui/lib/render/ime-cursor.mjs +135 -0
  47. package/vendor/tui/lib/render/inspector.mjs +93 -0
  48. package/vendor/tui/lib/render/lines.mjs +538 -0
  49. package/vendor/tui/lib/render/markdown.mjs +592 -0
  50. package/vendor/tui/lib/render/projection.mjs +1862 -0
  51. package/vendor/tui/lib/render/status.mjs +591 -0
  52. package/vendor/tui/lib/render/text.mjs +157 -0
  53. package/vendor/tui/lib/render/tool-detail.mjs +187 -0
  54. package/vendor/tui/lib/render/tool-preview.mjs +79 -0
  55. package/vendor/tui/lib/render/usage.mjs +336 -0
  56. package/vendor/tui/lib/render/width.mjs +191 -0
  57. package/vendor/tui/lib/session-directory.mjs +329 -0
  58. package/vendor/tui/lib/session-query.mjs +184 -0
  59. package/vendor/tui/lib/session-switch.mjs +51 -0
  60. package/vendor/tui/lib/settings-file.mjs +75 -0
  61. package/vendor/tui/lib/skills.mjs +119 -0
  62. package/vendor/tui/lib/startup.mjs +111 -0
  63. package/vendor/tui/lib/store.mjs +129 -0
  64. package/vendor/tui/lib/subagents.mjs +200 -0
  65. package/vendor/tui/lib/terminal-title.mjs +186 -0
  66. package/vendor/tui/lib/theme-panel.mjs +60 -0
  67. package/vendor/tui/lib/theme.mjs +379 -0
  68. package/vendor/tui/lib/update-panel.mjs +239 -0
  69. package/vendor/tui/lib/update.mjs +101 -0
  70. package/vendor/tui/lib/version.mjs +105 -0
  71. package/vendor/tui/lib/whale-glyph.mjs +20 -0
  72. package/vendor/tui/LICENSE +0 -21
  73. package/vendor/tui/devtools-CdTl3MNy.mjs +0 -3643
  74. package/vendor/tui/dscode-email/cli.mjs +0 -32
  75. package/vendor/tui/dscode-email/contacts.mjs +0 -36
  76. package/vendor/tui/dscode-email/gmail-oauth.mjs +0 -69
  77. package/vendor/tui/dscode-email/gmail-store.mjs +0 -2
  78. package/vendor/tui/dscode-email/gmail.mjs +0 -194
  79. package/vendor/tui/dscode-email/imap.mjs +0 -136
  80. package/vendor/tui/dscode-email/inbox.d.mts +0 -25
  81. package/vendor/tui/dscode-email/inbox.mjs +0 -76
  82. package/vendor/tui/dscode-email/smtp.mjs +0 -69
  83. package/vendor/tui/dscode-email/store.mjs +0 -32
  84. package/vendor/tui/dscode-providers/catalog.mjs +0 -136
  85. package/vendor/tui/dscode-providers/effort.mjs +0 -35
  86. package/vendor/tui/dscode-providers/openrouter-account.mjs +0 -171
  87. package/vendor/tui/index.mjs +0 -44441
  88. package/vendor/tui/invariant.mjs +0 -21
  89. package/vendor/tui/rolldown-runtime-CMFfr-1z.mjs +0 -26
  90. package/vendor/tui/session-query.mjs +0 -150
  91. package/vendor/tui/startup.mjs +0 -109
  92. package/vendor/tui/theme-7u5Qo3dF.mjs +0 -1265
  93. package/vendor/tui/types/app.d.ts +0 -348
  94. package/vendor/tui/types/approval.d.ts +0 -59
  95. package/vendor/tui/types/attachments.d.ts +0 -52
  96. package/vendor/tui/types/authorization-panel.d.ts +0 -22
  97. package/vendor/tui/types/authorization.d.ts +0 -36
  98. package/vendor/tui/types/commands.d.ts +0 -52
  99. package/vendor/tui/types/editor-keys.d.ts +0 -105
  100. package/vendor/tui/types/editor.d.ts +0 -6
  101. package/vendor/tui/types/fork.d.ts +0 -8
  102. package/vendor/tui/types/git-workflow.d.ts +0 -121
  103. package/vendor/tui/types/index.d.ts +0 -223
  104. package/vendor/tui/types/input-split.d.ts +0 -54
  105. package/vendor/tui/types/internals.d.ts +0 -26
  106. package/vendor/tui/types/invariant.d.ts +0 -15
  107. package/vendor/tui/types/kernel-panels.d.ts +0 -245
  108. package/vendor/tui/types/keyboard.d.ts +0 -80
  109. package/vendor/tui/types/language-panel.d.ts +0 -12
  110. package/vendor/tui/types/locales/en.d.ts +0 -450
  111. package/vendor/tui/types/locales/zh.d.ts +0 -9
  112. package/vendor/tui/types/mentions.d.ts +0 -85
  113. package/vendor/tui/types/model-capabilities.d.ts +0 -82
  114. package/vendor/tui/types/models.d.ts +0 -133
  115. package/vendor/tui/types/permissions.d.ts +0 -27
  116. package/vendor/tui/types/plugin-inventory.d.ts +0 -11
  117. package/vendor/tui/types/presets.d.ts +0 -22
  118. package/vendor/tui/types/provider-settings.d.ts +0 -239
  119. package/vendor/tui/types/questions.d.ts +0 -54
  120. package/vendor/tui/types/rainbow.d.ts +0 -69
  121. package/vendor/tui/types/render/animations.d.ts +0 -307
  122. package/vendor/tui/types/render/editor.d.ts +0 -163
  123. package/vendor/tui/types/render/export.d.ts +0 -9
  124. package/vendor/tui/types/render/fuzzy.d.ts +0 -21
  125. package/vendor/tui/types/render/ime-cursor.d.ts +0 -60
  126. package/vendor/tui/types/render/inspector.d.ts +0 -62
  127. package/vendor/tui/types/render/lines.d.ts +0 -86
  128. package/vendor/tui/types/render/markdown.d.ts +0 -29
  129. package/vendor/tui/types/render/projection.d.ts +0 -587
  130. package/vendor/tui/types/render/status.d.ts +0 -196
  131. package/vendor/tui/types/render/text.d.ts +0 -64
  132. package/vendor/tui/types/render/tool-detail.d.ts +0 -94
  133. package/vendor/tui/types/render/tool-preview.d.ts +0 -28
  134. package/vendor/tui/types/render/usage.d.ts +0 -113
  135. package/vendor/tui/types/render/width.d.ts +0 -29
  136. package/vendor/tui/types/session-directory.d.ts +0 -190
  137. package/vendor/tui/types/session-query.d.ts +0 -92
  138. package/vendor/tui/types/session-switch.d.ts +0 -17
  139. package/vendor/tui/types/settings-file.d.ts +0 -41
  140. package/vendor/tui/types/skills.d.ts +0 -47
  141. package/vendor/tui/types/startup.d.ts +0 -65
  142. package/vendor/tui/types/store.d.ts +0 -58
  143. package/vendor/tui/types/subagents.d.ts +0 -70
  144. package/vendor/tui/types/terminal-title.d.ts +0 -66
  145. package/vendor/tui/types/theme-panel.d.ts +0 -24
  146. package/vendor/tui/types/theme.d.ts +0 -434
  147. package/vendor/tui/types/update-panel.d.ts +0 -49
  148. package/vendor/tui/types/update.d.ts +0 -75
  149. package/vendor/tui/types/version.d.ts +0 -19
  150. package/vendor/tui/types/whale-glyph.d.ts +0 -6
  151. /package/vendor/tui/{dscode-clipboard-image → lib/dscode/clipboard-image}/clipboard-image.swift +0 -0
  152. /package/vendor/tui/{dscode-clipboard-image → lib/dscode/clipboard-image}/index.mjs +0 -0
@@ -0,0 +1,600 @@
1
+ /**
2
+ * Provider settings adapter for the TUI `/model` provider-management panel:
3
+ * the same-process equivalent of the web host's Models page join
4
+ * (`packages/client/ui-settings-models`), reading the advisory `ctx.llm`
5
+ * registry, the redacted `ctx.settings` descriptors, and the value-free
6
+ * `ctx.credentials` facts directly. Secrets never cross this module: settings
7
+ * are read with `redactSecrets: true`, credentials are only ever described
8
+ * (never resolved), and every message is single-line without embedding key
9
+ * data.
10
+ *
11
+ * @module @deepseek-ai/dsh-tui/provider-settings
12
+ */
13
+ import { normalizeApiKey } from '@deepseek-ai/dsh-llm';
14
+ /* ------------------------------------------------------------------ *
15
+ * Pure helpers.
16
+ * ------------------------------------------------------------------ */
17
+ /** Human text for a rejection value (mirrors the web page's `messageOf`). */
18
+ function messageOf(error) {
19
+ return error instanceof Error ? error.message : String(error);
20
+ }
21
+ /** Collapse every whitespace/control run to one space so a notice stays one line. */
22
+ function singleLine(message) {
23
+ return message.replace(/[\u0000-\u001F\u007F]/g, ' ').replace(/\s+/g, ' ').trim();
24
+ }
25
+ /** Keep a misbehaving credential provider from reflecting the submitted secret. */
26
+ function credentialWriteMessage(error, secret) {
27
+ const message = singleLine(messageOf(error));
28
+ return message.includes(secret) ? 'credentials service rejected the API key' : message;
29
+ }
30
+ /** Obvious shell-assignment paste; mirrors the official Web Models editor. */
31
+ export const ENV_ASSIGNMENT = /^[A-Z][A-Z0-9_]*=[^=]/;
32
+ /** Whether the whole draft is wrapped in one matching quote pair. */
33
+ export function hasWrappingQuotes(value) {
34
+ const first = value[0];
35
+ return (first === '"' || first === '\'' || first === '`') && value.length > 1 && value.endsWith(first);
36
+ }
37
+ /** Read the value at a path through plain objects; undefined when any segment misses. */
38
+ function getPath(value, path) {
39
+ let current = value;
40
+ for (const segment of path) {
41
+ if (typeof current !== 'object' || current === null)
42
+ return undefined;
43
+ current = current[segment];
44
+ }
45
+ return current;
46
+ }
47
+ /** Whether a path resolves to a defined value (the empty path reads the root). */
48
+ function hasPath(value, path) {
49
+ return path.length === 0 ? value !== undefined : getPath(value, path) !== undefined;
50
+ }
51
+ /** The credential reference a resolved profile names (its `apiKeyEnv` field). */
52
+ function profileRefOf(profile) {
53
+ if (typeof profile !== 'object' || profile === null)
54
+ return undefined;
55
+ const ref = profile.apiKeyEnv;
56
+ return typeof ref === 'string' && ref.length > 0 ? ref : undefined;
57
+ }
58
+ /** Extract only fields the terminal can round-trip without touching provider-specific extras. */
59
+ function configurationOf(profile) {
60
+ if (typeof profile !== 'object' || profile === null)
61
+ return { models: [] };
62
+ const record = profile;
63
+ const source = Array.isArray(record.models) ? record.models : [];
64
+ const models = source.flatMap((value) => {
65
+ if (typeof value !== 'object' || value === null)
66
+ return [];
67
+ const entry = value;
68
+ if (typeof entry.id !== 'string' || entry.id.trim() === '')
69
+ return [];
70
+ // Fields the editor does not understand ride along untouched: dropping
71
+ // them here would let the next save strip hand-written reasoningEfforts
72
+ // or compat declarations out of the stored profile.
73
+ const { id: _id, name: _name, contextWindow: _contextWindow, maxTokens: _maxTokens, ...extras } = entry;
74
+ return [{
75
+ id: entry.id,
76
+ ...(typeof entry.name === 'string' && entry.name.trim() !== '' ? { name: entry.name } : {}),
77
+ ...(typeof entry.contextWindow === 'number' && Number.isFinite(entry.contextWindow) ? { contextWindow: entry.contextWindow } : {}),
78
+ ...(typeof entry.maxTokens === 'number' && Number.isFinite(entry.maxTokens) ? { maxTokens: entry.maxTokens } : {}),
79
+ ...Object.keys(extras).length > 0 ? { extras } : {},
80
+ }];
81
+ });
82
+ return {
83
+ ...(typeof record.baseURL === 'string' && record.baseURL.trim() !== '' ? { baseURL: record.baseURL } : {}),
84
+ ...(typeof record.api === 'string' && record.api.trim() !== '' ? { api: record.api } : {}),
85
+ models,
86
+ };
87
+ }
88
+ /* ------------------------------------------------------------------ *
89
+ * Exported contracts.
90
+ * ------------------------------------------------------------------ */
91
+ /**
92
+ * The conventional credential reference for a provider route: `<ROUTE>_API_KEY`
93
+ * with the route uppercased and every non-alphanumeric run collapsed to one
94
+ * underscore — the exact derivation the official Models page uses
95
+ * (`deriveKeyRef` in `ui-settings-models`), so a key saved here is found there.
96
+ * @param provider - provider route id (e.g. `pi-ai`, `minimax-cn`).
97
+ * @returns the derived reference name (e.g. `PI_AI_API_KEY`).
98
+ */
99
+ export function deriveCredentialRef(provider) {
100
+ return `${provider.toUpperCase().replace(/[^A-Z0-9]+/g, '_')}_API_KEY`;
101
+ }
102
+ /**
103
+ * The seven canonical reasoning levels a reasoningEfforts key may name -
104
+ * pi-ai's THINKING_LEVELS. A pi-ai upgrade that adds or removes one fails
105
+ * upstream's own drift gate; this mirror exists so the terminal editor can
106
+ * validate drafts without importing the pi-ai package.
107
+ */
108
+ export const REASONING_EFFORT_LEVELS = ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'];
109
+ /** Whether a raw extras value is a declared efforts dict (non-empty, non-false). */
110
+ export function isDeclaredReasoningEfforts(value) {
111
+ return typeof value === 'object' && value !== null && !Array.isArray(value) && Object.keys(value).length > 0;
112
+ }
113
+ /**
114
+ * Parse the setup page's compact efforts draft into a storable declaration.
115
+ * Grammar: empty = clear back to inherit; the single token "false" = disable
116
+ * the picker; otherwise space-separated level:wire pairs where level is one
117
+ * of REASONING_EFFORT_LEVELS and wire is any non-empty string or the literal
118
+ * "null" (send no parameter).
119
+ */
120
+ export function parseReasoningEffortsDraft(draft) {
121
+ const text = draft.trim();
122
+ if (text === '')
123
+ return { ok: true, value: undefined };
124
+ if (text === 'false')
125
+ return { ok: true, value: false };
126
+ const value = {};
127
+ for (const token of text.split(/\s+/u)) {
128
+ const split = token.indexOf(':');
129
+ if (split <= 0 || split === token.length - 1) {
130
+ return { ok: false, error: 'each entry needs level:wire, got "' + token + '"' };
131
+ }
132
+ const level = token.slice(0, split);
133
+ const wire = token.slice(split + 1);
134
+ if (!REASONING_EFFORT_LEVELS.includes(level)) {
135
+ return { ok: false, error: '"' + level + '" is not a level; use one of ' + REASONING_EFFORT_LEVELS.join('/') };
136
+ }
137
+ if (level in value) {
138
+ return { ok: false, error: 'level "' + level + '" appears twice' };
139
+ }
140
+ value[level] = wire === 'null' ? null : wire;
141
+ }
142
+ return { ok: true, value };
143
+ }
144
+ /** Serialize a stored declaration back to the compact draft form (stored key order preserved). */
145
+ export function serializeReasoningEfforts(value) {
146
+ if (value === false)
147
+ return 'false';
148
+ if (!isDeclaredReasoningEfforts(value))
149
+ return '';
150
+ return Object.entries(value)
151
+ .map(([level, wire]) => level + ':' + (wire === null ? 'null' : String(wire)))
152
+ .join(' ');
153
+ }
154
+ /** Events that invalidate the official Models provider/settings/credential join. */
155
+ const PROVIDER_SETTINGS_EVENTS = [
156
+ 'credentials/reference-updated',
157
+ 'settings/document-updated',
158
+ 'llm/adapters-updated',
159
+ ];
160
+ /** Subscribe to the same provider-directory invalidations as the official Web Models page. */
161
+ export function subscribeProviderSettings(ctx, listener) {
162
+ const events = ctx;
163
+ const disposers = PROVIDER_SETTINGS_EVENTS.map(event => events.on(event, () => listener()));
164
+ return () => {
165
+ for (const dispose of disposers)
166
+ dispose();
167
+ };
168
+ }
169
+ /** A single-line, bounded error from the provider-management adapter. */
170
+ export class ProviderSettingsError extends Error {
171
+ constructor(message) {
172
+ super(message);
173
+ this.name = 'ProviderSettingsError';
174
+ }
175
+ }
176
+ /* ------------------------------------------------------------------ *
177
+ * Load.
178
+ * ------------------------------------------------------------------ */
179
+ /**
180
+ * Join the configurable-provider directory, the redacted settings
181
+ * namespaces, and the referenced credentials into panel rows, web-parity:
182
+ * - directory entries merge with `listProviders()` to mark each live or
183
+ * dormant, and routes registered without a directory declaration appear as
184
+ * read-only/unmanaged rows (no settings address);
185
+ * - a whole-section entry is configured whenever its namespace resolves;
186
+ * a path-addressed one only when the profile resolves there;
187
+ * - a row is removable when the user layer alone carries its profile;
188
+ * - only refs named by resolved profiles are described, and a per-ref failure
189
+ * degrades to that row's bounded error instead of losing it.
190
+ * Absent `settings`/`credentials` services are tolerated the same way.
191
+ * @param ctx - context carrying the `llm` service (settings/credentials optional).
192
+ * @returns the resolved directory; empty rows when `llm` is unavailable.
193
+ */
194
+ export async function loadProviderSettings(ctx) {
195
+ const llm = ctx.get('llm');
196
+ if (llm === undefined)
197
+ return { rows: [], writable: false, failures: [] };
198
+ // Call listProviders/listConfigurableProviders AS METHODS: destructured off
199
+ // the service they lose `this` and throw on the first read (same lesson as
200
+ // loadModelDirectory).
201
+ const registered = llm.listProviders();
202
+ const failures = [];
203
+ const directoryEntries = [];
204
+ if (llm.listConfigurableProviders !== undefined) {
205
+ try {
206
+ directoryEntries.push(...llm.listConfigurableProviders());
207
+ }
208
+ catch (error) {
209
+ failures.push(`configurable-provider directory failed: ${singleLine(messageOf(error))}`);
210
+ }
211
+ }
212
+ const settings = ctx.get('settings');
213
+ let descriptors = [];
214
+ let writable = false;
215
+ if (settings !== undefined) {
216
+ try {
217
+ descriptors = settings.describe({ redactSecrets: true });
218
+ writable = settings.writable === true;
219
+ }
220
+ catch (error) {
221
+ failures.push(`settings describe failed: ${singleLine(messageOf(error))}`);
222
+ }
223
+ }
224
+ const namespaces = new Map(descriptors.map(descriptor => [descriptor.ns, descriptor]));
225
+ const active = new Set(registered.map(provider => provider.id));
226
+ const declared = new Set(directoryEntries.map(entry => entry.provider));
227
+ // Directory order first, then registered-but-undeclared routes (web parity:
228
+ // they exist and serve models, just with no settings address).
229
+ const bases = [
230
+ ...directoryEntries.map(entry => ({
231
+ provider: entry.provider,
232
+ displayName: entry.displayName,
233
+ active: active.has(entry.provider),
234
+ settingsNs: entry.settingsNs,
235
+ settingsPath: entry.settingsPath,
236
+ ...entry.declared === undefined ? {} : { declared: entry.declared },
237
+ ...entry.error === undefined ? {} : { error: singleLine(entry.error) },
238
+ })),
239
+ ...registered
240
+ .filter(provider => !declared.has(provider.id))
241
+ .map(provider => ({
242
+ provider: provider.id,
243
+ displayName: provider.name,
244
+ active: true,
245
+ settingsNs: '',
246
+ settingsPath: [],
247
+ })),
248
+ ];
249
+ const rows = bases.map((base) => {
250
+ const namespace = base.settingsNs.length === 0 ? undefined : namespaces.get(base.settingsNs);
251
+ const profile = namespace === undefined
252
+ ? undefined
253
+ : base.settingsPath.length === 0
254
+ ? namespace.value
255
+ : getPath(namespace.value, base.settingsPath);
256
+ const configured = namespace !== undefined
257
+ && (base.settingsPath.length === 0 || profile !== undefined);
258
+ const removable = namespace !== undefined
259
+ && base.settingsPath.length > 0
260
+ && hasPath(namespace.user, base.settingsPath)
261
+ && !hasPath(namespace.base, base.settingsPath);
262
+ const credentialRef = profileRefOf(profile);
263
+ return {
264
+ provider: base.provider,
265
+ displayName: base.displayName,
266
+ active: base.active,
267
+ settingsNs: base.settingsNs,
268
+ settingsPath: base.settingsPath,
269
+ settingsRevision: namespace?.revision ?? 0,
270
+ configured,
271
+ removable,
272
+ configuration: configurationOf(profile),
273
+ ...credentialRef === undefined ? {} : { credentialRef },
274
+ suggestedRef: deriveCredentialRef(base.provider),
275
+ ...base.declared === undefined ? {} : { declared: base.declared },
276
+ ...base.error === undefined ? {} : { diagnostic: base.error },
277
+ };
278
+ });
279
+ const refs = [...new Set(rows.flatMap(row => row.credentialRef === undefined ? [] : [row.credentialRef]))];
280
+ const credentialViews = new Map();
281
+ const credentials = ctx.get('credentials');
282
+ if (refs.length > 0) {
283
+ if (credentials === undefined) {
284
+ for (const ref of refs) {
285
+ credentialViews.set(ref, { kind: 'error', message: 'credentials service is unavailable' });
286
+ }
287
+ }
288
+ else {
289
+ await Promise.all(refs.map(async (ref) => {
290
+ try {
291
+ const facts = await credentials.describe(ref);
292
+ credentialViews.set(ref, {
293
+ kind: 'facts',
294
+ configured: facts.configured,
295
+ writable: facts.writable,
296
+ ...facts.source === undefined ? {} : { source: facts.source },
297
+ });
298
+ }
299
+ catch (error) {
300
+ credentialViews.set(ref, { kind: 'error', message: singleLine(messageOf(error)) });
301
+ }
302
+ }));
303
+ }
304
+ }
305
+ return {
306
+ rows: rows.map(row => ({
307
+ ...row,
308
+ credential: row.credentialRef === undefined
309
+ ? undefined
310
+ : credentialViews.get(row.credentialRef) ?? { kind: 'error', message: 'credential describe returned no view' },
311
+ })),
312
+ writable,
313
+ failures,
314
+ };
315
+ }
316
+ /* ------------------------------------------------------------------ *
317
+ * Writes. Official web ordering is kept: settings.mutate first whenever a
318
+ * profile/path or apiKeyEnv must be materialized, then credentials.set; a
319
+ * removal unsets the managed credential before the profile. All operations
320
+ * are idempotent, so re-running one after a partial failure is safe.
321
+ * ------------------------------------------------------------------ */
322
+ /**
323
+ * Store a provider API key, web-parity: validate with `normalizeApiKey`
324
+ * (single-line, actionable errors that never echo the key), materialize the
325
+ * profile/`apiKeyEnv` through `settings.mutate` first when the resolved
326
+ * profile names no reference (dormant route or ref-less profile), then store
327
+ * under the trusted named ref or the derived conventional ref. An existing
328
+ * whole-section DeepSeek whose resolved profile already names
329
+ * `DEEPSEEK_API_KEY` needs no settings mutation. Env-supplied read-only keys
330
+ * are refused before any service call.
331
+ * @param ctx - context carrying `settings` (when materializing) and `credentials`.
332
+ * @param target - the joined row to write through.
333
+ * @param rawKey - the key exactly as typed; surrounding whitespace is trimmed.
334
+ * @throws {@link ProviderSettingsError} with a single-line, key-free message.
335
+ */
336
+ export async function saveProviderCredential(ctx, target, rawKey) {
337
+ const trimmed = rawKey.trim();
338
+ if (ENV_ASSIGNMENT.test(trimmed) || hasWrappingQuotes(trimmed)) {
339
+ throw new ProviderSettingsError('paste only the API key, without an environment-variable name or wrapping quotes');
340
+ }
341
+ const checked = normalizeApiKey(rawKey);
342
+ if (!checked.ok) {
343
+ throw new ProviderSettingsError(checked.reason === 'empty'
344
+ ? 'the API key is empty after trimming surrounding whitespace'
345
+ : 'the API key contains characters an HTTP header cannot carry; type a plain printable-ASCII key');
346
+ }
347
+ if (target.settingsNs.length === 0) {
348
+ throw new ProviderSettingsError(`provider "${target.provider}" has no managed settings namespace; configure it in settings.yaml`);
349
+ }
350
+ if (target.credential?.kind === 'facts' && target.credential.writable === false) {
351
+ throw new ProviderSettingsError(`the key for provider "${target.provider}" is supplied read-only by the environment; unset it in the shell instead of overwriting it here`);
352
+ }
353
+ const credentials = ctx.get('credentials');
354
+ if (credentials === undefined) {
355
+ throw new ProviderSettingsError('credentials service is unavailable; cannot store the API key');
356
+ }
357
+ const ref = target.credentialRef ?? deriveCredentialRef(target.provider);
358
+ if (target.credentialRef === undefined) {
359
+ const settings = ctx.get('settings');
360
+ if (settings === undefined) {
361
+ throw new ProviderSettingsError('settings service is unavailable; cannot materialize the credential reference');
362
+ }
363
+ try {
364
+ await settings.mutate(target.settingsNs, [{ op: 'set', path: [...target.settingsPath, 'apiKeyEnv'], value: ref }]);
365
+ }
366
+ catch (error) {
367
+ throw new ProviderSettingsError(singleLine(messageOf(error)));
368
+ }
369
+ }
370
+ try {
371
+ await credentials.set(ref, checked.value);
372
+ }
373
+ catch (error) {
374
+ throw new ProviderSettingsError(credentialWriteMessage(error, checked.value));
375
+ }
376
+ }
377
+ /** Save the endpoint and an explicit model allow-list without rebuilding the profile. */
378
+ export async function saveProviderConfiguration(ctx, target, configuration) {
379
+ if (target.settingsNs.length === 0) {
380
+ throw new ProviderSettingsError(`provider "${target.provider}" has no managed settings namespace; configure it in settings.yaml`);
381
+ }
382
+ const settings = ctx.get('settings');
383
+ if (settings === undefined || settings.writable !== true) {
384
+ throw new ProviderSettingsError('settings are read-only; provider configuration cannot be changed here');
385
+ }
386
+ const baseURL = configuration.baseURL?.trim();
387
+ if (baseURL !== undefined && baseURL !== '') {
388
+ try {
389
+ const parsed = new URL(baseURL);
390
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
391
+ throw new Error('unsupported protocol');
392
+ }
393
+ catch {
394
+ throw new ProviderSettingsError('base URL must be an absolute http or https URL');
395
+ }
396
+ }
397
+ const seen = new Set();
398
+ const models = configuration.models.map((model) => {
399
+ const id = model.id.trim();
400
+ if (id === '' || seen.has(id))
401
+ throw new ProviderSettingsError('each selected model must have a unique non-empty id');
402
+ seen.add(id);
403
+ for (const [label, value] of [['context window', model.contextWindow], ['output window', model.maxTokens]]) {
404
+ if (value !== undefined && (!Number.isSafeInteger(value) || value <= 0)) {
405
+ throw new ProviderSettingsError(`${label} must be a positive integer`);
406
+ }
407
+ }
408
+ return {
409
+ id,
410
+ // Editor-invisible fields ride along after the id; the modelled keys
411
+ // spread last so a panel edit (or clear) always wins over a carried
412
+ // value. extras never contains those keys — see configurationOf.
413
+ ...model.extras,
414
+ ...(model.name === undefined || model.name.trim() === '' ? {} : { name: model.name.trim() }),
415
+ ...(model.contextWindow === undefined ? {} : { contextWindow: model.contextWindow }),
416
+ ...(model.maxTokens === undefined ? {} : { maxTokens: model.maxTokens }),
417
+ };
418
+ });
419
+ const root = target.settingsPath;
420
+ const ops = [
421
+ baseURL === undefined || baseURL === ''
422
+ ? { op: 'unset', path: [...root, 'baseURL'] }
423
+ : { op: 'set', path: [...root, 'baseURL'], value: baseURL },
424
+ // An explicit empty list is deliberate: it prevents a provider's shipped
425
+ // catalog from silently becoming the active selection.
426
+ { op: 'set', path: [...root, 'models'], value: models },
427
+ ];
428
+ try {
429
+ await settings.mutate(target.settingsNs, ops, target.settingsRevision);
430
+ }
431
+ catch (error) {
432
+ throw new ProviderSettingsError(singleLine(messageOf(error)));
433
+ }
434
+ }
435
+ /**
436
+ * Interrogate a provider endpoint for the models it really serves, through
437
+ * the model-discovery capability the provider's settings namespace
438
+ * registered — the same pipe the official Web Models page uses. The request
439
+ * is a draft: a typed key forces direct endpoint interrogation (gateway
440
+ * truth), while an empty key lets the harness resolve the route's stored
441
+ * credential; with neither baseURL nor route the adapter answers from its
442
+ * own knowledge.
443
+ * @param ctx - context carrying the `llm` service (optional discovery).
444
+ * @param target - provider row whose settings namespace serves the draft.
445
+ * @param request - typed key and/or endpoint override for this one probe.
446
+ * @param signal - caller cancellation (panel navigation aborts the probe).
447
+ * @returns the advertised models in endpoint order, deduplicated.
448
+ */
449
+ export async function discoverProviderModels(ctx, target, request, signal) {
450
+ if (target.settingsNs.length === 0) {
451
+ throw new ProviderSettingsError(`provider "${target.provider}" has no managed settings namespace; its models cannot be discovered here`);
452
+ }
453
+ const llm = ctx.get('llm');
454
+ if (llm?.discoverModels === undefined) {
455
+ throw new ProviderSettingsError('model discovery is unavailable in this profile; enter models by hand');
456
+ }
457
+ const typedKey = request.apiKey?.trim();
458
+ const baseURL = request.baseURL?.trim();
459
+ const hasUrl = baseURL !== undefined && baseURL !== '';
460
+ let oneShotKey = typedKey;
461
+ // A filled endpoint means the user wants THAT endpoint's real list: sending
462
+ // the route id alongside would make the adapter short-circuit to its
463
+ // installed catalog (official providers) and ignore the URL entirely. The
464
+ // probe therefore goes out as a draft — with the typed key, or with the
465
+ // stored credential resolved once for this request (never displayed,
466
+ // never persisted; exactly what provider-mode resolution does internally).
467
+ if (hasUrl && (oneShotKey === undefined || oneShotKey === '')) {
468
+ const credentialsService = ctx.get('credentials');
469
+ const ref = target.credentialRef ?? target.suggestedRef;
470
+ if (credentialsService?.resolve !== undefined && ref !== undefined) {
471
+ try {
472
+ // Call resolve AS A METHOD on the service: destructured off it the
473
+ // call loses `this` and throws on the provider's first field read
474
+ // (the same lesson loadProviderSettings documents for listProviders).
475
+ const resolved = await credentialsService.resolve(ref);
476
+ oneShotKey = resolved?.value;
477
+ }
478
+ catch {
479
+ // A failed resolution degrades to an unauthenticated probe; the
480
+ // endpoint's own 401 names the problem better than we can.
481
+ }
482
+ }
483
+ }
484
+ const draft = hasUrl
485
+ ? {
486
+ ...(oneShotKey !== undefined && oneShotKey !== '' ? { apiKey: oneShotKey } : {}),
487
+ baseURL: baseURL,
488
+ ...target.configuration.api === undefined ? {} : { api: target.configuration.api },
489
+ }
490
+ : {
491
+ // No endpoint override: the route's own knowledge answers (the official
492
+ // catalog for builtin providers — richer than any listing).
493
+ provider: target.provider,
494
+ };
495
+ try {
496
+ const discovered = signal === undefined
497
+ ? await llm.discoverModels(target.settingsNs, draft)
498
+ : await llm.discoverModels(target.settingsNs, draft, signal);
499
+ // Defensive dedupe in endpoint order (the service dedupes too; an older
500
+ // one must not leak duplicate rows into the checkable list).
501
+ const seen = new Set();
502
+ const rows = [];
503
+ for (const model of discovered) {
504
+ if (typeof model.id !== 'string' || model.id.trim() === '' || seen.has(model.id))
505
+ continue;
506
+ seen.add(model.id);
507
+ rows.push({
508
+ id: model.id,
509
+ ...model.name === undefined ? {} : { name: model.name },
510
+ ...model.contextWindow === undefined ? {} : { contextWindow: model.contextWindow },
511
+ ...model.maxTokens === undefined ? {} : { maxTokens: model.maxTokens },
512
+ });
513
+ }
514
+ return rows;
515
+ }
516
+ catch (error) {
517
+ if (signal?.aborted === true)
518
+ throw error;
519
+ throw new ProviderSettingsError(singleLine(messageOf(error)));
520
+ }
521
+ }
522
+ /**
523
+ * Remove the currently named credential without touching the provider
524
+ * profile. Only the resolved profile's own reference is unset; a dormant or
525
+ * ref-less row (nothing to remove), an already-absent key, and an
526
+ * env-supplied read-only key are rejected safely before any service call.
527
+ * @param ctx - context carrying the `credentials` service.
528
+ * @param target - the joined row whose named credential to unset.
529
+ * @throws {@link ProviderSettingsError} with a single-line, key-free message.
530
+ */
531
+ export async function unsetProviderCredential(ctx, target) {
532
+ const ref = target.credentialRef;
533
+ if (ref === undefined) {
534
+ throw new ProviderSettingsError(`provider "${target.provider}" names no credential reference to remove`);
535
+ }
536
+ const facts = target.credential;
537
+ if (facts?.kind === 'facts' && facts.configured === false) {
538
+ throw new ProviderSettingsError(`provider "${target.provider}" has no configured credential to remove`);
539
+ }
540
+ if (facts?.kind === 'facts' && facts.writable === false) {
541
+ throw new ProviderSettingsError(`the key for provider "${target.provider}" is supplied read-only by the environment; unset it in the shell instead`);
542
+ }
543
+ const credentials = ctx.get('credentials');
544
+ if (credentials === undefined) {
545
+ throw new ProviderSettingsError('credentials service is unavailable; cannot remove the API key');
546
+ }
547
+ try {
548
+ await credentials.unset(ref);
549
+ }
550
+ catch (error) {
551
+ throw new ProviderSettingsError(singleLine(messageOf(error)));
552
+ }
553
+ }
554
+ /**
555
+ * Remove a user-added provider profile, web-parity: only `removable` rows may
556
+ * be removed; a page-managed credential — the derived ref, configured and
557
+ * writable — is unset first (so a second-step failure leaves the row visible
558
+ * and the operation retryable), then `settings.mutate` unsets
559
+ * `target.settingsPath`. Both steps are idempotent. A hand-named credential
560
+ * ref may be shared elsewhere and is left alone.
561
+ * @param ctx - context carrying `credentials` and `settings`.
562
+ * @param target - the joined row to remove.
563
+ * @throws {@link ProviderSettingsError} with a single-line, key-free message.
564
+ */
565
+ export async function removeProviderSettings(ctx, target) {
566
+ if (!target.removable) {
567
+ throw new ProviderSettingsError(`provider "${target.provider}" is not removable from the user settings layer`);
568
+ }
569
+ if (target.settingsNs.length === 0) {
570
+ throw new ProviderSettingsError(`provider "${target.provider}" has no managed settings profile to remove`);
571
+ }
572
+ const managedRef = target.credentialRef === target.suggestedRef
573
+ && target.credential?.kind === 'facts'
574
+ && target.credential.configured === true
575
+ && target.credential.writable === true
576
+ ? target.credentialRef
577
+ : undefined;
578
+ if (managedRef !== undefined) {
579
+ const credentials = ctx.get('credentials');
580
+ if (credentials === undefined) {
581
+ throw new ProviderSettingsError('credentials service is unavailable; cannot remove the managed API key');
582
+ }
583
+ try {
584
+ await credentials.unset(managedRef);
585
+ }
586
+ catch (error) {
587
+ throw new ProviderSettingsError(singleLine(messageOf(error)));
588
+ }
589
+ }
590
+ const settings = ctx.get('settings');
591
+ if (settings === undefined) {
592
+ throw new ProviderSettingsError('settings service is unavailable; cannot remove the provider profile');
593
+ }
594
+ try {
595
+ await settings.mutate(target.settingsNs, [{ op: 'unset', path: [...target.settingsPath] }]);
596
+ }
597
+ catch (error) {
598
+ throw new ProviderSettingsError(singleLine(messageOf(error)));
599
+ }
600
+ }