dsh-neotui 0.1.2 → 0.1.4

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.2",
3
+ "version": "0.1.4",
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/api.js CHANGED
@@ -26,6 +26,7 @@ export class Api {
26
26
  this.onStateChange = onStateChange;
27
27
  this.ws = null;
28
28
  this.hostWs = null;
29
+ this.muxWs = null;
29
30
  this.closed = false;
30
31
  this.connected = false;
31
32
  this.retryDelay = 500;
@@ -82,7 +83,9 @@ export class Api {
82
83
  #connect(url, kind) {
83
84
  if (this.closed) return;
84
85
  const ws = new WebSocket(url);
85
- (kind === "mux" ? this : this).ws = kind === "mux" ? ws : this.hostWs;
86
+ if (kind === "mux") this.muxWs = ws;
87
+ else this.hostWs = ws;
88
+ this.ws = ws; // most recent socket (for diagnostics)
86
89
  ws.onopen = () => {
87
90
  this.connected = true;
88
91
  this.retryDelay = 500;
@@ -130,10 +133,20 @@ export class Api {
130
133
  return body.result.value;
131
134
  }
132
135
 
136
+ /** Reconnect the mux stream. The host re-pushes the session baseline
137
+ * (session/subscribed + session/jobs snapshots) on every fresh mux
138
+ * connection, so this doubles as a "refresh jobs snapshots" request —
139
+ * e.g. when the connect-time snapshot arrived before a session opened. */
140
+ refreshMux() {
141
+ if (this.closed) return;
142
+ try { this.muxWs?.close(); } catch {}
143
+ // the onclose handler reconnects with backoff
144
+ }
145
+
133
146
  close() {
134
147
  this.closed = true;
135
148
  if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
136
- try { this.ws?.close(); } catch {}
149
+ try { this.muxWs?.close(); } catch {}
137
150
  try { this.hostWs?.close(); } catch {}
138
151
  try { this.rpcWs?.close(); } catch {}
139
152
  }
package/src/md.js CHANGED
@@ -161,6 +161,9 @@ function highlightLine(line, lang) {
161
161
  // Returns array of lines; each line = array of segments.
162
162
 
163
163
  export function renderMd(text, width, sink = null, opts = {}) {
164
+ // defensive: snapshot-derived blocks can carry non-string text; a `.replace`
165
+ // on an object would throw mid-frame and freeze the whole terminal
166
+ if (typeof text !== "string") text = String(text ?? "");
164
167
  const hardBreaks = !!opts.hardBreaks;
165
168
  const lines = [];
166
169
  const pushLine = (segs = []) => {
package/src/text.js CHANGED
@@ -26,6 +26,7 @@ export function wcwidth(cp) {
26
26
  }
27
27
 
28
28
  export function strWidth(s) {
29
+ if (typeof s !== "string") s = String(s ?? "");
29
30
  let w = 0;
30
31
  for (const ch of s) w += wcwidth(ch.codePointAt(0));
31
32
  return w;
package/src/views.js CHANGED
@@ -860,7 +860,7 @@ export class ChatView extends Widget {
860
860
  case "session/title": this.title = frame.title ?? this.title; this.queueRebuild(); break;
861
861
  case "session/jobs": {
862
862
  this.running = (frame.jobs ?? []).some((j) => j.status === "running");
863
- this.app.setJobs(frame.jobs ?? []);
863
+ this.app.setJobs(frame.jobs ?? [], frame.sessionId);
864
864
  break;
865
865
  }
866
866
  case "session/subscribed": {
@@ -1806,6 +1806,7 @@ export class App {
1806
1806
  this.toastMsg = null;
1807
1807
  this.toastUntil = 0;
1808
1808
  this.jobs = [];
1809
+ this.jobsBySession = new Map(); // sessionId → latest session/jobs snapshot
1809
1810
  this.focused = null;
1810
1811
  this.provider = "";
1811
1812
  this.model = "";
@@ -1938,7 +1939,14 @@ export class App {
1938
1939
  }
1939
1940
 
1940
1941
  setStatus(msg) { this.statusMsg = msg; this.redraw(); }
1941
- setJobs(jobs) { this.jobs = jobs; this.layout(); this.redraw(); }
1942
+ setJobs(jobs, sessionId = null) {
1943
+ // snapshot buffered per session: the mux baseline arrives at connect
1944
+ // time, possibly before the chat opens that session
1945
+ if (sessionId != null) this.jobsBySession.set(sessionId, jobs);
1946
+ if (sessionId == null || sessionId === this.currentSession) this.jobs = jobs;
1947
+ this.layout();
1948
+ this.redraw();
1949
+ }
1942
1950
 
1943
1951
  #startPolling() {
1944
1952
  // Self-rescheduling so the interval actually tracks run state (a fixed
@@ -2019,7 +2027,6 @@ export class App {
2019
2027
  }
2020
2028
  case "session/event":
2021
2029
  case "session/title":
2022
- case "session/jobs":
2023
2030
  case "session/subscribed":
2024
2031
  case "session/queue":
2025
2032
  if (this.chat.sessionId === frame.sessionId) {
@@ -2028,6 +2035,13 @@ export class App {
2028
2035
  // refresh list on title updates
2029
2036
  if (frame.type === "session/title") this.refreshSessions();
2030
2037
  break;
2038
+ case "session/jobs":
2039
+ // buffer every session's snapshot even before it is opened (the
2040
+ // connect-time baseline would otherwise be dropped by the filter
2041
+ // below and the footer would stick at "0已完成")
2042
+ this.setJobs(frame.jobs ?? [], frame.sessionId ?? null);
2043
+ if (this.chat.sessionId === frame.sessionId) this.chat.onFrame(frame);
2044
+ break;
2031
2045
  case "session/projection":
2032
2046
  this.projections[frame.key] = frame.value;
2033
2047
  if (frame.key === "tokenUsage") this.tokenUsage = frame.value;
@@ -2323,6 +2337,16 @@ export class App {
2323
2337
 
2324
2338
  async openSession(sessionId) {
2325
2339
  this.currentSession = sessionId;
2340
+ // apply the buffered jobs snapshot; if this session's connect-time
2341
+ // baseline was never seen, reconnect the mux to re-fetch it (the host
2342
+ // re-pushes the full snapshot on every fresh mux connection)
2343
+ const snap = this.jobsBySession.get(sessionId);
2344
+ this.jobs = snap ?? [];
2345
+ if (snap !== undefined) {
2346
+ this.chat.running = snap.some((j) => j.status === "running");
2347
+ } else if (this.api.connected && typeof this.api.refreshMux === "function") {
2348
+ this.api.refreshMux();
2349
+ }
2326
2350
  await this.chat.open(sessionId);
2327
2351
  this.loadFeedback();
2328
2352
  this.updateModel();
@@ -2927,6 +2951,15 @@ export class App {
2927
2951
  if (this.toastMsg && Date.now() > this.toastUntil) { this.toastMsg = null; this.dirty = true; }
2928
2952
  } catch (e) {
2929
2953
  this.log("render error (kept running):", e);
2954
+ // stderr is invisible under the alt screen — record the stack where
2955
+ // it can be read, then flush whatever composed before the throw so
2956
+ // the terminal never sits frozen on the previous frame.
2957
+ try {
2958
+ const dir = process.env.DSH_HOME ?? join(process.env.HOME ?? ".", ".dsh");
2959
+ mkdirSync(dir, { recursive: true });
2960
+ appendFileSync(join(dir, "tui-error.log"), `${new Date().toISOString()} ${e?.stack ?? e}\n`);
2961
+ } catch {}
2962
+ try { this.term?.output?.write?.(this.screen.render()); } catch {}
2930
2963
  this.dirty = true;
2931
2964
  }
2932
2965
  this.timer = setTimeout(tick, 33);