dsh-neotui 0.0.13 → 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 -16
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-neotui",
3
- "version": "0.0.13",
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,22 +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. This applies ALWAYS even at the
855
- // bottom: the tail-follow snap would otherwise fling the clicked header
856
- // out of view when a folded block re-expands.
857
- const reanchor = (match) => {
858
- 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) {
859
861
  for (let i = 0; i < this.lineMap.length; i++) {
860
- if (!match(this.lineMap[i])) continue;
861
- if ((this.lines[i] ?? []).some((g) => g.t.trim() !== "")) {
862
- this.view.scrollY = Math.max(0, Math.min(i, this.view.maxScroll()));
863
- return;
864
- }
865
- 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;
866
873
  }
867
- 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()));
868
877
  };
869
878
  if (node?.kind === "assistant" && info.blockIdx !== null) {
870
879
  const b = node.blocks[info.blockIdx];
@@ -880,7 +889,7 @@ export class ChatView extends Widget {
880
889
  else this.collapsedBlocks.add(key);
881
890
  }
882
891
  this.#rebuild();
883
- reanchor((m) => m?.nodeIdx === info.nodeIdx && m?.blockIdx === info.blockIdx);
892
+ reanchor();
884
893
  return true;
885
894
  }
886
895
  }
@@ -888,7 +897,7 @@ export class ChatView extends Widget {
888
897
  if (this.expanded.has(info.nodeIdx)) this.expanded.delete(info.nodeIdx);
889
898
  else this.expanded.add(info.nodeIdx);
890
899
  this.#rebuild();
891
- reanchor((m) => m?.nodeIdx === info.nodeIdx);
900
+ reanchor();
892
901
  return true;
893
902
  }
894
903
  }