pi-web-ui 0.2.1 → 0.2.2

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.
@@ -12,7 +12,7 @@
12
12
  import { existsSync } from "node:fs";
13
13
  import { join } from "node:path";
14
14
  import { createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, getAgentDir, SessionManager, } from "@earendil-works/pi-coding-agent";
15
- import { serializeMessage, serializeStreamingMessage } from "./serialize.js";
15
+ import { serializeMessage, serializeStreamingMessage, } from "./serialize.js";
16
16
  import { loadCommands, saveCommandsFile, TerminalManager, } from "./terminals.js";
17
17
  const SNAPSHOT_INTERVAL_MS = 60;
18
18
  const WIDGET_REFRESH_MS = 2000;
@@ -286,7 +286,20 @@ export class ClientSession {
286
286
  snapshotTimer = null;
287
287
  sessionsTimer = null;
288
288
  version = 0;
289
- seq = 0;
289
+ /**
290
+ * Stable per-message ids: assigned once per (role, timestamp) so snapshot ids
291
+ * don't change every 60ms — changing ids would remount the whole list in
292
+ * React (collapse open thinking blocks, reset scroll, jank on long chats).
293
+ */
294
+ msgIds = new Map();
295
+ nextMsgId = 1;
296
+ /** Serialized UiMessage cache — object-reference-stable across snapshots, so
297
+ * the frontend's React.memo can skip unchanged messages entirely. */
298
+ uiMessageCache = new Map();
299
+ /** Reused when the message set didn't change, keeping state.messages
300
+ * reference-stable so the frontend can memoize derived maps. */
301
+ lastMessagesSig = "";
302
+ lastMessagesArray = [];
290
303
  queueSteering = 0;
291
304
  queueFollowUp = 0;
292
305
  disposed = false;
@@ -434,6 +447,25 @@ export class ClientSession {
434
447
  void this.pushSessions();
435
448
  }, 800);
436
449
  }
450
+ /** Serialize a persisted message with a STABLE id + cached object reference. */
451
+ serializeCached(m) {
452
+ const key = m.role === "toolResult"
453
+ ? `t:${m.toolCallId}`
454
+ : `${m.role}:${m.timestamp}`;
455
+ let n = this.msgIds.get(key);
456
+ if (n === undefined) {
457
+ n = this.nextMsgId++;
458
+ this.msgIds.set(key, n);
459
+ }
460
+ const cacheKey = `${key}#${n}`;
461
+ const cached = this.uiMessageCache.get(cacheKey);
462
+ if (cached)
463
+ return cached;
464
+ const msg = serializeMessage(m, n);
465
+ if (msg)
466
+ this.uiMessageCache.set(cacheKey, msg);
467
+ return msg;
468
+ }
437
469
  snapshot() {
438
470
  const state = this.session.agent.state;
439
471
  const model = state.model;
@@ -461,14 +493,22 @@ export class ClientSession {
461
493
  catch {
462
494
  // stats are best-effort
463
495
  }
496
+ const rawMessages = state.messages
497
+ .map((m) => this.serializeCached(m))
498
+ .filter((m) => m !== null);
499
+ // Reuse the previous array when nothing changed: the element objects are
500
+ // cached (reference-stable) anyway, and a stable array reference lets the
501
+ // frontend memoize derived maps instead of rebuilding them every 60ms.
502
+ const sig = rawMessages.map((m) => m.id).join("\u0001");
503
+ const messages = sig === this.lastMessagesSig ? this.lastMessagesArray : rawMessages;
504
+ this.lastMessagesSig = sig;
505
+ this.lastMessagesArray = rawMessages;
464
506
  return {
465
507
  clientId: this.clientId,
466
508
  cwd: this.cwd,
467
509
  sessionId: this.session.sessionId,
468
510
  sessionFile: this.session.sessionFile,
469
- messages: state.messages
470
- .map((m) => serializeMessage(m, ++this.seq))
471
- .filter((m) => m !== null),
511
+ messages,
472
512
  // The in-progress assistant message lives in state.streamingMessage
473
513
  // (the SDK only pushes it into state.messages at message_end). Surfacing
474
514
  // it here is what makes thinking + text stream into the browser at
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-web-ui",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Web chat interface for the pi coding agent, powered by the pi SDK (@earendil-works/pi-coding-agent) — one-command run, Docker/systemd/launchd deployable",
5
5
  "license": "MIT",
6
6
  "type": "module",