dsh-neotui 0.0.10 → 0.0.11

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 (3) hide show
  1. package/README.md +3 -2
  2. package/package.json +1 -1
  3. package/src/views.js +69 -26
package/README.md CHANGED
@@ -59,7 +59,8 @@ tui/app/ dsh-neotui-app bundle:cordis.patch.yml + tui-startup/tui-r
59
59
  | 设置次级页 | 在「设置」页内 **Shift+Tab** 翻次级页(常规 / 插件) |
60
60
  | 主页切换 | **Shift+Tab** 在对话 ↔ 轨迹之间切换;顶部标签页(对话/轨迹/工作区/设置/技能/子代理)可点击直达 |
61
61
  | 输入模式 | `i` 或点击进入,**Esc 退出**(nvim normal/insert);未聚焦时字母=快捷键,多字粘贴直接输入 |
62
- | 思考/工具折叠 | `t` 思考块展开折叠,`b` 工具块(bash 等)展开折叠;右键消息 →「展开 / 折叠」折叠单个输出块 |
62
+ | 思考/工具折叠 | `t` 思考块展开折叠,`b` 工具块(bash 等)展开折叠;右键消息 →「展开 / 折叠」折叠单个输出块;正文文本块同样支持点击/右键折叠(▸/▾ + 「…共 N 字」尾注);展开的思考块渲染**全文**,不再截断 |
63
+ | 右键菜单 | 菜单打开后**再次右键**任意位置:立即关闭当前菜单并在新位置打开新菜单(OS 式);左键点击菜单外仍是关闭 |
63
64
  | 对话 ↔ 轨迹转跳 | 对话里右键消息 →「转跳轨迹」定位到对应 step(自动展开+高亮);轨迹里右键 step →「转跳对话」回到该消息 |
64
65
  | 步骤快速转跳 | **Ctrl+E**:fzf 式过滤 step 列表(编号/耗时/工具/消息预览),回车定位到该 step 的轨迹窗口 |
65
66
  | 轨迹窗口导航 | 转跳后只显示 `step±20` 窗口;**PgUp/PgDn** 上下各加载 10 步,**Home/End** 跳到最早/最新 20 步,`r` 回最新 |
@@ -102,7 +103,7 @@ tui/app/ dsh-neotui-app bundle:cordis.patch.yml + tui-startup/tui-r
102
103
  - 轨迹 详细/简略:每个 step 左键单击(▸/▾)或右键菜单第一项 →「展开(详细)/ 折叠(简略)」内联展开事件列表(仿对话页);跳转定位的 step 自动展开 + 高亮闪烁
103
104
  - 轨迹窗口模型:转跳只显示 `step±20`;PgUp/PgDn 按 10 步扩展窗口(按需向后翻页加载),Home 加载到 step 1–20,End 回到最新 20 步,窗口指示行显示「窗口 step A–B · 共 N 步」
104
105
  - 工作区文件搜索:工作区面板内直接打字(`/`)模糊搜文件名,回车预览
105
- - 消息反馈:助手消息右键 👍/👎(`messageFeedback/put` Typert RPC),已评状态回显、可删除
106
+ - 消息反馈:助手消息右键 👍/👎(`messageFeedback/put` Typert RPC,`{request:…}` 载荷),已评状态回显、可删除
106
107
  - 图片画廊:多图时 ←/→ 切换,标题显示 (N/M) + 尺寸
107
108
  - 会话内搜索高亮:Ctrl+F 跳转后匹配词高亮(黄底),Esc 清除
