dsh-neotui 0.0.21 → 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 +44 -6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-neotui",
3
- "version": "0.0.21",
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
@@ -2,7 +2,8 @@
2
2
  import { Screen } from "./screen.js";
3
3
  import { renderMd, C } from "./md.js";
4
4
  import { truncate, strWidth, bars, fmtDuration } from "./text.js";
5
- import { readFileSync } from "node:fs";
5
+ import { readFileSync, appendFileSync } from "node:fs";
6
+ import { join } from "node:path";
6
7
  import { Widget, List, ScrollView, Input, Popup, Menu, StatusBar } from "./widgets.js";
7
8
  import { userPrefix, saveTuiConfig, loadTuiConfig, userName } from "./config.js";
8
9
  export { userPrefix, saveTuiConfig, loadTuiConfig, userName } from "./config.js";
@@ -839,6 +840,15 @@ export class ChatView extends Widget {
839
840
  }).catch((e) => this.app.toast(`发送失败: ${e.message}`));
840
841
  }
841
842
 
843
+ /** File-based click diagnostics (DSH_TUI_DEBUG_CLICK=1): stderr lines are
844
+ * painted over by the next frame, so the trace goes to a log file. */
845
+ #clickLog(msg) {
846
+ try {
847
+ const dir = process.env.DSH_HOME ?? join(process.env.HOME ?? ".", ".dsh");
848
+ appendFileSync(join(dir, "tui-click-debug.log"), `${new Date().toISOString()} ${msg}\n`);
849
+ } catch {}
850
+ }
851
+
842
852
  #clickLine(y, ev) {
843
853
  // NO flush here: the click maps against the exact frame the user saw.
844
854
  // Flushing would re-derive the streaming tail (the chunk-stream and the
@@ -853,7 +863,7 @@ export class ChatView extends Widget {
853
863
  * shift the hit identity between press and release (the 4-line mystery:
854
864
  * the rendered frame and the hit test were consistent, but the identity
855
865
  * was re-resolved at release against a stream that had moved). */
856
- #anchorCtx(info) {
866
+ #anchorCtx(info, pressY = null) {
857
867
  const lineKey = (m) => (m ? `${m.nodeIdx}:${m.blockIdx ?? "n"}` : null);
858
868
  const topKey = lineKey(this.lineMap[this.view.scrollY]) ?? null;
859
869
  let topFirst = -1;
@@ -874,7 +884,8 @@ export class ChatView extends Widget {
874
884
  };
875
885
  const preHeaderIdx = info ? firstNonEmpty(match) : -1;
876
886
  const preHeaderRow = preHeaderIdx >= 0 ? preHeaderIdx - this.view.scrollY : null;
877
- 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 };
878
889
  }
879
890
 
880
891
  /** Toggle the block/node under a line-mark, then re-anchor the viewport.
@@ -889,8 +900,29 @@ export class ChatView extends Widget {
889
900
  }
890
901
  const node = this.nodes[info.nodeIdx];
891
902
  if (!node) return false;
892
- 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;
905
+ if (process.env.DSH_TUI_DEBUG_CLICK) {
906
+ this.#clickLog(`toggle mark=${JSON.stringify(info)} kind=${node.kind} blockIdx=${info.blockIdx} preHeaderRow=${preHeaderRow} topKey=${topKey} topOffset=${topOffset}`);
907
+ }
893
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
+ }
894
926
  // primary anchor: the clicked block's header stays at its pre-click
895
927
  // viewport row (zero shift) — only when the header was ON-SCREEN.
896
928
  // When the fold removed the tail content below, the anchored position
@@ -933,14 +965,20 @@ export class ChatView extends Widget {
933
965
  if (b.kind === "reasoning") {
934
966
  // clean two-state override: expand ⇄ collapse (never a no-op click)
935
967
  const open = this.expanded.has(key) || (!this.collapsedBlocks.has(key) && this.thinkMode === "expanded");
968
+ collapsing = open;
936
969
  if (open) { this.expanded.delete(key); this.collapsedBlocks.add(key); }
937
970
  else { this.collapsedBlocks.delete(key); this.expanded.add(key); }
938
971
  } else {
972
+ collapsing = !this.collapsedBlocks.has(key);
939
973
  if (this.collapsedBlocks.has(key)) this.collapsedBlocks.delete(key);
940
974
  else this.collapsedBlocks.add(key);
941
975
  }
942
976
  this.#rebuild();
943
977
  reanchor(); nudge();
978
+ if (process.env.DSH_TUI_DEBUG_CLICK) {
979
+ const t = this.lines[this.view.scrollY]?.map((g) => g.t).join("") ?? "";
980
+ this.#clickLog(`after scrollY=${this.view.scrollY} topKey="${topKey}" topText="${t.slice(0, 40)}"`);
981
+ }
944
982
  return true;
945
983
  }
946
984
  }
@@ -1330,10 +1368,10 @@ export class ChatView extends Widget {
1330
1368
  // between press and release, so resolving the block at release would
1331
1369
  // toggle a block a few lines away from the one the user saw.
1332
1370
  this.pressInfo = this.lineMap?.[this.pressY] ?? null;
1333
- this.pressCtx = this.pressInfo ? this.#anchorCtx(this.pressInfo) : null;
1371
+ this.pressCtx = this.pressInfo ? this.#anchorCtx(this.pressInfo, this.pressY) : null;
1334
1372
  if (process.env.DSH_TUI_DEBUG_CLICK) {
1335
1373
  const t = this.lines[this.pressY]?.map((g) => g.t).join("") ?? "";
1336
- this.app.log(`[click-dbg] press screenY=${ev.y} lineIdx=${this.pressY} mark=${JSON.stringify(this.pressInfo)} text="${t.slice(0, 40)}" scrollY=${this.view.scrollY}`);
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}`);
1337
1375
  }
1338
1376
  return true;
1339
1377
  }