dsh-neotui 0.0.23 → 0.0.24

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/views.js +40 -22
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-neotui",
3
- "version": "0.0.23",
3
+ "version": "0.0.24",
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
@@ -565,6 +565,8 @@ export class ChatView extends Widget {
565
565
  this.expanded = new Set(); // node indexes (user-message full text)
566
566
  this.expandedTools = new Set();
567
567
  this.collapsedBlocks = new Set(); // per-block COLLAPSE (default expanded): `${realIdx}:${bi}`
568
+ this.blockHeights = new Map(); // collapsed block → its rendered height when collapsed
569
+ // (fixed-height fold: the space below stays put)
568
570
  this.thinkMode = "expanded"; // think blocks: expanded by default (t toggles)
569
571
  this.bashMode = "expanded"; // tool blocks: expanded | collapsed (b toggles)
570
572
  this.todosVisible = true; // todo block above the input (Shift+T toggles)
@@ -906,28 +908,11 @@ export class ChatView extends Widget {
906
908
  this.#clickLog(`toggle mark=${JSON.stringify(info)} kind=${node.kind} blockIdx=${info.blockIdx} preHeaderRow=${preHeaderRow} topKey=${topKey} topOffset=${topOffset}`);
907
909
  }
908
910
  const reanchor = () => {
909
- // COLLAPSING a block by clicking one of its CONTENT lines: the clicked
910
- // text itself vanishes, and the content below slides up by the fold
911
- // size the "4-line offset" the user sees. Land the fold's LAST line
912
- // (the trailer/summary) exactly at the clicked row so the eye stays
913
- // anchored and nothing below visibly shifts.
914
- if (collapsing && pressY !== null && pressY !== preHeaderIdx && pressRow !== null && pressRow >= 0) {
915
- let lastIdx = -1;
916
- for (let i = 0; i < this.lineMap.length; i++) {
917
- if (match(this.lineMap[i])) lastIdx = i;
918
- }
919
- if (lastIdx >= 0) {
920
- const sy = lastIdx - pressRow;
921
- this.view.scrollY = Math.max(0, sy);
922
- if (sy > this.view.maxScroll()) this.view.anchorLock = sy;
923
- return;
924
- }
925
- }
911
+ // Fixed-height folds keep the buffer size UNCHANGED (the collapsed
912
+ // block pads its recorded height with ghost rows), so nothing below
913
+ // the fold can move the anchor is only a safety net now.
926
914
  // primary anchor: the clicked block's header stays at its pre-click
927
915
  // viewport row (zero shift) — only when the header was ON-SCREEN.
928
- // When the fold removed the tail content below, the anchored position
929
- // may exceed maxScroll: hold it via the view's anchorLock instead of
930
- // clamping (the clamp is exactly the slide the user sees).
931
916
  if (preHeaderRow !== null && preHeaderRow >= 0 && preHeaderIdx >= 0) {
932
917
  const h2 = firstNonEmpty(match);
933
918
  if (h2 >= 0) {
@@ -951,17 +936,25 @@ export class ChatView extends Widget {
951
936
  if (target > this.view.maxScroll()) this.view.anchorLock = target;
952
937
  };
953
938
  // never park the viewport on a blank separator row — it reads as a
954
- // spurious offset right after the click
939
+ // spurious offset right after the click. Ghost rows of a fixed-height
940
+ // fold are INTENTIONAL blanks (marked with a blockIdx) — leave them.
955
941
  const nudge = () => {
956
942
  while (
957
943
  this.view.scrollY < this.lineMap.length - 1 &&
958
- !(this.lines[this.view.scrollY] ?? []).some((g) => g.t.trim() !== "")
944
+ !(this.lines[this.view.scrollY] ?? []).some((g) => g.t.trim() !== "") &&
945
+ (this.lineMap[this.view.scrollY]?.blockIdx ?? null) === null
959
946
  ) this.view.scrollY++;
960
947
  };
961
948
  if (node.kind === "assistant" && info.blockIdx !== null) {
962
949
  const b = node.blocks[info.blockIdx];
963
950
  if (b && (b.kind === "tool" || b.kind === "reasoning" || b.kind === "other" || b.kind === "text")) {
964
951
  const key = `${info.nodeIdx}:${info.blockIdx}`;
952
+ // fixed-height fold: remember how tall the block is right now so the
953
+ // collapsed form can pad the SAME height (nothing below moves)
954
+ let firstM = -1, lastM = -1;
955
+ for (let i = 0; i < this.lineMap.length; i++) {
956
+ if (match(this.lineMap[i])) { if (firstM < 0) firstM = i; lastM = i; }
957
+ }
965
958
  if (b.kind === "reasoning") {
966
959
  // clean two-state override: expand ⇄ collapse (never a no-op click)
967
960
  const open = this.expanded.has(key) || (!this.collapsedBlocks.has(key) && this.thinkMode === "expanded");
@@ -973,6 +966,8 @@ export class ChatView extends Widget {
973
966
  if (this.collapsedBlocks.has(key)) this.collapsedBlocks.delete(key);
974
967
  else this.collapsedBlocks.add(key);
975
968
  }
969
+ if (collapsing && firstM >= 0) this.blockHeights.set(key, lastM - firstM + 1);
970
+ else if (!collapsing) this.blockHeights.delete(key);
976
971
  this.#rebuild();
977
972
  reanchor(); nudge();
978
973
  if (process.env.DSH_TUI_DEBUG_CLICK) {
@@ -1116,6 +1111,7 @@ export class ChatView extends Widget {
1116
1111
  timing = " 已完成";
1117
1112
  }
1118
1113
  const thinkMeta = `${stepTag}(${b.text?.length ?? 0} 字)${timing}`;
1114
+ const blkLines0 = lines.length;
1119
1115
  lines.push([{ t: "💭 思考" + (b.streaming ? "…" : "") + thinkMeta + (open ? " [t 折叠]" : " [t 展开]"), fg: K.FAINT }]);
1120
1116
  mark(realIdx, bi);
1121
1117
  if (open) {
@@ -1128,6 +1124,13 @@ export class ChatView extends Widget {
1128
1124
  mark(realIdx, bi);
1129
1125
  }
1130
1126
  }
1127
+ // fixed-height fold: pad the recorded height with ghost rows so
1128
+ // nothing below the block moves
1129
+ if (!open) {
1130
+ const h = this.blockHeights.get(key) ?? 0;
1131
+ const filler = Math.max(0, h - (lines.length - blkLines0));
1132
+ for (let fi = 0; fi < filler; fi++) { lines.push([{ t: "" }]); mark(realIdx, bi); }
1133
+ }
1131
1134
  sep();
1132
1135
  } else if (b.kind === "tool") {
1133
1136
  const key = `${realIdx}:${bi}`;
@@ -1142,6 +1145,7 @@ export class ChatView extends Widget {
1142
1145
  const glyph = orphan ? "✗" : running ? "⏳" : exitCode !== undefined && exitCode !== 0 ? "✗" : "✓";
1143
1146
  const card = b.view ? renderToolCard(b.view, w, open) : [];
1144
1147
  beginCard(status);
1148
+ const blkLines0 = lines.length;
1145
1149
  let timing = "";
1146
1150
  if (running) {
1147
1151
  timing = ` 已经过 ${fmtDuration(Date.now() - (b.startedAt ?? Date.now()))}`;
@@ -1165,6 +1169,11 @@ export class ChatView extends Widget {
1165
1169
  lines.push([{ t: " " + truncate(summary, w - 6), fg: K.FAINT }]);
1166
1170
  mark(realIdx, bi);
1167
1171
  }
1172
+ // fixed-height fold: pad the recorded height with ghost rows so
1173
+ // nothing below the block moves
1174
+ const h = this.blockHeights.get(key) ?? 0;
1175
+ const filler = Math.max(0, h - (lines.length - blkLines0));
1176
+ for (let fi = 0; fi < filler; fi++) { lines.push([{ t: "" }]); mark(realIdx, bi); }
1168
1177
  }
1169
1178
  if (open) {
1170
1179
  for (const ln of card) { lines.push(ln); mark(realIdx, bi); }
@@ -1187,6 +1196,7 @@ export class ChatView extends Widget {
1187
1196
  sep();
1188
1197
  } else {
1189
1198
  beginCard("CARD");
1199
+ const blkLines0 = lines.length;
1190
1200
  // Text blocks collapse per-block like tool/reasoning blocks:
1191
1201
  // collapsed shows a 3-line preview + trailer, expanded the whole
1192
1202
  // text. Clicking (or the right-click 展开/折叠) toggles it.
@@ -1210,6 +1220,13 @@ export class ChatView extends Widget {
1210
1220
  lines.push([{ t: ` …共 ${text.length} 字(点击展开)`, fg: K.FAINT }]);
1211
1221
  mark(realIdx, bi);
1212
1222
  }
1223
+ // fixed-height fold: pad the recorded height with ghost rows so
1224
+ // nothing below the block moves
1225
+ if (!open) {
1226
+ const h = this.blockHeights.get(key) ?? 0;
1227
+ const filler = Math.max(0, h - (lines.length - blkLines0));
1228
+ for (let fi = 0; fi < filler; fi++) { lines.push([{ t: "" }]); mark(realIdx, bi); }
1229
+ }
1213
1230
  sep();
1214
1231
  }
1215
1232
  }
@@ -2271,6 +2288,7 @@ export class App {
2271
2288
  this.chat.cache.clear();
2272
2289
  this.chat.nodes = [];
2273
2290
  this.chat.collapsedBlocks.clear();
2291
+ this.chat.blockHeights.clear();
2274
2292
  this.chat.expanded.clear();
2275
2293
  this.chat.expandedTools.clear();
2276
2294
  this.chat.queueRebuild();