@circuitwall/jarela 2.2.0 → 2.2.1

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 (86) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/app-path-routes-manifest.json +2 -2
  3. package/.next/standalone/.next/build-manifest.json +2 -2
  4. package/.next/standalone/.next/prerender-manifest.json +3 -3
  5. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  6. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  7. package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  11. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  12. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  13. package/.next/standalone/.next/server/app/_not-found.rsc +1 -1
  14. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  15. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  16. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  17. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  18. package/.next/standalone/.next/server/app/api/v1/models/[name]/route.js +61 -20
  19. package/.next/standalone/.next/server/app/api/v1/models/route.js +5 -3
  20. package/.next/standalone/.next/server/app/api/v1/providers/[provider]/models/route.js +27 -24
  21. package/.next/standalone/.next/server/app/api/v1/update/apply/route.js.nft.json +1 -1
  22. package/.next/standalone/.next/server/app/page.js +313 -251
  23. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  24. package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
  25. package/.next/standalone/.next/server/app-paths-manifest.json +2 -2
  26. package/.next/standalone/.next/server/chunks/2471.js +17 -17
  27. package/.next/standalone/.next/server/chunks/2563.js +24 -50
  28. package/.next/standalone/.next/server/chunks/3399.js +10695 -10695
  29. package/.next/standalone/.next/server/chunks/3524.js +5 -33
  30. package/.next/standalone/.next/server/chunks/424.js +10 -5
  31. package/.next/standalone/.next/server/chunks/4508.js +2 -2
  32. package/.next/standalone/.next/server/chunks/46.js +111 -46
  33. package/.next/standalone/.next/server/chunks/466.js +88 -13
  34. package/.next/standalone/.next/server/chunks/6649.js +1 -1
  35. package/.next/standalone/.next/server/chunks/9590.js +108 -46
  36. package/.next/standalone/.next/server/chunks/9640.js +3 -6
  37. package/.next/standalone/.next/server/middleware-build-manifest.js +2 -2
  38. package/.next/standalone/.next/server/middleware.js +104 -45
  39. package/.next/standalone/.next/server/pages/404.html +1 -1
  40. package/.next/standalone/.next/server/pages/500.html +1 -1
  41. package/.next/standalone/.next/server/preview-props.json +3 -3
  42. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  43. package/.next/standalone/.next/static/chunks/{1395-f201e3e2e1f57715.js → 1395-a340fb0a345382a4.js} +89 -14
  44. package/.next/standalone/.next/static/chunks/{4576-13f7546a145a3624.js → 7045-8bc2f14e430ca661.js} +58 -2
  45. package/.next/standalone/.next/static/chunks/app/{page-c6e972a7fd13dae8.js → page-7017f07f8a7a083c.js} +170 -151
  46. package/.next/standalone/package.json +1 -1
  47. package/CHANGELOG.md +8 -0
  48. package/api/client.models.test.ts +31 -0
  49. package/api/client.ts +16 -1
  50. package/api/types.ts +8 -0
  51. package/app/api/v1/models/[name]/route.test.ts +89 -0
  52. package/app/api/v1/models/[name]/route.ts +54 -3
  53. package/app/api/v1/models/route.ts +4 -2
  54. package/app/api/v1/providers/[provider]/models/route.test.ts +53 -0
  55. package/app/api/v1/providers/[provider]/models/route.ts +27 -16
  56. package/components/agents/agent-editor/AdvancedSection.tsx +8 -2
  57. package/components/agents/agent-editor/ModelSection.tsx +6 -3
  58. package/components/models/ModelEditor.tsx +1 -1
  59. package/components/models/ModelsPanel.tsx +34 -8
  60. package/components/models/model-editor/GitHubCopilotAuth.tsx +1 -1
  61. package/components/models/model-editor/save-helpers.ts +2 -2
  62. package/components/models/model-editor/useModelEditorForm.ts +1 -1
  63. package/components/models/model-editor/useModelSaveHandlers.ts +1 -1
  64. package/components/setup/wizard/StepCredentials.tsx +1 -1
  65. package/components/setup/wizard/StepModel.tsx +3 -8
  66. package/hooks/useModels.ts +17 -1
  67. package/lib/agents/effective-model.test.ts +45 -0
  68. package/lib/agents/effective-model.ts +11 -5
  69. package/lib/agents/llm.ts +2 -2
  70. package/lib/agents/run-thread.ts +10 -5
  71. package/lib/agents/warm-summary-background.test.ts +1 -2
  72. package/lib/agents/warm-summary-background.ts +4 -7
  73. package/lib/db/migrations.ts +10 -0
  74. package/lib/health/probes.test.ts +8 -11
  75. package/lib/health/probes.ts +5 -31
  76. package/lib/models/editor-payload.ts +1 -1
  77. package/lib/providers/github-copilot-auth.ts +2 -3
  78. package/lib/providers/github-copilot.test.ts +27 -0
  79. package/lib/providers/github-copilot.ts +21 -44
  80. package/lib/stores/integrations.test.ts +7 -0
  81. package/lib/stores/integrations.ts +4 -4
  82. package/lib/stores/model-config.test.ts +88 -0
  83. package/lib/stores/model-config.ts +90 -8
  84. package/package.json +1 -1
  85. /package/.next/standalone/.next/static/{e2NV1wk6mWg3SIeELXevg → NUrC9xax1sp1Q70nlFqS6}/_buildManifest.js +0 -0
  86. /package/.next/standalone/.next/static/{e2NV1wk6mWg3SIeELXevg → NUrC9xax1sp1Q70nlFqS6}/_ssgManifest.js +0 -0