108
109
  - 轨迹逐事件详情:点击回合 → 事件列表 → 选事件看全文
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-neotui",
3
- "version": "0.0.10",
3
+ "version": "0.0.11",
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": { "dsh-neotui": "bin/dsh-tui.js" },
package/src/views.js CHANGED
@@ -850,7 +850,7 @@ export class ChatView extends Widget {
850
850
  const node = this.nodes[info.nodeIdx];
851
851
  if (node?.kind === "assistant" && info.blockIdx !== null) {
852
852
  const b = node.blocks[info.blockIdx];
853
- if (b && (b.kind === "tool" || b.kind === "reasoning" || b.kind === "other")) {
853
+ if (b && (b.kind === "tool" || b.kind === "reasoning" || b.kind === "other" || b.kind === "text")) {
854
854
  const key = `${info.nodeIdx}:${info.blockIdx}`;
855
855
  if (b.kind === "reasoning") {
856
856
  // clean two-state override: expand ⇄ collapse (never a no-op click)
@@ -891,7 +891,6 @@ export class ChatView extends Widget {
891
891
  if (this.hasMore) { lines.push([{ t: "▲ 更早的记录", fg: K.FAINT }]); mark(-1); }
892
892
  if (skipCount > 0) { lines.push([{ t: `… 更早 ${skipCount} 条记录(PgUp 加载)`, fg: K.FAINT }]); mark(-1); }
893
893
 
894
- const MAX_TEXT = 4000;
895
894
  for (let ni = 0; ni < nodes.length; ni++) {
896
895
  const node = nodes[ni];
897
896
  const realIdx = ni + skipCount;
@@ -999,7 +998,8 @@ export class ChatView extends Widget {
999
998
  lines.push([{ t: "💭 思考" + (b.streaming ? "…" : "") + thinkMeta + (open ? " [t 折叠]" : " [t 展开]"), fg: K.FAINT }]);
1000
999
  mark(realIdx, bi);
1001
1000
  if (open) {
1002
- for (const ln of renderMd(truncateText(b.text, MAX_TEXT), w - 4)) { lines.push([{ t: " " }, ...ln]); mark(realIdx, bi); }
1001
+ // Expanded = the WHOLE reasoning (no hidden remainder).
1002
+ for (const ln of renderMd(b.text ?? "", w - 4)) { lines.push([{ t: " " }, ...ln]); mark(realIdx, bi); }
1003
1003
  } else {
1004
1004
  // collapsed: three-line preview
1005
1005
  for (const ln of renderMd(truncateText(b.text, 400), w - 4).slice(0, 3)) {
@@ -1059,18 +1059,27 @@ export class ChatView extends Widget {
1059
1059
  sep();
1060
1060
  } else {
1061
1061
  beginCard("CARD");
1062
+ // Text blocks collapse per-block like tool/reasoning blocks:
1063
+ // collapsed shows a 3-line preview + trailer, expanded the whole
1064
+ // text. Clicking (or the right-click 展开/折叠) toggles it.
1065
+ const key = `${realIdx}:${bi}`;
1066
+ const open = !this.collapsedBlocks.has(key);
1062
1067
  const text = b.text ?? "";
1063
- const md = renderMd(text, w - 4 - strWidth(stepTag));
1068
+ const glyph = open ? "▾" : "▸";
1069
+ const mdW = Math.max(10, w - 6 - strWidth(stepTag));
1070
+ const md = open
1071
+ ? renderMd(text, mdW)
1072
+ : renderMd(truncateText(text, 600), mdW).slice(0, 3);
1064
1073
  if (md.length > 0) {
1065
- lines.push([{ t: " " }, { t: stepTag, fg: K.FAINT }, ...md[0]]);
1066
- mark(realIdx);
1067
- for (const ln of md.slice(1)) { lines.push([{ t: " " }, ...ln]); mark(realIdx); }
1068
- // Label the block's LAST line with its blockIdx IN PLACE. A
1069
- // pushed mark without a line would desync lineMap from lines
1070
- // and shift every later click mapping (the off-by-one bug).
1071
- lineMap[lineMap.length - 1] = { nodeIdx: realIdx, blockIdx: bi };
1074
+ lines.push([{ t: " " }, { t: glyph + " ", fg: K.FAINT }, { t: stepTag, fg: K.FAINT }, ...md[0]]);
1075
+ mark(realIdx, bi);
1076
+ for (const ln of md.slice(1)) { lines.push([{ t: " " }, ...ln]); mark(realIdx, bi); }
1072
1077
  } else {
1073
- lines.push([{ t: " " + stepTag, fg: K.FAINT }]);
1078
+ lines.push([{ t: " " + glyph + " " + stepTag, fg: K.FAINT }]);
1079
+ mark(realIdx, bi);
1080
+ }
1081
+ if (!open && text.length > 0) {
1082
+ lines.push([{ t: ` …共 ${text.length} 字(点击展开)`, fg: K.FAINT }]);
1074
1083
  mark(realIdx, bi);
1075
1084
  }
1076
1085
  sep();
@@ -1298,13 +1307,13 @@ export class ChatView extends Widget {
1298
1307
  case "pgup": if (this.view.scrollY === 0) { this.loadOlder(); return true; } return this.view.scroll(-this.view.h);
1299
1308
  case "pgdn": return this.view.scroll(this.view.h);
1300
1309
  }
1301
- if (ev.name === "char" && ev.key === "g" && !ev.ctrl) {
1310
+ if (ev.name === "char" && ev.key === "g" && !ev.ctrl && !ev.shift) {
1302
1311
  if (this.gKey) { this.gKey = false; this.view.scrollY = 0; return true; }
1303
1312
  this.gKey = true;
1304
1313
  this.app.toast("再按 g 回顶");
1305
1314
  return true;
1306
1315
  }
1307
- if (ev.name === "char" && ev.key === "G") { this.view.scrollY = this.view.maxScroll(); return true; }
1316
+ if (ev.name === "char" && ev.key === "g" && ev.shift) { this.view.scrollY = this.view.maxScroll(); return true; }
1308
1317
  if (ev.name === "escape" && this.app.searchQuery) { this.app.searchQuery = null; this.queueRebuild(); return true; }
1309
1318
  if (ev.name === "escape" && this.selStart !== null) { this.selStart = this.selEnd = null; this.app.redraw(); return true; }
1310
1319
  if (ev.name === "char" && ev.key === "i" && !ev.ctrl) { this.app.focus(this.input); return true; }
@@ -1891,18 +1900,26 @@ export class App {
1891
1900
  async loadFeedback() {
1892
1901
  if (!this.currentSession) return;
1893
1902
  try {
1894
- const res = await this.api.rpcCall("messageFeedback/list", { sessionId: this.currentSession });
1903
+ const res = await this.api.rpcCall("messageFeedback/list", { request: { sessionId: this.currentSession } });
1895
1904
  this.feedbackMap = new Map();
1896
- for (const item of res.items ?? []) this.feedbackMap.set(item.messageId, item);
1905
+ for (const item of res?.value?.items ?? res?.items ?? []) this.feedbackMap.set(item.messageId, item);
1897
1906
  } catch { this.feedbackMap = new Map(); }
1898
1907
  }
1899
1908
 
1900
1909
  async feedback(messageId, rating) {
1901
1910
  const existing = this.feedbackMap?.get(messageId);
1902
1911
  try {
1903
- const item = await this.api.rpcCall("messageFeedback/put", {
1904
- sessionId: this.currentSession, messageId, rating, ifVersion: existing?.version ?? null,
1912
+ const res = await this.api.rpcCall("messageFeedback/put", {
1913
+ request: {
1914
+ sessionId: this.currentSession, messageId, rating,
1915
+ ifVersion: existing?.version ?? null,
1916
+ },
1905
1917
  });
1918
+ if (res?.ok === false) {
1919
+ this.toast(`反馈失败: ${res.error?.code ?? res.error?.message ?? "unknown"}`);
1920
+ return;
1921
+ }
1922
+ const item = res?.value ?? res;
1906
1923
  this.feedbackMap = this.feedbackMap ?? new Map();
1907
1924
  this.feedbackMap.set(messageId, item);
1908
1925
  this.toast(rating === "positive" ? "已记录 👍" : "已记录 👎");
@@ -1913,7 +1930,13 @@ export class App {
1913
1930
  const existing = this.feedbackMap?.get(messageId);
1914
1931
  if (!existing) return;
1915
1932
  try {
1916
- await this.api.rpcCall("messageFeedback/delete", { sessionId: this.currentSession, messageId, ifVersion: existing.version });
1933
+ const res = await this.api.rpcCall("messageFeedback/delete", {
1934
+ request: { sessionId: this.currentSession, messageId, ifVersion: existing.version },
1935
+ });
1936
+ if (res?.ok === false) {
1937
+ this.toast(`删除反馈失败: ${res.error?.code ?? res.error?.message ?? "unknown"}`);
1938
+ return;
1939
+ }
1917
1940
  this.feedbackMap.delete(messageId);
1918
1941
  this.toast("已删除反馈");
1919
1942
  } catch (e) { this.toast(`删除反馈失败: ${e.message}`); }
@@ -2290,16 +2313,29 @@ export class App {
2290
2313
  }
2291
2314
  if (this.menu) {
2292
2315
  const before = this.menu;
2293
- if (ev.type === "key") this.menu.onKey(ev);
2294
- else if (ev.type === "mouse") {
2295
- if (ev.kind === "press" && ev.button === 0 && !this.menu.inside(ev.x, ev.y)) { this.menu = null; this.swallowRelease = true; }
2296
- else {
2316
+ if (ev.type === "key") { this.menu.onKey(ev); this.redraw(); return; }
2317
+ if (ev.type === "mouse") {
2318
+ if (ev.kind === "press" && ev.button === 0 && !this.menu.inside(ev.x, ev.y)) {
2319
+ this.menu = null; this.swallowRelease = true; this.redraw(); return;
2320
+ }
2321
+ if (ev.kind === "press" && ev.button === 2) {
2322
+ if (this.menu.inside(ev.x, ev.y)) { this.redraw(); return; } // right-click on the menu itself: keep it open
2323
+ // OS-style: a right-click anywhere else closes the current menu and
2324
+ // the SAME press falls through to open a fresh menu at the new spot.
2325
+ this.menu = null;
2326
+ this.swallowRelease = true;
2327
+ this.redraw();
2328
+ // fall through to the normal mouse routing below
2329
+ } else {
2297
2330
  this.menu.onMouse(ev);
2298
2331
  if (ev.kind === "press" && this.menu !== before) this.swallowRelease = true;
2332
+ this.redraw();
2333
+ return;
2299
2334
  }
2335
+ } else {
2336
+ this.redraw();
2337
+ return;
2300
2338
  }
2301
- this.redraw();
2302
- return;
2303
2339
  }
2304
2340
  if (this.overlay) {
2305
2341
  const before = this.overlay;
@@ -2443,7 +2479,14 @@ export class App {
2443
2479
  return;
2444
2480
  }
2445
2481
  if (ev.text.length === 1) {
2446
- const asKey = { type: "key", name: "char", key: ev.text, text: ev.text, ctrl: false, alt: false, shift: false };
2482
+ // Legacy terminals deliver Shift+letter as the UPPERCASE char with no
2483
+ // modifier info — recover the shift flag from the case so Shift+T
2484
+ // (todo fold) and G (scroll bottom) work everywhere.
2485
+ const asKey = {
2486
+ type: "key", name: "char",
2487
+ key: ev.text.toLowerCase(), text: ev.text,
2488
+ ctrl: false, alt: false, shift: ev.text !== ev.text.toLowerCase(),
2489
+ };
2447
2490
  if (this.chat.onKey(asKey)) { this.redraw(); return; }
2448
2491
  if (this.focused && this.focused !== this.chat.input && this.focused.onKey(asKey)) { this.redraw(); return; }
2449
2492
  this.toast("按 i 进入输入");