@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.
- package/model-config.schema.json +8 -0
- package/package.json +1 -1
- package/src/command.ts +32 -16
- package/src/config.ts +1 -0
- package/src/i18n.ts +18 -0
- package/src/image-apis/common.ts +2 -2
- package/src/panel.ts +27 -2
- package/src/selector.ts +17 -1
- package/src/types.ts +17 -0
package/model-config.schema.json
CHANGED
|
@@ -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
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
|
-
...
|
|
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
|
|
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
|
-
|
|
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",
|
package/src/image-apis/common.ts
CHANGED
|
@@ -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") ||
|
|
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}`;
|