@wuyaos/pi-model-config 0.1.6 → 0.1.7

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.
@@ -192,6 +192,14 @@
192
192
  },
193
193
  "uniqueItems": true
194
194
  },
195
+ "hiddenRemoteIds": {
196
+ "description": "Remote catalog model IDs hidden by the user across discovery refreshes.",
197
+ "type": "array",
198
+ "items": {
199
+ "type": "string"
200
+ },
201
+ "uniqueItems": true
202
+ },
195
203
  "models": {
196
204
  "type": "array",
197
205
  "items": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wuyaos/pi-model-config",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "type": "module",
5
5
  "description": "Pi provider 连接、远程发现与 chat/classifier/image 模型配置 TUI",
6
6
  "keywords": [
package/src/command.ts CHANGED
@@ -18,6 +18,7 @@ import {
18
18
  showPreviewPanel,
19
19
  showProviderPanel,
20
20
  showConfirmationPanel,
21
+ showRestoreHiddenPanel,
21
22
  type MenuPanelState,
22
23
  type OverviewPanelState,
23
24
  type PanelItem,
@@ -36,7 +37,7 @@ import type {
36
37
  ProviderConnectionConfig,
37
38
  ProviderDraft,
38
39
  } from "./types.ts";
39
- import { identityOf } from "./types.ts";
40
+ import { hiddenRemoteKey, identityOf, isHiddenRemote, normalizeHiddenRemoteKeys } from "./types.ts";
40
41
  import { loadUiLanguage, saveUiLanguage } from "./ui-config.ts";
41
42
  import { secretInput, withCancellableLoader } from "./ui.ts";
42
43
  import { showModelTestFlow } from "./model-test.ts";
@@ -157,6 +158,7 @@ function draftFrom(providerId: string, provider: ModelsJsonProvider | undefined,
157
158
  classifierApi: config?.classifierApi ?? "typesafe-system-one",
158
159
  imageApi: config?.imageApi ?? "openrouter-images",
159
160
  defaultType: config?.defaultType ?? "chat",
161
+ hiddenRemoteIds: normalizeHiddenRemoteKeys(config?.hiddenRemoteIds ?? []),
160
162
  };
161
163
  }
162
164
 
@@ -248,9 +250,9 @@ function connectionPanelFields(language: UiLanguage, draft: ProviderDraft): Pane
248
250
  ];
249
251
  }
250
252
 
