pi-studio 0.5.2 → 0.5.3

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 +15 -0
  2. package/index.ts +160 -11
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -88,6 +88,21 @@ All notable changes to `pi-studio` are documented here.
88
88
 
89
89
  ## [Unreleased]
90
90
 
91
+ ## [0.5.3] — 2026-03-06
92
+
93
+ ### Added
94
+ - New terminal command: `/studio-current <path>` loads a file into currently open Studio tab(s) without opening a new browser session.
95
+ - `/studio --help` now includes `/studio-current` usage.
96
+
97
+ ### Changed
98
+ - Footer compact action label is now **Compact**.
99
+ - Footer metadata now includes in-Studio npm update hint text when an update is available (`Update: installed → latest`).
100
+ - Update notification timing now runs after Studio open notifications, so the update message is not immediately overwritten.
101
+ - Slash-command autocomplete order now lists `/studio` before `/studio-current`.
102
+
103
+ ### Fixed
104
+ - Removed low-value terminal toasts for Studio websocket connect/disconnect that could overwrite more important notifications.
105
+
91
106
  ## [0.5.2] — 2026-03-06
92
107
 
93
108
  ### Changed
package/index.ts CHANGED
@@ -2931,7 +2931,7 @@ ${cssVarsBlock}
2931
2931
 
2932
2932
  <footer>
2933
2933
  <span id="statusLine"><span id="statusSpinner" aria-hidden="true"> </span><span id="status">Booting studio…</span></span>
2934
- <span id="footerMeta" class="footer-meta"><span id="footerMetaText" class="footer-meta-text">Model: ${initialModel} · Terminal: ${initialTerminal} · Context: unknown</span><button id="compactBtn" class="footer-compact-btn" type="button" title="Trigger pi context compaction now.">Compact context</button></span>
2934
+ <span id="footerMeta" class="footer-meta"><span id="footerMetaText" class="footer-meta-text">Model: ${initialModel} · Terminal: ${initialTerminal} · Context: unknown</span><button id="compactBtn" class="footer-compact-btn" type="button" title="Trigger pi context compaction now.">Compact</button></span>
2935
2935
  <span class="shortcut-hint">Focus pane: Cmd/Ctrl+Esc (or F10), Esc to exit · Run editor text: Cmd/Ctrl+Enter</span>
2936
2936
  </footer>
2937
2937
 
@@ -3065,6 +3065,8 @@ ${cssVarsBlock}
3065
3065
  let contextTokens = null;
3066
3066
  let contextWindow = null;
3067
3067
  let contextPercent = null;
3068
+ let updateInstalledVersion = null;
3069
+ let updateLatestVersion = null;
3068
3070
 
3069
3071
  function parseFiniteNumber(value) {
3070
3072
  if (value == null || value === "") return null;
@@ -3072,6 +3074,12 @@ ${cssVarsBlock}
3072
3074
  return Number.isFinite(parsed) ? parsed : null;
3073
3075
  }
3074
3076
 
3077
+ function parseNonEmptyString(value) {
3078
+ if (typeof value !== "string") return null;
3079
+ const trimmed = value.trim();
3080
+ return trimmed ? trimmed : null;
3081
+ }
3082
+
3075
3083
  contextTokens = parseFiniteNumber(document.body && document.body.dataset ? document.body.dataset.contextTokens : null);
3076
3084
  contextWindow = parseFiniteNumber(document.body && document.body.dataset ? document.body.dataset.contextWindow : null);
3077
3085
  contextPercent = parseFiniteNumber(document.body && document.body.dataset ? document.body.dataset.contextPercent : null);
@@ -3199,6 +3207,12 @@ ${cssVarsBlock}
3199
3207
  if (typeof message.contextTokens === "number") summary.contextTokens = message.contextTokens;
3200
3208
  if (typeof message.contextWindow === "number") summary.contextWindow = message.contextWindow;
3201
3209
  if (typeof message.contextPercent === "number") summary.contextPercent = message.contextPercent;
3210
+ if (typeof message.updateInstalledVersion === "string") summary.updateInstalledVersion = message.updateInstalledVersion;
3211
+ if (typeof message.updateLatestVersion === "string") summary.updateLatestVersion = message.updateLatestVersion;
3212
+ if (message.document && typeof message.document === "object" && typeof message.document.text === "string") {
3213
+ summary.documentLength = message.document.text.length;
3214
+ if (typeof message.document.label === "string") summary.documentLabel = message.document.label;
3215
+ }
3202
3216
  if (typeof message.compactInProgress === "boolean") summary.compactInProgress = message.compactInProgress;
