dsh-neotui 0.4.4-pre.0 → 0.4.4

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/CHANGELOG.md CHANGED
@@ -1,11 +1,15 @@
1
1
  # Changelog
2
2
 
3
- ## 0.4.4-pre.0 — 2026-08-26
3
+ ## 0.4.4 — 2026-08-26
4
4
 
5
- > PRE-RELEASE:`npm install dsh-neotui@pre`;`latest` 仍为 0.4.3,不会自动升级。
5
+ ### Fixed
6
+
7
+ - **兼容 dsh 0.1.2-rc.1**:dsh-base 收编 storage 行与 session-projection-cache 后,组合树同 id 条目按新加载器规则去重(app 补丁仅保留裸 id 覆盖行做 attach 隔离);`dsh-host-apiproxy` 包停更,其 /api 网关职责并入 connection 行的 node 半端(旧包在新版下启动即崩);补挂 message-feedback 行。
6
8
 
7
9
  ### Added
8
10
 
11
+ - **统一前缀体系**:Ctrl+Space 面板前缀页与前缀引擎共用同一张表(which-key 模式)——面板内按键与 NORMAL 前缀同义,`p` 在面板内链入窗格子表(权限迁至 `a`);`Ctrl+H`/`Ctrl+L` 切换主窗口标签页(上一/下一,`Shift+Tab` 保留;`Ctrl+H` 原技能键释放);前缀触发字符可在 tui-config.json `prefixKeys` 段重定义(`Ctrl+K` 打开配置)。
12
+
9
13
  - **窗口/标签页操作模型重构**:会话列表/窗口模式——`Ctrl+←→` 或 `Tab` 在「会话列表 ↔ 主窗口」间切换,`Shift+Tab` 在主窗口内循环标签页(对话/轨迹/子代理/后台任务,顶部标签条);INSERT 内所有结构键保持编辑语义不冲突;侧栏预览卡片触发键 `p`(仅列表窗口作用域生效)。
10
14
  - **nvim 式逐层前缀键**:NORMAL 下按 `p` 进入待定层(状态栏提示 + Esc/2s/错误键取消),
11
15
  - `p r/l/u/n`:把当前窗口向 右/左/上/下 分屏(新窗口自动成为**空白会话**窗口,窗口=会话容器;(p c) 关窗后会话保留在侧栏,关掉全部主窗口自动生成空白会话保底);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-neotui",
3
- "version": "0.4.4-pre.0",
3
+ "version": "0.4.4",
4
4
  "description": "Keyboard-first Vim/tmux-style terminal client for DeepSeek Harness",
5
5
  "type": "module",
