dsh-neotui 0.0.23 → 0.0.24
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 +40 -22
package/package.json
CHANGED
package/src/views.js
CHANGED
|
@@ -565,6 +565,8 @@ export class ChatView extends Widget {
|
|
|
565
565
|
this.expanded = new Set(); // node indexes (user-message full text)
|
|
566
566
|
this.expandedTools = new Set();
|
|
567
567
|
this.collapsedBlocks = new Set(); // per-block COLLAPSE (default expanded): `${realIdx}:${bi}`
|
|
568
|
+
this.blockHeights = new Map(); // collapsed block → its rendered height when collapsed
|
|
569
|
+
// (fixed-height fold: the space below stays put)
|
|
568
570
|
this.thinkMode = "expanded"; // think blocks: expanded by default (t toggles)
|
|
569
571
|
this.bashMode = "expanded"; // tool blocks: expanded | collapsed (b toggles)
|
|
570
572
|
this.todosVisible = true; // todo block above the input (Shift+T toggles)
|
|
@@ -906,28 +908,11 @@ export class ChatView extends Widget {
|
|
|
906
908
|
this.#clickLog(`toggle mark=${JSON.stringify(info)} kind=${node.kind} blockIdx=${info.blockIdx} preHeaderRow=${preHeaderRow} topKey=${topKey} topOffset=${topOffset}`);
|
|
907
909
|
}
|
|
908
910
|
const reanchor = () => {
|
|
909
|
-
//
|
|
910
|
-
//
|
|
911
|
-
//
|
|
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
|
-
}
|
|
911
|
+
// Fixed-height folds keep the buffer size UNCHANGED (the collapsed
|
|
912
|
+
// block pads its recorded height with ghost rows), so nothing below
|
|
913
|
+
// the fold can move — the anchor is only a safety net now.
|
|
926
914
|
// primary anchor: the clicked block's header stays at its pre-click
|
|
927
915
|
// viewport row (zero shift) — only when the header was ON-SCREEN.
|
|
928
|
-
// When the fold removed the tail content below, the anchored position
|
|
929
|
-
// may exceed maxScroll: hold it via the view's anchorLock instead of
|
|
930
|
-
// clamping (the clamp is exactly the slide the user sees).
|
|
931
916
|
if (preHeaderRow !== null && preHeaderRow >= 0 && preHeaderIdx >= 0) {
|
|
932
917
|
const h2 = firstNonEmpty(match);
|
|
933
918
|
if (h2 >= 0) {
|
|
@@ -951,17 +936,25 @@ export class ChatView extends Widget {
|
|
|
951
936
|
if (target > this.view.maxScroll()) this.view.anchorLock = target;
|
|
952
937
|
};
|
|
953
938
|
// never park the viewport on a blank separator row — it reads as a
|
|
954
|
-
// spurious offset right after the click
|
|
939
|
+
// spurious offset right after the click. Ghost rows of a fixed-height
|
|
940
|
+
// fold are INTENTIONAL blanks (marked with a blockIdx) — leave them.
|
|
955
941
|
const nudge = () => {
|
|
956
942
|
while (
|
|
957
943
|
this.view.scrollY < this.lineMap.length - 1 &&
|
|
958
|
-
!(this.lines[this.view.scrollY] ?? []).some((g) => g.t.trim() !== "")
|
|
944
|
+
!(this.lines[this.view.scrollY] ?? []).some((g) => g.t.trim() !== "") &&
|
|
945
|
+
(this.lineMap[this.view.scrollY]?.blockIdx ?? null) === null
|
|
959
946
|
) this.view.scrollY++;
|
|
960
947
|
};
|
|
961
948
|
if (node.kind === "assistant" && info.blockIdx !== null) {
|
|
962
949
|
const b = node.blocks[info.blockIdx];
|
|
963
950
|
if (b && (b.kind === "tool" || b.kind === "reasoning" || b.kind === "other" || b.kind === "text")) {
|
|
964
951
|
const key = `${info.nodeIdx}:${info.blockIdx}`;
|
|
952
|
+
// fixed-height fold: remember how tall the block is right now so the
|
|
953
|
+
// collapsed form can pad the SAME height (nothing below moves)
|
|
954
|
+
let firstM = -1, lastM = -1;
|
|
955
|
+
for (let i = 0; i < this.lineMap.length; i++) {
|
|
956
|
+
if (match(this.lineMap[i])) { if (firstM < 0) firstM = i; lastM = i; }
|
|
957
|
+
}
|
|
965
958
|
if (b.kind === "reasoning") {
|
|
966
959
|
// clean two-state override: expand ⇄ collapse (never a no-op click)
|
|
967
960
|
const open = this.expanded.has(key) || (!this.collapsedBlocks.has(key) && this.thinkMode === "expanded");
|
|
@@ -973,6 +966,8 @@ export class ChatView extends Widget {
|
|
|
973
966
|
if (this.collapsedBlocks.has(key)) this.collapsedBlocks.delete(key);
|
|
974
967
|
else this.collapsedBlocks.add(key);
|
|
975
968
|
}
|
|
969
|
+
if (collapsing && firstM >= 0) this.blockHeights.set(key, lastM - firstM + 1);
|
|
970
|
+
else if (!collapsing) this.blockHeights.delete(key);
|
|
976
971
|
this.#rebuild();
|
|
977
972
|
reanchor(); nudge();
|
|
978
973
|
if (process.env.DSH_TUI_DEBUG_CLICK) {
|
|
@@ -1116,6 +1111,7 @@ export class ChatView extends Widget {
|
|
|
1116
1111
|
timing = " 已完成";
|
|
1117
1112
|
}
|
|
1118
1113
|
const thinkMeta = `${stepTag}(${b.text?.length ?? 0} 字)${timing}`;
|
|
1114
|
+
const blkLines0 = lines.length;
|
|
1119
1115
|
lines.push([{ t: "💭 思考" + (b.streaming ? "…" : "") + thinkMeta + (open ? " [t 折叠]" : " [t 展开]"), fg: K.FAINT }]);
|
|
1120
1116
|
mark(realIdx, bi);
|
|
1121
1117
|
if (open) {
|
|
@@ -1128,6 +1124,13 @@ export class ChatView extends Widget {
|
|
|
1128
1124
|
mark(realIdx, bi);
|
|
1129
1125
|
}
|
|
1130
1126
|
}
|
|
1127
|
+
// fixed-height fold: pad the recorded height with ghost rows so
|
|
1128
|
+
// nothing below the block moves
|
|
1129
|
+
if (!open) {
|
|
1130
|
+
const h = this.blockHeights.get(key) ?? 0;
|
|
1131
|
+
const filler = Math.max(0, h - (lines.length - blkLines0));
|
|
1132
|
+
for (let fi = 0; fi < filler; fi++) { lines.push([{ t: "" }]); mark(realIdx, bi); }
|
|
1133
|
+
}
|
|
1131
1134
|
sep();
|
|
1132
1135
|
} else if (b.kind === "tool") {
|
|
1133
1136
|
const key = `${realIdx}:${bi}`;
|
|
@@ -1142,6 +1145,7 @@ export class ChatView extends Widget {
|
|
|
1142
1145
|
const glyph = orphan ? "✗" : running ? "⏳" : exitCode !== undefined && exitCode !== 0 ? "✗" : "✓";
|
|
1143
1146
|
const card = b.view ? renderToolCard(b.view, w, open) : [];
|
|
1144
1147
|
beginCard(status);
|
|
1148
|
+
const blkLines0 = lines.length;
|
|
1145
1149
|
let timing = "";
|
|
1146
1150
|
if (running) {
|
|
1147
1151
|
timing = ` 已经过 ${fmtDuration(Date.now() - (b.startedAt ?? Date.now()))}`;
|
|
@@ -1165,6 +1169,11 @@ export class ChatView extends Widget {
|
|
|
1165
1169
|
lines.push([{ t: " " + truncate(summary, w - 6), fg: K.FAINT }]);
|
|
1166
1170
|
mark(realIdx, bi);
|
|
1167
1171
|
}
|
|
1172
|
+
// fixed-height fold: pad the recorded height with ghost rows so
|
|
1173
|
+
// nothing below the block moves
|
|
1174
|
+
const h = this.blockHeights.get(key) ?? 0;
|
|
1175
|
+
const filler = Math.max(0, h - (lines.length - blkLines0));
|
|
1176
|
+
for (let fi = 0; fi < filler; fi++) { lines.push([{ t: "" }]); mark(realIdx, bi); }
|
|
1168
1177
|
}
|
|
1169
1178
|
if (open) {
|
|
1170
1179
|
for (const ln of card) { lines.push(ln); mark(realIdx, bi); }
|
|
@@ -1187,6 +1196,7 @@ export class ChatView extends Widget {
|
|
|
1187
1196
|
sep();
|
|
1188
1197
|
} else {
|
|
1189
1198
|
beginCard("CARD");
|
|
1199
|
+
const blkLines0 = lines.length;
|
|
1190
1200
|
// Text blocks collapse per-block like tool/reasoning blocks:
|
|
1191
1201
|
// collapsed shows a 3-line preview + trailer, expanded the whole
|
|
1192
1202
|
// text. Clicking (or the right-click 展开/折叠) toggles it.
|
|
@@ -1210,6 +1220,13 @@ export class ChatView extends Widget {
|
|
|
1210
1220
|
lines.push([{ t: ` …共 ${text.length} 字(点击展开)`, fg: K.FAINT }]);
|
|
1211
1221
|
mark(realIdx, bi);
|
|
1212
1222
|
}
|
|
1223
|
+
// fixed-height fold: pad the recorded height with ghost rows so
|
|
1224
|
+
// nothing below the block moves
|
|
1225
|
+
if (!open) {
|
|
1226
|
+
const h = this.blockHeights.get(key) ?? 0;
|
|
1227
|
+
const filler = Math.max(0, h - (lines.length - blkLines0));
|
|
1228
|
+
for (let fi = 0; fi < filler; fi++) { lines.push([{ t: "" }]); mark(realIdx, bi); }
|
|
1229
|
+
}
|
|
1213
1230
|
sep();
|
|
1214
1231
|
}
|
|
1215
1232
|
}
|
|
@@ -2271,6 +2288,7 @@ export class App {
|
|
|
2271
2288
|
this.chat.cache.clear();
|
|
2272
2289
|
this.chat.nodes = [];
|
|
2273
2290
|
this.chat.collapsedBlocks.clear();
|
|
2291
|
+
this.chat.blockHeights.clear();
|
|
2274
2292
|
this.chat.expanded.clear();
|
|
2275
2293
|
this.chat.expandedTools.clear();
|
|
2276
2294
|
this.chat.queueRebuild();
|