dsh-neotui 0.0.12 → 0.0.14

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 +25 -17
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-neotui",
3
- "version": "0.0.12",
3
+ "version": "0.0.14",
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
@@ -849,23 +849,31 @@ export class ChatView extends Widget {
849
849
  }
850
850
  const node = this.nodes[info.nodeIdx];
851
851
  // Expand/collapse changes the line count ABOVE the viewport, which would
852
- // leave scrollY pointing at unrelated content (the "chaotic" jump).
853
- // Anchor: after the rebuild, scroll so the clicked block's first line
854
- // sits at the top of the viewport. At the bottom the tail-follow snap
855
- // in setLines is the right behavior instead.
856
- const wasAtBottom = this.view.scrollY + this.view.h >= this.view.lines.length - 1;
857
- const reanchor = (match) => {
858
- if (wasAtBottom) return;
859
- let fallback = -1;
852
+ // leave scrollY pointing at unrelated content. Anchor: remember the
853
+ // viewport-top line's identity AND its offset within that block, then
854
+ // restore the same position after the rebuild the view does not move
855
+ // at all when the top line survives (e.g. clicking a block header), and
856
+ // lands on the fold's last line when the top line itself was collapsed.
857
+ const lineKey = (m) => (m ? `${m.nodeIdx}:${m.blockIdx ?? "n"}` : null);
858
+ const topKey = lineKey(this.lineMap[this.view.scrollY]);
859
+ let topFirst = -1;
860
+ if (topKey !== null) {
860
861
  for (let i = 0; i < this.lineMap.length; i++) {
861
- if (!match(this.lineMap[i])) continue;
862
- if ((this.lines[i] ?? []).some((g) => g.t.trim() !== "")) {
863
- this.view.scrollY = Math.max(0, Math.min(i, this.view.maxScroll()));
864
- return;
865
- }
866
- if (fallback < 0) fallback = i;
862
+ if (lineKey(this.lineMap[i]) === topKey) { topFirst = i; break; }
863
+ }
864
+ }
865
+ const topOffset = topFirst >= 0 ? this.view.scrollY - topFirst : 0;
866
+ const reanchor = () => {
867
+ if (topKey === null || topFirst < 0) return;
868
+ let first = -1, last = -1;
869
+ for (let i = 0; i < this.lineMap.length; i++) {
870
+ if (lineKey(this.lineMap[i]) !== topKey) continue;
871
+ if (first < 0) first = i;
872
+ last = i;
867
873
  }
868
- if (fallback >= 0) this.view.scrollY = Math.max(0, Math.min(fallback, this.view.maxScroll()));
874
+ if (first < 0) return;
875
+ const target = Math.min(first + topOffset, last);
876
+ this.view.scrollY = Math.max(0, Math.min(target, this.view.maxScroll()));
869
877
  };
870
878
  if (node?.kind === "assistant" && info.blockIdx !== null) {
871
879
  const b = node.blocks[info.blockIdx];
@@ -881,7 +889,7 @@ export class ChatView extends Widget {
881
889
  else this.collapsedBlocks.add(key);
882
890
  }
883
891
  this.#rebuild();
884
- reanchor((m) => m?.nodeIdx === info.nodeIdx && m?.blockIdx === info.blockIdx);
892
+ reanchor();
885
893
  return true;
886
894
  }
887
895
  }
@@ -889,7 +897,7 @@ export class ChatView extends Widget {
889
897
  if (this.expanded.has(info.nodeIdx)) this.expanded.delete(info.nodeIdx);
890
898
  else this.expanded.add(info.nodeIdx);
891
899
  this.#rebuild();
892
- reanchor((m) => m?.nodeIdx === info.nodeIdx);
900
+ reanchor();
893
901
  return true;
894
902
  }
895
903
  }