pi-studio 0.5.7 → 0.5.8

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.
Files changed (3) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/index.ts +92 -21
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,12 @@ All notable changes to `pi-studio` are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.5.8] — 2026-03-12
8
+
9
+ ### Changed
10
+ - Studio browser tabs now auto-reconnect after unexpected websocket disconnects (for example transient local connection loss or sleep/wake), while intentional invalidation/shutdown still requires a fresh `/studio`.
11
+ - Same-tab reconnect now preserves the currently selected response-history item instead of jumping back to the latest response on every `hello_ack` resync.
12
+
7
13
  ## [0.5.7] — 2026-03-12
8
14
 
9
15
  ### Changed
package/index.ts CHANGED
@@ -3301,6 +3301,8 @@ ${cssVarsBlock}
3301
3301
  let wsState = "Connecting";
3302
3302
  let statusMessage = "Connecting · Studio script starting…";
3303
3303
  let statusLevel = "";
3304
+ let reconnectTimer = null;
3305
+ let reconnectAttempt = 0;
3304
3306
  let pendingRequestId = null;
3305
3307
  let pendingKind = null;
3306
3308
  let stickyStudioKind = null;
@@ -5824,8 +5826,8 @@ ${cssVarsBlock}
5824
5826
  let appliedHistory = false;
5825
5827
  if (Array.isArray(message.responseHistory)) {
5826
5828
  appliedHistory = setResponseHistory(message.responseHistory, {
5827
- autoSelectLatest: true,
5828
- preserveSelection: false,
5829
+ autoSelectLatest: !initialDocumentApplied,
5830
+ preserveSelection: initialDocumentApplied,
5829
5831
  silent: true,
5830
5832
  });
5831
5833
  }
@@ -6184,7 +6186,42 @@ ${cssVarsBlock}
6184
6186
  }
6185
6187
  }
6186
6188
 
6189
+ function clearScheduledReconnect() {
6190
+ if (reconnectTimer !== null) {
6191
+ window.clearTimeout(reconnectTimer);
6192
+ reconnectTimer = null;
6193
+ }
6194
+ }
6195
+
6196
+ function formatReconnectDelay(delayMs) {
6197
+ const delay = Math.max(0, Number(delayMs) || 0);
6198
+ if (delay < 1000) return delay + "ms";
6199
+ const seconds = delay / 1000;
6200
+ return (Number.isInteger(seconds) ? String(seconds) : seconds.toFixed(1)) + "s";
6201
+ }
6202
+
6203
+ function scheduleReconnect(reasonMessage) {
6204
+ if (reconnectTimer !== null) return;
6205
+
6206
+ reconnectAttempt += 1;
6207
+ const delayMs = Math.min(8000, 600 * Math.pow(2, Math.max(0, reconnectAttempt - 1)));
6208
+ setBusy(true);
6209
+ setWsState("Connecting");
6210
+ setStatus((reasonMessage || "Connection lost.") + " Reconnecting in " + formatReconnectDelay(delayMs) + "…", "warning");
6211
+
6212
+ reconnectTimer = window.setTimeout(() => {
6213
+ reconnectTimer = null;
6214
+ connect();
6215
+ }, delayMs);
6216
+ }
6217
+
6187
6218
  function connect() {
6219
+ clearScheduledReconnect();
6220
+
6221
+ if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) {
6222
+ return;
6223
+ }
6224
+
6188
6225
  const token = getToken();
