dsh-neotui 0.0.19 → 0.0.20
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 +39 -20
package/package.json
CHANGED
package/src/views.js
CHANGED
|
@@ -590,6 +590,8 @@ export class ChatView extends Widget {
|
|
|
590
590
|
this.cardRanges = []; // absolute line ranges of card-backed message blocks
|
|
591
591
|
this.welcomeModes = []; // absolute row y → agent preset id (welcome screen)
|
|
592
592
|
this.pressY = null;
|
|
593
|
+
this.pressInfo = null; // hit identity locked at press time
|
|
594
|
+
this.pressCtx = null;
|
|
593
595
|
this.selStart = null;
|
|
594
596
|
this.selEnd = null;
|
|
595
597
|
this.stepState = { step: null }; // step/start tracking for the mux merge path
|
|
@@ -845,20 +847,13 @@ export class ChatView extends Widget {
|
|
|
845
847
|
return this.#toggleAt(this.lineMap?.[y]);
|
|
846
848
|
}
|
|
847
849
|
|
|
848
|
-
/**
|
|
849
|
-
*
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
if (ref) { this.app.openImage(ref, { all: node.images, index: info.imgIdx }); return true; }
|
|
856
|
-
}
|
|
857
|
-
const node = this.nodes[info.nodeIdx];
|
|
858
|
-
if (!node) return false;
|
|
859
|
-
// anchor context captured from the frame the user saw:
|
|
860
|
-
// - the clicked block's header line + its viewport row (primary anchor)
|
|
861
|
-
// - the viewport-top line's identity + offset (fallback for deep clicks)
|
|
850
|
+
/** Anchor context for a click: the clicked block's header line + its
|
|
851
|
+
* viewport row (primary anchor) and the viewport-top identity + offset
|
|
852
|
+
* (fallback for deep clicks). Captured at PRESS time so the stream cannot
|
|
853
|
+
* shift the hit identity between press and release (the 4-line mystery:
|
|
854
|
+
* the rendered frame and the hit test were consistent, but the identity
|
|
855
|
+
* was re-resolved at release against a stream that had moved). */
|
|
856
|
+
#anchorCtx(info) {
|
|
862
857
|
const lineKey = (m) => (m ? `${m.nodeIdx}:${m.blockIdx ?? "n"}` : null);
|
|
863
858
|
const topKey = lineKey(this.lineMap[this.view.scrollY]) ?? null;
|
|
864
859
|
let topFirst = -1;
|
|
@@ -868,23 +863,39 @@ export class ChatView extends Widget {
|
|
|
868
863
|
}
|
|
869
864
|
}
|
|
870
865
|
const topOffset = topFirst >= 0 ? this.view.scrollY - topFirst : 0;
|
|
871
|
-
const match = info
|
|
872
|
-
? (m) => m?.nodeIdx === info
|
|
873
|
-
: (m) => m?.nodeIdx === info
|
|
866
|
+
const match = info?.blockIdx !== null
|
|
867
|
+
? (m) => m?.nodeIdx === info?.nodeIdx && m?.blockIdx === info?.blockIdx
|
|
868
|
+
: (m) => m?.nodeIdx === info?.nodeIdx;
|
|
874
869
|
const firstNonEmpty = (m) => {
|
|
875
870
|
for (let i = 0; i < this.lineMap.length; i++) {
|
|
876
871
|
if (m(this.lineMap[i]) && (this.lines[i] ?? []).some((g) => g.t.trim() !== "")) return i;
|
|
877
872
|
}
|
|
878
873
|
return -1;
|
|
879
874
|
};
|
|
880
|
-
const preHeaderIdx = firstNonEmpty(match);
|
|
875
|
+
const preHeaderIdx = info ? firstNonEmpty(match) : -1;
|
|
881
876
|
const preHeaderRow = preHeaderIdx >= 0 ? preHeaderIdx - this.view.scrollY : null;
|
|
877
|
+
return { lineKey, topKey, topFirst, topOffset, match, firstNonEmpty, preHeaderIdx, preHeaderRow };
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
/** Toggle the block/node under a line-mark, then re-anchor the viewport.
|
|
881
|
+
* `info` must be the mark of the frame the user clicked on; `ctx` its
|
|
882
|
+
* press-time anchor context. */
|
|
883
|
+
#toggleAt(info, ctx = null) {
|
|
884
|
+
if (!info) return false;
|
|
885
|
+
if (info.imgIdx !== undefined) {
|
|
886
|
+
const node = this.nodes[info.nodeIdx];
|
|
887
|
+
const ref = node?.images?.[info.imgIdx];
|
|
888
|
+
if (ref) { this.app.openImage(ref, { all: node.images, index: info.imgIdx }); return true; }
|
|
889
|
+
}
|
|
890
|
+
const node = this.nodes[info.nodeIdx];
|
|
891
|
+
if (!node) return false;
|
|
892
|
+
const { lineKey, topKey, topFirst, topOffset, match, firstNonEmpty, preHeaderIdx, preHeaderRow } = ctx ?? this.#anchorCtx(info);
|
|
882
893
|
const reanchor = () => {
|
|
883
894
|
// primary anchor: the clicked block's header stays at its pre-click
|
|
884
895
|
// viewport row (zero shift) — only when the header was ON-SCREEN.
|
|
885
896
|
// When the fold removed the tail content below, the anchored position
|
|
886
897
|
// may exceed maxScroll: hold it via the view's anchorLock instead of
|
|
887
|
-
// clamping (the clamp is exactly the
|
|
898
|
+
// clamping (the clamp is exactly the slide the user sees).
|
|
888
899
|
if (preHeaderRow !== null && preHeaderRow >= 0 && preHeaderIdx >= 0) {
|
|
889
900
|
const h2 = firstNonEmpty(match);
|
|
890
901
|
if (h2 >= 0) {
|
|
@@ -1315,6 +1326,11 @@ export class ChatView extends Widget {
|
|
|
1315
1326
|
if (ev.kind === "wheel-up" && this.view.scrollY === 0 && this.hasMore) { this.loadOlder(); return true; }
|
|
1316
1327
|
if (ev.kind === "press" && ev.button === 0) {
|
|
1317
1328
|
this.pressY = ev.y - this.view.y + this.view.scrollY;
|
|
1329
|
+
// LOCK the hit identity at press time: the stream keeps growing
|
|
1330
|
+
// between press and release, so resolving the block at release would
|
|
1331
|
+
// toggle a block a few lines away from the one the user saw.
|
|
1332
|
+
this.pressInfo = this.lineMap?.[this.pressY] ?? null;
|
|
1333
|
+
this.pressCtx = this.pressInfo ? this.#anchorCtx(this.pressInfo) : null;
|
|
1318
1334
|
return true;
|
|
1319
1335
|
}
|
|
1320
1336
|
if (ev.kind === "drag" && ev.button === 0 && this.pressY !== null) {
|
|
@@ -1330,6 +1346,7 @@ export class ChatView extends Widget {
|
|
|
1330
1346
|
const wasPress = this.pressY;
|
|
1331
1347
|
this.pressY = null;
|
|
1332
1348
|
if (this.selStart !== null && this.selEnd !== null) {
|
|
1349
|
+
this.pressInfo = null; this.pressCtx = null;
|
|
1333
1350
|
const text = this.lines.slice(this.selStart, this.selEnd + 1).map((l) => l.map((g) => g.t).join("")).join("\n");
|
|
1334
1351
|
const rows = this.selEnd - this.selStart + 1;
|
|
1335
1352
|
this.selStart = this.selEnd = null;
|
|
@@ -1338,7 +1355,9 @@ export class ChatView extends Widget {
|
|
|
1338
1355
|
return true;
|
|
1339
1356
|
}
|
|
1340
1357
|
this.selStart = this.selEnd = null;
|
|
1341
|
-
this.#
|
|
1358
|
+
this.#toggleAt(this.pressInfo, this.pressCtx);
|
|
1359
|
+
this.pressInfo = null;
|
|
1360
|
+
this.pressCtx = null;
|
|
1342
1361
|
return true;
|
|
1343
1362
|
}
|
|
1344
1363
|
if (ev.kind === "press" && ev.button === 2) {
|