dsh-neotui 0.1.24 → 0.1.26

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 +73 -28
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-neotui",
3
- "version": "0.1.24",
3
+ "version": "0.1.26",
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: " ↑/↓ 换供应商 · →/Tab 进表单(再按下移) · ← 上移/回列表 · Enter 编辑或执行 · Esc 退出", fg: K.FAINT }]);
1980
+ formLines.push([{ t: isSub ? " ↑/↓ 移动 · Enter 编辑或执行 · Esc 返回" : " ↑/↓ 移动 · → 进入选项 · 返回列表 · Enter 编辑或执行 · Esc 退出", fg: K.FAINT }]);
1951
1981
  }
1952
1982
  this.formView.setLines(formLines);
1953
1983
  }
@@ -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,40 +2226,47 @@ export class ModelPanel extends Widget {
2194
2226
  if (ev.name === "enter") { this.#scanCommit(); return true; }
2195
2227
  return false;
2196
2228
  }
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;
2244
+ }
2197
2245
  if (ev.name === "escape") { this.editing = null; return false; } // App falls back to chat mode
2198
- // ↑/↓ ALWAYS walk the provider column (the list is the primary axis
2199
- // UCAS and 添加供应商 are both reachable by the arrow keys alone)
2246
+ // dual-focus navigation: ↑/↓ move the cursor INSIDE the focused region
2247
+ // the provider column in list focus, the option rows in form focus.
2248
+ // → enters the form, ← returns to the list.
2200
2249
  if (ev.name === "up" || (ev.name === "char" && ev.key === "k" && !ev.ctrl)) {
2201
- this.sel = Math.max(0, this.sel - 1);
2202
- this.mode = "list";
2250
+ if (this.mode === "list") this.sel = Math.max(0, this.sel - 1);
2251
+ else this.formIdx = Math.max(0, this.formIdx - 1);
2203
2252
  this.#rebuild();
2204
2253
  this.app.redraw();
2205
2254
  return true;
2206
2255
  }
2207
2256
  if (ev.name === "down" || (ev.name === "char" && ev.key === "j" && !ev.ctrl)) {
2208
- this.sel = Math.min(this.routes.length, this.sel + 1);
2209
- this.mode = "list";
2257
+ if (this.mode === "list") this.sel = Math.min(this.routes.length, this.sel + 1);
2258
+ else this.formIdx = Math.min(Math.max(0, this.formItems.length - 1), this.formIdx + 1);
2210
2259
  this.#rebuild();
2211
2260
  this.app.redraw();
2212
2261
  return true;
2213
2262
  }
2214
- // →/Tab: into the form, then down its rows; ←/Shift+Tab: up the form,
2215
- // and at the top back to the provider list
2216
2263
  if (ev.name === "right" || (ev.name === "char" && ev.key === "l" && !ev.ctrl) || ev.name === "tab") {
2217
- if (this.#route() != null) {
2218
- if (this.mode === "form") this.formIdx = Math.min(Math.max(0, this.formItems.length - 1), this.formIdx + 1);
2219
- else this.mode = "form";
2220
- this.#rebuild();
2221
- }
2264
+ if (this.#route() != null && this.mode !== "form") { this.mode = "form"; this.#rebuild(); }
2222
2265
  this.app.redraw();
2223
2266
  return true;
2224
2267
  }
2225
2268
  if (ev.name === "left" || (ev.name === "char" && ev.key === "h" && !ev.ctrl) || ev.name === "backtab") {
2226
- if (this.mode === "form") {
2227
- if (this.formIdx > 0) this.formIdx--;
2228
- else this.mode = "list";
2229
- this.#rebuild();
2230
- }
2269
+ if (this.mode === "form") { this.mode = "list"; this.#rebuild(); }
2231
2270
  this.app.redraw();
2232
2271
  return true;
2233
2272
  }
@@ -2257,6 +2296,12 @@ export class ModelPanel extends Widget {
2257
2296
  return false;
2258
2297
  }
2259
2298
  const idx = ev.y - this.formView.y + this.formView.scrollY;
2299
+ if (this.sub != null) {
2300
+ // the sub-buffer's title row is visual-only; rows below it are items
2301
+ const itemIdx = idx - 1;
2302
+ if (itemIdx >= 0 && itemIdx < this.#subItems().length) { this.sub.cursor = itemIdx; this.#activateItem(); return true; }
2303
+ return false;
2304
+ }
2260
2305
  if (idx >= 0 && idx < this.formItems.length) { this.formIdx = idx; this.mode = "form"; this.#activateItem(); return true; }
2261
2306
  return false;
2262
2307
  }