dsh-neotui 0.0.25 → 0.0.26
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 +16 -4
- package/src/widgets.js +4 -1
package/package.json
CHANGED
package/src/views.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
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, appendFileSync } from "node:fs";
|
|
5
|
+
import { readFileSync, appendFileSync, mkdirSync } from "node:fs";
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import { Widget, List, ScrollView, Input, Popup, Menu, StatusBar } from "./widgets.js";
|
|
8
8
|
import { userPrefix, saveTuiConfig, loadTuiConfig, userName } from "./config.js";
|
|
@@ -702,7 +702,7 @@ export class ChatView extends Widget {
|
|
|
702
702
|
if (idx < 0 || idx >= this.nodes.length) return false;
|
|
703
703
|
for (let li = 0; li < this.lineMap.length; li++) {
|
|
704
704
|
if (this.lineMap[li]?.nodeIdx === idx) {
|
|
705
|
-
this.view.anchorLock = null; this.view.scrollY = Math.max(0, li - 2);
|
|
705
|
+
this.view.anchorLock = null; this.view.follow = false; this.view.scrollY = Math.max(0, li - 2);
|
|
706
706
|
this.app.redraw();
|
|
707
707
|
return true;
|
|
708
708
|
}
|
|
@@ -782,6 +782,7 @@ export class ChatView extends Widget {
|
|
|
782
782
|
// Jump to the LIVE tail (the newest content) on open — the view would
|
|
783
783
|
// otherwise sit at the top showing the oldest turns.
|
|
784
784
|
this.view.anchorLock = null; this.view.scrollY = this.view.maxScroll();
|
|
785
|
+
this.view.follow = true;
|
|
785
786
|
}
|
|
786
787
|
|
|
787
788
|
async loadOlder() {
|
|
@@ -853,6 +854,7 @@ export class ChatView extends Widget {
|
|
|
853
854
|
#clickLog(msg) {
|
|
854
855
|
try {
|
|
855
856
|
const dir = process.env.DSH_HOME ?? join(process.env.HOME ?? ".", ".dsh");
|
|
857
|
+
mkdirSync(dir, { recursive: true });
|
|
856
858
|
appendFileSync(join(dir, "tui-click-debug.log"), `${new Date().toISOString()} ${msg}\n`);
|
|
857
859
|
} catch {}
|
|
858
860
|
}
|
|
@@ -951,6 +953,8 @@ export class ChatView extends Widget {
|
|
|
951
953
|
(this.lineMap[this.view.scrollY]?.blockIdx ?? null) === null
|
|
952
954
|
) this.view.scrollY++;
|
|
953
955
|
};
|
|
956
|
+
// interacting with a block = reading mode: stop following the stream
|
|
957
|
+
this.view.follow = false;
|
|
954
958
|
if (node.kind === "assistant" && info.blockIdx !== null) {
|
|
955
959
|
const b = node.blocks[info.blockIdx];
|
|
956
960
|
if (b && (b.kind === "tool" || b.kind === "reasoning" || b.kind === "other" || b.kind === "text")) {
|
|
@@ -1471,12 +1475,12 @@ export class ChatView extends Widget {
|
|
|
1471
1475
|
case "pgdn": return this.view.scroll(this.view.h);
|
|
1472
1476
|
}
|
|
1473
1477
|
if (ev.name === "char" && ev.key === "g" && !ev.ctrl && !ev.shift) {
|
|
1474
|
-
if (this.gKey) { this.gKey = false; this.view.anchorLock = null; this.view.scrollY = 0; return true; }
|
|
1478
|
+
if (this.gKey) { this.gKey = false; this.view.anchorLock = null; this.view.follow = false; this.view.scrollY = 0; return true; }
|
|
1475
1479
|
this.gKey = true;
|
|
1476
1480
|
this.app.toast("再按 g 回顶");
|
|
1477
1481
|
return true;
|
|
1478
1482
|
}
|
|
1479
|
-
if (ev.name === "char" && ev.key === "g" && ev.shift) { this.view.anchorLock = null; this.view.scrollY = this.view.maxScroll(); return true; }
|
|
1483
|
+
if (ev.name === "char" && ev.key === "g" && ev.shift) { this.view.anchorLock = null; this.view.follow = true; this.view.scrollY = this.view.maxScroll(); return true; }
|
|
1480
1484
|
if (ev.name === "escape" && this.app.searchQuery) { this.app.searchQuery = null; this.queueRebuild(); return true; }
|
|
1481
1485
|
if (ev.name === "escape" && this.selStart !== null) { this.selStart = this.selEnd = null; this.app.redraw(); return true; }
|
|
1482
1486
|
if (ev.name === "char" && ev.key === "i" && !ev.ctrl) { this.app.focus(this.input); return true; }
|
|
@@ -2867,6 +2871,14 @@ export class App {
|
|
|
2867
2871
|
if (stats.ttftMs) row1.right.push({ t: ` 首响${Math.round(stats.ttftMs / stats.ttftSteps)}ms `, fg: T.FAINT, bg: T.STATUSBG });
|
|
2868
2872
|
}
|
|
2869
2873
|
rows.push(row1);
|
|
2874
|
+
// frozen-view indicator: N lines of new content arrived below
|
|
2875
|
+
{
|
|
2876
|
+
const c = this.chat;
|
|
2877
|
+
const below = c.lines.length - 1 - (c.view.scrollY + c.view.h);
|
|
2878
|
+
if (!c.view.follow && below > 0) {
|
|
2879
|
+
row1.left.push({ t: ` ↓${below} 条新内容 · G 跟随 `, fg: T.SELFG, bg: T.ACCENT, bold: true });
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2870
2882
|
// ── row 2: background jobs — one summary line, always present so its
|
|
2871
2883
|
// appearance/disappearance never reflows the layout ──
|
|
2872
2884
|
{
|
package/src/widgets.js
CHANGED
|
@@ -28,6 +28,7 @@ export class ScrollView extends Widget {
|
|
|
28
28
|
this.lines = []; // array of arrays of segs: {t, fg, bg, bold, italic, underline, strike, code, link}
|
|
29
29
|
this.scrollY = 0;
|
|
30
30
|
this.anchorLock = null; // click anchor held beyond maxScroll (fold at the tail)
|
|
31
|
+
this.follow = opts.follow ?? true; // auto-follow the tail while pinned
|
|
31
32
|
this.autoScroll = opts.autoScroll ?? false;
|
|
32
33
|
this.onClick = opts.onClick ?? null; // (y, ev) => bool
|
|
33
34
|
this.onWheel = null; // optional custom wheel handler
|
|
@@ -35,7 +36,7 @@ export class ScrollView extends Widget {
|
|
|
35
36
|
this.title = opts.title ?? "";
|
|
36
37
|
}
|
|
37
38
|
setLines(lines, { keep = false } = {}) {
|
|
38
|
-
const atBottom = this.autoScroll && this.scrollY + this.h >= this.lines.length - 1 || (keep && this.scrollY + this.h >= this.lines.length);
|
|
39
|
+
const atBottom = this.autoScroll && this.follow && this.scrollY + this.h >= this.lines.length - 1 || (keep && this.scrollY + this.h >= this.lines.length);
|
|
39
40
|
this.lines = lines;
|
|
40
41
|
if (this.anchorLock != null) {
|
|
41
42
|
// A click fold removed the tail content: hold the exact anchored
|
|
@@ -52,6 +53,7 @@ export class ScrollView extends Widget {
|
|
|
52
53
|
const before = this.scrollY;
|
|
53
54
|
this.anchorLock = null; // explicit scroll releases the click anchor
|
|
54
55
|
this.scrollY = Math.max(0, Math.min(this.maxScroll(), this.scrollY + dy));
|
|
56
|
+
this.follow = this.scrollY >= this.maxScroll(); // reaching the bottom re-follows
|
|
55
57
|
return this.scrollY !== before;
|
|
56
58
|
}
|
|
57
59
|
render(screen) {
|
|
@@ -123,6 +125,7 @@ export class ScrollView extends Widget {
|
|
|
123
125
|
}
|
|
124
126
|
#scrubTo(ey) {
|
|
125
127
|
this.anchorLock = null; // scrubbing releases the click anchor
|
|
128
|
+
this.follow = this.scrollY >= this.maxScroll();
|
|
126
129
|
const trackH = Math.max(1, this.h - 2);
|
|
127
130
|
const total = Math.max(1, this.lines.length);
|
|
128
131
|
const thumbH = Math.max(1, Math.floor(this.h * this.h / total));
|