dsh-neotui 0.1.21 → 0.1.22
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/package.json +1 -1
- package/src/panels.js +432 -1
- package/src/views.js +10 -5
package/package.json
CHANGED
package/src/panels.js
CHANGED
|
@@ -111,6 +111,11 @@ export class Picker extends Widget {
|
|
|
111
111
|
|
|
112
112
|
export function buildModelPicker(app) {
|
|
113
113
|
const w = Math.min(70, app.screen.w - 4), h = Math.min(24, app.screen.h - 4);
|
|
114
|
+
const manageProviders = {
|
|
115
|
+
label: "⚙ 管理供应商…(填表式)", hint: "M",
|
|
116
|
+
provider: null, model: null,
|
|
117
|
+
action: () => { app.overlay = null; app.setMode("models"); },
|
|
118
|
+
};
|
|
114
119
|
const selectModel = async (it) => {
|
|
115
120
|
app.overlay = null; app.redraw();
|
|
116
121
|
if (!app.currentSession) { app.toast("先打开一个会话"); return; }
|
|
@@ -126,6 +131,7 @@ export function buildModelPicker(app) {
|
|
|
126
131
|
items: [],
|
|
127
132
|
onCancel: () => { app.overlay = null; app.redraw(); },
|
|
128
133
|
onPick: (it) => {
|
|
134
|
+
if (it === manageProviders) { manageProviders.action(); return; }
|
|
129
135
|
const efforts = it.efforts ?? [];
|
|
130
136
|
if (efforts.length > 0) {
|
|
131
137
|
// second step: reasoning effort
|
|
@@ -160,7 +166,7 @@ export function buildModelPicker(app) {
|
|
|
160
166
|
});
|
|
161
167
|
}
|
|
162
168
|
}
|
|
163
|
-
picker.items = items;
|
|
169
|
+
picker.items = [manageProviders, ...items];
|
|
164
170
|
app.redraw();
|
|
165
171
|
}).catch((e) => app.toast(`模型列表失败: ${e.message}`));
|
|
166
172
|
return picker;
|
|
@@ -1634,6 +1640,11 @@ export class SettingsPanel extends Widget {
|
|
|
1634
1640
|
ns: "默认展开/折叠", applies: "live", local: true,
|
|
1635
1641
|
value: { 思考块默认展开: fd.think, 工具块默认展开: fd.bash, 任务清单默认显示: fd.todos },
|
|
1636
1642
|
});
|
|
1643
|
+
// the model provider manager opens its own big form buffer (CC Switch 式)
|
|
1644
|
+
this.namespaces.splice(2, 0, {
|
|
1645
|
+
ns: "模型供应商…(填表式)", applies: "live", local: true, modelsEntry: true,
|
|
1646
|
+
value: {},
|
|
1647
|
+
});
|
|
1637
1648
|
this.selectNs(0);
|
|
1638
1649
|
}
|
|
1639
1650
|
selectNs(i) {
|
|
@@ -1641,6 +1652,7 @@ export class SettingsPanel extends Widget {
|
|
|
1641
1652
|
this.pendingOps = [];
|
|
1642
1653
|
this.editing = false;
|
|
1643
1654
|
const ns = this.namespaces[this.nsIdx];
|
|
1655
|
+
if (ns.modelsEntry) { this.app.setMode("models"); return; }
|
|
1644
1656
|
this.secrets = new Set((ns.secrets ?? []).map((s) => JSON.stringify(s.path ?? [])));
|
|
1645
1657
|
this.rebuildRows();
|
|
1646
1658
|
const items = this.namespaces.map((n) => ({
|
|
@@ -1804,6 +1816,425 @@ export class SettingsPanel extends Widget {
|
|
|
1804
1816
|
}
|
|
1805
1817
|
}
|
|
1806
1818
|
|
|
1819
|
+
// ---- Model provider management (CC Switch-style big form buffer) ----
|
|
1820
|
+
|
|
1821
|
+
const MODEL_APIS = ["openai-completions", "anthropic-messages", "google-genai"];
|
|
1822
|
+
|
|
1823
|
+
export class ModelPanel extends Widget {
|
|
1824
|
+
constructor(app) {
|
|
1825
|
+
super({ x: 30, y: 0, w: app.screen.w - 30, h: app.screen.h - 1 });
|
|
1826
|
+
this.app = app;
|
|
1827
|
+
this.providers = {}; // route → profile (mirror of settings llm-pi-ai.providers)
|
|
1828
|
+
this.revision = 0;
|
|
1829
|
+
this.loaded = false;
|
|
1830
|
+
this.routes = [];
|
|
1831
|
+
this.sel = 0; // list cursor (routes.length = the + 添加供应商 row)
|
|
1832
|
+
this.mode = "list"; // list | form
|
|
1833
|
+
this.formIdx = 0; // form item cursor
|
|
1834
|
+
this.formItems = []; // {kind:"field"|"model"|"button", ...}
|
|
1835
|
+
this.modelsSel = -1; // selected model row (shows its subfields)
|
|
1836
|
+
this.editing = null; // { label, commit } while the inline editor is open
|
|
1837
|
+
this.scanMode = false;
|
|
1838
|
+
this.scanItems = [];
|
|
1839
|
+
this.scanSel = new Set();
|
|
1840
|
+
this.scanCursor = 0;
|
|
1841
|
+
this.scanning = false;
|
|
1842
|
+
const listW = 26;
|
|
1843
|
+
this.listView = new ScrollView({ x: this.x + 1, y: this.y + 1, w: listW, h: this.h - 2, showScrollbar: true });
|
|
1844
|
+
this.formView = new ScrollView({ x: this.x + listW + 1, y: this.y + 1, w: this.w - listW - 2, h: this.h - 3, showScrollbar: true });
|
|
1845
|
+
this.input = new Input({ x: this.x + listW + 1, y: this.y + this.h - 2, w: this.w - listW - 2, h: 1, prompt: "值: ", placeholder: "Enter 提交 · Esc 取消" });
|
|
1846
|
+
}
|
|
1847
|
+
relayout(x, y, w, h) {
|
|
1848
|
+
this.x = x; this.y = y; this.w = w; this.h = h;
|
|
1849
|
+
const listW = 26;
|
|
1850
|
+
this.listView.x = x + 1; this.listView.y = y + 1; this.listView.w = listW; this.listView.h = h - 2;
|
|
1851
|
+
this.formView.x = x + listW + 1; this.formView.y = y + 1; this.formView.w = w - listW - 2; this.formView.h = h - 3;
|
|
1852
|
+
this.input.x = x + listW + 1; this.input.y = y + h - 2; this.input.w = w - listW - 2;
|
|
1853
|
+
}
|
|
1854
|
+
async load() {
|
|
1855
|
+
try {
|
|
1856
|
+
const d = await this.app.api.call("settings.describe");
|
|
1857
|
+
const ns = (d.namespaces ?? []).find((n) => n.ns === "llm-pi-ai");
|
|
1858
|
+
this.providers = { ...(ns?.value?.providers ?? {}) };
|
|
1859
|
+
this.revision = ns?.revision ?? 0;
|
|
1860
|
+
this.routes = Object.keys(this.providers);
|
|
1861
|
+
} catch (e) { this.app.toast(`模型配置加载失败: ${e.message}`); }
|
|
1862
|
+
this.loaded = true;
|
|
1863
|
+
this.modelsSel = -1;
|
|
1864
|
+
this.#rebuild();
|
|
1865
|
+
this.app.redraw();
|
|
1866
|
+
}
|
|
1867
|
+
#route() { return this.routes[this.sel] ?? null; }
|
|
1868
|
+
#profile(route) { return route == null ? null : this.providers[route] ?? {}; }
|
|
1869
|
+
#formRows() {
|
|
1870
|
+
const route = this.#route();
|
|
1871
|
+
if (route == null) return [];
|
|
1872
|
+
const p = this.#profile(route);
|
|
1873
|
+
const items = [];
|
|
1874
|
+
items.push({ kind: "field", key: "route", label: "路由名", value: route, editable: !(route in this.providers && this.routes.includes(route)) });
|
|
1875
|
+
items.push({ kind: "field", key: "displayName", label: "显示名", value: p.displayName ?? "" });
|
|
1876
|
+
items.push({ kind: "field", key: "api", label: "协议 api", value: p.api ?? "openai-completions", cycle: MODEL_APIS });
|
|
1877
|
+
items.push({ kind: "field", key: "baseURL", label: "baseURL", value: p.baseURL ?? "" });
|
|
1878
|
+
items.push({ kind: "field", key: "apiKeyEnv", label: "apiKeyEnv", value: p.apiKeyEnv ?? "", note: "环境变量名(密钥不落盘)" });
|
|
1879
|
+
const models = p.models ?? [];
|
|
1880
|
+
for (let mi = 0; mi < models.length; mi++) {
|
|
1881
|
+
const m = models[mi];
|
|
1882
|
+
items.push({ kind: "model", idx: mi, id: m.id ?? "", name: m.name ?? "", ctx: m.contextWindow ?? null, max: m.maxTokens ?? null });
|
|
1883
|
+
if (this.modelsSel === mi) {
|
|
1884
|
+
items.push({ kind: "field", key: `model.${mi}.id`, label: " 模型 id", value: m.id ?? "" });
|
|
1885
|
+
items.push({ kind: "field", key: `model.${mi}.name`, label: " 模型名", value: m.name ?? "" });
|
|
1886
|
+
items.push({ kind: "field", key: `model.${mi}.contextWindow`, label: " 上下文窗口", value: m.contextWindow ?? "", numeric: true });
|
|
1887
|
+
items.push({ kind: "field", key: `model.${mi}.maxTokens`, label: " 最大输出", value: m.maxTokens ?? "", numeric: true });
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
items.push({ kind: "button", label: "+ 添加模型", action: () => this.#addModel() });
|
|
1891
|
+
items.push({ kind: "button", label: "🗑 删除选中模型", action: () => this.#deleteModel() });
|
|
1892
|
+
items.push({ kind: "button", label: "🔄 自动扫描模型(读 /models 端点)", action: () => this.#scan() });
|
|
1893
|
+
items.push({ kind: "button", label: "◉ 设为当前会话模型(选中模型)", action: () => this.#setDefaultModel() });
|
|
1894
|
+
items.push({ kind: "button", label: "💾 保存配置", action: () => this.#save() });
|
|
1895
|
+
items.push({ kind: "button", label: "🗑 删除供应商", action: () => this.#deleteProvider() });
|
|
1896
|
+
return items;
|
|
1897
|
+
}
|
|
1898
|
+
#rebuild() {
|
|
1899
|
+
// left list
|
|
1900
|
+
const listLines = [];
|
|
1901
|
+
for (let i = 0; i < this.routes.length; i++) {
|
|
1902
|
+
const r = this.routes[i];
|
|
1903
|
+
const p = this.providers[r] ?? {};
|
|
1904
|
+
const sel = i === this.sel && this.mode === "list";
|
|
1905
|
+
listLines.push([{ t: ` ${sel ? "▸" : " "} ${truncate(p.displayName || r, 18)}`, fg: sel ? T.SELFG : T.TXT, bg: sel ? T.MENUSEL : T.BG2, bold: sel }]);
|
|
1906
|
+
}
|
|
1907
|
+
const addSel = this.sel === this.routes.length && this.mode === "list";
|
|
1908
|
+
listLines.push([{ t: ` ${addSel ? "▸" : " "} + 添加供应商`, fg: addSel ? T.SELFG : T.ACCENT, bg: addSel ? T.MENUSEL : T.BG2, bold: true }]);
|
|
1909
|
+
this.listView.setLines(listLines);
|
|
1910
|
+
// right form
|
|
1911
|
+
const route = this.#route();
|
|
1912
|
+
const formLines = [];
|
|
1913
|
+
if (route == null) {
|
|
1914
|
+
formLines.push([{ t: " 选择左侧供应商,或“+ 添加供应商”新建(CC Switch 式填表)", fg: K.FAINT }]);
|
|
1915
|
+
this.formItems = [];
|
|
1916
|
+
} else if (this.scanMode) {
|
|
1917
|
+
formLines.push([{ t: ` 扫描 ${truncate(this.#profile(route).baseURL ?? "", 44)} — 空格勾选,Enter 添加,↑/↓ 移动`, fg: K.ACCENT, bold: true }]);
|
|
1918
|
+
if (this.scanning) formLines.push([{ t: " 扫描中…", fg: K.WARN }]);
|
|
1919
|
+
for (let i = 0; i < this.scanItems.length; i++) {
|
|
1920
|
+
const m = this.scanItems[i];
|
|
1921
|
+
const on = this.scanSel.has(m.id);
|
|
1922
|
+
const cur = i === this.scanCursor;
|
|
1923
|
+
formLines.push([{ t: ` ${cur ? "▸" : " "} [${on ? "x" : " "}] ${truncate(m.id, this.formView.w - 10)}`, fg: on ? K.OK : cur ? T.TXT : K.DIM, bg: cur ? T.MENUSEL : T.BG2 }]);
|
|
1924
|
+
}
|
|
1925
|
+
formLines.push([{ t: " Enter 添加选中 · Esc 取消扫描", fg: K.FAINT }]);
|
|
1926
|
+
this.formItems = [];
|
|
1927
|
+
} else {
|
|
1928
|
+
this.formItems = this.#formRows();
|
|
1929
|
+
const w = Math.max(30, this.formView.w - 4);
|
|
1930
|
+
for (let i = 0; i < this.formItems.length; i++) {
|
|
1931
|
+
const it = this.formItems[i];
|
|
1932
|
+
const cur = this.mode === "form" && i === this.formIdx;
|
|
1933
|
+
let t;
|
|
1934
|
+
if (it.kind === "field") {
|
|
1935
|
+
const v = it.value === "" || it.value == null ? "(空)" : String(it.value);
|
|
1936
|
+
t = ` ${cur ? "▸" : " "} ${it.label}: ${truncate(v, w - strWidth(it.label) - 6)}${it.note ? ` [${it.note}]` : ""}`;
|
|
1937
|
+
} else if (it.kind === "model") {
|
|
1938
|
+
const extras = [it.ctx != null ? `ctx ${it.ctx}` : "", it.max != null ? `max ${it.max}` : ""].filter(Boolean).join(" ");
|
|
1939
|
+
t = ` ${cur ? "▸" : " "} 模型 ${truncate(it.id || "(未命名)", 24)} ${truncate(it.name || "", 20)} ${truncate(extras, 24)}`;
|
|
1940
|
+
} else {
|
|
1941
|
+
t = ` ${cur ? "▸" : " "} ${it.label}`;
|
|
1942
|
+
}
|
|
1943
|
+
formLines.push([{ t: truncate(t, w), fg: cur ? T.SELFG : T.TXT, bg: cur ? T.MENUSEL : T.BG2 }]);
|
|
1944
|
+
}
|
|
1945
|
+
formLines.push([{ t: " Enter 编辑/执行 · ← 回列表 · Esc 退出面板", fg: K.FAINT }]);
|
|
1946
|
+
}
|
|
1947
|
+
this.formView.setLines(formLines);
|
|
1948
|
+
}
|
|
1949
|
+
render(screen) {
|
|
1950
|
+
screen.fillRect(this.x, this.y, this.x + this.w - 1, this.y + this.h - 1, " ", {});
|
|
1951
|
+
const mid = this.x + 26;
|
|
1952
|
+
screen.vline(mid, this.y, this.y + this.h - 1, "│", { fg: T.BORDER });
|
|
1953
|
+
screen.text(this.x + 1, this.y, " 模型供应商 — CC Switch 式", { fg: K.DIM });
|
|
1954
|
+
this.listView.render(screen);
|
|
1955
|
+
this.formView.render(screen);
|
|
1956
|
+
if (this.editing) {
|
|
1957
|
+
screen.text(this.x + 28, this.y + this.h - 3, `编辑 ${this.editing.label}`, { fg: K.WARN, bold: true });
|
|
1958
|
+
this.input.render(screen);
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
#startEdit(label, value, commit) {
|
|
1962
|
+
this.editing = { label, commit };
|
|
1963
|
+
this.input.setValue(value ?? "", { select: true });
|
|
1964
|
+
this.app.redraw();
|
|
1965
|
+
}
|
|
1966
|
+
#commitEdit() {
|
|
1967
|
+
if (!this.editing) return;
|
|
1968
|
+
const { label, commit } = this.editing;
|
|
1969
|
+
this.editing = null;
|
|
1970
|
+
commit(this.input.value);
|
|
1971
|
+
this.#rebuild();
|
|
1972
|
+
this.app.redraw();
|
|
1973
|
+
}
|
|
1974
|
+
#activateItem() {
|
|
1975
|
+
if (this.mode === "list") {
|
|
1976
|
+
if (this.sel === this.routes.length) {
|
|
1977
|
+
// + 添加供应商
|
|
1978
|
+
let name = "新供应商", i = 2;
|
|
1979
|
+
while (this.providers[name] !== undefined) name = `新供应商${i++}`;
|
|
1980
|
+
this.providers[name] = { displayName: "", api: "openai-completions", baseURL: "", apiKeyEnv: "", models: [] };
|
|
1981
|
+
this.routes = Object.keys(this.providers);
|
|
1982
|
+
this.sel = this.routes.indexOf(name);
|
|
1983
|
+
this.mode = "form";
|
|
1984
|
+
this.formIdx = 0;
|
|
1985
|
+
this.#rebuild();
|
|
1986
|
+
this.app.redraw();
|
|
1987
|
+
return;
|
|
1988
|
+
}
|
|
1989
|
+
this.mode = "form";
|
|
1990
|
+
this.formIdx = 0;
|
|
1991
|
+
this.modelsSel = -1;
|
|
1992
|
+
this.#rebuild();
|
|
1993
|
+
this.app.redraw();
|
|
1994
|
+
return;
|
|
1995
|
+
}
|
|
1996
|
+
// form
|
|
1997
|
+
const it = this.formItems[this.formIdx];
|
|
1998
|
+
if (!it) return;
|
|
1999
|
+
const route = this.#route();
|
|
2000
|
+
const p = this.#profile(route);
|
|
2001
|
+
if (it.kind === "field") {
|
|
2002
|
+
if (it.cycle) {
|
|
2003
|
+
// cycle through the allowed api values
|
|
2004
|
+
const next = it.cycle[(it.cycle.indexOf(it.value) + 1) % it.cycle.length];
|
|
2005
|
+
p[it.key] = next;
|
|
2006
|
+
this.#rebuild();
|
|
2007
|
+
this.app.redraw();
|
|
2008
|
+
return;
|
|
2009
|
+
}
|
|
2010
|
+
this.#startEdit(it.label, it.value, (text) => {
|
|
2011
|
+
if (it.key === "route") {
|
|
2012
|
+
// renaming the route key
|
|
2013
|
+
const t = text.trim() || route;
|
|
2014
|
+
if (t !== route && this.providers[t] !== undefined) { this.app.toast(`路由 ${t} 已存在`); return; }
|
|
2015
|
+
if (t !== route) {
|
|
2016
|
+
this.providers[t] = this.providers[route];
|
|
2017
|
+
delete this.providers[route];
|
|
2018
|
+
this.routes = Object.keys(this.providers);
|
|
2019
|
+
this.sel = this.routes.indexOf(t);
|
|
2020
|
+
}
|
|
2021
|
+
} else if (it.numeric) {
|
|
2022
|
+
const n = text.trim() === "" ? undefined : Number(text);
|
|
2023
|
+
if (n !== undefined && !isFinite(n)) { this.app.toast("请输入数字"); return; }
|
|
2024
|
+
const [, mi, field] = it.key.split(".");
|
|
2025
|
+
p.models[Number(mi)][field] = n;
|
|
2026
|
+
} else if (it.key.startsWith("model.")) {
|
|
2027
|
+
const [, mi, field] = it.key.split(".");
|
|
2028
|
+
p.models[Number(mi)][field] = text;
|
|
2029
|
+
} else {
|
|
2030
|
+
p[it.key] = text;
|
|
2031
|
+
}
|
|
2032
|
+
});
|
|
2033
|
+
return;
|
|
2034
|
+
}
|
|
2035
|
+
if (it.kind === "model") {
|
|
2036
|
+
this.modelsSel = this.modelsSel === it.idx ? -1 : it.idx;
|
|
2037
|
+
this.#rebuild();
|
|
2038
|
+
this.app.redraw();
|
|
2039
|
+
return;
|
|
2040
|
+
}
|
|
2041
|
+
if (it.kind === "button") {
|
|
2042
|
+
it.action();
|
|
2043
|
+
this.app.redraw();
|
|
2044
|
+
return;
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
#addModel() {
|
|
2048
|
+
const route = this.#route();
|
|
2049
|
+
if (!route) return;
|
|
2050
|
+
const p = this.#profile(route);
|
|
2051
|
+
p.models ??= [];
|
|
2052
|
+
p.models.push({ id: "", name: "" });
|
|
2053
|
+
this.modelsSel = p.models.length - 1;
|
|
2054
|
+
this.#rebuild();
|
|
2055
|
+
this.app.redraw();
|
|
2056
|
+
}
|
|
2057
|
+
#deleteModel() {
|
|
2058
|
+
const route = this.#route();
|
|
2059
|
+
if (!route || this.modelsSel < 0) { this.app.toast("先选中一个模型"); return; }
|
|
2060
|
+
this.#profile(route).models.splice(this.modelsSel, 1);
|
|
2061
|
+
this.modelsSel = -1;
|
|
2062
|
+
this.#rebuild();
|
|
2063
|
+
this.app.redraw();
|
|
2064
|
+
}
|
|
2065
|
+
async #setDefaultModel() {
|
|
2066
|
+
const route = this.#route();
|
|
2067
|
+
if (!route) return;
|
|
2068
|
+
const p = this.#profile(route);
|
|
2069
|
+
const m = p.models?.[this.modelsSel];
|
|
2070
|
+
if (!m?.id) { this.app.toast("先选中一个模型"); return; }
|
|
2071
|
+
if (!this.app.currentSession) { this.app.toast("先打开一个会话"); return; }
|
|
2072
|
+
try {
|
|
2073
|
+
await this.app.api.call("session.selectModel", { sessionId: this.app.currentSession, provider: route, model: m.id });
|
|
2074
|
+
this.app.updateModel();
|
|
2075
|
+
this.app.toast(`已切换 ${route}/${m.id}`);
|
|
2076
|
+
} catch (e) { this.app.toast(`切换失败: ${e.message}`); }
|
|
2077
|
+
}
|
|
2078
|
+
async #save() {
|
|
2079
|
+
try {
|
|
2080
|
+
const res = await this.app.api.call("settings.mutate", {
|
|
2081
|
+
ns: "llm-pi-ai",
|
|
2082
|
+
ops: [{ op: "set", path: ["providers"], value: this.providers }],
|
|
2083
|
+
expectedRevision: this.revision,
|
|
2084
|
+
});
|
|
2085
|
+
this.revision = res?.revision ?? this.revision;
|
|
2086
|
+
this.app.toast(`已保存 ${Object.keys(this.providers).length} 个供应商`);
|
|
2087
|
+
} catch (e) { this.app.toast(`保存失败: ${e.message}`); }
|
|
2088
|
+
}
|
|
2089
|
+
async #deleteProvider() {
|
|
2090
|
+
const route = this.#route();
|
|
2091
|
+
if (!route) return;
|
|
2092
|
+
delete this.providers[route];
|
|
2093
|
+
this.routes = Object.keys(this.providers);
|
|
2094
|
+
this.sel = Math.min(this.sel, this.routes.length - 1);
|
|
2095
|
+
this.modelsSel = -1;
|
|
2096
|
+
await this.#save();
|
|
2097
|
+
this.#rebuild();
|
|
2098
|
+
this.app.redraw();
|
|
2099
|
+
}
|
|
2100
|
+
async #scan() {
|
|
2101
|
+
const route = this.#route();
|
|
2102
|
+
if (!route) return;
|
|
2103
|
+
const p = this.#profile(route);
|
|
2104
|
+
const base = String(p.baseURL ?? "").replace(/\/+$/, "");
|
|
2105
|
+
if (!base) { this.app.toast("先填写 baseURL"); return; }
|
|
2106
|
+
this.scanning = true;
|
|
2107
|
+
this.scanMode = true;
|
|
2108
|
+
this.scanItems = [];
|
|
2109
|
+
this.scanCursor = 0;
|
|
2110
|
+
this.#rebuild();
|
|
2111
|
+
this.app.redraw();
|
|
2112
|
+
const key = p.apiKeyEnv ? process.env[p.apiKeyEnv] : null;
|
|
2113
|
+
const headers = key ? { Authorization: `Bearer ${key}` } : {};
|
|
2114
|
+
const tryFetch = async (dispatcher) => {
|
|
2115
|
+
const res = await fetch(`${base}/models`, { headers, dispatcher });
|
|
2116
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
2117
|
+
return res.json();
|
|
2118
|
+
};
|
|
2119
|
+
try {
|
|
2120
|
+
let body;
|
|
2121
|
+
try { body = await tryFetch(undefined); }
|
|
2122
|
+
catch (e0) {
|
|
2123
|
+
// self-signed gateways: retry with TLS verification disabled
|
|
2124
|
+
try {
|
|
2125
|
+
const { Agent } = await import("undici");
|
|
2126
|
+
body = await tryFetch(new Agent({ connect: { rejectUnauthorized: false } }));
|
|
2127
|
+
} catch { throw e0; }
|
|
2128
|
+
}
|
|
2129
|
+
const list = Array.isArray(body?.data) ? body.data : Array.isArray(body?.models) ? body.models : [];
|
|
2130
|
+
const seen = new Set();
|
|
2131
|
+
const items = [];
|
|
2132
|
+
for (const e of list) {
|
|
2133
|
+
if (!e || typeof e !== "object") continue;
|
|
2134
|
+
const id = String(e.id ?? e.name ?? "").trim();
|
|
2135
|
+
if (!id || seen.has(id)) continue;
|
|
2136
|
+
seen.add(id);
|
|
2137
|
+
items.push({ id, name: e.name ?? e.id ?? id });
|
|
2138
|
+
}
|
|
2139
|
+
this.scanItems = items;
|
|
2140
|
+
this.scanSel = new Set(items.map((m) => m.id));
|
|
2141
|
+
if (items.length === 0) this.app.toast("扫描完成:未发现模型");
|
|
2142
|
+
else this.app.toast(`发现 ${items.length} 个模型,空格勾选,Enter 添加`);
|
|
2143
|
+
} catch (e) {
|
|
2144
|
+
this.app.toast(`扫描失败: ${e.message}`);
|
|
2145
|
+
this.scanMode = false;
|
|
2146
|
+
}
|
|
2147
|
+
this.scanning = false;
|
|
2148
|
+
this.#rebuild();
|
|
2149
|
+
this.app.redraw();
|
|
2150
|
+
}
|
|
2151
|
+
#scanCommit() {
|
|
2152
|
+
const route = this.#route();
|
|
2153
|
+
if (!route) return;
|
|
2154
|
+
const p = this.#profile(route);
|
|
2155
|
+
p.models ??= [];
|
|
2156
|
+
const existing = new Set(p.models.map((m) => m.id));
|
|
2157
|
+
let added = 0;
|
|
2158
|
+
for (const m of this.scanItems) {
|
|
2159
|
+
if (!this.scanSel.has(m.id) || existing.has(m.id)) continue;
|
|
2160
|
+
p.models.push({ id: m.id, name: m.name });
|
|
2161
|
+
added++;
|
|
2162
|
+
}
|
|
2163
|
+
this.scanMode = false;
|
|
2164
|
+
this.app.toast(`已添加 ${added} 个模型(保存后生效)`);
|
|
2165
|
+
this.#rebuild();
|
|
2166
|
+
this.app.redraw();
|
|
2167
|
+
}
|
|
2168
|
+
onKey(ev) {
|
|
2169
|
+
if (ev.type !== "key") return false;
|
|
2170
|
+
if (this.editing) {
|
|
2171
|
+
if (ev.name === "escape") { this.editing = null; this.#rebuild(); return true; }
|
|
2172
|
+
if (ev.name === "enter") { this.#commitEdit(); return true; }
|
|
2173
|
+
const handled = this.input.onKey(ev);
|
|
2174
|
+
if (handled) this.app.redraw();
|
|
2175
|
+
return true;
|
|
2176
|
+
}
|
|
2177
|
+
if (this.scanMode) {
|
|
2178
|
+
if (ev.name === "escape") { this.scanMode = false; this.#rebuild(); return true; }
|
|
2179
|
+
if (ev.name === "up") { this.scanCursor = Math.max(0, this.scanCursor - 1); this.app.redraw(); return true; }
|
|
2180
|
+
if (ev.name === "down") { this.scanCursor = Math.min(this.scanItems.length - 1, this.scanCursor + 1); this.app.redraw(); return true; }
|
|
2181
|
+
if (ev.name === "char" && ev.key === " " && !ev.ctrl) {
|
|
2182
|
+
const m = this.scanItems[this.scanCursor];
|
|
2183
|
+
if (m) { if (this.scanSel.has(m.id)) this.scanSel.delete(m.id); else this.scanSel.add(m.id); }
|
|
2184
|
+
this.app.redraw();
|
|
2185
|
+
return true;
|
|
2186
|
+
}
|
|
2187
|
+
if (ev.name === "enter") { this.#scanCommit(); return true; }
|
|
2188
|
+
return false;
|
|
2189
|
+
}
|
|
2190
|
+
if (ev.name === "escape") { this.editing = null; return false; } // App falls back to chat mode
|
|
2191
|
+
if (ev.name === "left" || (ev.name === "char" && ev.key === "h" && !ev.ctrl)) { this.mode = "list"; this.#rebuild(); return true; }
|
|
2192
|
+
if (ev.name === "right" || (ev.name === "char" && ev.key === "l" && !ev.ctrl) || ev.name === "tab") {
|
|
2193
|
+
if (this.#route() != null) { this.mode = "form"; this.#rebuild(); }
|
|
2194
|
+
return true;
|
|
2195
|
+
}
|
|
2196
|
+
if (ev.name === "up" || (ev.name === "char" && ev.key === "k" && !ev.ctrl)) {
|
|
2197
|
+
if (this.mode === "list") this.sel = Math.max(0, this.sel - 1);
|
|
2198
|
+
else { this.formIdx = Math.max(0, this.formIdx - 1); this.#rebuild(); }
|
|
2199
|
+
this.app.redraw();
|
|
2200
|
+
return true;
|
|
2201
|
+
}
|
|
2202
|
+
if (ev.name === "down" || (ev.name === "char" && ev.key === "j" && !ev.ctrl)) {
|
|
2203
|
+
if (this.mode === "list") this.sel = Math.min(this.routes.length, this.sel + 1);
|
|
2204
|
+
else { this.formIdx = Math.min(Math.max(0, this.formItems.length - 1), this.formIdx + 1); this.#rebuild(); }
|
|
2205
|
+
this.app.redraw();
|
|
2206
|
+
return true;
|
|
2207
|
+
}
|
|
2208
|
+
if (ev.name === "enter") { this.#activateItem(); return true; }
|
|
2209
|
+
return false;
|
|
2210
|
+
}
|
|
2211
|
+
onMouse(ev) {
|
|
2212
|
+
if (ev.kind === "wheel-up") { this.formView.scroll(-3); return true; }
|
|
2213
|
+
if (ev.kind === "wheel-down") { this.formView.scroll(3); return true; }
|
|
2214
|
+
if (ev.kind !== "press" || ev.button !== 0) return false;
|
|
2215
|
+
if (this.editing && this.input.inside(ev.x, ev.y)) return this.input.onMouse(ev);
|
|
2216
|
+
if (ev.x < this.x + 26) {
|
|
2217
|
+
const idx = ev.y - this.listView.y + this.listView.scrollY;
|
|
2218
|
+
if (idx >= 0 && idx <= this.routes.length) { this.sel = idx; this.mode = "list"; this.#rebuild(); this.app.redraw(); return true; }
|
|
2219
|
+
return false;
|
|
2220
|
+
}
|
|
2221
|
+
if (this.scanMode) {
|
|
2222
|
+
const idx = ev.y - this.formView.y + this.formView.scrollY - 1;
|
|
2223
|
+
if (idx >= 0 && idx < this.scanItems.length) {
|
|
2224
|
+
this.scanCursor = idx;
|
|
2225
|
+
const m = this.scanItems[idx];
|
|
2226
|
+
if (this.scanSel.has(m.id)) this.scanSel.delete(m.id); else this.scanSel.add(m.id);
|
|
2227
|
+
this.app.redraw();
|
|
2228
|
+
return true;
|
|
2229
|
+
}
|
|
2230
|
+
return false;
|
|
2231
|
+
}
|
|
2232
|
+
const idx = ev.y - this.formView.y + this.formView.scrollY;
|
|
2233
|
+
if (idx >= 0 && idx < this.formItems.length) { this.formIdx = idx; this.mode = "form"; this.#activateItem(); return true; }
|
|
2234
|
+
return false;
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
|
|
1807
2238
|
function flattenJson(value, path, out, depth = 0) {
|
|
1808
2239
|
if (depth === 0 && path.length === 0 && value !== null && typeof value === "object") {
|
|
1809
2240
|
for (const k of Object.keys(value)) flattenJson(value[k], [k], out, 1);
|
package/src/views.js
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
Picker, buildCommandPalette, buildModelPicker, buildModePicker, buildPermissionPicker,
|
|
12
12
|
modeName, permName, WorkspacePanel, TrajectoryPanel, DirPicker,
|
|
13
13
|
ImagePopup, kittyCapable, buildGoalPopup, SettingsPanel, SubagentPanel,
|
|
14
|
-
SkillsPanel, ControlPanel, JobsPanel, fmtMs,
|
|
14
|
+
SkillsPanel, ControlPanel, JobsPanel, ModelPanel, fmtMs,
|
|
15
15
|
} from "./panels.js";
|
|
16
16
|
|
|
17
17
|
import { T, themeName, cycleTheme } from "./theme.js";
|
|
@@ -1946,6 +1946,7 @@ export class App {
|
|
|
1946
1946
|
this.workspacePanel = null;
|
|
1947
1947
|
this.trajectoryPanel = null;
|
|
1948
1948
|
this.settingsPanel = null;
|
|
1949
|
+
this.modelPanel = null;
|
|
1949
1950
|
this.subagentPanel = null;
|
|
1950
1951
|
this.skillsPanel = null;
|
|
1951
1952
|
|
|
@@ -1973,7 +1974,7 @@ export class App {
|
|
|
1973
1974
|
this.sidebar.x = 0; this.sidebar.y = 0; this.sidebar.w = this.sidebarWidth; this.sidebar.h = this.screen.h - 1;
|
|
1974
1975
|
this.searchInput.w = this.sidebarWidth;
|
|
1975
1976
|
this.chat.resize(x, 1, w, mainH);
|
|
1976
|
-
for (const p of [this.workspacePanel, this.trajectoryPanel, this.settingsPanel, this.subagentPanel, this.skillsPanel]) {
|
|
1977
|
+
for (const p of [this.workspacePanel, this.trajectoryPanel, this.settingsPanel, this.modelPanel, this.subagentPanel, this.skillsPanel]) {
|
|
1977
1978
|
if (p?.relayout) p.relayout(x, 1, w, mainH);
|
|
1978
1979
|
}
|
|
1979
1980
|
this.status.y = this.screen.h - footerH;
|
|
@@ -2516,6 +2517,9 @@ export class App {
|
|
|
2516
2517
|
} else if (mode === "settings") {
|
|
2517
2518
|
if (!this.settingsPanel) this.settingsPanel = new SettingsPanel(this);
|
|
2518
2519
|
this.settingsPanel.load();
|
|
2520
|
+
} else if (mode === "models") {
|
|
2521
|
+
if (!this.modelPanel) this.modelPanel = new ModelPanel(this);
|
|
2522
|
+
this.modelPanel.load();
|
|
2519
2523
|
} else if (mode === "subagent") {
|
|
2520
2524
|
if (!this.currentSession) { this.toast("先打开一个会话"); this.mode = "chat"; this.redraw(); return; }
|
|
2521
2525
|
if (!this.subagentPanel) this.subagentPanel = new SubagentPanel(this);
|
|
@@ -2534,6 +2538,7 @@ export class App {
|
|
|
2534
2538
|
case "workspace": return this.workspacePanel;
|
|
2535
2539
|
case "trajectory": return this.trajectoryPanel;
|
|
2536
2540
|
case "settings": return this.settingsPanel;
|
|
2541
|
+
case "models": return this.modelPanel;
|
|
2537
2542
|
case "subagent": return this.subagentPanel;
|
|
2538
2543
|
case "skills": return this.skillsPanel;
|
|
2539
2544
|
default: return null;
|
|
@@ -2578,7 +2583,7 @@ export class App {
|
|
|
2578
2583
|
this.closeOverlay();
|
|
2579
2584
|
this.menu = null;
|
|
2580
2585
|
this.popup = null;
|
|
2581
|
-
for (const p of [this.workspacePanel, this.trajectoryPanel, this.settingsPanel, this.subagentPanel, this.skillsPanel]) {
|
|
2586
|
+
for (const p of [this.workspacePanel, this.trajectoryPanel, this.settingsPanel, this.modelPanel, this.subagentPanel, this.skillsPanel]) {
|
|
2582
2587
|
if (p?.dispose) { try { p.dispose(); } catch {} }
|
|
2583
2588
|
}
|
|
2584
2589
|
this.workspacePanel = this.trajectoryPanel = this.settingsPanel = this.subagentPanel = this.skillsPanel = null;
|
|
@@ -2754,7 +2759,7 @@ export class App {
|
|
|
2754
2759
|
}
|
|
2755
2760
|
|
|
2756
2761
|
#panelLabel(mode) {
|
|
2757
|
-
return { workspace: "工作区", settings: "设置", skills: "技能", subagent: "子代理" }[mode] ?? null;
|
|
2762
|
+
return { workspace: "工作区", settings: "设置", models: "模型供应商", skills: "技能", subagent: "子代理" }[mode] ?? null;
|
|
2758
2763
|
}
|
|
2759
2764
|
|
|
2760
2765
|
#renderTabBar(s) {
|
|
@@ -2784,7 +2789,7 @@ export class App {
|
|
|
2784
2789
|
const seg = ` ${label} `;
|
|
2785
2790
|
if (px >= tx && px < tx + strWidth(seg)) {
|
|
2786
2791
|
if (id === this.mode && panelLabel && id !== "chat" && id !== "trajectory") { /* click active panel tab: stay */ return true; }
|
|
2787
|
-
this.setMode(id === "chat" || id === "trajectory" ? id : "chat");
|
|
2792
|
+
this.setMode(id === "chat" || id === "trajectory" || this.#panelLabel(id) ? id : "chat");
|
|
2788
2793
|
return true;
|
|
2789
2794
|
}
|
|
2790
2795
|
tx += strWidth(seg);
|