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.
- package/package.json +1 -1
- package/src/views.js +44 -6
package/package.json
CHANGED
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
|
-
|
|
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
|
|
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
|
}
|