dsh-neotui 0.0.17 → 0.0.19
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 +53 -14
- package/src/widgets.js +11 -2
package/package.json
CHANGED
package/src/views.js
CHANGED
|
@@ -696,7 +696,7 @@ export class ChatView extends Widget {
|
|
|
696
696
|
if (idx < 0 || idx >= this.nodes.length) return false;
|
|
697
697
|
for (let li = 0; li < this.lineMap.length; li++) {
|
|
698
698
|
if (this.lineMap[li]?.nodeIdx === idx) {
|
|
699
|
-
this.view.scrollY = Math.max(0, li - 2);
|
|
699
|
+
this.view.anchorLock = null; this.view.scrollY = Math.max(0, li - 2);
|
|
700
700
|
this.app.redraw();
|
|
701
701
|
return true;
|
|
702
702
|
}
|
|
@@ -770,7 +770,7 @@ export class ChatView extends Widget {
|
|
|
770
770
|
this.#rebuild();
|
|
771
771
|
// Jump to the LIVE tail (the newest content) on open — the view would
|
|
772
772
|
// otherwise sit at the top showing the oldest turns.
|
|
773
|
-
this.view.scrollY = this.view.maxScroll();
|
|
773
|
+
this.view.anchorLock = null; this.view.scrollY = this.view.maxScroll();
|
|
774
774
|
}
|
|
775
775
|
|
|
776
776
|
async loadOlder() {
|
|
@@ -817,7 +817,8 @@ export class ChatView extends Widget {
|
|
|
817
817
|
send(text) {
|
|
818
818
|
if (!this.sessionId) return;
|
|
819
819
|
const trimmed = text.trim();
|
|
820
|
-
if (trimmed === "/reload") { this.app.
|
|
820
|
+
if (trimmed === "/reload") { this.app.softReload(); return; }
|
|
821
|
+
if (trimmed === "/restart") { this.app.restartApp(); return; }
|
|
821
822
|
if (!trimmed) return;
|
|
822
823
|
const { parts, images, errors } = buildPromptParts(trimmed, {
|
|
823
824
|
readFile: (p) => {
|
|
@@ -880,11 +881,16 @@ export class ChatView extends Widget {
|
|
|
880
881
|
const preHeaderRow = preHeaderIdx >= 0 ? preHeaderIdx - this.view.scrollY : null;
|
|
881
882
|
const reanchor = () => {
|
|
882
883
|
// primary anchor: the clicked block's header stays at its pre-click
|
|
883
|
-
// viewport row (zero shift) — only when the header was ON-SCREEN
|
|
884
|
+
// viewport row (zero shift) — only when the header was ON-SCREEN.
|
|
885
|
+
// When the fold removed the tail content below, the anchored position
|
|
886
|
+
// may exceed maxScroll: hold it via the view's anchorLock instead of
|
|
887
|
+
// clamping (the clamp is exactly the 5–6 line slide the user sees).
|
|
884
888
|
if (preHeaderRow !== null && preHeaderRow >= 0 && preHeaderIdx >= 0) {
|
|
885
889
|
const h2 = firstNonEmpty(match);
|
|
886
890
|
if (h2 >= 0) {
|
|
887
|
-
|
|
891
|
+
const sy = h2 - preHeaderRow;
|
|
892
|
+
this.view.scrollY = Math.max(0, sy);
|
|
893
|
+
if (sy > this.view.maxScroll()) this.view.anchorLock = sy;
|
|
888
894
|
return;
|
|
889
895
|
}
|
|
890
896
|
}
|
|
@@ -898,7 +904,8 @@ export class ChatView extends Widget {
|
|
|
898
904
|
}
|
|
899
905
|
if (first < 0) return;
|
|
900
906
|
const target = Math.min(first + topOffset, last);
|
|
901
|
-
this.view.scrollY = Math.max(0,
|
|
907
|
+
this.view.scrollY = Math.max(0, target);
|
|
908
|
+
if (target > this.view.maxScroll()) this.view.anchorLock = target;
|
|
902
909
|
};
|
|
903
910
|
// never park the viewport on a blank separator row — it reads as a
|
|
904
911
|
// spurious offset right after the click
|
|
@@ -1380,12 +1387,12 @@ export class ChatView extends Widget {
|
|
|
1380
1387
|
case "pgdn": return this.view.scroll(this.view.h);
|
|
1381
1388
|
}
|
|
1382
1389
|
if (ev.name === "char" && ev.key === "g" && !ev.ctrl && !ev.shift) {
|
|
1383
|
-
if (this.gKey) { this.gKey = false; this.view.scrollY = 0; return true; }
|
|
1390
|
+
if (this.gKey) { this.gKey = false; this.view.anchorLock = null; this.view.scrollY = 0; return true; }
|
|
1384
1391
|
this.gKey = true;
|
|
1385
1392
|
this.app.toast("再按 g 回顶");
|
|
1386
1393
|
return true;
|
|
1387
1394
|
}
|
|
1388
|
-
if (ev.name === "char" && ev.key === "g" && ev.shift) { this.view.scrollY = this.view.maxScroll(); return true; }
|
|
1395
|
+
if (ev.name === "char" && ev.key === "g" && ev.shift) { this.view.anchorLock = null; this.view.scrollY = this.view.maxScroll(); return true; }
|
|
1389
1396
|
if (ev.name === "escape" && this.app.searchQuery) { this.app.searchQuery = null; this.queueRebuild(); return true; }
|
|
1390
1397
|
if (ev.name === "escape" && this.selStart !== null) { this.selStart = this.selEnd = null; this.app.redraw(); return true; }
|
|
1391
1398
|
if (ev.name === "char" && ev.key === "i" && !ev.ctrl) { this.app.focus(this.input); return true; }
|
|
@@ -2188,17 +2195,49 @@ export class App {
|
|
|
2188
2195
|
showModePicker() { this.overlay = buildModePicker(this); this.redraw(); }
|
|
2189
2196
|
showPermissionPicker() { this.overlay = buildPermissionPicker(this); this.redraw(); }
|
|
2190
2197
|
|
|
2191
|
-
/** /reload:
|
|
2192
|
-
*
|
|
2193
|
-
*
|
|
2194
|
-
|
|
2195
|
-
|
|
2198
|
+
/** /reload: in-place soft reload — fresh session list, fresh chat history,
|
|
2199
|
+
* panels rebuilt, screen re-rendered. No process churn, no terminal
|
|
2200
|
+
* handoff (the old process-restart approach leaked mouse bytes into the
|
|
2201
|
+
* boot of the new instance). */
|
|
2202
|
+
async softReload() {
|
|
2203
|
+
this.closeOverlay();
|
|
2204
|
+
this.menu = null;
|
|
2205
|
+
this.popup = null;
|
|
2206
|
+
for (const p of [this.workspacePanel, this.trajectoryPanel, this.settingsPanel, this.subagentPanel, this.skillsPanel]) {
|
|
2207
|
+
if (p?.dispose) { try { p.dispose(); } catch {} }
|
|
2208
|
+
}
|
|
2209
|
+
this.workspacePanel = this.trajectoryPanel = this.settingsPanel = this.subagentPanel = this.skillsPanel = null;
|
|
2210
|
+
this.chat.cache.clear();
|
|
2211
|
+
this.chat.nodes = [];
|
|
2212
|
+
this.chat.collapsedBlocks.clear();
|
|
2213
|
+
this.chat.expanded.clear();
|
|
2214
|
+
this.chat.expandedTools.clear();
|
|
2215
|
+
this.chat.queueRebuild();
|
|
2216
|
+
this.chat.view.anchorLock = null;
|
|
2217
|
+
this.mode = "chat";
|
|
2218
|
+
this.focus(this.chat);
|
|
2219
|
+
this.toast("正在重新加载…");
|
|
2220
|
+
await this.refreshSessions();
|
|
2221
|
+
if (this.currentSession) await this.openSession(this.currentSession);
|
|
2222
|
+
else await this.newSessionIn(null);
|
|
2223
|
+
this.layout();
|
|
2224
|
+
this.redraw();
|
|
2225
|
+
this.toast("已重新加载会话与界面");
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2228
|
+
/** /restart: restart the TUI process in the same terminal so a freshly
|
|
2229
|
+
* published build (the profile symlinks the repo) takes effect. The new
|
|
2230
|
+
* instance starts one second after this process restores the terminal —
|
|
2231
|
+
* no stray mouse/keyboard bytes leak into its boot. */
|
|
2232
|
+
async restartApp() {
|
|
2233
|
+
this.toast("正在重启 TUI(加载新版本代码)…");
|
|
2196
2234
|
this.redraw();
|
|
2197
2235
|
await new Promise((r) => setTimeout(r, 250));
|
|
2198
2236
|
try { this.term?.stop?.(); } catch {}
|
|
2199
2237
|
try {
|
|
2200
2238
|
const { spawn } = await import("node:child_process");
|
|
2201
|
-
const
|
|
2239
|
+
const argv = process.argv.slice(1);
|
|
2240
|
+
const child = spawn("sh", ["-c", 'sleep 1; exec "$@"', "sh", ...argv], { detached: true, stdio: "inherit" });
|
|
2202
2241
|
child.unref();
|
|
2203
2242
|
} catch (e) {
|
|
2204
2243
|
this.toast(`重启失败: ${e.message}(请手动重启)`);
|
package/src/widgets.js
CHANGED
|
@@ -27,6 +27,7 @@ export class ScrollView extends Widget {
|
|
|
27
27
|
super(opts);
|
|
28
28
|
this.lines = []; // array of arrays of segs: {t, fg, bg, bold, italic, underline, strike, code, link}
|
|
29
29
|
this.scrollY = 0;
|
|
30
|
+
this.anchorLock = null; // click anchor held beyond maxScroll (fold at the tail)
|
|
30
31
|
this.autoScroll = opts.autoScroll ?? false;
|
|
31
32
|
this.onClick = opts.onClick ?? null; // (y, ev) => bool
|
|
32
33
|
this.onWheel = null; // optional custom wheel handler
|
|
@@ -36,7 +37,12 @@ export class ScrollView extends Widget {
|
|
|
36
37
|
setLines(lines, { keep = false } = {}) {
|
|
37
38
|
const atBottom = this.autoScroll && this.scrollY + this.h >= this.lines.length - 1 || (keep && this.scrollY + this.h >= this.lines.length);
|
|
38
39
|
this.lines = lines;
|
|
39
|
-
if (
|
|
40
|
+
if (this.anchorLock != null) {
|
|
41
|
+
// A click fold removed the tail content: hold the exact anchored
|
|
42
|
+
// position even though the buffer now ends above it (no delayed snap).
|
|
43
|
+
this.scrollY = this.anchorLock;
|
|
44
|
+
if (this.scrollY <= Math.max(0, this.lines.length - this.h)) this.anchorLock = null;
|
|
45
|
+
} else if (atBottom || this.scrollY > Math.max(0, this.lines.length - this.h)) {
|
|
40
46
|
this.scrollY = Math.max(0, this.lines.length - this.h);
|
|
41
47
|
}
|
|
42
48
|
}
|
|
@@ -44,6 +50,7 @@ export class ScrollView extends Widget {
|
|
|
44
50
|
maxScroll() { return Math.max(0, this.lines.length - this.h); }
|
|
45
51
|
scroll(dy) {
|
|
46
52
|
const before = this.scrollY;
|
|
53
|
+
this.anchorLock = null; // explicit scroll releases the click anchor
|
|
47
54
|
this.scrollY = Math.max(0, Math.min(this.maxScroll(), this.scrollY + dy));
|
|
48
55
|
return this.scrollY !== before;
|
|
49
56
|
}
|
|
@@ -78,7 +85,8 @@ export class ScrollView extends Widget {
|
|
|
78
85
|
const trackH = Math.max(1, this.h - 2);
|
|
79
86
|
const total = Math.max(1, this.lines.length);
|
|
80
87
|
const thumbH = Math.max(1, Math.floor(this.h * this.h / total));
|
|
81
|
-
const
|
|
88
|
+
const frac = Math.min(1, this.scrollY / Math.max(1, this.maxScroll()));
|
|
89
|
+
const thumbY = Math.floor((this.h - 2) * frac);
|
|
82
90
|
for (let i = 0; i < this.h; i++) {
|
|
83
91
|
const inThumb = i >= 1 + thumbY && i < 1 + thumbY + thumbH;
|
|
84
92
|
const inTrack = i >= 1 && i < this.h - 1;
|
|
@@ -114,6 +122,7 @@ export class ScrollView extends Widget {
|
|
|
114
122
|
return false;
|
|
115
123
|
}
|
|
116
124
|
#scrubTo(ey) {
|
|
125
|
+
this.anchorLock = null; // scrubbing releases the click anchor
|
|
117
126
|
const trackH = Math.max(1, this.h - 2);
|
|
118
127
|
const total = Math.max(1, this.lines.length);
|
|
119
128
|
const thumbH = Math.max(1, Math.floor(this.h * this.h / total));
|