dsh-neotui 0.1.23 → 0.1.25

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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/panels.js +93 -20
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-neotui",
3
- "version": "0.1.23",
3
+ "version": "0.1.25",
4
4
  "description": "Neo-TUI: mouse-driven terminal UI client for DeepSeek Harness (B-tier per dsh-tui-design.md)",
5
5
  "type": "module",
6
6
  "bin": {
package/src/panels.js CHANGED
@@ -1833,6 +1833,8 @@ export class ModelPanel extends Widget {
1833
1833
  this.modelsSel = -1; // selected model row (shows its subfields)
1834
1834
  this.draftRoute = null; // the un-saved new provider's route (shows the rename field)
1835
1835
  this.editing = null; // { label, commit } while the inline editor is open
1836
+ this.sub = null; // the 模型管理 sub-buffer ({ cursor })
1837
+ this.subItems = [];
1836
1838
  this.scanMode = false;
1837
1839
  this.scanItems = [];
1838
1840
  this.scanSel = new Set();
@@ -1875,6 +1877,22 @@ export class ModelPanel extends Widget {
1875
1877
  items.push({ kind: "field", key: "displayName", label: "显示名", value: p.displayName ?? "" });
1876
1878
  items.push({ kind: "field", key: "baseURL", label: "baseURL", value: p.baseURL ?? "" });
1877
1879
  items.push({ kind: "field", key: "apiKeyEnv", label: "apiKeyEnv", value: p.apiKeyEnv ?? "", note: "环境变量名(密钥不落盘)" });
1880
+ // models are NOT flat here: one 模型管理 entry summarizing the first
1881
+ // five, which opens its own sub-buffer (scan on top, model form below)
1882
+ const models = p.models ?? [];
1883
+ const names = models.slice(0, 5).map((m) => m.id || "(未命名)").join(" · ");
1884
+ items.push({ kind: "button", label: "模型管理", sub: names + (models.length > 5 ? " · …" : ""), action: () => this.#openModels() });
1885
+ items.push({ kind: "button", label: "💾 保存配置", action: () => this.#save() });
1886
+ items.push({ kind: "button", label: "🗑 删除供应商", action: () => this.#deleteProvider() });
1887
+ return items;
1888
+ }
1889
+ /** The 模型管理 sub-buffer: scan first, then the model-info form rows. */
1890
+ #subItems() {
1891
+ const route = this.#route();
1892
+ if (route == null) return [];
1893
+ const p = this.#profile(route);
1894
+ const items = [];
1895
+ items.push({ kind: "button", label: "🔄 自动扫描可用模型(读 /models 端点)", action: () => this.#scan() });
1878
1896
  const models = p.models ?? [];
1879
1897
  for (let mi = 0; mi < models.length; mi++) {
1880
1898
  const m = models[mi];
@@ -1888,12 +1906,15 @@ export class ModelPanel extends Widget {
1888
1906
  }
1889
1907
  items.push({ kind: "button", label: "+ 添加模型", action: () => this.#addModel() });
1890
1908
  items.push({ kind: "button", label: "🗑 删除选中模型", action: () => this.#deleteModel() });
1891
- items.push({ kind: "button", label: "🔄 自动扫描模型(读 /models 端点)", action: () => this.#scan() });
1892
1909
  items.push({ kind: "button", label: "◉ 设为当前会话模型(选中模型)", action: () => this.#setDefaultModel() });
1893
- items.push({ kind: "button", label: "💾 保存配置", action: () => this.#save() });
1894
- items.push({ kind: "button", label: "🗑 删除供应商", action: () => this.#deleteProvider() });
1895
1910
  return items;
1896
1911
  }
1912
+ #openModels() {
1913
+ this.sub = { cursor: 0 };
1914
+ this.modelsSel = -1;
1915
+ this.#rebuild();
1916
+ this.app.redraw();
1917
+ }
1897
1918
  #rebuild() {
1898
1919
  // left list: the cursor is ALWAYS visible (● on the selected provider),
1899
1920
  // and the row being edited shows ✎ — the mode only changes the color.
@@ -1930,11 +1951,16 @@ export class ModelPanel extends Widget {
1930
1951
  formLines.push([{ t: " Enter 添加选中 · Esc 取消扫描", fg: K.FAINT }]);
1931
1952
  this.formItems = [];
1932
1953
  } else {
1933
- this.formItems = this.#formRows();
1954
+ const isSub = this.sub != null;
1955
+ const items = isSub ? this.#subItems() : this.#formRows();
1956
+ if (isSub) this.subItems = items;
1957
+ else this.formItems = items;
1934
1958
  const w = Math.max(30, this.formView.w - 4);
1935
- for (let i = 0; i < this.formItems.length; i++) {
1936
- const it = this.formItems[i];
1937
- const cur = this.mode === "form" && i === this.formIdx;
1959
+ const cursor = isSub ? this.sub.cursor : this.formIdx;
1960
+ if (isSub) formLines.push([{ t: ` 模型管理 — ${truncate(this.#profile(route).displayName || route, 30)} (Esc 返回)`, fg: K.ACCENT, bold: true }]);
1961
+ for (let i = 0; i < items.length; i++) {
1962
+ const it = items[i];
1963
+ const cur = i === cursor;
1938
1964
  let t;
1939
1965
  if (it.kind === "field") {
1940
1966
  const v = it.value === "" || it.value == null ? "(空)" : String(it.value);
@@ -1946,8 +1972,12 @@ export class ModelPanel extends Widget {
1946
1972
  t = ` ${cur ? "▸" : " "} ${it.label}`;
1947
1973
  }
1948
1974
  formLines.push([{ t: truncate(t, w), fg: cur ? T.SELFG : T.TXT, bg: cur ? T.MENUSEL : T.BG2 }]);
1975
+ // the 模型管理 preview: an indented, non-focusable summary line
1976
+ if (!isSub && it.kind === "button" && it.sub) {
1977
+ formLines.push([{ t: ` ${truncate(it.sub, w - 8)}`, fg: K.FAINT, bg: T.BG2 }]);
1978
+ }
1949
1979
  }
1950
- formLines.push([{ t: " ↑/↓ 移动 · Enter 编辑或执行 · ← 回列表 · Esc 退出", fg: K.FAINT }]);
1980
+ formLines.push([{ t: isSub ? " ↑/↓ 移动 · Enter 编辑或执行 · Esc 返回" : " ↑/↓ 换供应商 · →/Tab 进表单(再按下移) · 上移/回列表 · Enter 编辑或执行 · Esc 退出", fg: K.FAINT }]);
1951
1981
  }
1952
1982
  this.formView.setLines(formLines);
1953
1983
  }
@@ -1955,7 +1985,7 @@ export class ModelPanel extends Widget {
1955
1985
  screen.fillRect(this.x, this.y, this.x + this.w - 1, this.y + this.h - 1, " ", {});
1956
1986
  const mid = this.x + 26;
1957
1987
  screen.vline(mid, this.y, this.y + this.h - 1, "│", { fg: T.BORDER });
1958
- screen.text(this.x + 1, this.y, " 模型供应商 — CC Switch 式", { fg: K.DIM });
1988
+ screen.text(this.x + 1, this.y, " 模型供应商", { fg: K.DIM });
1959
1989
  this.listView.render(screen);
1960
1990
  this.formView.render(screen);
1961
1991
  if (this.editing) {
@@ -1999,8 +2029,10 @@ export class ModelPanel extends Widget {
1999
2029
  this.app.redraw();
2000
2030
  return;
2001
2031
  }
2002
- // form
2003
- const it = this.formItems[this.formIdx];
2032
+ // form (or the 模型管理 sub-buffer — same item kinds, different source)
2033
+ const items = this.sub != null ? this.subItems : this.formItems;
2034
+ const idx = this.sub != null ? this.sub.cursor : this.formIdx;
2035
+ const it = items[idx];
2004
2036
  if (!it) return;
2005
2037
  const route = this.#route();
2006
2038
  const p = this.#profile(route);
@@ -2194,21 +2226,56 @@ export class ModelPanel extends Widget {
2194
2226
  if (ev.name === "enter") { this.#scanCommit(); return true; }
2195
2227
  return false;
2196
2228
  }
2197
- if (ev.name === "escape") { this.editing = null; return false; } // App falls back to chat mode
2198
- if (ev.name === "left" || (ev.name === "char" && ev.key === "h" && !ev.ctrl)) { this.mode = "list"; this.#rebuild(); return true; }
2199
- if (ev.name === "right" || (ev.name === "char" && ev.key === "l" && !ev.ctrl) || ev.name === "tab") {
2200
- if (this.#route() != null) { this.mode = "form"; this.#rebuild(); }
2201
- return true;
2229
+ if (this.sub != null) {
2230
+ // inside the 模型管理 sub-buffer: ↑/↓ walk its rows; Esc returns
2231
+ if (ev.name === "escape") { this.sub = null; this.#rebuild(); return true; }
2232
+ if (ev.name === "up" || (ev.name === "char" && ev.key === "k" && !ev.ctrl)) {
2233
+ this.sub.cursor = Math.max(0, this.sub.cursor - 1);
2234
+ this.app.redraw();
2235
+ return true;
2236
+ }
2237
+ if (ev.name === "down" || (ev.name === "char" && ev.key === "j" && !ev.ctrl)) {
2238
+ this.sub.cursor = Math.min(Math.max(0, this.#subItems().length - 1), this.sub.cursor + 1);
2239
+ this.app.redraw();
2240
+ return true;
2241
+ }
2242
+ if (ev.name === "enter") { this.#activateItem(); return true; }
2243
+ return false;
2202
2244
  }
2245
+ if (ev.name === "escape") { this.editing = null; return false; } // App falls back to chat mode
2246
+ // ↑/↓ ALWAYS walk the provider column (the list is the primary axis —
2247
+ // UCAS and + 添加供应商 are both reachable by the arrow keys alone)
2203
2248
  if (ev.name === "up" || (ev.name === "char" && ev.key === "k" && !ev.ctrl)) {
2204
- if (this.mode === "list") this.sel = Math.max(0, this.sel - 1);
2205
- else { this.formIdx = Math.max(0, this.formIdx - 1); this.#rebuild(); }
2249
+ this.sel = Math.max(0, this.sel - 1);
2250
+ this.mode = "list";
2251
+ this.#rebuild();
2206
2252
  this.app.redraw();
2207
2253
  return true;
2208
2254
  }
2209
2255
  if (ev.name === "down" || (ev.name === "char" && ev.key === "j" && !ev.ctrl)) {
2210
- if (this.mode === "list") this.sel = Math.min(this.routes.length, this.sel + 1);
2211
- else { this.formIdx = Math.min(Math.max(0, this.formItems.length - 1), this.formIdx + 1); this.#rebuild(); }
2256
+ this.sel = Math.min(this.routes.length, this.sel + 1);
2257
+ this.mode = "list";
2258
+ this.#rebuild();
2259
+ this.app.redraw();
2260
+ return true;
2261
+ }
2262
+ // →/Tab: into the form, then down its rows; ←/Shift+Tab: up the form,
2263
+ // and at the top back to the provider list
2264
+ if (ev.name === "right" || (ev.name === "char" && ev.key === "l" && !ev.ctrl) || ev.name === "tab") {
2265
+ if (this.#route() != null) {
2266
+ if (this.mode === "form") this.formIdx = Math.min(Math.max(0, this.formItems.length - 1), this.formIdx + 1);
2267
+ else this.mode = "form";
2268
+ this.#rebuild();
2269
+ }
2270
+ this.app.redraw();
2271
+ return true;
2272
+ }
2273
+ if (ev.name === "left" || (ev.name === "char" && ev.key === "h" && !ev.ctrl) || ev.name === "backtab") {
2274
+ if (this.mode === "form") {
2275
+ if (this.formIdx > 0) this.formIdx--;
2276
+ else this.mode = "list";
2277
+ this.#rebuild();
2278
+ }
2212
2279
  this.app.redraw();
2213
2280
  return true;
2214
2281
  }
@@ -2238,6 +2305,12 @@ export class ModelPanel extends Widget {
2238
2305
  return false;
2239
2306
  }
2240
2307
  const idx = ev.y - this.formView.y + this.formView.scrollY;
2308
+ if (this.sub != null) {
2309
+ // the sub-buffer's title row is visual-only; rows below it are items
2310
+ const itemIdx = idx - 1;
2311
+ if (itemIdx >= 0 && itemIdx < this.#subItems().length) { this.sub.cursor = itemIdx; this.#activateItem(); return true; }
2312
+ return false;
2313
+ }
2241
2314
  if (idx >= 0 && idx < this.formItems.length) { this.formIdx = idx; this.mode = "form"; this.#activateItem(); return true; }
2242
2315
  return false;
2243
2316
  }