dsh-neotui 0.1.19 → 0.1.20

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.1.19",
3
+ "version": "0.1.20",
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": {
package/src/panels.js CHANGED
@@ -560,6 +560,9 @@ export class TrajectoryPanel extends Widget {
560
560
  this.flashUntil = 0;
561
561
  this.loadPromise = null; // dedupes concurrent load(currentSession)
562
562
  this.loadTarget = null;
563
+ this.liveTickAt = 0; // ⏱ live timer re-render throttle
564
+ this.tailFetchAt = 0; // tail-window auto-refresh throttle
565
+ this.refreshing = false;
563
566
  this.winSeqLo = null; // visible window = first-event SEQ range; null = follow the tail
564
567
  this.winSeqHi = null;
565
568
  // LEFT click toggles a step's 详细/简略 expansion (the ▸/▾ triangle).
@@ -977,13 +980,37 @@ export class TrajectoryPanel extends Widget {
977
980
  screen.text(this.x + 2, this.y + 1, "加载轨迹…", { fg: K.FAINT });
978
981
  return;
979
982
  }
980
- // the live step's ⏱ timer re-renders once per second while the turn runs
981
- if (this.app.chat?.running && this.winSeqLo == null && Date.now() - (this.liveTickAt ?? 0) > 1000) {
982
- this.liveTickAt = Date.now();
983
- this.buildLines();
983
+ // the live step's ⏱ timer re-renders once per second while the turn runs,
984
+ // and the tail window refreshes periodically so a NEW turn's step (and
985
+ // its timer) appears without pressing r
986
+ if (this.app.chat?.running && this.winSeqLo == null) {
987
+ if (Date.now() - (this.liveTickAt ?? 0) > 1000) {
988
+ this.liveTickAt = Date.now();
989
+ this.buildLines();
990
+ }
991
+ if (!this.refreshing && Date.now() - (this.tailFetchAt ?? 0) > 4000) {
992
+ this.tailFetchAt = Date.now();
993
+ this.#refreshTail();
994
+ }
984
995
  }
985
996
  this.view.render(screen);
986
997
  }
998
+ /** Re-fetch the tail window while following the live turn. */
999
+ async #refreshTail() {
1000
+ if (!this.sessionId) return;
1001
+ this.refreshing = true;
1002
+ try {
1003
+ const h = await this.app.api.call("session.history", { sessionId: this.sessionId, maxMessages: 20 });
1004
+ this.allEvents = h.events;
1005
+ this.minSeq = h.events[0]?.event?.seq ?? this.minSeq;
1006
+ this.hasMore = h.hasMore;
1007
+ this.stats = h.projections?.values?.sessionStats ?? this.stats;
1008
+ this.build();
1009
+ this.buildLines();
1010
+ this.app.redraw();
1011
+ } catch { /* next tick retries */ }
1012
+ this.refreshing = false;
1013
+ }
987
1014
  onMouse(ev) {
988
1015
  // RIGHT click on a step: context menu (expand/collapse · jump · detail).
989
1016
  if (ev.kind === "press" && ev.button === 2) {
package/src/views.js CHANGED
@@ -130,8 +130,11 @@ function applyEvent(nodes, event, view, log, state = null) {
130
130
  const text = partsToText(d.content ?? d.message?.content);
131
131
  const images = partsToImages(d.content ?? d.message?.content);
132
132
  const id = d.id ?? null;
133
- if (text !== null) nodes.push({ kind: "user", text, images, id, step: st.step });
134
- else if (images) nodes.push({ kind: "user", text: "", images, id, step: st.step });
133
+ // the turn timer starts HERE: the user message carries the turn start
134
+ // so the 🕐 ticker shows even before the first assistant chunk lands
135
+ const turnStartAt = st.turnStart ?? event.time ?? Date.now();
136
+ if (text !== null) nodes.push({ kind: "user", text, images, id, step: st.step, turnStartAt });
137
+ else if (images) nodes.push({ kind: "user", text: "", images, id, step: st.step, turnStartAt });
135
138
  break;
136
139
  }
137
140
  case "assistant/message": {
@@ -1201,7 +1204,9 @@ export class ChatView extends Widget {
1201
1204
  const ckey = `${realIdx}|${w}|${expKey}|${blockKeys}|${this.thinkMode}|${this.bashMode}|${node.streaming ? "s" : "f"}|${themeName()}|${node.step ?? "-"}|${userPrefix()}|${node.turnMs ?? "-"}`;
1202
1205
  // Streaming nodes re-render every frame: their text grows without any
1203
1206
  // change to the cache key, so caching them freezes the live think/tool/text.
1204
- const hit = node.streaming ? undefined : this.cache.get(ckey);
1207
+ // The LAST node re-renders too while a turn runs — its ticking 🕐 timer
1208
+ // must not be baked into a cached entry.
1209
+ const hit = (node.streaming || (this.running && realIdx === this.nodes.length - 1)) ? undefined : this.cache.get(ckey);
1205
1210
  if (hit) {
1206
1211
  for (const [rs, re, bg] of hit.cards ?? []) {
1207
1212
  this.cardRanges.push([lines.length + rs, lines.length + re, bg]);
@@ -1263,6 +1268,13 @@ export class ChatView extends Widget {
1263
1268
  markImg(realIdx, ii);
1264
1269
  }
1265
1270
  }
1271
+ // the turn timer starts at the QUESTION: while the reply is still
1272
+ // in the request/queue phase (no assistant node yet), the ticker
1273
+ // lives under the user message — web-style deep-dive from t=0.
1274
+ if (this.running && realIdx === this.nodes.length - 1 && node.turnStartAt != null) {
1275
+ lines.push([{ t: ` 🕐 本轮进行中…已经过 ${fmtDuration(Date.now() - node.turnStartAt)}`, fg: T.WARN, bold: true }]);
1276
+ mark(realIdx);
1277
+ }
1266
1278
  sep();
1267
1279
  break;
1268
1280
  }