3203
3217
  if (typeof message.stopReason === "string") summary.stopReason = message.stopReason;
3204
3218
  if (typeof message.markdown === "string") summary.markdownLength = message.markdown.length;
@@ -3388,6 +3402,30 @@ ${cssVarsBlock}
3388
3402
  return changed;
3389
3403
  }
3390
3404
 
3405
+ function applyUpdateInfoFromMessage(message) {
3406
+ if (!message || typeof message !== "object") return false;
3407
+
3408
+ let changed = false;
3409
+
3410
+ if (Object.prototype.hasOwnProperty.call(message, "updateInstalledVersion")) {
3411
+ const nextInstalled = parseNonEmptyString(message.updateInstalledVersion);
3412
+ if (nextInstalled !== updateInstalledVersion) {
3413
+ updateInstalledVersion = nextInstalled;
3414
+ changed = true;
3415
+ }
3416
+ }
3417
+
3418
+ if (Object.prototype.hasOwnProperty.call(message, "updateLatestVersion")) {
3419
+ const nextLatest = parseNonEmptyString(message.updateLatestVersion);
3420
+ if (nextLatest !== updateLatestVersion) {
3421
+ updateLatestVersion = nextLatest;
3422
+ changed = true;
3423
+ }
3424
+ }
3425
+
3426
+ return changed;
3427
+ }
3428
+
3391
3429
  function updateDocumentTitle() {
3392
3430
  const modelText = modelLabel && modelLabel.trim() ? modelLabel.trim() : "none";
3393
3431
  const terminalText = terminalSessionLabel && terminalSessionLabel.trim() ? terminalSessionLabel.trim() : "unknown";
@@ -3401,7 +3439,13 @@ ${cssVarsBlock}
3401
3439
  const modelText = modelLabel && modelLabel.trim() ? modelLabel.trim() : "none";
3402
3440
  const terminalText = terminalSessionLabel && terminalSessionLabel.trim() ? terminalSessionLabel.trim() : "unknown";
3403
3441
  const contextText = formatContextUsageText();
3404
- const text = "Model: " + modelText + " · Terminal: " + terminalText + " · " + contextText;
3442
+ let updateText = "";
3443
+ if (updateLatestVersion) {
3444
+ updateText = updateInstalledVersion
3445
+ ? "Update: " + updateInstalledVersion + " → " + updateLatestVersion
3446
+ : "Update: " + updateLatestVersion + " available";
3447
+ }
3448
+ const text = "Model: " + modelText + " · Terminal: " + terminalText + " · " + contextText + (updateText ? " · " + updateText : "");
3405
3449
  if (footerMetaTextEl) {
3406
3450
  footerMetaTextEl.textContent = text;
3407
3451
  footerMetaTextEl.title = text;
@@ -5332,7 +5376,9 @@ ${cssVarsBlock}
5332
5376
 
5333
5377
  debugTrace("server_message", summarizeServerMessage(message));
5334
5378
 
5335
- if (applyContextUsageFromMessage(message)) {
5379
+ const contextChanged = applyContextUsageFromMessage(message);
5380
+ const updateInfoChanged = applyUpdateInfoFromMessage(message);
5381
+ if (contextChanged || updateInfoChanged) {
5336
5382
  updateFooterMeta();
5337
5383
  }
5338
5384
 
@@ -5621,6 +5667,35 @@ ${cssVarsBlock}
5621
5667
  return;
5622
5668
  }
5623
5669
 
5670
+ if (message.type === "studio_document") {
5671
+ const nextDoc = message.document;
5672
+ if (!nextDoc || typeof nextDoc !== "object" || typeof nextDoc.text !== "string") {
5673
+ return;
5674
+ }
5675
+
5676
+ const nextSource =
5677
+ nextDoc.source === "file" || nextDoc.source === "last-response"
5678
+ ? nextDoc.source
5679
+ : "blank";
5680
+ const nextLabel = typeof nextDoc.label === "string" && nextDoc.label.trim()
5681
+ ? nextDoc.label.trim()
5682
+ : (nextSource === "file" ? "file" : "studio document");
5683
+ const nextPath = typeof nextDoc.path === "string" && nextDoc.path.trim()
5684
+ ? nextDoc.path
5685
+ : null;
5686
+
5687
+ setEditorText(nextDoc.text, { preserveScroll: false, preserveSelection: false });
5688
+ setSourceState({ source: nextSource, label: nextLabel, path: nextPath });
5689
+ refreshResponseUi();
5690
+ setStatus(
5691
+ typeof message.message === "string" && message.message.trim()
5692
+ ? message.message
5693
+ : "Loaded document from terminal.",
5694
+ "success",
5695
+ );
5696
+ return;
5697
+ }
5698
+
5624
5699
  if (message.type === "studio_state") {
5625
5700
  const busy = Boolean(message.busy);
5626
5701
  agentBusyFromServer = Boolean(message.agentBusy);
@@ -6487,6 +6562,8 @@ export default function (pi: ExtensionAPI) {
6487
6562
  let updateCheckStarted = false;
6488
6563
  let updateCheckCompleted = false;
6489
6564
  const packageMetadata = readLocalPackageMetadata();
6565
+ const installedPackageVersion = packageMetadata?.version ?? null;
6566
+ let updateAvailableLatestVersion: string | null = null;
6490
6567
 
6491
6568
  const isStudioBusy = () => agentBusy || activeRequest !== null || compactInProgress;
6492
6569
 
@@ -6579,10 +6656,14 @@ export default function (pi: ExtensionAPI) {
6579
6656
  const latest = await fetchLatestNpmVersion(metadata.name, UPDATE_CHECK_TIMEOUT_MS);
6580
6657
  if (!latest) return;
6581
6658
  if (!isVersionBehind(metadata.version, latest)) return;
6582
- ctx.ui.notify(
6583
- `Update available for ${metadata.name}: ${metadata.version} → ${latest}. Run: pi install npm:${metadata.name}`,
6584
- "info",
6585
- );
6659
+
6660
+ updateAvailableLatestVersion = latest;
6661
+ broadcastState();
6662
+
6663
+ const notification =
6664
+ `Update available for ${metadata.name}: ${metadata.version} → ${latest}. Run: pi install npm:${metadata.name}`;
6665
+ ctx.ui.notify(notification, "info");
6666
+ broadcast({ type: "info", message: notification, level: "info" });
6586
6667
  } finally {
6587
6668
  updateCheckCompleted = true;
6588
6669
  }
@@ -6689,6 +6770,8 @@ export default function (pi: ExtensionAPI) {
6689
6770
  contextTokens: contextUsageSnapshot.tokens,
6690
6771
  contextWindow: contextUsageSnapshot.contextWindow,
6691
6772
  contextPercent: contextUsageSnapshot.percent,
6773
+ updateInstalledVersion: installedPackageVersion,
6774
+ updateLatestVersion: updateAvailableLatestVersion,
6692
6775
  compactInProgress,
6693
6776
  activeRequestId: activeRequest?.id ?? compactRequestId ?? null,
6694
6777
  activeRequestKind: activeRequest?.kind ?? (compactInProgress ? "compact" : null),
@@ -6793,6 +6876,8 @@ export default function (pi: ExtensionAPI) {
6793
6876
  contextTokens: contextUsageSnapshot.tokens,
6794
6877
  contextWindow: contextUsageSnapshot.contextWindow,
6795
6878
  contextPercent: contextUsageSnapshot.percent,
6879
+ updateInstalledVersion: installedPackageVersion,
6880
+ updateLatestVersion: updateAvailableLatestVersion,
6796
6881
  compactInProgress,
6797
6882
  activeRequestId: activeRequest?.id ?? compactRequestId ?? null,
6798
6883
  activeRequestKind: activeRequest?.kind ?? (compactInProgress ? "compact" : null),
@@ -7424,7 +7509,7 @@ export default function (pi: ExtensionAPI) {
7424
7509
 
7425
7510
  wsServer.on("connection", (ws) => {
7426
7511
  clients.add(ws);
7427
- notifyStudio("Studio browser websocket connected.", "info");
7512
+ emitDebugEvent("studio_ws_connected", { clients: clients.size });
7428
7513
  broadcastState();
7429
7514
 
7430
7515
  ws.on("message", (data) => {
@@ -7438,7 +7523,7 @@ export default function (pi: ExtensionAPI) {
7438
7523
 
7439
7524
  ws.on("close", () => {
7440
7525
  clients.delete(ws);
7441
- notifyStudio("Studio browser websocket disconnected.", "warning");
7526
+ emitDebugEvent("studio_ws_disconnected", { clients: clients.size });
7442
7527
  });
7443
7528
 
7444
7529
  ws.on("error", () => {
@@ -7771,7 +7856,8 @@ export default function (pi: ExtensionAPI) {
7771
7856
  + " /studio --blank Open with blank editor\n"
7772
7857
  + " /studio --last Open with last model response\n"
7773
7858
  + " /studio --status Show studio status\n"
7774
- + " /studio --stop Stop studio server",
7859
+ + " /studio --stop Stop studio server\n"
7860
+ + " /studio-current <path> Load a file into currently open Studio tab(s)",
7775
7861
  "info",
7776
7862
  );
7777
7863
  return;
@@ -7784,7 +7870,6 @@ export default function (pi: ExtensionAPI) {
7784
7870
  syncStudioResponseHistory(ctx.sessionManager.getBranch());
7785
7871
  broadcastState();
7786
7872
  broadcastResponseHistory();
7787
- void maybeNotifyUpdateAvailable(ctx);
7788
7873
  // Seed theme vars so first ping doesn't trigger a false update
7789
7874
  try {
7790
7875
  const currentStyle = getStudioThemeStyle(ctx.ui.theme);
@@ -7884,7 +7969,71 @@ export default function (pi: ExtensionAPI) {
7884
7969
  ctx.ui.notify(`Studio URL: ${url}`, "info");
7885
7970
  } catch (error) {
7886
7971
  ctx.ui.notify(`Failed to open browser: ${error instanceof Error ? error.message : String(error)}`, "error");
7972
+ } finally {
7973
+ void maybeNotifyUpdateAvailable(ctx);
7974
+ }
7975
+ },
7976
+ });
7977
+
7978
+ pi.registerCommand("studio-current", {
7979
+ description: "Load a file into current open Studio tab(s) without opening a new browser session",
7980
+ handler: async (args: string, ctx: ExtensionCommandContext) => {
7981
+ const trimmed = args.trim();
7982
+ if (!trimmed || trimmed === "help" || trimmed === "--help" || trimmed === "-h") {
7983
+ ctx.ui.notify(
7984
+ "Usage: /studio-current <path>\n"
7985
+ + " Load a file into currently open Studio tab(s) without opening a new browser window.",
7986
+ "info",
7987
+ );
7988
+ return;
7989
+ }
7990
+
7991
+ const pathArg = parsePathArgument(trimmed);
7992
+ if (!pathArg) {
7993
+ ctx.ui.notify("Invalid file path argument.", "error");
7994
+ return;
7995
+ }
7996
+
7997
+ const file = readStudioFile(pathArg, ctx.cwd);
7998
+ if (!file.ok) {
7999
+ ctx.ui.notify(file.message, "error");
8000
+ return;
8001
+ }
8002
+
8003
+ if (!serverState || serverState.clients.size === 0) {
8004
+ ctx.ui.notify("No open Studio tab is connected. Run /studio first.", "warning");
8005
+ return;
8006
+ }
8007
+
8008
+ await ctx.waitForIdle();
8009
+ lastCommandCtx = ctx;
8010
+ refreshRuntimeMetadata({ cwd: ctx.cwd, model: ctx.model });
8011
+ refreshContextUsage(ctx);
8012
+ syncStudioResponseHistory(ctx.sessionManager.getBranch());
8013
+
8014
+ const nextDoc: InitialStudioDocument = {
8015
+ text: file.text,
8016
+ label: file.label,
8017
+ source: "file",
8018
+ path: file.resolvedPath,
8019
+ };
8020
+ initialStudioDocument = nextDoc;
8021
+
8022
+ broadcastState();
8023
+ broadcastResponseHistory();
8024
+ broadcast({
8025
+ type: "studio_document",
8026
+ document: nextDoc,
8027
+ message: `Loaded ${file.label} from terminal command.`,
8028
+ });
8029
+
8030
+ if (file.text.length > 200_000) {
8031
+ ctx.ui.notify(
8032
+ "Loaded a large file into Studio. Critique requests currently reject documents over 200k characters.",
8033
+ "warning",
8034
+ );
7887
8035
  }
8036
+ ctx.ui.notify(`Loaded file into open Studio tab(s): ${file.label}`, "info");
7888
8037
  },
7889
8038
  });
7890
8039
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-studio",
3
- "version": "0.5.2",
3
+ "version": "0.5.3",
4
4
  "description": "Browser GUI for structured critique workflows in pi",
5
5
  "type": "module",
6
6
  "license": "MIT",