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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-neotui",
3
- "version": "0.0.25",
3
+ "version": "0.0.26",
4
4
  "description": "Neo-TUI: mouse-driven terminal UI client for DeepSeek Harness (B-tier per dsh-tui-design.md)",
5
5
  "type": "module",
6
6
  "bin": { "dsh-neotui": "bin/dsh-tui.js" },
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));