pi-studio 0.5.2 → 0.5.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/CHANGELOG.md CHANGED
@@ -88,6 +88,33 @@ All notable changes to `pi-studio` are documented here.
88
88
 
89
89
  ## [Unreleased]
90
90
 
91
+ ## [0.5.4] — 2026-03-09
92
+
93
+ ### Added
94
+ - New right-pane **Thinking (Raw)** view for assistant/model thinking when available.
95
+
96
+ ### Changed
97
+ - Response history and latest-response syncing now preserve associated thinking content.
98
+ - In Thinking view, right-pane actions adapt to the selected reasoning trace:
99
+ - **Load thinking into editor**
100
+ - **Copy thinking text**
101
+ - thinking-aware reference/sync badges
102
+
103
+ ## [0.5.3] — 2026-03-06
104
+
105
+ ### Added
106
+ - New terminal command: `/studio-current <path>` loads a file into currently open Studio tab(s) without opening a new browser session.
107
+ - `/studio --help` now includes `/studio-current` usage.
108
+
109
+ ### Changed
110
+ - Footer compact action label is now **Compact**.
111
+ - Footer metadata now includes in-Studio npm update hint text when an update is available (`Update: installed → latest`).
112
+ - Update notification timing now runs after Studio open notifications, so the update message is not immediately overwritten.
113
+ - Slash-command autocomplete order now lists `/studio` before `/studio-current`.
114
+
115
+ ### Fixed
116
+ - Removed low-value terminal toasts for Studio websocket connect/disconnect that could overwrite more important notifications.
117
+
91
118
  ## [0.5.2] — 2026-03-06
92
119
 
93
120
  ### Changed