6
6
  "engines": {
package/src/config.js CHANGED
@@ -75,6 +75,21 @@ export function busyEnter() {
75
75
  /** Persist the newest 20 unique cross-session search queries (oldest→newest). */
76
76
 
77
77
 
78
+ /** 前缀键字符表(leader/面板首屏与窗格子表共用)。可在 tui-config.json 的
79
+ * prefixKeys 段覆盖单键(如 { "rewind": "R" });非法字符回退默认。 */
80
+ const PREFIX_KEY_DEFAULTS = {
81
+ search: "s", model: "m", theme: "d", history: "h", rewind: "r",
82
+ config: "c", help: "?", editor: "G", permission: "a", panes: "p",
83
+ };
84
+ export function prefixKeys() {
85
+ const raw = loadTuiConfig().prefixKeys ?? {};
86
+ const out = { ...PREFIX_KEY_DEFAULTS };
87
+ for (const [k, v] of Object.entries(raw)) {
88
+ if (k in out && typeof v === "string" && v.length === 1) out[k] = v;
89
+ }
90
+ return out;
91
+ }
92
+
78
93
  /** Last ~50 user prompts typed into the chat input (persisted, deduped). */
79
94
  export function promptHistory() {
80
95
  const raw = loadTuiConfig().promptHistory;
@@ -38,7 +38,7 @@ export const DEFAULT_KEYBINDINGS = {
38
38
  workspace: { mode: "normal", key: "Ctrl+W", key2: "" },
39
39
  settings: { mode: "normal", key: "Ctrl+S", key2: "" },
40
40
  subagent: { mode: "normal", key: "", key2: "" }, // freed: subagent lives on the unified panel page 2 (Ctrl+T → Tab)
41
- skills: { mode: "normal", key: "Ctrl+H", key2: "" },
41
+ skills: { mode: "normal", key: "", key2: "" }, // freed: Ctrl+H is now tabPrev
42
42
  goal: { mode: "normal", key: "Ctrl+G", key2: "" },
43
43
  jobs: { mode: "normal", key: "", key2: "" }, // freed: jobs live on the unified panel page 3 (Ctrl+T → Tab)
44
44
  queue: { mode: "normal", key: "", key2: "" }, // freed: queue lives on the unified panel page 3 (Ctrl+T → Tab)
@@ -53,13 +53,15 @@ export const DEFAULT_KEYBINDINGS = {
53
53
  themePicker: { mode: "normal", key: "", key2: "" }, // freed: theme picker moved to the Ctrl+Space prefix page (d)
54
54
  logoPicker: { mode: "normal", key: "Ctrl+R", key2: "" },
55
55
  help: { mode: "normal", key: "Shift+/", key2: "Shift+?" }, // 场景帮助(Shift+/ 或 Shift+?)——按焦点路由
56
+ tabNext: { mode: "normal", key: "Ctrl+L", key2: "" }, // 主窗口标签页下一页
57
+ tabPrev: { mode: "normal", key: "Ctrl+H", key2: "" }, // 主窗口标签页上一页
56
58
  quitDouble: { mode: "normal", key: "Ctrl+C", key2: "" },
57
59
  quit: { mode: "all", key: "Ctrl+Q", key2: "" },
58
60
  };
59
61
 
60
62
  /** App-level dispatch precedence: the first matching binding wins. */
61
63
  export const KEYBINDING_ORDER = [
62
- "sessionFilter", "help", "panel", "panePrev", "paneNext", "permissionRotate", "editConfig", "addWorkspace", "commandPalette", "modePicker", "themePicker", "logoPicker", "copySelection", "quitDouble", "quit",
64
+ "sessionFilter", "help", "tabNext", "tabPrev", "panel", "panePrev", "paneNext", "permissionRotate", "editConfig", "addWorkspace", "commandPalette", "modePicker", "themePicker", "logoPicker", "copySelection", "quitDouble", "quit",
63
65
  "model", "trajectory", "workspace", "settings", "subagent", "skills", "goal",
64
66
  "jobs", "queue", "busyEnter", "attachments", "stepJump", "sidebar",
65
67
  ];
package/src/panels.js CHANGED
@@ -1761,17 +1761,8 @@ export class ControlPanel extends Widget {
1761
1761
  ];
1762
1762
  }
1763
1763
  prefixRows() {
1764
- return [
1765
- ["s", "跨会话全文搜索", "Ctrl+F 已释放", () => { this.app.closeOverlay(); this.app.startSearch(); this.app.redraw(); }],
1766
- ["h", "输入历史搜索(可筛选)", "最近 50 条提问", () => { this.app.closeOverlay(); this.app.showHistorySearch(); }],
1767
- ["?", "帮助(按场景)", "Shift+/ 或前缀 ?", () => { this.app.closeOverlay(); this.app.showHelp(this.app.focusBeforePanel ?? null); }],
1768
- ["G", "外部编辑器编辑输入", "$VISUAL · 非零退出保留原稿", () => { this.app.closeOverlay(); this.app.editExternal(); }],
1769
- ["r", "回退(分支会话 + 原消息回填)", "/rewind", () => { this.app.closeOverlay(); this.app.showRewindPicker(); }],
1770
- ["m", "切换模型", "Ctrl+M 已释放", () => { this.app.closeOverlay(); this.app.overlay = buildModelPicker(this.app); this.app.redraw(); }],
1771
- ["d", "配色主题", "Ctrl+D 已释放", () => { this.app.closeOverlay(); this.app.showThemePicker(); }],
1772
- ["p", "权限模式(沙箱 + 审批)", "F8 已释放", () => { this.app.closeOverlay(); this.app.showPermissionPicker(); }],
1773
- ["c", "打开配置文件(tui-config.json)", "等价: Ctrl+K", () => { this.app.closeOverlay(); this.app.editConfigFile(); }],
1774
- ];
1764
+ // 前缀页 = 引擎同一张表的可视化(which-key);键路由走同一引擎。
1765
+ return this.app.prefixRowsData();
1775
1766
  }
1776
1767
  items() {
1777
1768
  if (this.page === 0) return this.prefixRows().map(([k, d, hint, action]) => [`${k} ${d}`, hint, action]);
@@ -1837,7 +1828,7 @@ export class ControlPanel extends Widget {
1837
1828
  else if(this.page===1){const [mode,key1,key2]=label.split("\t");s.text(this.x+2,this.y+2+i,pad(mode,9),{fg:T.PURPLE,bg:sel?T.MENUSEL:T.PANEL,attrs:sel?1:0});s.text(this.x+13,this.y+2+i,pad(truncate(key1,16),17),{fg:T.ACCENT,bg:sel?T.MENUSEL:T.PANEL,attrs:sel?1:0});s.text(this.x+31,this.y+2+i,pad(truncate(key2,16),17),{fg:T.ACCENT,bg:sel?T.MENUSEL:T.PANEL,attrs:sel?1:0});s.text(this.x+49,this.y+2+i,truncate(it[1],this.w-52),{fg:T.OK,bg:sel?T.MENUSEL:T.PANEL,attrs:sel?1:0});}
1838
1829
  else{s.text(this.x + 2, this.y + 2 + i, truncate(label, this.w - 34), { fg: sel ? T.BOLD : T.TXT, bg: sel ? T.MENUSEL : T.PANEL, attrs: sel ? 1 : 0 });if (it[1]) s.text(this.x + this.w - 30, this.y + 2 + i, truncate(it[1], 28), { fg: T.FAINT, bg: sel ? T.MENUSEL : T.PANEL });}
1839
1830
  }
1840
- s.text(this.x + 2, this.y + this.h - 1, this.page===0?" r 执行 · ↑↓/Enter 也可以 · Esc 关闭":this.page===1?"↑↓ 选择 · Enter 编辑 · Shift+Tab 轮换模式 · Alt+Enter 恢复默认 · Esc 关闭":this.page===3?`/ 筛选插件 · Ctrl+/ 清除 · ↑↓ 选择 · Esc 关闭${this.pluginQuery?` · ${this.pluginQuery}`:""}`:"↑↓ 选择 · Enter 执行 · Esc 关闭", { fg: T.FAINT });
1831
+ s.text(this.x + 2, this.y + this.h - 1, this.page===0?"按行首键执行(同 NORMAL 前缀层) · ↑↓/Enter 也可以 · Esc 关闭":this.page===1?"↑↓ 选择 · Enter 编辑 · Shift+Tab 轮换模式 · Alt+Enter 恢复默认 · Esc 关闭":this.page===3?`/ 筛选插件 · Ctrl+/ 清除 · ↑↓ 选择 · Esc 关闭${this.pluginQuery?` · ${this.pluginQuery}`:""}`:"↑↓ 选择 · Enter 执行 · Esc 关闭", { fg: T.FAINT });
1841
1832
  }
1842
1833
  onKey(ev) {
1843
1834
  if(this.page===3&&this.pluginFilter){if(ev.type==="text"){this.pluginQuery+=ev.text;this.sel=0;return true;}if(ev.type==="key"&&ev.name==="backspace"){this.pluginQuery=this.pluginQuery.slice(0,-1);this.sel=0;return true;}if(ev.type==="key"&&ev.name==="enter"){this.pluginFilter=false;return true;}if(ev.type==="key"&&ev.ctrl&&(ev.key==="/"||ev.key==="_")){this.pluginFilter=false;this.pluginQuery="";return true;}}
package/src/views.js CHANGED
@@ -10,7 +10,7 @@ import { tmpdir, homedir } from "node:os";
10
10
  import { createRequire } from "node:module";
11
11
  import { Widget, ScrollView, Input, Popup, Menu, StatusBar, wrapIndex } from "./widgets.js";
12
12
  import { UploadPicker } from "./file-picker.js";
13
- import { userPrefix, saveTuiConfig, loadTuiConfig, userName, busyEnter, foldDefaults, keyBindings, tuiConfigFile, reloadTuiConfig, searchHistory, rememberSearchQuery, promptHistory, rememberPrompt } from "./config.js";
13
+ import { userPrefix, saveTuiConfig, loadTuiConfig, userName, busyEnter, foldDefaults, keyBindings, tuiConfigFile, reloadTuiConfig, searchHistory, rememberSearchQuery, promptHistory, rememberPrompt, prefixKeys } from "./config.js";
14
14
  import { bindingMatchFor, matchKeyBinding, CHAT_BINDING_ORDER, SIDEBAR_BINDING_ORDER, KEYBINDING_ORDER, INPUT_BINDING_ORDER, INPUT_EDIT_BINDING_ORDER } from "./keybindings.js";
15
15
  export { userPrefix, saveTuiConfig, loadTuiConfig, userName, busyEnter, foldDefaults, promptHistory, rememberPrompt } from "./config.js";
16
16
  import {
@@ -4194,6 +4194,28 @@ export class App {
4194
4194
  return walk(this.winTree, rootRect);
4195
4195
  }
4196
4196
 
4197
+ /** Prefix rows for the panel reference page — THE SAME table the engine
4198
+ * dispatches (single source of truth; chars from prefixKeys config). */
4199
+ prefixRowsData() {
4200
+ const k = prefixKeys();
4201
+ const t = this.#prefixTables().main;
4202
+ const meta = [
4203
+ ["search", "跨会话全文搜索", "Ctrl+F 已释放"],
4204
+ ["history", "输入历史搜索(可筛选)", "最近 50 条提问"],
4205
+ ["rewind", "回退(分支会话 + 原消息回填)", "/rewind"],
4206
+ ["model", "切换模型", "Ctrl+M 已释放"],
4207
+ ["theme", "配色主题", "Ctrl+D 已释放"],
4208
+ ["permission", "权限模式(沙箱 + 审批)", "F8 已释放"],
4209
+ ["config", "打开配置文件(改 prefixKeys 前缀键)", "Ctrl+K"],
4210
+ ["help", "帮助(按场景)", "Shift+/ 或前缀 ?"],
4211
+ ["editor", "外部编辑器编辑输入", "$VISUAL · 非零退出保留原稿"],
4212
+ ["panes", "窗格/分屏子表", "p r/l/u/n/c(NORMAL 亦可用)"],
4213
+ ];
4214
+ return meta
4215
+ .filter(([action]) => t.entries[k[action]])
4216
+ .map(([action, desc, hint]) => [k[action], desc, hint, () => { this.closeOverlay(); t.entries[k[action]](); }]);
4217
+ }
4218
+
4197
4219
  /** Public window-tree inspection (tests/diagnostics). */
4198
4220
  splitLeaves() { return this.#leaves(); }
4199
4221
  mainLeafCount() { return this.#mainLeaves().length; }
@@ -5530,6 +5552,8 @@ export class App {
5530
5552
  case "help": this.showHelp(); return true;
5531
5553
  case "panePrev": this.focusWindow(-1); return true;
5532
5554
  case "paneNext": this.focusWindow(1); return true;
5555
+ case "tabNext": this.#tabCycle(1); return true;
5556
+ case "tabPrev": this.#tabCycle(-1); return true;
5533
5557
  case "permissionRotate": this.rotatePermission(); return true;
5534
5558
  case "editConfig": this.editConfigFile(); return true;
5535
5559
  case "quit": this.stop(); return true;
@@ -5743,12 +5767,37 @@ export class App {
5743
5767
  return false;
5744
5768
  }
5745
5769
 
5770
+ /** Tab cycling shared by Shift+Tab (structural) and Ctrl+H/L bindings:
5771
+ * from the list window, focus the main window first. */
5772
+ #tabCycle(delta) {
5773
+ if (this.focusedWindow !== "main") this.setFocusedWindow("main");
5774
+ this.cycleTab(delta);
5775
+ return true;
5776
+ }
5777
+
5746
5778
  // ---- chained prefix layers (nvim-style pending keys) ----
5747
5779
 
5748
5780
  /** Generic pending-layer tables. The p table is the first user; future
5749
5781
  * g/z tables register here too. Entries: (ev) => run. */
5750
5782
  #prefixTables() {
5783
+ const k = prefixKeys();
5751
5784
  return {
5785
+ // 主表:Ctrl+Space 面板首屏与 NORMAL 前缀层共用(同一张表,同一语义)
5786
+ main: {
5787
+ hint: `${k.search}=搜索 ${k.model}=模型 ${k.theme}=主题 ${k.history}=历史 ${k.rewind}=回退 ${k.config}=配置 ${k.help}=帮助 ${k.editor}=编辑 ${k.permission}=权限 ${k.panes}=窗格 · Esc 取消`,
5788
+ entries: {
5789
+ [k.search]: () => { this.closeOverlay(); this.startSearch(); this.redraw(); },
5790
+ [k.model]: () => { this.closeOverlay(); this.overlay = buildModelPicker(this); this.redraw(); },
5791
+ [k.theme]: () => { this.closeOverlay(); this.showThemePicker(); },
5792
+ [k.history]: () => { this.closeOverlay(); this.showHistorySearch(); },
5793
+ [k.rewind]: () => { this.closeOverlay(); this.showRewindPicker(); },
5794
+ [k.config]: () => { this.closeOverlay(); this.editConfigFile(); },
5795
+ [k.help]: () => { this.closeOverlay(); this.showHelp(this.focusBeforePanel ?? null); },
5796
+ [k.editor]: () => { this.closeOverlay(); this.editExternal(); },
5797
+ [k.permission]: () => { this.closeOverlay(); this.showPermissionPicker(); },
5798
+ [k.panes]: () => { this.closeOverlay(); this.armPrefix("p"); },
5799
+ },
5800
+ },
5752
5801
  p: {
5753
5802
  hint: "p · r=右 l=左 u=上 n=下 c=关窗 · Esc/2s 取消",
5754
5803
  entries: {
@@ -5761,6 +5810,17 @@ export class App {
5761
5810
  },
5762
5811
  };
5763
5812
  }
5813
+ /** Arm a pending layer programmatically (e.g. the panel chaining into p). */
5814
+ armPrefix(key) {
5815
+ const table = this.#prefixTables()[key];
5816
+ if (!table) return false;
5817
+ this.pendingPrefix = { table, key, timer: null };
5818
+ this.pendingPrefix.timer = setTimeout(() => {
5819
+ if (this.pendingPrefix?.key === key) this.#cancelPrefix();
5820
+ }, this.prefixTimeoutMs);
5821
+ this.redraw();
5822
+ return true;
5823
+ }
5764
5824
  #handlePrefixKey(ev) {
5765
5825
  if (ev.type !== "key") return false;
5766
5826
  const pending = this.pendingPrefix;
@@ -5774,12 +5834,7 @@ export class App {
5774
5834
  // Arm only from a main window, not in INSERT (list window: p = preview).
5775
5835
  if (ev.name === "char" && ev.key === "p" && !ev.ctrl && !ev.alt && !ev.shift
5776
5836
  && this.focusedWindow === "main" && !this.#inInsertMode()) {
5777
- const table = this.#prefixTables().p;
5778
- this.pendingPrefix = { table, key: "p", timer: null };
5779
- this.pendingPrefix.timer = setTimeout(() => {
5780
- if (this.pendingPrefix?.key === "p") this.#cancelPrefix();
5781
- }, this.prefixTimeoutMs);
5782
- this.redraw();
5837
+ this.armPrefix("p");
5783
5838
  return true;
5784
5839
  }
5785
5840
  return false;