dsh-neotui 0.0.14 → 0.0.16
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 +66 -22
package/package.json
CHANGED
package/src/views.js
CHANGED
|
@@ -836,8 +836,35 @@ export class ChatView extends Widget {
|
|
|
836
836
|
}
|
|
837
837
|
|
|
838
838
|
#clickLine(y, ev) {
|
|
839
|
+
// Capture the viewport geometry BEFORE any flush: the flush can grow the
|
|
840
|
+
// streaming tail (and atBottom snaps the view 1–2 rows), so anchoring on
|
|
841
|
+
// the post-flush state would reproduce exactly the shift the user saw.
|
|
842
|
+
// The pre-flush lineMap is what the user clicked on.
|
|
843
|
+
const lineKey = (m) => (m ? `${m.nodeIdx}:${m.blockIdx ?? "n"}` : null);
|
|
844
|
+
const topKey = lineKey(this.lineMap?.[this.view.scrollY]) ?? null;
|
|
845
|
+
let topFirst = -1;
|
|
846
|
+
if (topKey !== null) {
|
|
847
|
+
for (let i = 0; i < this.lineMap.length; i++) {
|
|
848
|
+
if (lineKey(this.lineMap[i]) === topKey) { topFirst = i; break; }
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
const topOffset = topFirst >= 0 ? this.view.scrollY - topFirst : 0;
|
|
852
|
+
// the clicked block's header (first non-empty line with its mark) and its
|
|
853
|
+
// pre-click viewport row — the PRIMARY anchor: keep the clicked thing put
|
|
854
|
+
const preInfo = this.lineMap?.[y];
|
|
855
|
+
const clickedMatch = preInfo?.blockIdx !== null
|
|
856
|
+
? (m) => m?.nodeIdx === preInfo?.nodeIdx && m?.blockIdx === preInfo?.blockIdx
|
|
857
|
+
: (m) => m?.nodeIdx === preInfo?.nodeIdx;
|
|
858
|
+
const firstNonEmpty = (match) => {
|
|
859
|
+
for (let i = 0; i < this.lineMap.length; i++) {
|
|
860
|
+
if (match(this.lineMap[i]) && (this.lines[i] ?? []).some((g) => g.t.trim() !== "")) return i;
|
|
861
|
+
}
|
|
862
|
+
return -1;
|
|
863
|
+
};
|
|
864
|
+
const preHeaderIdx = preInfo ? firstNonEmpty(clickedMatch) : -1;
|
|
865
|
+
const preHeaderRow = preHeaderIdx >= 0 ? preHeaderIdx - this.view.scrollY : null;
|
|
839
866
|
// A queued (deferred) rebuild would leave lineMap one frame behind the
|
|
840
|
-
// nodes array; flush
|
|
867
|
+
// nodes array; flush now so the click maps to the CURRENT node/block.
|
|
841
868
|
this.flushRebuild();
|
|
842
869
|
// map rendered line back to node via cached line→node map
|
|
843
870
|
const info = this.lineMap?.[y];
|
|
@@ -848,22 +875,20 @@ export class ChatView extends Widget {
|
|
|
848
875
|
if (ref) { this.app.openImage(ref, { all: node.images, index: info.imgIdx }); return true; }
|
|
849
876
|
}
|
|
850
877
|
const node = this.nodes[info.nodeIdx];
|
|
851
|
-
// Expand/collapse changes the line count
|
|
852
|
-
//
|
|
853
|
-
//
|
|
854
|
-
//
|
|
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) {
|
|
861
|
-
for (let i = 0; i < this.lineMap.length; i++) {
|
|
862
|
-
if (lineKey(this.lineMap[i]) === topKey) { topFirst = i; break; }
|
|
863
|
-
}
|
|
864
|
-
}
|
|
865
|
-
const topOffset = topFirst >= 0 ? this.view.scrollY - topFirst : 0;
|
|
878
|
+
// Expand/collapse changes the line count around the click. Primary
|
|
879
|
+
// anchor: the clicked block's header stays at its pre-click viewport
|
|
880
|
+
// row (zero shift). Fallback (header was above the viewport — a deep
|
|
881
|
+
// content click): restore the pre-click viewport-top position.
|
|
866
882
|
const reanchor = () => {
|
|
883
|
+
// primary anchor applies only when the header was ON-SCREEN (a deep
|
|
884
|
+
// content click has the header above the viewport — use the fallback)
|
|
885
|
+
if (preHeaderRow !== null && preHeaderRow >= 0 && preHeaderIdx >= 0) {
|
|
886
|
+
const h2 = firstNonEmpty((m) => m?.nodeIdx === info.nodeIdx && m?.blockIdx === info.blockIdx);
|
|
887
|
+
if (h2 >= 0) {
|
|
888
|
+
this.view.scrollY = Math.max(0, Math.min(h2 - preHeaderRow, this.view.maxScroll()));
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
867
892
|
if (topKey === null || topFirst < 0) return;
|
|
868
893
|
let first = -1, last = -1;
|
|
869
894
|
for (let i = 0; i < this.lineMap.length; i++) {
|
|
@@ -875,6 +900,14 @@ export class ChatView extends Widget {
|
|
|
875
900
|
const target = Math.min(first + topOffset, last);
|
|
876
901
|
this.view.scrollY = Math.max(0, Math.min(target, this.view.maxScroll()));
|
|
877
902
|
};
|
|
903
|
+
// never park the viewport on a blank separator row — it reads as a
|
|
904
|
+
// spurious offset right after the click
|
|
905
|
+
const nudge = () => {
|
|
906
|
+
while (
|
|
907
|
+
this.view.scrollY < this.lineMap.length - 1 &&
|
|
908
|
+
!(this.lines[this.view.scrollY] ?? []).some((g) => g.t.trim() !== "")
|
|
909
|
+
) this.view.scrollY++;
|
|
910
|
+
};
|
|
878
911
|
if (node?.kind === "assistant" && info.blockIdx !== null) {
|
|
879
912
|
const b = node.blocks[info.blockIdx];
|
|
880
913
|
if (b && (b.kind === "tool" || b.kind === "reasoning" || b.kind === "other" || b.kind === "text")) {
|
|
@@ -889,7 +922,7 @@ export class ChatView extends Widget {
|
|
|
889
922
|
else this.collapsedBlocks.add(key);
|
|
890
923
|
}
|
|
891
924
|
this.#rebuild();
|
|
892
|
-
reanchor();
|
|
925
|
+
reanchor(); nudge();
|
|
893
926
|
return true;
|
|
894
927
|
}
|
|
895
928
|
}
|
|
@@ -897,7 +930,7 @@ export class ChatView extends Widget {
|
|
|
897
930
|
if (this.expanded.has(info.nodeIdx)) this.expanded.delete(info.nodeIdx);
|
|
898
931
|
else this.expanded.add(info.nodeIdx);
|
|
899
932
|
this.#rebuild();
|
|
900
|
-
reanchor();
|
|
933
|
+
reanchor(); nudge();
|
|
901
934
|
return true;
|
|
902
935
|
}
|
|
903
936
|
}
|
|
@@ -1016,12 +1049,16 @@ export class ChatView extends Widget {
|
|
|
1016
1049
|
const open = manuallyExpanded || (!manuallyCollapsed && this.thinkMode === "expanded");
|
|
1017
1050
|
beginCard("THINKBG");
|
|
1018
1051
|
// Live blocks tick (已经过…); finished blocks freeze at their
|
|
1019
|
-
// total (已完成,耗时…).
|
|
1052
|
+
// total (已完成,耗时…). Snapshot-derived blocks may lack a
|
|
1053
|
+
// per-block start (the history projection has no block-start):
|
|
1054
|
+
// show a plain 已完成 then — never a bogus ticking timer.
|
|
1020
1055
|
let timing = "";
|
|
1021
1056
|
if (b.streaming) {
|
|
1022
1057
|
timing = ` 已经过 ${fmtDuration(Date.now() - (b.startedAt ?? Date.now()))}`;
|
|
1023
1058
|
} else if (b.startedAt !== undefined && b.endedAt !== undefined) {
|
|
1024
1059
|
timing = ` 已完成,耗时 ${fmtDuration(b.endedAt - b.startedAt)}`;
|
|
1060
|
+
} else if (b.endedAt !== undefined) {
|
|
1061
|
+
timing = " 已完成";
|
|
1025
1062
|
}
|
|
1026
1063
|
const thinkMeta = `${stepTag}(${b.text?.length ?? 0} 字)${timing}`;
|
|
1027
1064
|
lines.push([{ t: "💭 思考" + (b.streaming ? "…" : "") + thinkMeta + (open ? " [t 折叠]" : " [t 展开]"), fg: K.FAINT }]);
|
|
@@ -1041,16 +1078,23 @@ export class ChatView extends Widget {
|
|
|
1041
1078
|
const key = `${realIdx}:${bi}`;
|
|
1042
1079
|
const open = this.bashMode !== "collapsed" && !this.collapsedBlocks.has(key);
|
|
1043
1080
|
const exitCode = b.view?.view?.exitCode;
|
|
1044
|
-
|
|
1045
|
-
|
|
1081
|
+
// A tool only TICKS while its turn is live. A finalized turn
|
|
1082
|
+
// whose result never matched (orphan) must freeze at 无结果 —
|
|
1083
|
+
// otherwise the timer runs forever ("timing chaos").
|
|
1084
|
+
const running = b.result == null && !b.done && node.streaming;
|
|
1085
|
+
const orphan = b.result == null && !b.done && !node.streaming;
|
|
1086
|
+
const status = orphan ? "TOOLERR" : running ? "TOOLBG" : exitCode !== undefined && exitCode !== 0 ? "TOOLERR" : "TOOLOK";
|
|
1087
|
+
const glyph = orphan ? "✗" : running ? "⏳" : exitCode !== undefined && exitCode !== 0 ? "✗" : "✓";
|
|
1046
1088
|
const card = b.view ? renderToolCard(b.view, w, open) : [];
|
|
1047
1089
|
beginCard(status);
|
|
1048
1090
|
let timing = "";
|
|
1049
|
-
if (
|
|
1091
|
+
if (running) {
|
|
1050
1092
|
timing = ` 已经过 ${fmtDuration(Date.now() - (b.startedAt ?? Date.now()))}`;
|
|
1051
1093
|
} else if (b.startedAt !== undefined && b.endedAt !== undefined) {
|
|
1052
1094
|
const failed = exitCode !== undefined && exitCode !== 0;
|
|
1053
1095
|
timing = ` ${failed ? "失败" : "已完成"},耗时 ${fmtDuration(b.endedAt - b.startedAt)}`;
|
|
1096
|
+
} else if (orphan) {
|
|
1097
|
+
timing = " 无结果";
|
|
1054
1098
|
}
|
|
1055
1099
|
lines.push([
|
|
1056
1100
|
{ t: open ? "▾ " : "▸ ", fg: K.ACCENT },
|