dsh-neotui 0.0.5 → 0.0.7

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.5",
3
+ "version": "0.0.7",
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
@@ -111,12 +111,12 @@ function diffText(oldText, newText, width) {
111
111
 
112
112
  // ---- Chat node model ----
113
113
 
114
- function nodeForEvents(events, log) {
115
- const nodes = [];
114
+ /** Apply ONE event onto a mutable nodes array (shared by full-window
115
+ * derivation and the incremental mux merge — the two paths must agree). */
116
+ function applyEvent(nodes, event, view, log) {
116
117
  const cur = () => nodes[nodes.length - 1];
117
- for (const { event, view } of events) {
118
- const d = event.data ?? {};
119
- switch (event.type) {
118
+ const d = event.data ?? {};
119
+ switch (event.type) {
120
120
  case "user/message": {
121
121
  const text = partsToText(d.content ?? d.message?.content);
122
122
  const images = partsToImages(d.content ?? d.message?.content);
@@ -238,7 +238,12 @@ function nodeForEvents(events, log) {
238
238
  }
239
239
  }
240
240
  }
241
- }
241
+ }
242
+
243
+ /** Re-derive the whole node list from a complete event window (open/poll). */
244
+ function nodeForEvents(events, log) {
245
+ const nodes = [];
246
+ for (const { event, view } of events) applyEvent(nodes, event, view, log);
242
247
  return nodes;
243
248
  }
244
249
 
@@ -541,7 +546,7 @@ export class ChatView extends Widget {
541
546
  this.input = new Input({
542
547
  x: this.x, y: this.y + this.h - 2, w: this.w, h: 1,
543
548
  multi: true, maxLines: 6,
544
- bg: T.BG2, border: T.BORDER2,
549
+ bg: T.PANEL,
545
550
  placeholder: "输入消息…(Ctrl+J 换行,Enter 发送)",
546
551
  onEnter: (v) => this.send(v),
547
552
  onChange: () => this.inputChanged(),
@@ -557,23 +562,13 @@ export class ChatView extends Widget {
557
562
  }
558
563
 
559
564
  /** Queue a rebuild; flushed on the next frame render (throttles streaming). */
560
- /** Merge freshly arrived events (mux frames or poll results) into the tail. */
565
+ /** Merge freshly arrived events (mux frames) into the tail INCREMENTALLY —
566
+ * each event mutates this.nodes directly via the same applyEvent the
567
+ * full-window re-derivation uses, so a lone reasoning-delta appends to the
568
+ * existing block instead of wiping it (the old "shows then deleted" bug). */
561
569
  mergeEvents(entries) {
562
- const nodes = nodeForEvents(entries, this.app.log);
563
- if (nodes.length === 0) return;
564
- const last = nodes[nodes.length - 1];
565
- const mine = this.nodes[this.nodes.length - 1];
566
- // Only merge into the ACTIVE streaming node; a newly started turn is pushed
567
- // as its own node (merging into a finalized turn would overwrite it).
568
- if (last.kind === "assistant" && mine && mine.kind === "assistant" && mine.streaming) {
569
- mine.blocks = last.blocks;
570
- mine.images = last.images ?? mine.images;
571
- mine.id = last.id ?? mine.id;
572
- mine.streaming = last.streaming;
573
- mine.finalized = false;
574
- } else {
575
- this.nodes.push(...nodes);
576
- this.expanded.add(this.nodes.length - 1);
570
+ for (const { event, view } of entries) {
571
+ applyEvent(this.nodes, event, view, this.app.log);
577
572
  }
578
573
  this.running = this.nodes.some((n) => n.kind === "assistant" && n.streaming);
579
574
  this.queueRebuild();
@@ -737,6 +732,9 @@ export class ChatView extends Widget {
737
732
  this.nodes = [{ kind: "system", text: `加载失败: ${e.message}` }];
738
733
  }
739
734
  this.#rebuild();
735
+ // Jump to the LIVE tail (the newest content) on open — the view would
736
+ // otherwise sit at the top showing the oldest turns.
737
+ this.view.scrollY = this.view.maxScroll();
740
738
  }
741
739
 
742
740
  async loadOlder() {
@@ -1094,7 +1092,6 @@ export class ChatView extends Widget {
1094
1092
  const isBlank = this.app.sessions.find((s) => s.sessionId === this.sessionId)?.blank ?? false;
1095
1093
  if (this.nodes.length === 0 && isBlank) {
1096
1094
  this.#renderWelcome(screen);
1097
- screen.hline(this.x, this.x + this.w - 1, this.input.y - 1, "─", { fg: T.BORDER2 });
1098
1095
  this.input.render(screen);
1099
1096
  return;
1100
1097
  }
@@ -1120,7 +1117,6 @@ export class ChatView extends Widget {
1120
1117
  }
1121
1118
  }
1122
1119
  this.#renderTodos(screen);
1123
- screen.hline(this.x, this.x + this.w - 1, this.input.y - 1, "─", { fg: T.BORDER2 });
1124
1120
  this.input.render(screen);
1125
1121
  }
1126
1122
 
package/src/widgets.js CHANGED
@@ -336,7 +336,6 @@ export class Input extends Widget {
336
336
  if (!this.multi) {
337
337
  // single-line: horizontal scroll (search/rename/picker inputs)
338
338
  screen.fillRect(this.x, this.y, this.x + this.w - 1, this.y, " ", { bg: this.bg });
339
- screen.hline(this.x, this.x + this.w - 1, this.y, "─", { fg: this.border, bg: this.bg });
340
339
  const promptW = strWidth(this.prompt);
341
340
  const inner = Math.max(0, this.w - promptW - 2);
342
341
  screen.text(this.x, this.y, this.prompt, { fg: T.ACCENT, bg: this.bg });
@@ -358,7 +357,6 @@ export class Input extends Widget {
358
357
  const rows = this.#visualRows();
359
358
  const h = Math.min(this.maxLines, rows.length);
360
359
  screen.fillRect(this.x, this.y, this.x + this.w - 1, this.y + h - 1, " ", { bg: this.bg });
361
- screen.hline(this.x, this.x + this.w - 1, this.y, "─", { fg: this.border, bg: this.bg });
362
360
  if (this.value === "" && this.placeholder) {
363
361
  screen.text(this.x, this.y, this.prompt, { fg: T.ACCENT, bg: this.bg });
364
362
  screen.text(this.x + strWidth(this.prompt), this.y, truncate(this.placeholder, this.w - strWidth(this.prompt) - 2), { fg: T.FAINT, bg: this.bg });