6189
6226
  if (!token) {
6190
6227
  setWsState("Disconnected");
@@ -6195,26 +6232,61 @@ ${cssVarsBlock}
6195
6232
 
6196
6233
  const wsProtocol = window.location.protocol === "https:" ? "wss" : "ws";
6197
6234
  const wsUrl = wsProtocol + "://" + window.location.host + "/ws?token=" + encodeURIComponent(token) + (DEBUG_ENABLED ? "&debug=1" : "");
6235
+ const wasReconnect = reconnectAttempt > 0;
6236
+ let disconnectHandled = false;
6198
6237
 
6199
6238
  setWsState("Connecting");
6200
- setStatus("Connecting to Studio server…");
6201
- ws = new WebSocket(wsUrl);
6239
+ setStatus(wasReconnect ? "Reconnecting to Studio server…" : "Connecting to Studio server…");
6240
+ const socket = new WebSocket(wsUrl);
6241
+ ws = socket;
6202
6242
 
6203
6243
  const connectWatchdog = window.setTimeout(() => {
6204
- if (ws && ws.readyState === WebSocket.CONNECTING) {
6244
+ if (ws === socket && socket.readyState === WebSocket.CONNECTING) {
6205
6245
  setWsState("Connecting");
6206
- setStatus("Still connecting…", "warning");
6246
+ setStatus(wasReconnect ? "Still reconnecting…" : "Still connecting…", "warning");
6207
6247
  }
6208
6248
  }, 3000);
6209
6249
 
6210
- ws.addEventListener("open", () => {
6250
+ const handleDisconnect = (kind, code) => {
6251
+ if (disconnectHandled) return;
6252
+ disconnectHandled = true;
6253
+ window.clearTimeout(connectWatchdog);
6254
+ if (ws === socket) {
6255
+ ws = null;
6256
+ }
6257
+ setBusy(true);
6258
+
6259
+ if (kind === "invalidated") {
6260
+ clearScheduledReconnect();
6261
+ reconnectAttempt = 0;
6262
+ setWsState("Disconnected");
6263
+ setStatus("This tab was invalidated by a newer /studio session.", "warning");
6264
+ return;
6265
+ }
6266
+
6267
+ if (kind === "shutdown") {
6268
+ clearScheduledReconnect();
6269
+ reconnectAttempt = 0;
6270
+ setWsState("Disconnected");
6271
+ setStatus("Studio server shut down. Re-run /studio.", "warning");
6272
+ return;
6273
+ }
6274
+
6275
+ const detail = typeof code === "number" && code > 0
6276
+ ? "Disconnected (code " + code + ")."
6277
+ : (kind === "error" ? "WebSocket error." : "Connection lost.");
6278
+ scheduleReconnect(detail);
6279
+ };
6280
+
6281
+ socket.addEventListener("open", () => {
6211
6282
  window.clearTimeout(connectWatchdog);
6212
6283
  setWsState("Ready");
6213
- setStatus("Connected. Syncing…");
6284
+ setStatus(wasReconnect ? "Reconnected. Syncing…" : "Connected. Syncing…");
6214
6285
  sendMessage({ type: "hello" });
6286
+ reconnectAttempt = 0;
6215
6287
  });
6216
6288
 
6217
- ws.addEventListener("message", (event) => {
6289
+ socket.addEventListener("message", (event) => {
6218
6290
  try {
6219
6291
  const message = JSON.parse(event.data);
6220
6292
  handleServerMessage(message);
@@ -6224,22 +6296,21 @@ ${cssVarsBlock}
6224
6296
  }
6225
6297
  });
6226
6298
 
6227
- ws.addEventListener("close", (event) => {
6228
- window.clearTimeout(connectWatchdog);
6229
- setBusy(true);
6230
- setWsState("Disconnected");
6299
+ socket.addEventListener("close", (event) => {
6231
6300
  if (event && event.code === 4001) {
6232
- setStatus("This tab was invalidated by a newer /studio session.", "warning");
6233
- } else {
6234
- const code = event && typeof event.code === "number" ? event.code : 0;
6235
- setStatus("Disconnected (code " + code + "). Re-run /studio.", "error");
6301
+ handleDisconnect("invalidated", 4001);
6302
+ return;
6303
+ }
6304
+ if (event && event.code === 1001) {
6305
+ handleDisconnect("shutdown", 1001);
6306
+ return;
6236
6307
  }
6308
+ const code = event && typeof event.code === "number" ? event.code : 0;
6309
+ handleDisconnect("close", code);
6237
6310
  });
6238
6311
 
6239
- ws.addEventListener("error", () => {
6240
- window.clearTimeout(connectWatchdog);
6241
- setWsState("Disconnected");
6242
- setStatus("WebSocket error. Check /studio --status and reopen.", "error");
6312
+ socket.addEventListener("error", () => {
6313
+ handleDisconnect("error");
6243
6314
  });
6244
6315
  }
6245
6316
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-studio",
3
- "version": "0.5.7",
3
+ "version": "0.5.8",
4
4
  "description": "Browser GUI for structured critique workflows in pi",
5
5
  "type": "module",
6
6
  "license": "MIT",