dsh-neotui 0.0.22 → 0.0.23

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 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-neotui",
3
- "version": "0.0.22",
3
+ "version": "0.0.23",
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
@@ -863,7 +863,7 @@ export class ChatView extends Widget {
863
863
  * shift the hit identity between press and release (the 4-line mystery:
864
864
  * the rendered frame and the hit test were consistent, but the identity
865
865
  * was re-resolved at release against a stream that had moved). */
866
- #anchorCtx(info) {
866
+ #anchorCtx(info, pressY = null) {
867
867
  const lineKey = (m) => (m ? `${m.nodeIdx}:${m.blockIdx ?? "n"}` : null);
868
868
  const topKey = lineKey(this.lineMap[this.view.scrollY]) ?? null;
869
869
  let topFirst = -1;
@@ -884,7 +884,8 @@ export class ChatView extends Widget {
884
884
  };
885
885
  const preHeaderIdx = info ? firstNonEmpty(match) : -1;
886
886
  const preHeaderRow = preHeaderIdx >= 0 ? preHeaderIdx - this.view.scrollY : null;
887
- return { lineKey, topKey, topFirst, topOffset, match, firstNonEmpty, preHeaderIdx, preHeaderRow };
887
+ const pressRow = pressY !== null && pressY !== undefined && pressY >= 0 ? pressY - this.view.scrollY : null;
888
+ return { lineKey, topKey, topFirst, topOffset, match, firstNonEmpty, preHeaderIdx, preHeaderRow, pressY, pressRow };
888
889
  }
889
890
 
890
891
  /** Toggle the block/node under a line-mark, then re-anchor the viewport.
@@ -899,11 +900,29 @@ export class ChatView extends Widget {
899
900
  }
900
901
  const node = this.nodes[info.nodeIdx];
901
902
  if (!node) return false;
902
- const { lineKey, topKey, topFirst, topOffset, match, firstNonEmpty, preHeaderIdx, preHeaderRow } = ctx ?? this.#anchorCtx(info);
903
+ const { lineKey, topKey, topFirst, topOffset, match, firstNonEmpty, preHeaderIdx, preHeaderRow, pressY, pressRow } = ctx ?? this.#anchorCtx(info);
904
+ let collapsing = false;
903
905
  if (process.env.DSH_TUI_DEBUG_CLICK) {
904
906
  this.#clickLog(`toggle mark=${JSON.stringify(info)} kind=${node.kind} blockIdx=${info.blockIdx} preHeaderRow=${preHeaderRow} topKey=${topKey} topOffset=${topOffset}`);
905
907
  }
906
908
  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
+ }
907
926
  // primary anchor: the clicked block's header stays at its pre-click
908
927
  // viewport row (zero shift) — only when the header was ON-SCREEN.
909
928
  // When the fold removed the tail content below, the anchored position
@@ -946,9 +965,11 @@ export class ChatView extends Widget {
946
965
  if (b.kind === "reasoning") {
947
966
  // clean two-state override: expand ⇄ collapse (never a no-op click)
948
967
  const open = this.expanded.has(key) || (!this.collapsedBlocks.has(key) && this.thinkMode === "expanded");
968
+ collapsing = open;
949
969
  if (open) { this.expanded.delete(key); this.collapsedBlocks.add(key); }
950
970
  else { this.collapsedBlocks.delete(key); this.expanded.add(key); }
951
971
  } else {
972
+ collapsing = !this.collapsedBlocks.has(key);
952
973
  if (this.collapsedBlocks.has(key)) this.collapsedBlocks.delete(key);
953
974
  else this.collapsedBlocks.add(key);
954
975
  }
@@ -1347,7 +1368,7 @@ export class ChatView extends Widget {
1347
1368
  // between press and release, so resolving the block at release would
1348
1369
  // toggle a block a few lines away from the one the user saw.
1349
1370
  this.pressInfo = this.lineMap?.[this.pressY] ?? null;
1350
- this.pressCtx = this.pressInfo ? this.#anchorCtx(this.pressInfo) : null;
1371
+ this.pressCtx = this.pressInfo ? this.#anchorCtx(this.pressInfo, this.pressY) : null;
1351
1372
  if (process.env.DSH_TUI_DEBUG_CLICK) {
1352
1373
  const t = this.lines[this.pressY]?.map((g) => g.t).join("") ?? "";
1353
1374
  this.#clickLog(`press screenY=${ev.y} screenX=${ev.x} lineIdx=${this.pressY} mark=${JSON.stringify(this.pressInfo)} text="${t.slice(0, 40)}" scrollY=${this.view.scrollY} viewY=${this.view.y} viewH=${this.view.h}`);