package/README.md CHANGED
@@ -14,11 +14,12 @@ Experimental extension for [pi](https://github.com/badlogic/pi-mono) that opens
14
14
 
15
15
  ## What it does
16
16
 
17
- - Opens a two-pane browser workspace: **Editor** (left) + **Response/Editor Preview** (right)
17
+ - Opens a two-pane browser workspace: **Editor** (left) + **Response/Thinking/Editor Preview** (right)
18
18
  - Runs editor text directly, or asks for structured critique (auto/writing/code focus)
19
19
  - Browses response history (`Prev/Next`) and loads either:
20
20
  - response text
21
21
  - critique notes/full critique
22
+ - assistant thinking (when available)
22
23
  - the prompt that generated a selected response
23
24
  - Supports an annotation workflow for `[an: ...]` markers:
24
25
  - inserts/removes the annotated-reply header
package/index.ts CHANGED
@@ -32,6 +32,7 @@ interface ActiveStudioRequest {
32
32
 
33
33
  interface LastStudioResponse {
34
34
  markdown: string;
35
+ thinking: string | null;
35
36
  timestamp: number;
36
37
  kind: StudioRequestKind;
37
38
  }
@@ -39,6 +40,7 @@ interface LastStudioResponse {
39
40
  interface StudioResponseHistoryItem {
40
41
  id: string;
41
42
  markdown: string;
43
+ thinking: string | null;
42
44
  timestamp: number;
43
45
  kind: StudioRequestKind;
44
46
  prompt: string | null;
@@ -1259,6 +1261,27 @@ function extractAssistantText(message: unknown): string | null {
1259
1261
  return text.length > 0 ? text : null;
1260
1262
  }
1261
1263
 
1264
+ function extractAssistantThinking(message: unknown): string | null {
1265
+ const msg = message as {
1266
+ role?: string;
1267
+ content?: Array<{ type?: string; thinking?: string }> | string;
1268
+ };
1269
+
1270
+ if (!msg || msg.role !== "assistant" || !Array.isArray(msg.content)) return null;
1271
+
1272
+ const blocks: string[] = [];
1273
+ for (const part of msg.content) {
1274
+ if (!part || typeof part !== "object") continue;
1275
+ if (part.type !== "thinking") continue;
1276
+ if (typeof part.thinking === "string" && part.thinking.trim()) {
1277
+ blocks.push(part.thinking);
1278
+ }
1279
+ }
1280
+
1281
+ const thinking = blocks.join("\n\n").trim();
1282
+ return thinking.length > 0 ? thinking : null;
1283
+ }
1284
+
1262
1285
  function extractLatestAssistantFromEntries(entries: SessionEntry[]): string | null {
1263
1286
  for (let i = entries.length - 1; i >= 0; i--) {
1264
1287
  const entry = entries[i];
@@ -1330,9 +1353,11 @@ function buildResponseHistoryFromEntries(entries: SessionEntry[], limit = RESPON
1330
1353
  if (role !== "assistant") continue;
1331
1354
  const markdown = extractAssistantText(message);
1332
1355
  if (!markdown) continue;
1356
+ const thinking = extractAssistantThinking(message);
1333
1357
  history.push({
1334
1358
  id: typeof (entry as { id?: unknown }).id === "string" ? (entry as { id: string }).id : randomUUID(),
1335
1359
  markdown,
1360
+ thinking,
1336
1361
  timestamp: parseEntryTimestamp((entry as { timestamp?: unknown }).timestamp),
1337
1362
  kind: inferStudioResponseKind(markdown),
1338
1363
  prompt: lastUserPrompt,
@@ -2889,6 +2914,7 @@ ${cssVarsBlock}
2889
2914
  <option value="markdown">Response (Raw)</option>
2890
2915
  <option value="preview" selected>Response (Preview)</option>
2891
2916
  <option value="editor-preview">Editor (Preview)</option>
2917
+ <option value="thinking">Thinking (Raw)</option>
2892
2918
  </select>
2893
2919
  </div>
2894
2920
  <div class="section-header-actions">
@@ -2931,7 +2957,7 @@ ${cssVarsBlock}
2931
2957
 
2932
2958
  <footer>
2933
2959
  <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>
2960
+ <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
2961
  <span class="shortcut-hint">Focus pane: Cmd/Ctrl+Esc (or F10), Esc to exit · Run editor text: Cmd/Ctrl+Enter</span>
2936
2962
  </footer>
2937
2963
 
@@ -3043,11 +3069,13 @@ ${cssVarsBlock}
3043
3069
  let followLatest = true;
3044
3070
  let queuedLatestResponse = null;
3045
3071
  let latestResponseMarkdown = "";
3072
+ let latestResponseThinking = "";
3046
3073
  let latestResponseTimestamp = 0;
3047
3074
  let latestResponseKind = "annotation";
3048
3075
  let latestResponseIsStructuredCritique = false;
3049
3076
  let latestResponseHasContent = false;
3050
3077
  let latestResponseNormalized = "";
3078
+ let latestResponseThinkingNormalized = "";
3051
3079
  let latestCritiqueNotes = "";
3052
3080
  let latestCritiqueNotesNormalized = "";
3053
3081
  let responseHistory = [];
@@ -3065,6 +3093,8 @@ ${cssVarsBlock}
3065
3093
  let contextTokens = null;
3066
3094
  let contextWindow = null;
3067
3095
  let contextPercent = null;
3096
+ let updateInstalledVersion = null;
3097
+ let updateLatestVersion = null;
3068
3098
 
3069
3099
  function parseFiniteNumber(value) {
3070
3100
  if (value == null || value === "") return null;
@@ -3072,6 +3102,12 @@ ${cssVarsBlock}
3072
3102
  return Number.isFinite(parsed) ? parsed : null;
3073
3103
  }
3074
3104
 
3105
+ function parseNonEmptyString(value) {
3106
+ if (typeof value !== "string") return null;
3107
+ const trimmed = value.trim();
3108
+ return trimmed ? trimmed : null;
3109
+ }
3110
+
3075
3111
  contextTokens = parseFiniteNumber(document.body && document.body.dataset ? document.body.dataset.contextTokens : null);
3076
3112
  contextWindow = parseFiniteNumber(document.body && document.body.dataset ? document.body.dataset.contextWindow : null);
3077
3113
  contextPercent = parseFiniteNumber(document.body && document.body.dataset ? document.body.dataset.contextPercent : null);
@@ -3199,6 +3235,12 @@ ${cssVarsBlock}
3199
3235
  if (typeof message.contextTokens === "number") summary.contextTokens = message.contextTokens;
3200
3236
  if (typeof message.contextWindow === "number") summary.contextWindow = message.contextWindow;
3201
3237
  if (typeof message.contextPercent === "number") summary.contextPercent = message.contextPercent;
3238
+ if (typeof message.updateInstalledVersion === "string") summary.updateInstalledVersion = message.updateInstalledVersion;
3239
+ if (typeof message.updateLatestVersion === "string") summary.updateLatestVersion = message.updateLatestVersion;
3240
+ if (message.document && typeof message.document === "object" && typeof message.document.text === "string") {
3241
+ summary.documentLength = message.document.text.length;
3242
+ if (typeof message.document.label === "string") summary.documentLabel = message.document.label;
3243
+ }
3202
3244
  if (typeof message.compactInProgress === "boolean") summary.compactInProgress = message.compactInProgress;
3203
3245
  if (typeof message.stopReason === "string") summary.stopReason = message.stopReason;
3204
3246
  if (typeof message.markdown === "string") summary.markdownLength = message.markdown.length;
@@ -3388,6 +3430,30 @@ ${cssVarsBlock}
3388
3430
  return changed;
3389
3431
  }
3390
3432
 
3433
+ function applyUpdateInfoFromMessage(message) {
3434
+ if (!message || typeof message !== "object") return false;
3435
+
3436
+ let changed = false;
3437
+
3438
+ if (Object.prototype.hasOwnProperty.call(message, "updateInstalledVersion")) {
3439
+ const nextInstalled = parseNonEmptyString(message.updateInstalledVersion);
3440
+ if (nextInstalled !== updateInstalledVersion) {
3441
+ updateInstalledVersion = nextInstalled;
3442
+ changed = true;
3443
+ }
3444
+ }
3445
+
3446
+ if (Object.prototype.hasOwnProperty.call(message, "updateLatestVersion")) {
3447
+ const nextLatest = parseNonEmptyString(message.updateLatestVersion);
3448
+ if (nextLatest !== updateLatestVersion) {
3449
+ updateLatestVersion = nextLatest;
3450
+ changed = true;
3451
+ }
3452
+ }
3453
+
3454
+ return changed;
3455
+ }
3456
+
3391
3457
  function updateDocumentTitle() {
3392
3458
  const modelText = modelLabel && modelLabel.trim() ? modelLabel.trim() : "none";
3393
3459
  const terminalText = terminalSessionLabel && terminalSessionLabel.trim() ? terminalSessionLabel.trim() : "unknown";
@@ -3401,7 +3467,13 @@ ${cssVarsBlock}
3401
3467
  const modelText = modelLabel && modelLabel.trim() ? modelLabel.trim() : "none";
3402
3468
  const terminalText = terminalSessionLabel && terminalSessionLabel.trim() ? terminalSessionLabel.trim() : "unknown";
3403
3469
  const contextText = formatContextUsageText();
3404
- const text = "Model: " + modelText + " · Terminal: " + terminalText + " · " + contextText;
3470
+ let updateText = "";
3471
+ if (updateLatestVersion) {
3472
+ updateText = updateInstalledVersion
3473
+ ? "Update: " + updateInstalledVersion + " → " + updateLatestVersion
3474
+ : "Update: " + updateLatestVersion + " available";
3475
+ }
3476
+ const text = "Model: " + modelText + " · Terminal: " + terminalText + " · " + contextText + (updateText ? " · " + updateText : "");
3405
3477
  if (footerMetaTextEl) {
3406
3478
  footerMetaTextEl.textContent = text;
3407
3479
  footerMetaTextEl.title = text;
@@ -3628,10 +3700,14 @@ ${cssVarsBlock}
3628
3700
  const prompt = typeof item.prompt === "string"
3629
3701
  ? item.prompt
3630
3702
  : (item.prompt == null ? null : String(item.prompt));
3703
+ const thinking = typeof item.thinking === "string"
3704
+ ? item.thinking
3705
+ : (item.thinking == null ? null : String(item.thinking));
3631
3706
 
3632
3707
  return {
3633
3708
  id,
3634
3709
  markdown,
3710
+ thinking,
3635
3711
  timestamp,
3636
3712
  kind: normalizeHistoryKind(item.kind),
3637
3713
  prompt,
@@ -3646,11 +3722,13 @@ ${cssVarsBlock}
3646
3722
 
3647
3723
  function clearActiveResponseView() {
3648
3724
  latestResponseMarkdown = "";
3725
+ latestResponseThinking = "";
3649
3726
  latestResponseKind = "annotation";
3650
3727
  latestResponseTimestamp = 0;
3651
3728
  latestResponseIsStructuredCritique = false;
3652
3729
  latestResponseHasContent = false;
3653
3730
  latestResponseNormalized = "";
3731
+ latestResponseThinkingNormalized = "";
3654
3732
  latestCritiqueNotes = "";
3655
3733
  latestCritiqueNotesNormalized = "";
3656
3734
  refreshResponseUi();
@@ -3687,7 +3765,7 @@ ${cssVarsBlock}
3687
3765
  clearActiveResponseView();
3688
3766
  return false;
3689
3767
  }
3690
- handleIncomingResponse(item.markdown, item.kind, item.timestamp);
3768
+ handleIncomingResponse(item.markdown, item.kind, item.timestamp, item.thinking);
3691
3769
  return true;
3692
3770
  }
3693
3771
 
@@ -3770,6 +3848,26 @@ ${cssVarsBlock}
3770
3848
  }
3771
3849
 
3772
3850
  const hasResponse = Boolean(latestResponseMarkdown && latestResponseMarkdown.trim());
3851
+ const hasThinking = Boolean(latestResponseThinking && latestResponseThinking.trim());
3852
+ if (rightView === "thinking") {
3853
+ if (!hasResponse && !hasThinking) {
3854
+ referenceBadgeEl.textContent = "Thinking: none";
3855
+ return;
3856
+ }
3857
+
3858
+ const time = formatReferenceTime(latestResponseTimestamp);
3859
+ const total = Array.isArray(responseHistory) ? responseHistory.length : 0;
3860
+ const selected = total > 0 && responseHistoryIndex >= 0 && responseHistoryIndex < total
3861
+ ? responseHistoryIndex + 1
3862
+ : 0;
3863
+ const historyPrefix = total > 0 ? "Response history " + selected + "/" + total + " · " : "";
3864
+ const thinkingLabel = hasThinking ? "assistant thinking" : "assistant thinking unavailable";
3865
+ referenceBadgeEl.textContent = time
3866
+ ? historyPrefix + thinkingLabel + " · " + time
3867
+ : historyPrefix + thinkingLabel;
3868
+ return;
3869
+ }
3870
+
3773
3871
  if (!hasResponse) {
3774
3872
  referenceBadgeEl.textContent = "Latest response: none";
3775
3873
  return;
@@ -3820,8 +3918,13 @@ ${cssVarsBlock}
3820
3918
  function updateSyncBadge(normalizedEditorText) {
3821
3919
  if (!syncBadgeEl) return;
3822
3920
 
3823
- if (!latestResponseHasContent) {
3824
- syncBadgeEl.textContent = "No response loaded";
3921
+ const showingThinking = rightView === "thinking";
3922
+ const hasComparableContent = showingThinking
3923
+ ? Boolean(latestResponseThinking && latestResponseThinking.trim())
3924
+ : latestResponseHasContent;
3925
+
3926
+ if (!hasComparableContent) {
3927
+ syncBadgeEl.textContent = showingThinking ? "No thinking loaded" : "No response loaded";
3825
3928
  syncBadgeEl.classList.remove("sync", "edited");
3826
3929
  return;
3827
3930
  }
@@ -3829,13 +3932,14 @@ ${cssVarsBlock}
3829
3932
  const normalizedEditor = typeof normalizedEditorText === "string"
3830
3933
  ? normalizedEditorText
3831
3934
  : normalizeForCompare(sourceTextEl.value);
3832
- const inSync = normalizedEditor === latestResponseNormalized;
3935
+ const targetNormalized = showingThinking ? latestResponseThinkingNormalized : latestResponseNormalized;
3936
+ const inSync = normalizedEditor === targetNormalized;
3833
3937
  if (inSync) {
3834
- syncBadgeEl.textContent = "In sync with response";
3938
+ syncBadgeEl.textContent = showingThinking ? "In sync with thinking" : "In sync with response";
3835
3939
  syncBadgeEl.classList.add("sync");
3836
3940
  syncBadgeEl.classList.remove("edited");
3837
3941
  } else {
3838
- syncBadgeEl.textContent = "Out of sync with response";
3942
+ syncBadgeEl.textContent = showingThinking ? "Out of sync with thinking" : "Out of sync with response";
3839
3943
  syncBadgeEl.classList.add("edited");
3840
3944
  syncBadgeEl.classList.remove("sync");
3841
3945
  }
@@ -4367,6 +4471,15 @@ ${cssVarsBlock}
4367
4471
  return;
4368
4472
  }
4369
4473
 
4474
+ if (rightView === "thinking") {
4475
+ const thinking = latestResponseThinking;
4476
+ finishPreviewRender(critiqueViewEl);
4477
+ critiqueViewEl.innerHTML = thinking && thinking.trim()
4478
+ ? buildPlainMarkdownHtml(thinking)
4479
+ : "<pre class='plain-markdown'>No thinking available for this response.</pre>";
4480
+ return;
4481
+ }
4482
+
4370
4483
  const markdown = latestResponseMarkdown;
4371
4484
  if (!markdown || !markdown.trim()) {
4372
4485
  finishPreviewRender(critiqueViewEl);
@@ -4402,36 +4515,56 @@ ${cssVarsBlock}
4402
4515
 
4403
4516
  function updateResultActionButtons(normalizedEditorText) {
4404
4517
  const hasResponse = latestResponseHasContent;
4518
+ const hasThinking = Boolean(latestResponseThinking && latestResponseThinking.trim());
4405
4519
  const normalizedEditor = typeof normalizedEditorText === "string"
4406
4520
  ? normalizedEditorText
4407
4521
  : normalizeForCompare(sourceTextEl.value);
4408
4522
  const responseLoaded = hasResponse && normalizedEditor === latestResponseNormalized;
4523
+ const thinkingLoaded = hasThinking && normalizedEditor === latestResponseThinkingNormalized;
4409
4524
  const isCritiqueResponse = hasResponse && latestResponseIsStructuredCritique;
4525
+ const showingThinking = rightView === "thinking";
4410
4526
 
4411
4527
  const critiqueNotes = isCritiqueResponse ? latestCritiqueNotes : "";
4412
4528
  const critiqueNotesLoaded = Boolean(critiqueNotes) && normalizedEditor === latestCritiqueNotesNormalized;
4413
4529
 
4414
- loadResponseBtn.hidden = isCritiqueResponse;
4415
- loadCritiqueNotesBtn.hidden = !isCritiqueResponse;
4416
- loadCritiqueFullBtn.hidden = !isCritiqueResponse;
4530
+ if (showingThinking) {
4531
+ loadResponseBtn.hidden = false;
4532
+ loadCritiqueNotesBtn.hidden = true;
4533
+ loadCritiqueFullBtn.hidden = true;
4417
4534
 
4418
- loadResponseBtn.disabled = uiBusy || !hasResponse || responseLoaded || isCritiqueResponse;
4419
- loadResponseBtn.textContent = responseLoaded ? "Response already in editor" : "Load response into editor";
4535
+ loadResponseBtn.disabled = uiBusy || !hasThinking || thinkingLoaded;
4536
+ loadResponseBtn.textContent = !hasThinking
4537
+ ? "Thinking unavailable"
4538
+ : (thinkingLoaded ? "Thinking already in editor" : "Load thinking into editor");
4420
4539
 
4421
- loadCritiqueNotesBtn.disabled = uiBusy || !isCritiqueResponse || !critiqueNotes || critiqueNotesLoaded;
4422
- loadCritiqueNotesBtn.textContent = critiqueNotesLoaded ? "Critique notes already in editor" : "Load critique notes into editor";
4540
+ copyResponseBtn.disabled = uiBusy || !hasThinking;
4541
+ copyResponseBtn.textContent = "Copy thinking text";
4542
+ } else {
4543
+ loadResponseBtn.hidden = isCritiqueResponse;
4544
+ loadCritiqueNotesBtn.hidden = !isCritiqueResponse;
4545
+ loadCritiqueFullBtn.hidden = !isCritiqueResponse;
4423
4546
 
4424
- loadCritiqueFullBtn.disabled = uiBusy || !isCritiqueResponse || responseLoaded;
4425
- loadCritiqueFullBtn.textContent = responseLoaded ? "Full critique already in editor" : "Load full critique into editor";
4547
+ loadResponseBtn.disabled = uiBusy || !hasResponse || responseLoaded || isCritiqueResponse;
4548
+ loadResponseBtn.textContent = responseLoaded ? "Response already in editor" : "Load response into editor";
4426
4549
 
4427
- copyResponseBtn.disabled = uiBusy || !hasResponse;
4550
+ loadCritiqueNotesBtn.disabled = uiBusy || !isCritiqueResponse || !critiqueNotes || critiqueNotesLoaded;
4551
+ loadCritiqueNotesBtn.textContent = critiqueNotesLoaded ? "Critique notes already in editor" : "Load critique notes into editor";
4552
+
4553
+ loadCritiqueFullBtn.disabled = uiBusy || !isCritiqueResponse || responseLoaded;
4554
+ loadCritiqueFullBtn.textContent = responseLoaded ? "Full critique already in editor" : "Load full critique into editor";
4555
+
4556
+ copyResponseBtn.disabled = uiBusy || !hasResponse;
4557
+ copyResponseBtn.textContent = "Copy response text";
4558
+ }
4428
4559
 
4429
4560
  const rightPaneShowsPreview = rightView === "preview" || rightView === "editor-preview";
4430
4561
  const exportText = rightView === "editor-preview" ? prepareEditorTextForPreview(sourceTextEl.value) : latestResponseMarkdown;
4431
4562
  const canExportPdf = rightPaneShowsPreview && Boolean(String(exportText || "").trim());
4432
4563
  if (exportPdfBtn) {
4433
4564
  exportPdfBtn.disabled = uiBusy || pdfExportInProgress || !canExportPdf;
4434
- if (rightView === "markdown") {
4565
+ if (rightView === "thinking") {
4566
+ exportPdfBtn.title = "Thinking view does not support PDF export yet.";
4567
+ } else if (rightView === "markdown") {
4435
4568
  exportPdfBtn.title = "Switch right pane to Response (Preview) or Editor (Preview) to export PDF.";
4436
4569
  } else if (!canExportPdf) {
4437
4570
  exportPdfBtn.title = "Nothing to export yet.";
@@ -4609,7 +4742,11 @@ ${cssVarsBlock}
4609
4742
  }
4610
4743
 
4611
4744
  function setRightView(nextView) {
4612
- rightView = nextView === "preview" ? "preview" : (nextView === "editor-preview" ? "editor-preview" : "markdown");
4745
+ rightView = nextView === "preview"
4746
+ ? "preview"
4747
+ : (nextView === "editor-preview"
4748
+ ? "editor-preview"
4749
+ : (nextView === "thinking" ? "thinking" : "markdown"));
4613
4750
  rightViewSelect.value = rightView;
4614
4751
 
4615
4752
  if (rightView !== "editor-preview" && responseEditorPreviewTimer) {
@@ -5286,18 +5423,20 @@ ${cssVarsBlock}
5286
5423
  return lower.indexOf("## critiques") !== -1 && lower.indexOf("## document") !== -1;
5287
5424
  }
5288
5425
 
5289
- function handleIncomingResponse(markdown, kind, timestamp) {
5426
+ function handleIncomingResponse(markdown, kind, timestamp, thinking) {
5290
5427
  const responseTimestamp =
5291
5428
  typeof timestamp === "number" && Number.isFinite(timestamp) && timestamp > 0
5292
5429
  ? timestamp
5293
5430
  : Date.now();
5294
5431
 
5295
5432
  latestResponseMarkdown = markdown;
5433
+ latestResponseThinking = typeof thinking === "string" ? thinking : "";
5296
5434
  latestResponseKind = kind === "critique" ? "critique" : "annotation";
5297
5435
  latestResponseTimestamp = responseTimestamp;
5298
5436
  latestResponseIsStructuredCritique = isStructuredCritique(markdown);
5299
5437
  latestResponseHasContent = Boolean(markdown && markdown.trim());
5300
5438
  latestResponseNormalized = normalizeForCompare(markdown);
5439
+ latestResponseThinkingNormalized = normalizeForCompare(latestResponseThinking);
5301
5440
 
5302
5441
  if (latestResponseIsStructuredCritique) {
5303
5442
  latestCritiqueNotes = buildCritiqueNotesMarkdown(markdown);
@@ -5313,7 +5452,7 @@ ${cssVarsBlock}
5313
5452
  function applyLatestPayload(payload) {
5314
5453
  if (!payload || typeof payload.markdown !== "string") return false;
5315
5454
  const responseKind = payload.kind === "critique" ? "critique" : "annotation";
5316
- handleIncomingResponse(payload.markdown, responseKind, payload.timestamp);
5455
+ handleIncomingResponse(payload.markdown, responseKind, payload.timestamp, payload.thinking);
5317
5456
  return true;
5318
5457
  }
5319
5458
 
@@ -5332,7 +5471,9 @@ ${cssVarsBlock}
5332
5471
 
5333
5472
  debugTrace("server_message", summarizeServerMessage(message));
5334
5473
 
5335
- if (applyContextUsageFromMessage(message)) {
5474
+ const contextChanged = applyContextUsageFromMessage(message);
5475
+ const updateInfoChanged = applyUpdateInfoFromMessage(message);
5476
+ if (contextChanged || updateInfoChanged) {
5336
5477
  updateFooterMeta();
5337
5478
  }
5338
5479
 
@@ -5410,7 +5551,7 @@ ${cssVarsBlock}
5410
5551
  message.lastResponse.kind === "critique"
5411
5552
  ? "critique"
5412
5553
  : (isStructuredCritique(lastMarkdown) ? "critique" : "annotation");
5413
- handleIncomingResponse(lastMarkdown, lastResponseKind, message.lastResponse.timestamp);
5554
+ handleIncomingResponse(lastMarkdown, lastResponseKind, message.lastResponse.timestamp, message.lastResponse.thinking);
5414
5555
  }
5415
5556
 
5416
5557
  if (pendingRequestId) {
@@ -5507,7 +5648,7 @@ ${cssVarsBlock}
5507
5648
  }
5508
5649
 
5509
5650
  if (!appliedFromHistory && typeof message.markdown === "string") {
5510
- handleIncomingResponse(message.markdown, responseKind, message.timestamp);
5651
+ handleIncomingResponse(message.markdown, responseKind, message.timestamp, message.thinking);
5511
5652
  }
5512
5653
 
5513
5654
  if (responseKind === "critique") {
@@ -5536,6 +5677,7 @@ ${cssVarsBlock}
5536
5677
  const payload = {
5537
5678
  kind: message.kind === "critique" ? "critique" : "annotation",
5538
5679
  markdown: message.markdown,
5680
+ thinking: typeof message.thinking === "string" ? message.thinking : null,
5539
5681
  timestamp: message.timestamp,
5540
5682
  };
5541
5683
 
@@ -5621,6 +5763,35 @@ ${cssVarsBlock}
5621
5763
  return;
5622
5764
  }
5623
5765
 
5766
+ if (message.type === "studio_document") {
5767
+ const nextDoc = message.document;
5768
+ if (!nextDoc || typeof nextDoc !== "object" || typeof nextDoc.text !== "string") {
5769
+ return;
5770
+ }
5771
+
5772
+ const nextSource =
5773
+ nextDoc.source === "file" || nextDoc.source === "last-response"
5774
+ ? nextDoc.source
5775
+ : "blank";
5776
+ const nextLabel = typeof nextDoc.label === "string" && nextDoc.label.trim()
5777
+ ? nextDoc.label.trim()
5778
+ : (nextSource === "file" ? "file" : "studio document");
5779
+ const nextPath = typeof nextDoc.path === "string" && nextDoc.path.trim()
5780
+ ? nextDoc.path
5781
+ : null;
5782
+
5783
+ setEditorText(nextDoc.text, { preserveScroll: false, preserveSelection: false });
5784
+ setSourceState({ source: nextSource, label: nextLabel, path: nextPath });
5785
+ refreshResponseUi();
5786
+ setStatus(
5787
+ typeof message.message === "string" && message.message.trim()
5788
+ ? message.message
5789
+ : "Loaded document from terminal.",
5790
+ "success",
5791
+ );
5792
+ return;
5793
+ }
5794
+
5624
5795
  if (message.type === "studio_state") {
5625
5796
  const busy = Boolean(message.busy);
5626
5797
  agentBusyFromServer = Boolean(message.agentBusy);
@@ -6097,6 +6268,17 @@ ${cssVarsBlock}
6097
6268
  });
6098
6269
 
6099
6270
  loadResponseBtn.addEventListener("click", () => {
6271
+ if (rightView === "thinking") {
6272
+ if (!latestResponseThinking.trim()) {
6273
+ setStatus("No thinking available for the selected response.", "warning");
6274
+ return;
6275
+ }
6276
+ setEditorText(latestResponseThinking, { preserveScroll: false, preserveSelection: false });
6277
+ setSourceState({ source: "blank", label: "assistant thinking", path: null });
6278
+ setStatus("Loaded thinking into editor.", "success");
6279
+ return;
6280
+ }
6281
+
6100
6282
  if (!latestResponseMarkdown.trim()) {
6101
6283
  setStatus("No response available yet.", "warning");
6102
6284
  return;
@@ -6135,14 +6317,15 @@ ${cssVarsBlock}
6135
6317
  });
6136
6318
 
6137
6319
  copyResponseBtn.addEventListener("click", async () => {
6138
- if (!latestResponseMarkdown.trim()) {
6139
- setStatus("No response available yet.", "warning");
6320
+ const content = rightView === "thinking" ? latestResponseThinking : latestResponseMarkdown;
6321
+ if (!content.trim()) {
6322
+ setStatus(rightView === "thinking" ? "No thinking available yet." : "No response available yet.", "warning");
6140
6323
  return;
6141
6324
  }
6142
6325
 
6143
6326
  try {
6144
- await navigator.clipboard.writeText(latestResponseMarkdown);
6145
- setStatus("Copied response text.", "success");
6327
+ await navigator.clipboard.writeText(content);
6328
+ setStatus(rightView === "thinking" ? "Copied thinking text." : "Copied response text.", "success");
6146
6329
  } catch (error) {
6147
6330
  setStatus("Clipboard write failed.", "warning");
6148
6331
  }
@@ -6487,6 +6670,8 @@ export default function (pi: ExtensionAPI) {
6487
6670
  let updateCheckStarted = false;
6488
6671
  let updateCheckCompleted = false;
6489
6672
  const packageMetadata = readLocalPackageMetadata();
6673
+ const installedPackageVersion = packageMetadata?.version ?? null;
6674
+ let updateAvailableLatestVersion: string | null = null;
6490
6675
 
6491
6676
  const isStudioBusy = () => agentBusy || activeRequest !== null || compactInProgress;
6492
6677
 
@@ -6558,6 +6743,7 @@ export default function (pi: ExtensionAPI) {
6558
6743
  }
6559
6744
  lastStudioResponse = {
6560
6745
  markdown: latest.markdown,
6746
+ thinking: latest.thinking,
6561
6747
  timestamp: latest.timestamp,
6562
6748
  kind: latest.kind,
6563
6749
  };
@@ -6579,10 +6765,14 @@ export default function (pi: ExtensionAPI) {
6579
6765
  const latest = await fetchLatestNpmVersion(metadata.name, UPDATE_CHECK_TIMEOUT_MS);
6580
6766
  if (!latest) return;
6581
6767
  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
- );
6768
+
6769
+ updateAvailableLatestVersion = latest;
6770
+ broadcastState();
6771
+
6772
+ const notification =
6773
+ `Update available for ${metadata.name}: ${metadata.version} → ${latest}. Run: pi install npm:${metadata.name}`;
6774
+ ctx.ui.notify(notification, "info");
6775
+ broadcast({ type: "info", message: notification, level: "info" });
6586
6776
  } finally {
6587
6777
  updateCheckCompleted = true;
6588
6778
  }
@@ -6689,6 +6879,8 @@ export default function (pi: ExtensionAPI) {
6689
6879
  contextTokens: contextUsageSnapshot.tokens,
6690
6880
  contextWindow: contextUsageSnapshot.contextWindow,
6691
6881
  contextPercent: contextUsageSnapshot.percent,
6882
+ updateInstalledVersion: installedPackageVersion,
6883
+ updateLatestVersion: updateAvailableLatestVersion,
6692
6884
  compactInProgress,
6693
6885
  activeRequestId: activeRequest?.id ?? compactRequestId ?? null,
6694
6886
  activeRequestKind: activeRequest?.kind ?? (compactInProgress ? "compact" : null),
@@ -6793,6 +6985,8 @@ export default function (pi: ExtensionAPI) {
6793
6985
  contextTokens: contextUsageSnapshot.tokens,
6794
6986
  contextWindow: contextUsageSnapshot.contextWindow,
6795
6987
  contextPercent: contextUsageSnapshot.percent,
6988
+ updateInstalledVersion: installedPackageVersion,
6989
+ updateLatestVersion: updateAvailableLatestVersion,
6796
6990
  compactInProgress,
6797
6991
  activeRequestId: activeRequest?.id ?? compactRequestId ?? null,
6798
6992
  activeRequestKind: activeRequest?.kind ?? (compactInProgress ? "compact" : null),
@@ -6812,6 +7006,7 @@ export default function (pi: ExtensionAPI) {
6812
7006
  type: "latest_response",
6813
7007
  kind: lastStudioResponse.kind,
6814
7008
  markdown: lastStudioResponse.markdown,
7009
+ thinking: lastStudioResponse.thinking,
6815
7010
  timestamp: lastStudioResponse.timestamp,
6816
7011
  responseHistory: studioResponseHistory,
6817
7012
  });
@@ -7424,7 +7619,7 @@ export default function (pi: ExtensionAPI) {
7424
7619
 
7425
7620
  wsServer.on("connection", (ws) => {
7426
7621
  clients.add(ws);
7427
- notifyStudio("Studio browser websocket connected.", "info");
7622
+ emitDebugEvent("studio_ws_connected", { clients: clients.size });
7428
7623
  broadcastState();
7429
7624
 
7430
7625
  ws.on("message", (data) => {
@@ -7438,7 +7633,7 @@ export default function (pi: ExtensionAPI) {
7438
7633
 
7439
7634
  ws.on("close", () => {
7440
7635
  clients.delete(ws);
7441
- notifyStudio("Studio browser websocket disconnected.", "warning");
7636
+ emitDebugEvent("studio_ws_disconnected", { clients: clients.size });
7442
7637
  });
7443
7638
 
7444
7639
  ws.on("error", () => {
@@ -7626,11 +7821,14 @@ export default function (pi: ExtensionAPI) {
7626
7821
  const stopReason = typeof message.stopReason === "string" ? message.stopReason : "";
7627
7822
  const role = typeof message.role === "string" ? message.role : "";
7628
7823
  const markdown = extractAssistantText(event.message);
7824
+ const thinking = extractAssistantThinking(event.message);
7629
7825
  emitDebugEvent("message_end", {
7630
7826
  role,
7631
7827
  stopReason,
7632
7828
  hasMarkdown: Boolean(markdown),
7633
7829
  markdownLength: markdown ? markdown.length : 0,
7830
+ hasThinking: Boolean(thinking),
7831
+ thinkingLength: thinking ? thinking.length : 0,
7634
7832
  activeRequestId: activeRequest?.id ?? null,
7635
7833
  activeRequestKind: activeRequest?.kind ?? null,
7636
7834
  });
@@ -7657,6 +7855,7 @@ export default function (pi: ExtensionAPI) {
7657
7855
  const fallbackHistoryItem: StudioResponseHistoryItem = {
7658
7856
  id: randomUUID(),
7659
7857
  markdown,
7858
+ thinking,
7660
7859
  timestamp: Date.now(),
7661
7860
  kind: inferStudioResponseKind(markdown),
7662
7861
  prompt: fallbackPrompt,
@@ -7667,12 +7866,14 @@ export default function (pi: ExtensionAPI) {
7667
7866
 
7668
7867
  const latestItem = studioResponseHistory[studioResponseHistory.length - 1];
7669
7868
  const responseTimestamp = latestItem?.timestamp ?? Date.now();
7869
+ const responseThinking = latestItem?.thinking ?? thinking ?? null;
7670
7870
 
7671
7871
  if (activeRequest) {
7672
7872
  const requestId = activeRequest.id;
7673
7873
  const kind = activeRequest.kind;
7674
7874
  lastStudioResponse = {
7675
7875
  markdown,
7876
+ thinking: responseThinking,
7676
7877
  timestamp: responseTimestamp,
7677
7878
  kind,
7678
7879
  };
@@ -7680,6 +7881,7 @@ export default function (pi: ExtensionAPI) {
7680
7881
  requestId,
7681
7882
  kind,
7682
7883
  markdownLength: markdown.length,
7884
+ thinkingLength: responseThinking ? responseThinking.length : 0,
7683
7885
  stopReason,
7684
7886
  });
7685
7887
  broadcast({
@@ -7687,6 +7889,7 @@ export default function (pi: ExtensionAPI) {
7687
7889
  requestId,
7688
7890
  kind,
7689
7891
  markdown,
7892
+ thinking: lastStudioResponse.thinking,
7690
7893
  timestamp: lastStudioResponse.timestamp,
7691
7894
  responseHistory: studioResponseHistory,
7692
7895
  });
@@ -7698,18 +7901,21 @@ export default function (pi: ExtensionAPI) {
7698
7901
  const inferredKind = inferStudioResponseKind(markdown);
7699
7902
  lastStudioResponse = {
7700
7903
  markdown,
7904
+ thinking: responseThinking,
7701
7905
  timestamp: responseTimestamp,
7702
7906
  kind: inferredKind,
7703
7907
  };
7704
7908
  emitDebugEvent("broadcast_latest_response", {
7705
7909
  kind: inferredKind,
7706
7910
  markdownLength: markdown.length,
7911
+ thinkingLength: responseThinking ? responseThinking.length : 0,
7707
7912
  stopReason,
7708
7913
  });
7709
7914
  broadcast({
7710
7915
  type: "latest_response",
7711
7916
  kind: inferredKind,
7712
7917
  markdown,
7918
+ thinking: lastStudioResponse.thinking,
7713
7919
  timestamp: lastStudioResponse.timestamp,
7714
7920
  responseHistory: studioResponseHistory,
7715
7921
  });
@@ -7771,7 +7977,8 @@ export default function (pi: ExtensionAPI) {
7771
7977
  + " /studio --blank Open with blank editor\n"
7772
7978
  + " /studio --last Open with last model response\n"
7773
7979
  + " /studio --status Show studio status\n"
7774
- + " /studio --stop Stop studio server",
7980
+ + " /studio --stop Stop studio server\n"
7981
+ + " /studio-current <path> Load a file into currently open Studio tab(s)",
7775
7982
  "info",
7776
7983
  );
7777
7984
  return;
@@ -7784,7 +7991,6 @@ export default function (pi: ExtensionAPI) {
7784
7991
  syncStudioResponseHistory(ctx.sessionManager.getBranch());
7785
7992
  broadcastState();
7786
7993
  broadcastResponseHistory();
7787
- void maybeNotifyUpdateAvailable(ctx);
7788
7994
  // Seed theme vars so first ping doesn't trigger a false update
7789
7995
  try {
7790
7996
  const currentStyle = getStudioThemeStyle(ctx.ui.theme);
@@ -7884,7 +8090,71 @@ export default function (pi: ExtensionAPI) {
7884
8090
  ctx.ui.notify(`Studio URL: ${url}`, "info");
7885
8091
  } catch (error) {
7886
8092
  ctx.ui.notify(`Failed to open browser: ${error instanceof Error ? error.message : String(error)}`, "error");
8093
+ } finally {
8094
+ void maybeNotifyUpdateAvailable(ctx);
8095
+ }
8096
+ },
8097
+ });
8098
+
8099
+ pi.registerCommand("studio-current", {
8100
+ description: "Load a file into current open Studio tab(s) without opening a new browser session",
8101
+ handler: async (args: string, ctx: ExtensionCommandContext) => {
8102
+ const trimmed = args.trim();
8103
+ if (!trimmed || trimmed === "help" || trimmed === "--help" || trimmed === "-h") {
8104
+ ctx.ui.notify(
8105
+ "Usage: /studio-current <path>\n"
8106
+ + " Load a file into currently open Studio tab(s) without opening a new browser window.",
8107
+ "info",
8108
+ );
8109
+ return;
8110
+ }
8111
+
8112
+ const pathArg = parsePathArgument(trimmed);
8113
+ if (!pathArg) {
8114
+ ctx.ui.notify("Invalid file path argument.", "error");
8115
+ return;
8116
+ }
8117
+
8118
+ const file = readStudioFile(pathArg, ctx.cwd);
8119
+ if (!file.ok) {
8120
+ ctx.ui.notify(file.message, "error");
8121
+ return;
8122
+ }
8123
+
8124
+ if (!serverState || serverState.clients.size === 0) {
8125
+ ctx.ui.notify("No open Studio tab is connected. Run /studio first.", "warning");
8126
+ return;
8127
+ }
8128
+
8129
+ await ctx.waitForIdle();
8130
+ lastCommandCtx = ctx;
8131
+ refreshRuntimeMetadata({ cwd: ctx.cwd, model: ctx.model });
8132
+ refreshContextUsage(ctx);
8133
+ syncStudioResponseHistory(ctx.sessionManager.getBranch());
8134
+
8135
+ const nextDoc: InitialStudioDocument = {
8136
+ text: file.text,
8137
+ label: file.label,
8138
+ source: "file",
8139
+ path: file.resolvedPath,
8140
+ };
8141
+ initialStudioDocument = nextDoc;
8142
+
8143
+ broadcastState();
8144
+ broadcastResponseHistory();
8145
+ broadcast({
8146
+ type: "studio_document",
8147
+ document: nextDoc,
8148
+ message: `Loaded ${file.label} from terminal command.`,
8149
+ });
8150
+
8151
+ if (file.text.length > 200_000) {
8152
+ ctx.ui.notify(
8153
+ "Loaded a large file into Studio. Critique requests currently reject documents over 200k characters.",
8154
+ "warning",
8155
+ );
7887
8156
  }
8157
+ ctx.ui.notify(`Loaded file into open Studio tab(s): ${file.label}`, "info");
7888
8158
  },
7889
8159
  });
7890
8160
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-studio",
3
- "version": "0.5.2",
3
+ "version": "0.5.4",
4
4
  "description": "Browser GUI for structured critique workflows in pi",
5
5
  "type": "module",
6
6
  "license": "MIT",