dsh-neotui 0.0.17 → 0.0.18

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-neotui",
3
- "version": "0.0.17",
3
+ "version": "0.0.18",
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
@@ -696,7 +696,7 @@ export class ChatView extends Widget {
696
696
  if (idx < 0 || idx >= this.nodes.length) return false;
697
697
  for (let li = 0; li < this.lineMap.length; li++) {
698
698
  if (this.lineMap[li]?.nodeIdx === idx) {
699
- this.view.scrollY = Math.max(0, li - 2);
699
+ this.view.anchorLock = null; this.view.scrollY = Math.max(0, li - 2);
700
700
  this.app.redraw();
701
701
  return true;
702
702
  }
@@ -770,7 +770,7 @@ export class ChatView extends Widget {
770
770
  this.#rebuild();
771
771
  // Jump to the LIVE tail (the newest content) on open — the view would
772
772
  // otherwise sit at the top showing the oldest turns.
773
- this.view.scrollY = this.view.maxScroll();
773
+ this.view.anchorLock = null; this.view.scrollY = this.view.maxScroll();
774
774
  }
775
775
 
776
776
  async loadOlder() {
@@ -880,11 +880,16 @@ export class ChatView extends Widget {
880
880
  const preHeaderRow = preHeaderIdx >= 0 ? preHeaderIdx - this.view.scrollY : null;
881
881
  const reanchor = () => {
882
882
  // primary anchor: the clicked block's header stays at its pre-click
883
- // viewport row (zero shift) — only when the header was ON-SCREEN
883
+ // viewport row (zero shift) — only when the header was ON-SCREEN.
884
+ // When the fold removed the tail content below, the anchored position
885
+ // may exceed maxScroll: hold it via the view's anchorLock instead of
886
+ // clamping (the clamp is exactly the 5–6 line slide the user sees).
884
887
  if (preHeaderRow !== null && preHeaderRow >= 0 && preHeaderIdx >= 0) {
885
888
  const h2 = firstNonEmpty(match);
886
889
  if (h2 >= 0) {
887
- this.view.scrollY = Math.max(0, Math.min(h2 - preHeaderRow, this.view.maxScroll()));
890
+ const sy = h2 - preHeaderRow;
891
+ this.view.scrollY = Math.max(0, sy);
892
+ if (sy > this.view.maxScroll()) this.view.anchorLock = sy;
888
893
  return;
889
894
  }
890
895
  }
@@ -898,7 +903,8 @@ export class ChatView extends Widget {
898
903
  }
899
904
  if (first < 0) return;
900
905
  const target = Math.min(first + topOffset, last);
901
- this.view.scrollY = Math.max(0, Math.min(target, this.view.maxScroll()));
906
+ this.view.scrollY = Math.max(0, target);
907
+ if (target > this.view.maxScroll()) this.view.anchorLock = target;
902
908
  };
903
909
  // never park the viewport on a blank separator row — it reads as a
904
910
  // spurious offset right after the click
@@ -1380,12 +1386,12 @@ export class ChatView extends Widget {
1380
1386
  case "pgdn": return this.view.scroll(this.view.h);
1381
1387
  }
1382
1388
  if (ev.name === "char" && ev.key === "g" && !ev.ctrl && !ev.shift) {
1383
- if (this.gKey) { this.gKey = false; this.view.scrollY = 0; return true; }
1389
+ if (this.gKey) { this.gKey = false; this.view.anchorLock = null; this.view.scrollY = 0; return true; }
1384
1390
  this.gKey = true;
1385
1391
  this.app.toast("再按 g 回顶");
1386
1392
  return true;
1387
1393
  }
1388
- if (ev.name === "char" && ev.key === "g" && ev.shift) { this.view.scrollY = this.view.maxScroll(); return true; }
1394
+ if (ev.name === "char" && ev.key === "g" && ev.shift) { this.view.anchorLock = null; this.view.scrollY = this.view.maxScroll(); return true; }
1389
1395
  if (ev.name === "escape" && this.app.searchQuery) { this.app.searchQuery = null; this.queueRebuild(); return true; }
1390
1396
  if (ev.name === "escape" && this.selStart !== null) { this.selStart = this.selEnd = null; this.app.redraw(); return true; }
1391
1397
  if (ev.name === "char" && ev.key === "i" && !ev.ctrl) { this.app.focus(this.input); return true; }
package/src/widgets.js CHANGED
@@ -27,6 +27,7 @@ export class ScrollView extends Widget {
27
27
  super(opts);
28
28
  this.lines = []; // array of arrays of segs: {t, fg, bg, bold, italic, underline, strike, code, link}
29
29
  this.scrollY = 0;
30
+ this.anchorLock = null; // click anchor held beyond maxScroll (fold at the tail)
30
31
  this.autoScroll = opts.autoScroll ?? false;
31
32
  this.onClick = opts.onClick ?? null; // (y, ev) => bool
32
33
  this.onWheel = null; // optional custom wheel handler
@@ -36,7 +37,12 @@ export class ScrollView extends Widget {
36
37
  setLines(lines, { keep = false } = {}) {
37
38
  const atBottom = this.autoScroll && this.scrollY + this.h >= this.lines.length - 1 || (keep && this.scrollY + this.h >= this.lines.length);
38
39
  this.lines = lines;
39
- if (atBottom || this.scrollY > Math.max(0, this.lines.length - this.h)) {
40
+ if (this.anchorLock != null) {
41
+ // A click fold removed the tail content: hold the exact anchored
42
+ // position even though the buffer now ends above it (no delayed snap).
43
+ this.scrollY = this.anchorLock;
44
+ if (this.scrollY <= Math.max(0, this.lines.length - this.h)) this.anchorLock = null;
45
+ } else if (atBottom || this.scrollY > Math.max(0, this.lines.length - this.h)) {
40
46
  this.scrollY = Math.max(0, this.lines.length - this.h);
41
47
  }
42
48
  }
@@ -44,6 +50,7 @@ export class ScrollView extends Widget {
44
50
  maxScroll() { return Math.max(0, this.lines.length - this.h); }
45
51
  scroll(dy) {
46
52
  const before = this.scrollY;
53
+ this.anchorLock = null; // explicit scroll releases the click anchor
47
54
  this.scrollY = Math.max(0, Math.min(this.maxScroll(), this.scrollY + dy));
48
55
  return this.scrollY !== before;
49
56
  }
@@ -78,7 +85,8 @@ export class ScrollView extends Widget {
78
85
  const trackH = Math.max(1, this.h - 2);
79
86
  const total = Math.max(1, this.lines.length);
80
87
  const thumbH = Math.max(1, Math.floor(this.h * this.h / total));
81
- const thumbY = Math.floor((this.h - 2) * this.scrollY / Math.max(1, this.maxScroll()));
88
+ const frac = Math.min(1, this.scrollY / Math.max(1, this.maxScroll()));
89
+ const thumbY = Math.floor((this.h - 2) * frac);
82
90
  for (let i = 0; i < this.h; i++) {
83
91
  const inThumb = i >= 1 + thumbY && i < 1 + thumbY + thumbH;
84
92
  const inTrack = i >= 1 && i < this.h - 1;
@@ -114,6 +122,7 @@ export class ScrollView extends Widget {
114
122
  return false;
115
123
  }
116
124
  #scrubTo(ey) {
125
+ this.anchorLock = null; // scrubbing releases the click anchor
117
126
  const trackH = Math.max(1, this.h - 2);
118
127
  const total = Math.max(1, this.lines.length);
119
128
  const thumbH = Math.max(1, Math.floor(this.h * this.h / total));