251
- async function editConnectionPanel(ctx: ExtensionCommandContext, language: UiLanguage, draft: ProviderDraft, state: MenuPanelState, canTakeover: boolean): Promise<"models" | "takeover" | "testModels" | "preview" | "back"> {
253
+ async function editConnectionPanel(ctx: ExtensionCommandContext, language: UiLanguage, draft: ProviderDraft, state: MenuPanelState, canTakeover: boolean, hiddenRemoteCount: number): Promise<"models" | "takeover" | "restoreHidden" | "testModels" | "preview" | "back"> {
252
254
  while (true) {
253
- const result = await showConnectionPanel(ctx, language, draft.id, connectionPanelFields(language, draft), state, canTakeover);
255
+ const result = await showConnectionPanel(ctx, language, draft.id, connectionPanelFields(language, draft), state, canTakeover, hiddenRemoteCount);
254
256
  if (result.action !== "field") return result.action;
255
257
  try {
256
258
  switch (result.fieldId) {
@@ -424,8 +426,10 @@ export function applyChanges(
424
426
  if (draft.authMode === "models-json") provider.apiKey = draft.apiKeyRaw; else delete provider.apiKey;
425
427
  models.providers[providerId] = provider;
426
428
 
429
+ const normalizedHiddenRemoteIds = normalizeHiddenRemoteKeys(draft.hiddenRemoteIds ?? []);
430
+ const { hiddenRemoteIds: _oldHiddenRemoteIds, ...oldConfigWithoutHiddenRemoteIds } = oldConfig ?? {};
427
431
  config.providers[providerId] = {
428
- ...(oldConfig ?? {}),
432
+ ...oldConfigWithoutHiddenRemoteIds,
429
433
  name: draft.name,
430
434
  owned: draft.owned,
431
435
  enabled: draft.enabled,
@@ -436,6 +440,7 @@ export function applyChanges(
436
440
  imageApi: draft.imageApi,
437
441
  defaultType: draft.defaultType,
438
442
  managedChatIds: selectedChat.map((model) => model.id),
443
+ ...(normalizedHiddenRemoteIds.length ? { hiddenRemoteIds: normalizedHiddenRemoteIds } : {}),
439
444
  models: selected.filter((model): model is Extract<ManagedModel, { type: "classifier" | "image" }> => model.type !== "chat"),
440
445
  modelMetadata: modelMetadata.map((entry) => structuredClone(entry)),
441
446
  };
@@ -514,6 +519,7 @@ export interface ProviderChoice {
514
519
  id: string;
515
520
  label: string;
516
521
  priority: number;
522
+ enabled: boolean;
517
523
  }
518
524
 
519
525
  export function buildConfiguredProviderChoices(
@@ -524,20 +530,21 @@ export function buildConfiguredProviderChoices(
524
530
  ): ProviderChoice[] {
525
531
  const ids = new Set([...Object.keys(models.providers), ...Object.keys(config.providers)]);
526
532
  const choices: ProviderChoice[] = [];
527
- for (const id of ids) {
533
+ for (const id of [...ids].sort((a, b) => a.localeCompare(b))) {
528
534
  const owned = config.providers[id]?.owned === true;
529
535
  const builtin = builtinProviderIds.has(id);
530
536
  const name = config.providers[id]?.name ?? models.providers[id]?.name;
531
537
  const displayBase = name && name !== id ? `${name} (${id})` : id;
532
538
  const display = config.providers[id]?.enabled === false ? `${displayBase} · ${t(language, "providerDisabled")}` : displayBase;
533
539
  const status = t(language, builtin ? "providerTagBuiltin" : "providerTagCustom");
540
+ const enabled = config.providers[id]?.enabled !== false;
534
541
  choices.push(owned
535
- ? { id, label: t(language, "providerLabelCustom", { display, status }), priority: 0 }
542
+ ? { id, label: t(language, "providerLabelCustom", { display, status }), priority: 0, enabled }
536
543
  : builtin
537
- ? { id, label: t(language, "providerLabelBuiltin", { display, status }), priority: 1 }
538
- : { id, label: t(language, "providerLabelCustom", { display, status }), priority: 2 });
544
+ ? { id, label: t(language, "providerLabelBuiltin", { display, status }), priority: 1, enabled }
545
+ : { id, label: t(language, "providerLabelCustom", { display, status }), priority: 2, enabled });
539
546
  }
540
- return choices.sort((a, b) => a.priority - b.priority || a.id.localeCompare(b.id));
547
+ return choices.sort((a, b) => Number(b.enabled) - Number(a.enabled) || a.priority - b.priority || a.id.localeCompare(b.id));
541
548
  }
542
549
 
543
550
  function buildProviderPanelItems(
@@ -591,7 +598,7 @@ function buildProviderPanelItems(
591
598
  export function buildBuiltinProviderChoices(language: UiLanguage, builtinProviderIds: Iterable<string>): ProviderChoice[] {
592
599
  return [...new Set(builtinProviderIds)]
593
600
  .sort((a, b) => a.localeCompare(b))
594
- .map((id) => ({ id, label: t(language, "providerLabelBuiltin", { display: id, status: t(language, "providerTagBuiltin") }), priority: 0 }));
601
+ .map((id) => ({ id, label: t(language, "providerLabelBuiltin", { display: id, status: t(language, "providerTagBuiltin") }), priority: 0, enabled: true }));
595
602
  }
596
603
 
597
604
  async function chooseBuiltinProvider(ctx: ExtensionCommandContext, language: UiLanguage): Promise<{ id: string; isNew: false } | undefined> {
@@ -657,6 +664,7 @@ function draftPanelItem(language: UiLanguage, draft: ProviderDraft, selected: Ma
657
664
  ],
658
665
  modelDetails: [
659
666
  t(language, "panelSummaryModels", { chat, classifier, image }),
667
+ ...(draft.hiddenRemoteIds?.length ? [t(language, "panelHiddenRemoteCount", { count: draft.hiddenRemoteIds.length })] : []),
660
668
  t(language, "detailFetchHint"),
661
669
  ...(adoptableCount ? [t(language, "panelTakeoverCount", { count: adoptableCount })] : []),
662
670
  ],
@@ -840,6 +848,7 @@ export function registerModelConfigCommand(pi: ExtensionAPI, startupIssues: Star
840
848
 
841
849
  let selectedDraft = structuredClone(baselineSelected);
842
850
  let cachedRemote: CandidateModel[] = [];
851
+ let fetchInFlight = false;
843
852
  let page: "connection" | "model" | "preview" = "connection";
844
853
  let previewOrigin: "connection" | "model" = "model";
845
854
  const connectionState: MenuPanelState = { focus: "list", actionIndex: 0, detailScroll: 0 };
@@ -898,10 +907,15 @@ export function registerModelConfigCommand(pi: ExtensionAPI, startupIssues: Star
898
907
 
899
908
  providerSession: while (true) {
900
909
  if (page === "connection") {
901
- const connection = await editConnectionPanel(ctx, language, draft, connectionState, adoptableIds().length > 0);
910
+ const connection = await editConnectionPanel(ctx, language, draft, connectionState, adoptableIds().length > 0, draft.hiddenRemoteIds?.length ?? 0);
902
911
  activeSecrets.push(draft.apiKeyRaw, draft.temporaryKey);
903
912
  if (connection === "models") { page = "model"; continue; }
904
913
  if (connection === "takeover") { if (await adoptExisting()) page = "model"; continue; }
914
+ if (connection === "restoreHidden") {
915
+ const restored = await showRestoreHiddenPanel(ctx, language, draft.hiddenRemoteIds ?? []);
916
+ if (restored.action === "apply") draft.hiddenRemoteIds = (draft.hiddenRemoteIds ?? []).filter((key) => !restored.restored.includes(key));
917
+ continue;
918
+ }
905
919
  if (connection === "testModels") {
906
920
  if (!draft.enabled) { ctx.ui.notify(t(language, "testProviderDisabled"), "warning"); continue; }
907
921
  if (draft.authMode === "temporary" || isDirty()) { ctx.ui.notify(t(language, "testDraftDirty"), "warning"); continue; }
@@ -934,7 +948,8 @@ export function registerModelConfigCommand(pi: ExtensionAPI, startupIssues: Star
934
948
  const installedRaw = installedModels(ctx, target.id);
935
949
  const installed = installedRaw.map(candidateFromInstalled);
936
950
  const external = externalModels.map((model) => candidateFromExternal(model, externalMetadata.get(model.id)));
937
- const candidates = mergeCandidates(target.id, [...external, ...installed], managedBefore.map((model) => candidateFromManaged(model, persistedMetadata.get(`${model.type}\0${model.id}`))), cachedRemote);
951
+ const visibleRemote = cachedRemote.filter((model) => !isHiddenRemote(model, draft.hiddenRemoteIds ?? []));
952
+ const candidates = mergeCandidates(target.id, [...external, ...installed], managedBefore.map((model) => candidateFromManaged(model, persistedMetadata.get(`${model.type}\0${model.id}`))), visibleRemote);
938
953
  const selection = await selectModels(ctx, language, target.id, draft, candidates, selectedDraft, modelState, selectedMetadata);
939
954
  selectedDraft = selection.selected;
940
955
  selectedMetadata = selection.metadata.map((entry) => ({ ...(selectedMetadata.find((old) => old.type === entry.type && old.id === entry.id) ?? {}), ...entry }));
@@ -987,6 +1002,9 @@ export function registerModelConfigCommand(pi: ExtensionAPI, startupIssues: Star
987
1002
  continue;
988
1003
  }
989
1004
  if (selection.action === "fetch") {
1005
+ if (fetchInFlight) continue;
1006
+ fetchInFlight = true;
1007
+ try {
990
1008
  const fallbackBases = [
991
1009
  ...(oldProvider?.models ?? []).map((model) => typeof model.baseUrl === "string" ? model.baseUrl : undefined),
992
1010
  ...installedRaw.map((model) => model.baseUrl),
@@ -1029,6 +1047,7 @@ export function registerModelConfigCommand(pi: ExtensionAPI, startupIssues: Star
1029
1047
  draft.discoveryUrl = result.value!.url;
1030
1048
  cachedRemote = [...result.value!.models];
1031
1049
  continue;
1050
+ } finally { fetchInFlight = false; }
1032
1051
  }
1033
1052
  if (await preparePreview()) { previewOrigin = "model"; page = "preview"; }
1034
1053
  continue;
@@ -1049,10 +1068,7 @@ export function registerModelConfigCommand(pi: ExtensionAPI, startupIssues: Star
1049
1068
  if (discard === "discard") continue mainLoop;
1050
1069
  continue;
1051
1070
  }
1052
- const compatItems = selectedMetadata.filter((entry) => entry.contextSource === "compat" || entry.maxTokensSource === "compat").map((entry) => entry.type + '/' + entry.id);
1053
- const risk = compatItems.length ? [t(language, "compatSaveRisk", { items: compatItems.join(", ") })] : [];
1054
- const confirmed = await confirmAction(ctx, language, t(language, "finalSaveTitle"), [t(language, "finalSaveBody"), ...risk], [...details.providerDetails, ...details.modelDetails], t(language, "confirmSave"));
1055
- if (!confirmed) continue;
1071
+ // Preview is the sole save confirmation: Ctrl+S there commits the reviewed draft.
1056
1072
  try {
1057
1073
  const next = applyChanges(files, target.id, draft, selectedDraft, new Set(externalModels.map((model) => model.id)), selectedMetadata);
1058
1074
  const transaction = writeConfigTransaction(files.modelsSnapshot, files.configSnapshot, next.models, next.config);
package/src/config.ts CHANGED
@@ -72,6 +72,7 @@ export function parseModelConfig(source: ConfigSnapshot): ModelConfigFile {
72
72
  if (provider.imageApi !== undefined && !IMAGE_API_NAMES.includes(String(provider.imageApi) as typeof IMAGE_API_NAMES[number])) {
73
73
  throw mcError("MCFG_E_UNSUPPORTED_API", { identity: `${id}/image`, api: String(provider.imageApi) });
74
74
  }
75
+ if (provider.hiddenRemoteIds !== undefined && (!Array.isArray(provider.hiddenRemoteIds) || provider.hiddenRemoteIds.some((modelId) => typeof modelId !== "string"))) throw mcError("MCFG_E_PROVIDER_SHAPE", { file: "model-config.json", provider: id });
75
76
  if (provider.models !== undefined && !Array.isArray(provider.models)) throw mcError("MCFG_E_MODELS_ARRAY", { file: "model-config.json", provider: id });
76
77
  if (Array.isArray(provider.models) && provider.models.some((model) => !isRecord(model))) throw mcError("MCFG_E_MODELS_ARRAY", { file: "model-config.json", provider: id });
77
78
  for (const model of Array.isArray(provider.models) ? provider.models : []) {
package/src/i18n.ts CHANGED
@@ -38,6 +38,13 @@ const ZH = {
38
38
  detailConnectionTitle: "连接摘要",
39
39
  detailModelsTitle: "模型概况",
40
40
  detailFetchHint: "进入“配置模型”后按 Ctrl+R 获取远程模型列表",
41
+ detailRestoreHidden: "[Ctrl+H] 恢复隐藏模型({count})",
42
+ panelHiddenRemoteCount: "已隐藏远程模型:{count}",
43
+ restoreHiddenTitle: "恢复隐藏远程模型",
44
+ restoreHiddenItemDetail: "勾选后恢复到后续远程获取结果;保存前仅修改内存草稿。",
45
+ restoreHiddenAll: "[Ctrl+A] 全部恢复",
46
+ restoreHiddenApply: "[Ctrl+S] 应用恢复",
47
+ restoreHiddenBack: "[Esc] 返回",
41
48
  detailEditConnection: "[Ctrl+E] 编辑连接",
42
49
  detailConfigureModels: "[Ctrl+M] 配置模型",
43
50
  detailTakeover: "[Ctrl+T] 全选并纳入管理",
@@ -99,6 +106,7 @@ const ZH = {
99
106
  modelProbeContext: "[Ctrl+K] 探测上下文容量",
100
107
  modelProbeThinking: "[Ctrl+L] 探测思考档位",
101
108
  modelFetch: "[Ctrl+R] 获取远程模型列表",
109
+ modelHideRemote: "[Ctrl+Del] 隐藏远程模型",
102
110
  modelPreview: "[Ctrl+S] 预览更改",
103
111
  modelBack: "[Esc] 返回服务商",
104
112
  addProviderTitle: "添加服务商",
@@ -312,6 +320,7 @@ const ZH = {
312
320
  selectorReadonlyRuntime: "此模型来自 Pi 内置目录或其他运行时扩展,不能在这里移除",
313
321
  selectorTypeConflict: "不能切换为{type}:{identity} 已存在",
314
322
  selectorReadonlyTag: "只读",
323
+ selectorRemoteHidden: "已隐藏远程模型 {id};保存后后续获取也会隐藏",
315
324
  selectorTypeChat: "聊天",
316
325
  selectorTypeClassifier: "判定",
317
326
  selectorTypeImage: "图像生成",
@@ -372,6 +381,13 @@ const EN = {
372
381
  detailConnectionTitle: "Connection Summary",
373
382
  detailModelsTitle: "Model Overview",
374
383
  detailFetchHint: "Open Configure models, then press Ctrl+R to fetch the remote model list",
384
+ detailRestoreHidden: "[Ctrl+H] Restore hidden models ({count})",
385
+ panelHiddenRemoteCount: "Hidden remote models: {count}",
386
+ restoreHiddenTitle: "Restore hidden remote models",
387
+ restoreHiddenItemDetail: "Checked entries return to future remote fetch results; this only changes the in-memory draft until save.",
388
+ restoreHiddenAll: "[Ctrl+A] Restore all",
389
+ restoreHiddenApply: "[Ctrl+S] Apply restore",
390
+ restoreHiddenBack: "[Esc] Back",
375
391
  detailEditConnection: "[Ctrl+E] Edit connection",
376
392
  detailConfigureModels: "[Ctrl+M] Configure models",
377
393
  detailTakeover: "[Ctrl+T] Select all and manage",
@@ -433,6 +449,7 @@ const EN = {
433
449
  modelProbeContext: "[Ctrl+K] Probe context capacity",
434
450
  modelProbeThinking: "[Ctrl+L] Probe thinking levels",
435
451
  modelFetch: "[Ctrl+R] Fetch remote model list",
452
+ modelHideRemote: "[Ctrl+Del] Hide remote model",
436
453
  modelPreview: "[Ctrl+S] Preview changes",
437
454
  modelBack: "[Esc] Back to provider",
438
455
  addProviderTitle: "Add Provider",
@@ -646,6 +663,7 @@ const EN = {
646
663
  selectorReadonlyRuntime: "This model comes from Pi's built-in catalog or another runtime extension and cannot be removed here",
647
664
  selectorTypeConflict: "Cannot change to {type}: {identity} already exists",
648
665
  selectorReadonlyTag: "read-only",
666
+ selectorRemoteHidden: "Remote model {id} is hidden and will remain hidden after saving and future fetches",
649
667
  selectorTypeChat: "Chat",
650
668
  selectorTypeClassifier: "Classification",
651
669
  selectorTypeImage: "Image generation",
@@ -130,13 +130,13 @@ function privateHostname(hostname: string): boolean {
130
130
  const low = Number.parseInt(mappedHex[2]!, 16);
131
131
  normalized = `${high >>> 8}.${high & 0xff}.${low >>> 8}.${low & 0xff}`;
132
132
  }
133
- if (normalized === "localhost" || normalized.endsWith(".localhost") || normalized === "::1" || normalized === "0:0:0:0:0:0:0:1") return true;
133
+ if (normalized === "localhost" || normalized.endsWith(".localhost") || normalized === "::" || normalized === "0:0:0:0:0:0:0:0" || normalized === "::1" || normalized === "0:0:0:0:0:0:0:1") return true;
134
134
  const ipv4 = normalized.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/u)?.slice(1).map(Number);
135
135
  if (ipv4 && ipv4.every((part) => part >= 0 && part <= 255)) {
136
136
  const [a, b] = ipv4;
137
137
  return a === 0 || a === 10 || a === 127 || a === 100 && b >= 64 && b <= 127 || a === 169 && b === 254 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168;
138
138
  }
139
- return normalized.startsWith("fc") || normalized.startsWith("fd") || normalized.startsWith("fe80:");
139
+ return normalized.startsWith("fc") || normalized.startsWith("fd") || /^(?:fe8|fe9|fea|feb)[0-9a-f]:/u.test(normalized);
140
140
  }
141
141
 
142
142
  export function validatePublicImageUrl(value: string): URL {
package/src/panel.ts CHANGED
@@ -43,7 +43,7 @@ export type ProviderPanelResult =
43
43
 
44
44
  export type ConnectionPanelResult =
45
45
  | { action: "field"; fieldId: string }
46
- | { action: "models" | "takeover" | "testModels" | "preview" | "back" };
46
+ | { action: "models" | "takeover" | "restoreHidden" | "testModels" | "preview" | "back" };
47
47
 
48
48
  export type PreviewPanelResult = { action: "save" | "back" | "discard" };
49
49
 
@@ -434,11 +434,13 @@ export async function showConnectionPanel(
434
434
  fields: PanelItem[],
435
435
  state: MenuPanelState = { focus: "list", actionIndex: 0, detailScroll: 0 },
436
436
  canTakeover = false,
437
+ hiddenRemoteCount = 0,
437
438
  ): Promise<ConnectionPanelResult> {
438
439
  const actions: FooterAction[] = [
439
440
  { id: "edit", label: t(language, "connectionModify"), key: Key.enter },
440
441
  { id: "models", label: t(language, "detailConfigureModels"), key: Key.ctrl("m") },
441
442
  ...(canTakeover ? [{ id: "takeover", label: t(language, "detailTakeover"), key: Key.ctrl("t") }] : []),
443
+ ...(hiddenRemoteCount > 0 ? [{ id: "restoreHidden", label: t(language, "detailRestoreHidden", { count: hiddenRemoteCount }), key: Key.ctrl("h") }] : []),
442
444
  { id: "testModels", label: t(language, "detailTestModels"), key: Key.ctrl("p") },
443
445
  { id: "preview", label: t(language, "connectionPreview"), key: Key.ctrl("s") },
444
446
  { id: "back", label: t(language, "connectionBack"), key: Key.escape },
@@ -448,10 +450,33 @@ export async function showConnectionPanel(
448
450
  if (result.kind === "back" || result.id === "back") return { action: "back" };
449
451
  if (result.kind === "item" && result.id) return { action: "field", fieldId: result.id };
450
452
  if (result.id === "edit" && state.selectedId) return { action: "field", fieldId: state.selectedId };
451
- if (result.id === "models" || result.id === "takeover" || result.id === "testModels" || result.id === "preview") return { action: result.id };
453
+ if (result.id === "models" || result.id === "takeover" || result.id === "restoreHidden" || result.id === "testModels" || result.id === "preview") return { action: result.id };
452
454
  return { action: "back" };
453
455
  }
454
456
 
457
+ export async function showRestoreHiddenPanel(
458
+ ctx: ExtensionCommandContext,
459
+ language: UiLanguage,
460
+ hiddenKeys: string[],
461
+ state: MenuPanelState = { focus: "list", actionIndex: 0, detailScroll: 0 },
462
+ ): Promise<{ action: "apply" | "back"; restored: string[] }> {
463
+ const selected = new Set<string>();
464
+ const actions: FooterAction[] = [
465
+ { id: "all", label: t(language, "restoreHiddenAll"), key: Key.ctrl("a") },
466
+ { id: "apply", label: t(language, "restoreHiddenApply"), key: Key.ctrl("s") },
467
+ { id: "back", label: t(language, "restoreHiddenBack"), key: Key.escape },
468
+ ];
469
+ while (true) {
470
+ const items = hiddenKeys.map((key) => ({ id: key, label: `${selected.has(key) ? "[✓]" : "[ ]"} ${key.replace("\u0000", "/")}`, details: [t(language, "restoreHiddenItemDetail")] }));
471
+ const result = await ctx.ui.custom<{ kind: "item" | "action" | "language" | "back"; id?: string }>((tui, theme, _keys, done) =>
472
+ new MenuDetailPanel(tui, theme, language, t(language, "restoreHiddenTitle"), items, actions, state, true, done), MODEL_CONFIG_OVERLAY_OPTIONS);
473
+ if (result.kind === "back" || result.id === "back") return { action: "back", restored: [] };
474
+ if (result.kind === "item" && result.id) { if (selected.has(result.id)) selected.delete(result.id); else selected.add(result.id); continue; }
475
+ if (result.id === "all") { for (const hiddenKey of hiddenKeys) selected.add(hiddenKey); continue; }
476
+ if (result.id === "apply") return { action: "apply", restored: [...selected] };
477
+ }
478
+ }
479
+
455
480
  export async function showPreviewPanel(
456
481
  ctx: ExtensionCommandContext,
457
482
  language: UiLanguage,
package/src/selector.ts CHANGED
@@ -15,7 +15,7 @@ import { t, type UiLanguage } from "./i18n.ts";
15
15
  import { COMPAT_CONTEXT_WINDOW, COMPAT_MAX_TOKENS } from "./discovery.ts";
16
16
  import { MODEL_CONFIG_OVERLAY_OPTIONS, overlayViewportRows, renderActionFooter, renderPanelBox, type FooterAction } from "./panel.ts";
17
17
  import type { CandidateModel, ManagedModel, ManagedModelType, ModelMetadataEntry, ProviderDraft, SelectionResult, ThinkingLevelMap, ThinkingMetadataSource } from "./types.ts";
18
- import { identityOf } from "./types.ts";
18
+ import { hiddenRemoteKey, identityOf } from "./types.ts";
19
19
  import { protocolThinkingMap } from "./thinking-defaults.ts";
20
20
  import { describeThinking } from "./model-editor.ts";
21
21
 
@@ -218,6 +218,19 @@ class ModelSelector implements Component, Focusable {
218
218
  this.tui.requestRender();
219
219
  }
220
220
 
221
+ private hideCurrentRemote(): void {
222
+ const row = this.current();
223
+ if (!row || row.source !== "remote") return;
224
+ const hidden = new Set(this.draft.hiddenRemoteIds ?? []);
225
+ hidden.add(hiddenRemoteKey(row));
226
+ this.draft.hiddenRemoteIds = [...hidden].sort((a, b) => a.localeCompare(b));
227
+ this.selected.delete(this.key(row));
228
+ const index = this.rows.indexOf(row);
229
+ if (index >= 0) this.rows.splice(index, 1);
230
+ this.status = t(this.language, "selectorRemoteHidden", { id: row.id });
231
+ this.refresh();
232
+ }
233
+
221
234
  private cycleType(): void {
222
235
  const row = this.filtered[this.index];
223
236
  if (!row?.mutableType) return;
@@ -277,6 +290,7 @@ class ModelSelector implements Component, Focusable {
277
290
  { id: "probe", label: t(this.language, "modelProbeContext"), key: Key.ctrl("k") },
278
291
  { id: "probeThinking", label: t(this.language, "modelProbeThinking"), key: Key.ctrl("l") },
279
292
  { id: "fetch", label: t(this.language, "modelFetch"), key: Key.ctrl("r") },
293
+ { id: "hideRemote", label: t(this.language, "modelHideRemote"), key: Key.ctrl("delete"), enabled: this.current()?.source === "remote" },
280
294
  { id: "preview", label: t(this.language, "modelPreview"), key: Key.ctrl("s") },
281
295
  { id: "back", label: t(this.language, "modelBack"), key: Key.escape },
282
296
  ];
@@ -301,12 +315,14 @@ class ModelSelector implements Component, Focusable {
301
315
  if (action.id === "selectFiltered") { for (const row of this.filtered) if (!row.readOnly) this.selected.add(this.key(row)); this.request(); return; }
302
316
  if (action.id === "deselectFiltered") { for (const row of this.filtered) if (!row.readOnly) this.selected.delete(this.key(row)); this.request(); return; }
303
317
  if (action.id === "cycleType") { this.cycleType(); return; }
318
+ if (action.id === "hideRemote") { this.hideCurrentRemote(); return; }
304
319
  this.finish(action.id as SelectionResult["action"]);
305
320
  }
306
321
 
307
322
  handleInput(data: string): void {
308
323
  if (data === "/") { this.state.focus = "search"; this.request(); return; }
309
324
  if (matchesKey(data, Key.ctrl("r"))) { this.finish("fetch"); return; }
325
+ if (matchesKey(data, Key.ctrl("delete"))) { this.hideCurrentRemote(); return; }
310
326
  if (matchesKey(data, Key.ctrl("e"))) { this.finish("edit"); return; }
311
327
  if (matchesKey(data, Key.ctrl("k"))) { this.finish("probe"); return; }
312
328
  if (matchesKey(data, Key.ctrl("l"))) { this.finish("probeThinking"); return; }
package/src/types.ts CHANGED
@@ -71,6 +71,8 @@ export interface ProviderConnectionConfig {
71
71
  defaultContextWindow?: number;
72
72
  defaultMaxTokens?: number;
73
73
  managedChatIds?: string[];
74
+ /** Remote catalog keys (`type\0id`) hidden by the user; retained across discovery refreshes. */
75
+ hiddenRemoteIds?: string[];
74
76
  models?: Array<ManagedClassifierModel | ManagedImageModel>;
75
77
  modelMetadata?: ModelMetadataEntry[];
76
78
  }
@@ -115,6 +117,7 @@ export interface ProviderDraft {
115
117
  classifierApi: string;
116
118
  imageApi: ImageApiName;
117
119
  defaultType: ManagedModelType;
120
+ hiddenRemoteIds?: string[];
118
121
  }
119
122
 
120
123
  export type CandidateModel = ManagedModel & {
@@ -142,6 +145,20 @@ export interface ConfigSnapshot {
142
145
  text?: string;
143
146
  }
144
147
 
148
+ /** Persistent remote-hide identity. Legacy id-only entries are interpreted as chat-only on read. */
149
+ export function hiddenRemoteKey(model: Pick<ManagedModel, "id" | "type">): string { return `${model.type}\u0000${model.id}`; }
150
+
151
+ export function isHiddenRemote(model: Pick<ManagedModel, "id" | "type">, hiddenKeys: readonly string[]): boolean {
152
+ const key = hiddenRemoteKey(model);
153
+ // Compatibility: prior releases stored bare IDs; treating those as chat-only prevents
154
+ // a legacy hidden chat model from also hiding classifier/image models with the same ID.
155
+ return hiddenKeys.includes(key) || model.type === "chat" && hiddenKeys.includes(model.id);
156
+ }
157
+
158
+ export function normalizeHiddenRemoteKeys(hiddenKeys: readonly string[]): string[] {
159
+ return [...new Set(hiddenKeys.map((key) => key.includes("\u0000") ? key : hiddenRemoteKey({ type: "chat", id: key })))].sort((a, b) => a.localeCompare(b));
160
+ }
161
+
145
162
  export function identityOf(model: Pick<ManagedModel, "id" | "type"> & { provider?: string }, provider?: string): ModelIdentity {
146
163
  const p = provider ?? model.provider ?? "";
147
164
  return `${p}\u0000${model.type}\u0000${model.id}`;