@@ -3507,14 +3507,14 @@ const INTEGRATIONS = {
3507
3507
  "github-copilot": {
3508
3508
  label: "GitHub Copilot",
3509
3509
  category: "llm",
3510
- description: "Routes chat-completion requests through your Copilot subscription. Paste a GitHub PAT " + "with `copilot` scope from github.com/settings/tokens, or run the device-flow sign-in in " + "the Models panel.",
3510
+ description: "Routes chat-completion requests through your Copilot subscription. Use the device-flow " + "sign-in in the Models panel; legacy GitHub PAT authentication is no longer supported.",
3511
3511
  fields: [
3512
3512
  {
3513
3513
  key: "api_key",
3514
- label: "GitHub PAT",
3515
- placeholder: "ghp_… or github_pat_…",
3514
+ label: "Legacy GitHub PAT (unsupported)",
3515
+ placeholder: "Use Models panel sign-in",
3516
3516
  secret: true,
3517
- required: true
3517
+ required: false
3518
3518
  }
3519
3519
  ]
3520
3520
  },
@@ -3784,6 +3784,7 @@ function saveIntegration(name, incoming) {
3784
3784
  if (f.required && !v.trim()) return {
3785
3785
  error: `"${f.key}" cannot be empty`
3786
3786
  };
3787
+ if (!v.trim()) continue;
3787
3788
  merged[f.key] = v;
3788
3789
  if (existing[f.key] !== v) touched.push(f.key);
3789
3790
  }
@@ -4361,6 +4362,7 @@ function runMigrations(db) {
4361
4362
  ensureCredentialsTable(db);
4362
4363
  ensureCredentialsLabelAndDefaultColumns(db);
4363
4364
  ensureModelConfigCredentialIdColumn(db);
4365
+ ensureModelConfigEnabledColumn(db);
4364
4366
  ensureAgentConfigsToolCredentialsColumn(db);
4365
4367
  seedAgentConfigs(db);
4366
4368
  backfillDeveloperInteractiveTerminalTools(db);
@@ -5347,6 +5349,14 @@ function ensureModelConfigCredentialIdColumn(db) {
5347
5349
  db.exec("ALTER TABLE model_configs ADD COLUMN credential_id TEXT");
5348
5350
  }
5349
5351
  }
5352
+ // ADR-0102: disabled model configs stay saved but are hidden from model
5353
+ // selection, routing, and default fallback. Existing rows default to enabled.
5354
+ function ensureModelConfigEnabledColumn(db) {
5355
+ const cols = db.prepare("PRAGMA table_info(model_configs)").all();
5356
+ if (!cols.some((c)=>c.name === "enabled")) {
5357
+ db.exec("ALTER TABLE model_configs ADD COLUMN enabled INTEGER NOT NULL DEFAULT 1");
5358
+ }
5359
+ }
5350
5360
  // Idempotent backfill: for each model_config still carrying an inline
5351
5361
  // `api_key` (and no `credential_id` yet), create a credential row and
5352
5362
  // link by id. Strips the api_key / base_url / extra_headers fields from
@@ -6724,11 +6734,14 @@ __webpack_require__.d(__webpack_exports__, {
6724
6734
  lB: () => (/* binding */ getDefaultModelConfig),
6725
6735
  Ac: () => (/* binding */ getModelConfig),
6726
6736
  mN: () => (/* binding */ getModelParams),
6737
+ FO: () => (/* binding */ isModelConfigEnabled),
6738
+ "if": () => (/* binding */ listEnabledModelConfigs),
6727
6739
  D1: () => (/* binding */ listModelConfigs),
6740
+ np: () => (/* binding */ resolveRunnableModelConfig),
6728
6741
  TN: () => (/* binding */ upsertModelConfig)
6729
6742
  });
6730
6743
 
6731
- // UNUSED EXPORTS: deleteModelConfig, withModelConfigMutation
6744
+ // UNUSED EXPORTS: ModelConfigStateError, deleteModelConfig, setDefaultModelConfig, setModelConfigEnabled, withModelConfigMutation
6732
6745
 
6733
6746
  // EXTERNAL MODULE: ./lib/db/index.ts + 7 modules
6734
6747
  var lib_db = __webpack_require__(39504);
@@ -6767,6 +6780,14 @@ const now = ()=>new Date().toISOString();
6767
6780
  function withModelConfigMutation(name, operation) {
6768
6781
  return withKeyedMutation(`model-config:${name}`, operation);
6769
6782
  }
6783
+ class ModelConfigStateError extends Error {
6784
+ constructor(message, status){
6785
+ super(message), this.status = status;
6786
+ }
6787
+ }
6788
+ function isModelConfigEnabled(cfg) {
6789
+ return !!cfg && cfg.enabled !== 0;
6790
+ }
6770
6791
  // `params` holds the provider API key alongside hyperparameters and is
6771
6792
  // stored encrypted at rest (ADR-0005). Decrypt before returning rows.
6772
6793
  function decryptRow(row) {
@@ -6779,18 +6800,74 @@ function listModelConfigs() {
6779
6800
  const rows = (0,lib_db/* getDb */.L)().prepare("SELECT * FROM model_configs ORDER BY is_default DESC, name ASC").all();
6780
6801
  return rows.map(decryptRow);
6781
6802
  }
6803
+ /** Model configs that may be offered for selection or routing. */ function listEnabledModelConfigs() {
6804
+ return listModelConfigs().filter(isModelConfigEnabled);
6805
+ }
6782
6806
  function getModelConfig(name) {
6783
6807
  const row = (0,lib_db/* getDb */.L)().prepare("SELECT * FROM model_configs WHERE name=?").get(name) ?? null;
6784
6808
  return row ? decryptRow(row) : null;
6785
6809
  }
6786
6810
  function getDefaultModelConfig() {
6787
6811
  const db = (0,lib_db/* getDb */.L)();
6788
- const row = db.prepare("SELECT * FROM model_configs WHERE is_default=1 LIMIT 1").get() ?? null;
6812
+ const row = db.prepare("SELECT * FROM model_configs WHERE is_default=1 AND enabled=1 LIMIT 1").get() ?? null;
6789
6813
  if (row) return decryptRow(row);
6790
- const fallback = db.prepare("SELECT * FROM model_configs ORDER BY name ASC LIMIT 1").get() ?? null;
6814
+ const fallback = db.prepare("SELECT * FROM model_configs WHERE enabled=1 ORDER BY name ASC LIMIT 1").get() ?? null;
6791
6815
  return fallback ? decryptRow(fallback) : null;
6792
6816
  }
6793
- function upsertModelConfig(name, provider, model_id, params, is_default, credential_id = null) {
6817
+ /**
6818
+ * Resolve the config a run should use: the named config when it exists and is
6819
+ * enabled, otherwise the workspace default. A disabled assignment therefore
6820
+ * falls back without being cleared, so re-enabling restores it.
6821
+ */ function resolveRunnableModelConfig(name) {
6822
+ const named = name ? getModelConfig(name) : null;
6823
+ if (named && isModelConfigEnabled(named)) return named;
6824
+ return getDefaultModelConfig();
6825
+ }
6826
+ /** Atomically make `name` the only default model config. */ function setDefaultModelConfig(name) {
6827
+ return withDbTransaction(()=>{
6828
+ const db = getDb();
6829
+ const target = getModelConfig(name);
6830
+ if (!target) throw new ModelConfigStateError(`Model config "${name}" not found`, 404);
6831
+ if (!isModelConfigEnabled(target)) {
6832
+ throw new ModelConfigStateError(`Model config "${name}" is disabled; enable it before making it the default`, 409);
6833
+ }
6834
+ const t = now();
6835
+ db.prepare("UPDATE model_configs SET is_default=0, updated_at=? WHERE is_default=1 AND name<>?").run(t, name);
6836
+ db.prepare("UPDATE model_configs SET is_default=1, updated_at=? WHERE name=?").run(t, name);
6837
+ return getModelConfig(name);
6838
+ });
6839
+ }
6840
+ /**
6841
+ * Enable or disable a model config. Disabling the default promotes the
6842
+ * alphabetically-first other enabled config in the same transaction, and the
6843
+ * last enabled config cannot be disabled so runs always have a model.
6844
+ */ function setModelConfigEnabled(name, enabled) {
6845
+ return withDbTransaction(()=>{
6846
+ const db = getDb();
6847
+ const target = getModelConfig(name);
6848
+ if (!target) throw new ModelConfigStateError(`Model config "${name}" not found`, 404);
6849
+ if (isModelConfigEnabled(target) === enabled) return target;
6850
+ const t = now();
6851
+ if (enabled) {
6852
+ db.prepare("UPDATE model_configs SET enabled=1, updated_at=? WHERE name=?").run(t, name);
6853
+ return getModelConfig(name);
6854
+ }
6855
+ const next = db.prepare("SELECT name FROM model_configs WHERE enabled=1 AND name<>? ORDER BY name ASC LIMIT 1").get(name);
6856
+ if (!next?.name) {
6857
+ throw new ModelConfigStateError("At least one model config must stay enabled", 409);
6858
+ }
6859
+ db.prepare("UPDATE model_configs SET enabled=0, is_default=0, updated_at=? WHERE name=?").run(t, name);
6860
+ if (target.is_default === 1) {
6861
+ db.prepare("UPDATE model_configs SET is_default=1, updated_at=? WHERE name=?").run(t, next.name);
6862
+ }
6863
+ return getModelConfig(name);
6864
+ });
6865
+ }
6866
+ /**
6867
+ * Insert or update a model config. `is_default` undefined keeps the current
6868
+ * flag (false for new rows); true atomically clears every other default and
6869
+ * re-enables the row, because the default must always be selectable.
6870
+ */ function upsertModelConfig(name, provider, model_id, params, is_default, credential_id = null) {
6794
6871
  return (0,transaction/* withDbTransaction */.J)(()=>{
6795
6872
  const t = now();
6796
6873
  const existing = getModelConfig(name);
@@ -6806,9 +6883,11 @@ function upsertModelConfig(name, provider, model_id, params, is_default, credent
6806
6883
  ...decodeInlineParams(existing),
6807
6884
  ...params
6808
6885
  };
6886
+ const finalDefault = is_default ?? existing?.is_default === 1;
6887
+ const finalEnabled = finalDefault || !existing || isModelConfigEnabled(existing);
6809
6888
  const db = (0,lib_db/* getDb */.L)();
6810
- if (is_default) db.prepare("UPDATE model_configs SET is_default=0").run();
6811
- db.prepare("INSERT OR REPLACE INTO model_configs (name,provider,model_id,params,is_default,credential_id,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?)").run(name, provider, model_id, (0,envelope/* encrypt */.w)(JSON.stringify(mergedParams)), is_default ? 1 : 0, finalCredId, created_at, t);
6889
+ if (finalDefault) db.prepare("UPDATE model_configs SET is_default=0 WHERE name<>?").run(name);
6890
+ db.prepare("INSERT OR REPLACE INTO model_configs (name,provider,model_id,params,is_default,enabled,credential_id,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?,?)").run(name, provider, model_id, (0,envelope/* encrypt */.w)(JSON.stringify(mergedParams)), finalDefault ? 1 : 0, finalEnabled ? 1 : 0, finalCredId, created_at, t);
6812
6891
  return getModelConfig(name);
6813
6892
  });
6814
6893
  }
@@ -6821,10 +6900,10 @@ function deleteModelConfig(name) {
6821
6900
  // Automatically reset any agent pointing to this deleted model config to NULL (automatic / default model)
6822
6901
  db.prepare("UPDATE agent_configs SET model_config_name = NULL WHERE model_config_name = ?").run(name);
6823
6902
  // Avoid the "no default" state: if the deleted row was the default and any
6824
- // other rows remain, promote the alphabetically-first remaining row so
6903
+ // other enabled rows remain, promote the alphabetically-first one so
6825
6904
  // agents that fall back to the default keep working.
6826
6905
  if (wasDefault) {
6827
- const next = db.prepare("SELECT name FROM model_configs ORDER BY name ASC LIMIT 1").get();
6906
+ const next = db.prepare("SELECT name FROM model_configs WHERE enabled=1 ORDER BY name ASC LIMIT 1").get();
6828
6907
  if (next?.name) db.prepare("UPDATE model_configs SET is_default=1 WHERE name=?").run(next.name);
6829
6908
  }
6830
6909
  return true;
@@ -7991,17 +8070,17 @@ function pickGitHubCompatOptions(params) {
7991
8070
  return out;
7992
8071
  }
7993
8072
  const tokenCache = new Map();
7994
- function isLikelyGitHubPat(token) {
7995
- return token.startsWith("ghp_") || token.startsWith("github_pat_") || token.startsWith("gho_");
8073
+ function isGitHubPat(token) {
8074
+ return token.startsWith("ghp_") || token.startsWith("github_pat_");
7996
8075
  }
7997
- async function getCopilotToken(pat) {
7998
- if (!pat) throw new Error("GitHub Copilot: no API key configured for token exchange.");
7999
- const cached = tokenCache.get(pat);
8076
+ async function exchangeCopilotSessionToken(githubToken) {
8077
+ if (!githubToken) throw new Error("GitHub Copilot: no API key configured for token exchange.");
8078
+ const cached = tokenCache.get(githubToken);
8000
8079
  if (cached && cached.expiresAt - Date.now() > 60000) return cached.token;
8001
- tokenCache.delete(pat);
8080
+ tokenCache.delete(githubToken);
8002
8081
  const res = await fetch("https://api.github.com/copilot_internal/v2/token", {
8003
8082
  headers: {
8004
- Authorization: `token ${pat}`,
8083
+ Authorization: `token ${githubToken}`,
8005
8084
  "User-Agent": "Jarela/1.0"
8006
8085
  },
8007
8086
  signal: AbortSignal.timeout(30000)
@@ -8009,17 +8088,17 @@ async function getCopilotToken(pat) {
8009
8088
  if (!res.ok) {
8010
8089
  const body = await (0,error/* readErrorBody */.N)(res);
8011
8090
  if (res.status === 401) {
8012
- throw new Error(`GitHub Copilot: token exchange failed (401). ` + `Ensure your PAT has the "copilot" scope and that you have an active Copilot subscription. ` + `Details: ${body}`);
8091
+ throw new Error(`GitHub Copilot: token exchange failed (401). ` + `Check that this GitHub account has an active Copilot subscription and the credential is valid. ` + `Details: ${body}`);
8013
8092
  }
8014
8093
  if (res.status === 404) {
8015
- throw new Error("GitHub Copilot token exchange endpoint was not found for this credential. " + "Personal Access Tokens are not accepted directly by Copilot chat. " + "Use a Copilot session token in params.copilot_session_token, or a credential that supports token exchange.");
8094
+ throw new Error("GitHub Copilot could not exchange this credential for a session token. " + "Use the in-app device-flow sign-in or set params.copilot_session_token.");
8016
8095
  }
8017
8096
  throw new Error(`GitHub Copilot: token exchange failed (${res.status}): ${body}`);
8018
8097
  }
8019
8098
  const json = await res.json();
8020
8099
  if (!json.token) throw new Error("GitHub Copilot: token exchange returned no token");
8021
8100
  const expiresAt = json.expires_at ? new Date(json.expires_at).getTime() : Date.now() + 28 * 60000;
8022
- tokenCache.set(pat, {
8101
+ tokenCache.set(githubToken, {
8023
8102
  token: json.token,
8024
8103
  expiresAt
8025
8104
  });
@@ -8050,7 +8129,6 @@ async function resolveCopilotAuth(params) {
8050
8129
  const explicitSessionToken = params.copilot_session_token?.trim();
8051
8130
  if (explicitSessionToken && explicitSessionToken.length > 0) {
8052
8131
  return {
8053
- kind: "copilot-api",
8054
8132
  baseURL: params.base_url ?? "https://api.githubcopilot.com",
8055
8133
  apiKey: explicitSessionToken,
8056
8134
  headers: {
@@ -8060,9 +8138,8 @@ async function resolveCopilotAuth(params) {
8060
8138
  }
8061
8139
  const oauthToken = (0,github_copilot_auth/* getStoredOAuthToken */.OS)();
8062
8140
  if (oauthToken) {
8063
- const token = await getCopilotToken(oauthToken);
8141
+ const token = await exchangeCopilotSessionToken(oauthToken);
8064
8142
  return {
8065
- kind: "copilot-api",
8066
8143
  baseURL: params.base_url ?? "https://api.githubcopilot.com",
8067
8144
  apiKey: token,
8068
8145
  headers: {
@@ -8070,23 +8147,14 @@ async function resolveCopilotAuth(params) {
8070
8147
  }
8071
8148
  };
8072
8149
  }
8073
- if (apiKey && isLikelyGitHubPat(apiKey)) {
8074
- return {
8075
- kind: "models-rest",
8076
- baseURL: params.base_url ?? "https://models.github.ai/inference",
8077
- apiKey,
8078
- headers: {
8079
- Accept: "application/vnd.github+json",
8080
- "X-GitHub-Api-Version": "2026-03-10"
8081
- }
8082
- };
8150
+ if (apiKey && isGitHubPat(apiKey)) {
8151
+ throw new Error("GitHub Copilot: this provider's GitHub PAT route used GitHub Models, which has been retired. " + "Use the in-app device-flow sign-in or set copilot_session_token.");
8083
8152
  }
8084
8153
  if (!apiKey) {
8085
8154
  throw new Error("GitHub Copilot: not signed in. Use the in-app device-flow login, or set api_key / copilot_session_token.");
8086
8155
  }
8087
- const token = await getCopilotToken(apiKey);
8156
+ const token = await exchangeCopilotSessionToken(apiKey);
8088
8157
  return {
8089
- kind: "copilot-api",
8090
8158
  baseURL: params.base_url ?? "https://api.githubcopilot.com",
8091
8159
  apiKey: token,
8092
8160
  headers: {
@@ -8097,14 +8165,9 @@ async function resolveCopilotAuth(params) {
8097
8165
  // Anthropic SDK client pointed at Copilot's native `/v1/messages` endpoint.
8098
8166
  // Copilot accepts the Anthropic request shape (including `cache_control`
8099
8167
  // ephemeral breakpoints) for Claude-family models when called with a
8100
- // session token. The PAT-only `models.github.ai` fallback does NOT expose
8101
- // this endpoint, so we surface a clear error there instead of silently
8102
- // downgrading to a path that strips the cache markers.
8168
+ // Copilot session token.
8103
8169
  async function resolvedAnthropicClient(params) {
8104
8170
  const auth = await resolveCopilotAuth(params);
8105
- if (auth.kind !== "copilot-api") {
8106
- throw new Error("GitHub Copilot: Claude models require a Copilot session token (OAuth or copilot_session_token). " + "Personal Access Tokens route to GitHub Models REST, which does not expose the native Messages API.");
8107
- }
8108
8171
  return new client/* Anthropic */.NB({
8109
8172
  // The SDK requires an apiKey field; we authenticate via authToken
8110
8173
  // (Authorization: Bearer ...) per Copilot's contract. The empty
@@ -10599,9 +10662,8 @@ function isRetryableProviderStreamError(msg) {
10599
10662
  /* harmony export */ });
10600
10663
  /* unused harmony exports startDeviceFlow, pollDeviceFlow, storeOAuthToken, clearStoredOAuthToken, getAuthStatus */
10601
10664
  /* harmony import */ var _lib_stores_memory__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(423);
10602
- // GitHub Copilot device-flow login. Lets users sign in with their Copilot
10603
- // subscription instead of pasting a PAT (which can't be exchanged for a
10604
- // Copilot session token and gets rate-limited on the Models REST API).
10665
+ // GitHub Copilot device-flow login. The legacy PAT path depended on GitHub
10666
+ // Models, which has been retired; use this flow for Copilot Chat access.
10605
10667
  //
10606
10668
  // Flow (RFC 8628):
10607
10669
  // 1. POST github.com/login/device/code with the VS Code Copilot client_id.
@@ -1819,8 +1819,7 @@ async function findTopicBoundary(threadId, boundarySeq) {
1819
1819
  if (!thread) return null;
1820
1820
  const agent = (0,_lib_stores_agent_configs__WEBPACK_IMPORTED_MODULE_2__/* .getAgentConfig */ .mJ)(thread.agent_id);
1821
1821
  if (!agent) return null;
1822
- const modelName = agent.model_config_name ?? (0,_lib_stores_model_config__WEBPACK_IMPORTED_MODULE_3__/* .getDefaultModelConfig */ .lB)()?.name ?? null;
1823
- const modelCfg = modelName ? (0,_lib_stores_model_config__WEBPACK_IMPORTED_MODULE_3__/* .getModelConfig */ .Ac)(modelName) : null;
1822
+ const modelCfg = (0,_lib_stores_model_config__WEBPACK_IMPORTED_MODULE_3__/* .resolveRunnableModelConfig */ .np)(agent.model_config_name);
1824
1823
  if (!modelCfg?.provider || !modelCfg.model_id) return null;
1825
1824
  const rows = (0,_lib_stores_threads__WEBPACK_IMPORTED_MODULE_5__/* .getRecentMessagesWindow */ .S0)(threadId, 0, undefined, "foreground").filter((row)=>row.role === "user" || row.role === "assistant");
1826
1825
  const boundaryIndex = rows.findIndex((row)=>row.seq >= boundarySeq);
@@ -1936,8 +1935,7 @@ async function compactThreadWarmContext(threadId, requestedBoundarySeq, options
1936
1935
  if (!thread) return null;
1937
1936
  const agent = (0,_lib_stores_agent_configs__WEBPACK_IMPORTED_MODULE_2__/* .getAgentConfig */ .mJ)(thread.agent_id);
1938
1937
  if (!agent) return null;
1939
- const modelName = agent.model_config_name ?? (0,_lib_stores_model_config__WEBPACK_IMPORTED_MODULE_3__/* .getDefaultModelConfig */ .lB)()?.name ?? null;
1940
- const modelCfg = modelName ? (0,_lib_stores_model_config__WEBPACK_IMPORTED_MODULE_3__/* .getModelConfig */ .Ac)(modelName) : null;
1938
+ const modelCfg = (0,_lib_stores_model_config__WEBPACK_IMPORTED_MODULE_3__/* .resolveRunnableModelConfig */ .np)(agent.model_config_name);
1941
1939
  if (!modelCfg?.provider || !modelCfg.model_id) return null;
1942
1940
  const baseParams = providerOverride?.params ?? (0,_lib_stores_model_config__WEBPACK_IMPORTED_MODULE_3__/* .getModelParams */ .mN)(modelCfg);
1943
1941
  const tier = (0,_lib_stores_agent_configs__WEBPACK_IMPORTED_MODULE_2__/* .getAgentTierProportions */ .fg)(agent);
@@ -2056,8 +2054,7 @@ async function buildAutomationChannelSummaryBefore(threadId, channel, boundarySe
2056
2054
  if (!thread) return null;
2057
2055
  const agent = (0,_lib_stores_agent_configs__WEBPACK_IMPORTED_MODULE_2__/* .getAgentConfig */ .mJ)(thread.agent_id);
2058
2056
  if (!agent) return null;
2059
- const modelName = agent.model_config_name ?? (0,_lib_stores_model_config__WEBPACK_IMPORTED_MODULE_3__/* .getDefaultModelConfig */ .lB)()?.name ?? null;
2060
- const modelCfg = modelName ? (0,_lib_stores_model_config__WEBPACK_IMPORTED_MODULE_3__/* .getModelConfig */ .Ac)(modelName) : null;
2057
+ const modelCfg = (0,_lib_stores_model_config__WEBPACK_IMPORTED_MODULE_3__/* .resolveRunnableModelConfig */ .np)(agent.model_config_name);
2061
2058
  if (!modelCfg?.provider || !modelCfg.model_id) return null;
2062
2059
  const baseParams = providerOverride?.params ?? (0,_lib_stores_model_config__WEBPACK_IMPORTED_MODULE_3__/* .getModelParams */ .mN)(modelCfg);
2063
2060
  const tier = (0,_lib_stores_agent_configs__WEBPACK_IMPORTED_MODULE_2__/* .getAgentTierProportions */ .fg)(agent);
@@ -4,8 +4,8 @@ globalThis.__BUILD_MANIFEST = {
4
4
  ],
5
5
  "devFiles": [],
6
6
  "lowPriorityFiles": [
7
- "static/e2NV1wk6mWg3SIeELXevg/_buildManifest.js",
8
- "static/e2NV1wk6mWg3SIeELXevg/_ssgManifest.js"
7
+ "static/NUrC9xax1sp1Q70nlFqS6/_buildManifest.js",
8
+ "static/NUrC9xax1sp1Q70nlFqS6/_ssgManifest.js"
9
9
  ],
10
10
  "rootMainFiles": [
11
11
  "static/chunks/webpack-22f2423f357dd0a1.js",
@@ -49442,6 +49442,7 @@ function runMigrations(db) {
49442
49442
  ensureCredentialsTable(db);
49443
49443
  ensureCredentialsLabelAndDefaultColumns(db);
49444
49444
  ensureModelConfigCredentialIdColumn(db);
49445
+ ensureModelConfigEnabledColumn(db);
49445
49446
  ensureAgentConfigsToolCredentialsColumn(db);
49446
49447
  seedAgentConfigs(db);
49447
49448
  backfillDeveloperInteractiveTerminalTools(db);
@@ -50428,6 +50429,14 @@ function ensureModelConfigCredentialIdColumn(db) {
50428
50429
  db.exec("ALTER TABLE model_configs ADD COLUMN credential_id TEXT");
50429
50430
  }
50430
50431
  }
50432
+ // ADR-0102: disabled model configs stay saved but are hidden from model
50433
+ // selection, routing, and default fallback. Existing rows default to enabled.
50434
+ function ensureModelConfigEnabledColumn(db) {
50435
+ const cols = db.prepare("PRAGMA table_info(model_configs)").all();
50436
+ if (!cols.some((c)=>c.name === "enabled")) {
50437
+ db.exec("ALTER TABLE model_configs ADD COLUMN enabled INTEGER NOT NULL DEFAULT 1");
50438
+ }
50439
+ }
50431
50440
  // Idempotent backfill: for each model_config still carrying an inline
50432
50441
  // `api_key` (and no `credential_id` yet), create a credential row and
50433
50442
  // link by id. Strips the api_key / base_url / extra_headers fields from
@@ -97262,9 +97271,8 @@ const geminiProvider = {
97262
97271
  };
97263
97272
 
97264
97273
  ;// ./lib/providers/github-copilot-auth.ts
97265
- // GitHub Copilot device-flow login. Lets users sign in with their Copilot
97266
- // subscription instead of pasting a PAT (which can't be exchanged for a
97267
- // Copilot session token and gets rate-limited on the Models REST API).
97274
+ // GitHub Copilot device-flow login. The legacy PAT path depended on GitHub
97275
+ // Models, which has been retired; use this flow for Copilot Chat access.
97268
97276
  //
97269
97277
  // Flow (RFC 8628):
97270
97278
  // 1. POST github.com/login/device/code with the VS Code Copilot client_id.
@@ -97437,17 +97445,17 @@ function pickGitHubCompatOptions(params) {
97437
97445
  return out;
97438
97446
  }
97439
97447
  const tokenCache = new Map();
97440
- function isLikelyGitHubPat(token) {
97441
- return token.startsWith("ghp_") || token.startsWith("github_pat_") || token.startsWith("gho_");
97448
+ function isGitHubPat(token) {
97449
+ return token.startsWith("ghp_") || token.startsWith("github_pat_");
97442
97450
  }
97443
- async function getCopilotToken(pat) {
97444
- if (!pat) throw new Error("GitHub Copilot: no API key configured for token exchange.");
97445
- const cached = tokenCache.get(pat);
97451
+ async function exchangeCopilotSessionToken(githubToken) {
97452
+ if (!githubToken) throw new Error("GitHub Copilot: no API key configured for token exchange.");
97453
+ const cached = tokenCache.get(githubToken);
97446
97454
  if (cached && cached.expiresAt - Date.now() > 60000) return cached.token;
97447
- tokenCache.delete(pat);
97455
+ tokenCache.delete(githubToken);
97448
97456
  const res = await fetch("https://api.github.com/copilot_internal/v2/token", {
97449
97457
  headers: {
97450
- Authorization: `token ${pat}`,
97458
+ Authorization: `token ${githubToken}`,
97451
97459
  "User-Agent": "Jarela/1.0"
97452
97460
  },
97453
97461
  signal: AbortSignal.timeout(30000)
@@ -97455,17 +97463,17 @@ async function getCopilotToken(pat) {
97455
97463
  if (!res.ok) {
97456
97464
  const body = await error_readErrorBody(res);
97457
97465
  if (res.status === 401) {
97458
- throw new Error(`GitHub Copilot: token exchange failed (401). ` + `Ensure your PAT has the "copilot" scope and that you have an active Copilot subscription. ` + `Details: ${body}`);
97466
+ throw new Error(`GitHub Copilot: token exchange failed (401). ` + `Check that this GitHub account has an active Copilot subscription and the credential is valid. ` + `Details: ${body}`);
97459
97467
  }
97460
97468
  if (res.status === 404) {
97461
- throw new Error("GitHub Copilot token exchange endpoint was not found for this credential. " + "Personal Access Tokens are not accepted directly by Copilot chat. " + "Use a Copilot session token in params.copilot_session_token, or a credential that supports token exchange.");
97469
+ throw new Error("GitHub Copilot could not exchange this credential for a session token. " + "Use the in-app device-flow sign-in or set params.copilot_session_token.");
97462
97470
  }
97463
97471
  throw new Error(`GitHub Copilot: token exchange failed (${res.status}): ${body}`);
97464
97472
  }
97465
97473
  const json = await res.json();
97466
97474
  if (!json.token) throw new Error("GitHub Copilot: token exchange returned no token");
97467
97475
  const expiresAt = json.expires_at ? new Date(json.expires_at).getTime() : Date.now() + 28 * 60000;
97468
- tokenCache.set(pat, {
97476
+ tokenCache.set(githubToken, {
97469
97477
  token: json.token,
97470
97478
  expiresAt
97471
97479
  });
@@ -97496,7 +97504,6 @@ async function resolveCopilotAuth(params) {
97496
97504
  const explicitSessionToken = params.copilot_session_token?.trim();
97497
97505
  if (explicitSessionToken && explicitSessionToken.length > 0) {
97498
97506
  return {
97499
- kind: "copilot-api",
97500
97507
  baseURL: params.base_url ?? "https://api.githubcopilot.com",
97501
97508
  apiKey: explicitSessionToken,
97502
97509
  headers: {
@@ -97506,9 +97513,8 @@ async function resolveCopilotAuth(params) {
97506
97513
  }
97507
97514
  const oauthToken = getStoredOAuthToken();
97508
97515
  if (oauthToken) {
97509
- const token = await getCopilotToken(oauthToken);
97516
+ const token = await exchangeCopilotSessionToken(oauthToken);
97510
97517
  return {
97511
- kind: "copilot-api",
97512
97518
  baseURL: params.base_url ?? "https://api.githubcopilot.com",
97513
97519
  apiKey: token,
97514
97520
  headers: {
@@ -97516,23 +97522,14 @@ async function resolveCopilotAuth(params) {
97516
97522
  }
97517
97523
  };
97518
97524
  }
97519
- if (apiKey && isLikelyGitHubPat(apiKey)) {
97520
- return {
97521
- kind: "models-rest",
97522
- baseURL: params.base_url ?? "https://models.github.ai/inference",
97523
- apiKey,
97524
- headers: {
97525
- Accept: "application/vnd.github+json",
97526
- "X-GitHub-Api-Version": "2026-03-10"
97527
- }
97528
- };
97525
+ if (apiKey && isGitHubPat(apiKey)) {
97526
+ throw new Error("GitHub Copilot: this provider's GitHub PAT route used GitHub Models, which has been retired. " + "Use the in-app device-flow sign-in or set copilot_session_token.");
97529
97527
  }
97530
97528
  if (!apiKey) {
97531
97529
  throw new Error("GitHub Copilot: not signed in. Use the in-app device-flow login, or set api_key / copilot_session_token.");
97532
97530
  }
97533
- const token = await getCopilotToken(apiKey);
97531
+ const token = await exchangeCopilotSessionToken(apiKey);
97534
97532
  return {
97535
- kind: "copilot-api",
97536
97533
  baseURL: params.base_url ?? "https://api.githubcopilot.com",
97537
97534
  apiKey: token,
97538
97535
  headers: {
@@ -97543,14 +97540,9 @@ async function resolveCopilotAuth(params) {
97543
97540
  // Anthropic SDK client pointed at Copilot's native `/v1/messages` endpoint.
97544
97541
  // Copilot accepts the Anthropic request shape (including `cache_control`
97545
97542
  // ephemeral breakpoints) for Claude-family models when called with a
97546
- // session token. The PAT-only `models.github.ai` fallback does NOT expose
97547
- // this endpoint, so we surface a clear error there instead of silently
97548
- // downgrading to a path that strips the cache markers.
97543
+ // Copilot session token.
97549
97544
  async function resolvedAnthropicClient(params) {
97550
97545
  const auth = await resolveCopilotAuth(params);
97551
- if (auth.kind !== "copilot-api") {
97552
- throw new Error("GitHub Copilot: Claude models require a Copilot session token (OAuth or copilot_session_token). " + "Personal Access Tokens route to GitHub Models REST, which does not expose the native Messages API.");
97553
- }
97554
97546
  return new Anthropic({
97555
97547
  // The SDK requires an apiKey field; we authenticate via authToken
97556
97548
  // (Authorization: Bearer ...) per Copilot's contract. The empty
@@ -102982,6 +102974,14 @@ const model_config_now = ()=>new Date().toISOString();
102982
102974
  function withModelConfigMutation(name, operation) {
102983
102975
  return withKeyedMutation(`model-config:${name}`, operation);
102984
102976
  }
102977
+ class ModelConfigStateError extends Error {
102978
+ constructor(message, status){
102979
+ super(message), this.status = status;
102980
+ }
102981
+ }
102982
+ function isModelConfigEnabled(cfg) {
102983
+ return !!cfg && cfg.enabled !== 0;
102984
+ }
102985
102985
  // `params` holds the provider API key alongside hyperparameters and is
102986
102986
  // stored encrypted at rest (ADR-0005). Decrypt before returning rows.
102987
102987
  function model_config_decryptRow(row) {
@@ -102994,18 +102994,74 @@ function listModelConfigs() {
102994
102994
  const rows = db_getDb().prepare("SELECT * FROM model_configs ORDER BY is_default DESC, name ASC").all();
102995
102995
  return rows.map(model_config_decryptRow);
102996
102996
  }
102997
+ /** Model configs that may be offered for selection or routing. */ function listEnabledModelConfigs() {
102998
+ return listModelConfigs().filter(isModelConfigEnabled);
102999
+ }
102997
103000
  function getModelConfig(name) {
102998
103001
  const row = db_getDb().prepare("SELECT * FROM model_configs WHERE name=?").get(name) ?? null;
102999
103002
  return row ? model_config_decryptRow(row) : null;
103000
103003
  }
103001
103004
  function getDefaultModelConfig() {
103002
103005
  const db = db_getDb();
103003
- const row = db.prepare("SELECT * FROM model_configs WHERE is_default=1 LIMIT 1").get() ?? null;
103006
+ const row = db.prepare("SELECT * FROM model_configs WHERE is_default=1 AND enabled=1 LIMIT 1").get() ?? null;
103004
103007
  if (row) return model_config_decryptRow(row);
103005
- const fallback = db.prepare("SELECT * FROM model_configs ORDER BY name ASC LIMIT 1").get() ?? null;
103008
+ const fallback = db.prepare("SELECT * FROM model_configs WHERE enabled=1 ORDER BY name ASC LIMIT 1").get() ?? null;
103006
103009
  return fallback ? model_config_decryptRow(fallback) : null;
103007
103010
  }
103008
- function upsertModelConfig(name, provider, model_id, params, is_default, credential_id = null) {
103011
+ /**
103012
+ * Resolve the config a run should use: the named config when it exists and is
103013
+ * enabled, otherwise the workspace default. A disabled assignment therefore
103014
+ * falls back without being cleared, so re-enabling restores it.
103015
+ */ function resolveRunnableModelConfig(name) {
103016
+ const named = name ? getModelConfig(name) : null;
103017
+ if (named && isModelConfigEnabled(named)) return named;
103018
+ return getDefaultModelConfig();
103019
+ }
103020
+ /** Atomically make `name` the only default model config. */ function setDefaultModelConfig(name) {
103021
+ return withDbTransaction(()=>{
103022
+ const db = getDb();
103023
+ const target = getModelConfig(name);
103024
+ if (!target) throw new ModelConfigStateError(`Model config "${name}" not found`, 404);
103025
+ if (!isModelConfigEnabled(target)) {
103026
+ throw new ModelConfigStateError(`Model config "${name}" is disabled; enable it before making it the default`, 409);
103027
+ }
103028
+ const t = model_config_now();
103029
+ db.prepare("UPDATE model_configs SET is_default=0, updated_at=? WHERE is_default=1 AND name<>?").run(t, name);
103030
+ db.prepare("UPDATE model_configs SET is_default=1, updated_at=? WHERE name=?").run(t, name);
103031
+ return getModelConfig(name);
103032
+ });
103033
+ }
103034
+ /**
103035
+ * Enable or disable a model config. Disabling the default promotes the
103036
+ * alphabetically-first other enabled config in the same transaction, and the
103037
+ * last enabled config cannot be disabled so runs always have a model.
103038
+ */ function setModelConfigEnabled(name, enabled) {
103039
+ return withDbTransaction(()=>{
103040
+ const db = getDb();
103041
+ const target = getModelConfig(name);
103042
+ if (!target) throw new ModelConfigStateError(`Model config "${name}" not found`, 404);
103043
+ if (isModelConfigEnabled(target) === enabled) return target;
103044
+ const t = model_config_now();
103045
+ if (enabled) {
103046
+ db.prepare("UPDATE model_configs SET enabled=1, updated_at=? WHERE name=?").run(t, name);
103047
+ return getModelConfig(name);
103048
+ }
103049
+ const next = db.prepare("SELECT name FROM model_configs WHERE enabled=1 AND name<>? ORDER BY name ASC LIMIT 1").get(name);
103050
+ if (!next?.name) {
103051
+ throw new ModelConfigStateError("At least one model config must stay enabled", 409);
103052
+ }
103053
+ db.prepare("UPDATE model_configs SET enabled=0, is_default=0, updated_at=? WHERE name=?").run(t, name);
103054
+ if (target.is_default === 1) {
103055
+ db.prepare("UPDATE model_configs SET is_default=1, updated_at=? WHERE name=?").run(t, next.name);
103056
+ }
103057
+ return getModelConfig(name);
103058
+ });
103059
+ }
103060
+ /**
103061
+ * Insert or update a model config. `is_default` undefined keeps the current
103062
+ * flag (false for new rows); true atomically clears every other default and
103063
+ * re-enables the row, because the default must always be selectable.
103064
+ */ function upsertModelConfig(name, provider, model_id, params, is_default, credential_id = null) {
103009
103065
  return withDbTransaction(()=>{
103010
103066
  const t = model_config_now();
103011
103067
  const existing = getModelConfig(name);
@@ -103021,9 +103077,11 @@ function upsertModelConfig(name, provider, model_id, params, is_default, credent
103021
103077
  ...decodeInlineParams(existing),
103022
103078
  ...params
103023
103079
  };
103080
+ const finalDefault = is_default ?? existing?.is_default === 1;
103081
+ const finalEnabled = finalDefault || !existing || isModelConfigEnabled(existing);
103024
103082
  const db = getDb();
103025
- if (is_default) db.prepare("UPDATE model_configs SET is_default=0").run();
103026
- db.prepare("INSERT OR REPLACE INTO model_configs (name,provider,model_id,params,is_default,credential_id,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?)").run(name, provider, model_id, encrypt(JSON.stringify(mergedParams)), is_default ? 1 : 0, finalCredId, created_at, t);
103083
+ if (finalDefault) db.prepare("UPDATE model_configs SET is_default=0 WHERE name<>?").run(name);
103084
+ db.prepare("INSERT OR REPLACE INTO model_configs (name,provider,model_id,params,is_default,enabled,credential_id,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?,?)").run(name, provider, model_id, encrypt(JSON.stringify(mergedParams)), finalDefault ? 1 : 0, finalEnabled ? 1 : 0, finalCredId, created_at, t);
103027
103085
  return getModelConfig(name);
103028
103086
  });
103029
103087
  }
@@ -103036,10 +103094,10 @@ function deleteModelConfig(name) {
103036
103094
  // Automatically reset any agent pointing to this deleted model config to NULL (automatic / default model)
103037
103095
  db.prepare("UPDATE agent_configs SET model_config_name = NULL WHERE model_config_name = ?").run(name);
103038
103096
  // Avoid the "no default" state: if the deleted row was the default and any
103039
- // other rows remain, promote the alphabetically-first remaining row so
103097
+ // other enabled rows remain, promote the alphabetically-first one so
103040
103098
  // agents that fall back to the default keep working.
103041
103099
  if (wasDefault) {
103042
- const next = db.prepare("SELECT name FROM model_configs ORDER BY name ASC LIMIT 1").get();
103100
+ const next = db.prepare("SELECT name FROM model_configs WHERE enabled=1 ORDER BY name ASC LIMIT 1").get();
103043
103101
  if (next?.name) db.prepare("UPDATE model_configs SET is_default=1 WHERE name=?").run(next.name);
103044
103102
  }
103045
103103
  return true;
@@ -105446,14 +105504,14 @@ const integrations_INTEGRATIONS = {
105446
105504
  "github-copilot": {
105447
105505
  label: "GitHub Copilot",
105448
105506
  category: "llm",
105449
- description: "Routes chat-completion requests through your Copilot subscription. Paste a GitHub PAT " + "with `copilot` scope from github.com/settings/tokens, or run the device-flow sign-in in " + "the Models panel.",
105507
+ description: "Routes chat-completion requests through your Copilot subscription. Use the device-flow " + "sign-in in the Models panel; legacy GitHub PAT authentication is no longer supported.",
105450
105508
  fields: [
105451
105509
  {
105452
105510
  key: "api_key",
105453
- label: "GitHub PAT",
105454
- placeholder: "ghp_… or github_pat_…",
105511
+ label: "Legacy GitHub PAT (unsupported)",
105512
+ placeholder: "Use Models panel sign-in",
105455
105513
  secret: true,
105456
- required: true
105514
+ required: false
105457
105515
  }
105458
105516
  ]
105459
105517
  },
@@ -105723,6 +105781,7 @@ function saveIntegration(name, incoming) {
105723
105781
  if (f.required && !v.trim()) return {
105724
105782
  error: `"${f.key}" cannot be empty`
105725
105783
  };
105784
+ if (!v.trim()) continue;
105726
105785
  merged[f.key] = v;
105727
105786
  if (existing[f.key] !== v) touched.push(f.key);
105728
105787
  }