pi-web-ui 0.15.6 → 0.16.0

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.
@@ -11,7 +11,7 @@
11
11
  * so reconnects just re-request a snapshot.
12
12
  */
13
13
  import { spawn } from "node:child_process";
14
- import { existsSync, readFileSync, statSync, writeFileSync, mkdirSync, } from "node:fs";
14
+ import { existsSync, readFileSync, statSync, writeFileSync, mkdirSync, watch, } from "node:fs";
15
15
  import { dirname, join, relative, resolve, sep } from "node:path";
16
16
  import { fileURLToPath } from "node:url";
17
17
  import { createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, getAgentDir, SessionManager, } from "@earendil-works/pi-coding-agent";
@@ -728,6 +728,14 @@ export class ClientSession {
728
728
  disposed = false;
729
729
  /** pi-config readiness check, cached briefly so 60ms snapshots don't hit disk. */
730
730
  piCheckCache = null;
731
+ /** fs.watch on the currently-listed directory — file changes push an instant
732
+ * refresh (`file_changed`) so the tree updates without waiting for the 10s
733
+ * poll. Only the listed directory is watched (one level); navigating
734
+ * re-watches the new target. fs.watch isn't available on every platform /
735
+ * filesystem — failures silently fall back to the poll. */
736
+ fsWatcher = null;
737
+ watchPath = null;
738
+ watchTimer = null;
731
739
  constructor(clientId, cwd, agentDir, stateStore) {
732
740
  this.clientId = clientId;
733
741
  this.cwd = cwd;
@@ -829,8 +837,11 @@ export class ClientSession {
829
837
  this.sinks.delete(send);
830
838
  // No sockets left for this client — kill its terminals so processes don't
831
839
  // survive a closed tab / dropped connection.
832
- if (this.sinks.size === 0)
840
+ if (this.sinks.size === 0) {
833
841
  this.terminals.killAll();
842
+ // No sockets → nobody to refresh; drop the dir watcher too.
843
+ this.unwatchDir();
844
+ }
834
845
  }
835
846
  /** Broadcast to every connected socket of this client. */
836
847
  emit(msg) {
@@ -2432,6 +2443,9 @@ export class ClientSession {
2432
2443
  // always use "/", but relative() returns "\\" on Windows.
2433
2444
  const rel = rawRel.split(sep).join("/");
2434
2445
  const { entries, truncated, error } = await readDirForUI(target, rel);
2446
+ // Watch the listed directory (only after a successful read — a missing
2447
+ // dir throws above and must not create a watcher on a phantom path).
2448
+ this.watchDir(target, rel);
2435
2449
  if (error) {
2436
2450
  // Windows-only: unreadable system dirs degrade to an empty list
2437
2451
  // with a warning instead of a hard error — the panel stays usable.
@@ -2461,6 +2475,56 @@ export class ClientSession {
2461
2475
  });
2462
2476
  }
2463
2477
  }
2478
+ /** Watch a directory for changes so the file panel refreshes instantly
2479
+ * instead of waiting for the 10s poll. Watches the directory exactly as
2480
+ * listed (one level); navigating re-watches the new target. fs.watch is
2481
+ * unavailable on some platforms/filesystems — failures silently fall back
2482
+ * to the poll. */
2483
+ watchDir(absPath, rel) {
2484
+ if (this.disposed || this.watchPath === rel)
2485
+ return;
2486
+ this.unwatchDir();
2487
+ this.watchPath = rel;
2488
+ try {
2489
+ // persistent: false — the watcher must not keep the process alive.
2490
+ this.fsWatcher = watch(absPath, { persistent: false }, () => {
2491
+ // Burst events (npm install, git ops, editor save→rename) are
2492
+ // debounced into a single refresh.
2493
+ if (this.watchTimer)
2494
+ return;
2495
+ this.watchTimer = setTimeout(() => {
2496
+ this.watchTimer = null;
2497
+ this.emit({ type: "file_changed", path: this.watchPath ?? "" });
2498
+ }, 400);
2499
+ });
2500
+ this.fsWatcher.on("error", () => {
2501
+ // Directory deleted / unsupported fs — stop watching; the poll (or
2502
+ // the next navigation) restores things.
2503
+ this.unwatchDir();
2504
+ });
2505
+ }
2506
+ catch {
2507
+ // fs.watch unsupported (some network mounts, containers) — poll covers it.
2508
+ this.fsWatcher = null;
2509
+ this.watchPath = null;
2510
+ }
2511
+ }
2512
+ unwatchDir() {
2513
+ if (this.watchTimer) {
2514
+ clearTimeout(this.watchTimer);
2515
+ this.watchTimer = null;
2516
+ }
2517
+ if (this.fsWatcher) {
2518
+ try {
2519
+ this.fsWatcher.close();
2520
+ }
2521
+ catch {
2522
+ // already closed
2523
+ }
2524
+ this.fsWatcher = null;
2525
+ }
2526
+ this.watchPath = null;
2527
+ }
2464
2528
  /** Read a workspace file for the preview panel (size-capped, binary-safe). */
2465
2529
  async readFile(relPath) {
2466
2530
  try {
@@ -2827,6 +2891,7 @@ export class ClientSession {
2827
2891
  clearInterval(this.widgetsTimer);
2828
2892
  this.widgetsTimer = null;
2829
2893
  }
2894
+ this.unwatchDir();
2830
2895
  this.webUi.dispose();
2831
2896
  for (const conv of this.convs.values()) {
2832
2897
  conv.unsubscribe?.();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-web-ui",
3
- "version": "0.15.6",
3
+ "version": "0.16.0",
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",