pi-studio 0.5.41 → 0.5.42

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
@@ -4,6 +4,19 @@ All notable changes to `pi-studio` are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.5.42] — 2026-03-31
8
+
9
+ ### Added
10
+ - Studio now includes a **Refresh from disk** action for file-backed documents, including files originally opened from disk and documents later saved to disk from Studio.
11
+
12
+ ### Changed
13
+ - `/studio-pdf` section, subsection, and deeper heading styling is now more robust for larger-font exports: headings avoid awkward hyphenation, paragraph-level headings (`####`) render cleanly, callout title badges scale better with larger body font sizes, and exported code blocks now use a subtle shaded background.
14
+
15
+ ### Fixed
16
+ - Re-selecting the same file in **Load file content** now reloads it reliably instead of sometimes doing nothing.
17
+ - `/studio-pdf` now supports LaTeX `[H]` float placement in exported documents.
18
+ - Response preview now resets to the top more reliably for genuinely new replies, while keeping editor preview behavior unchanged.
19
+
7
20
  ## [0.5.41] — 2026-03-30
8
21
 
9
22
  ### Changed
@@ -46,7 +46,7 @@
46
46
  const rightPaneEl = document.getElementById("rightPane");
47
47
  const sourceBadgeEl = document.getElementById("sourceBadge");
48
48
  const syncBadgeEl = document.getElementById("syncBadge");
49
- const critiqueViewEl = document.getElementById("critiqueView");
49
+ let critiqueViewEl = document.getElementById("critiqueView");
50
50
  const referenceBadgeEl = document.getElementById("referenceBadge");
51
51
  const editorViewSelect = document.getElementById("editorViewSelect");
52
52
  const rightViewSelect = document.getElementById("rightViewSelect");
@@ -74,6 +74,7 @@
74
74
  const loadHistoryPromptBtn = document.getElementById("loadHistoryPromptBtn");
75
75
  const saveAsBtn = document.getElementById("saveAsBtn");
76
76
  const saveOverBtn = document.getElementById("saveOverBtn");
77
+ const refreshFromDiskBtn = document.getElementById("refreshFromDiskBtn");
77
78
  const sendEditorBtn = document.getElementById("sendEditorBtn");
78
79
  const getEditorBtn = document.getElementById("getEditorBtn");
79
80
  const loadGitDiffBtn = document.getElementById("loadGitDiffBtn");
@@ -196,6 +197,7 @@
196
197
  label: initialSourceState.label,
197
198
  path: initialSourceState.path,
198
199
  };
200
+ let fileBackedBaselineText = null;
199
201
  let activePane = "left";
200
202
  let paneFocusTarget = "off";
201
203
  const EDITOR_HIGHLIGHT_MAX_CHARS = 100_000;
@@ -427,6 +429,7 @@
427
429
  if (kind === "send_to_editor") return "sending to pi editor";
428
430
  if (kind === "get_from_editor") return "loading from pi editor";
429
431
  if (kind === "load_git_diff") return "loading git diff";
432
+ if (kind === "refresh_from_disk") return "refreshing from disk";
430
433
  if (kind === "save_as" || kind === "save_over") return "saving editor text";
431
434
  return "submitting request";
432
435
  }
@@ -779,11 +782,29 @@
779
782
  }
780
783
  });
781
784
 
785
+ function markFileBackedBaseline(text) {
786
+ fileBackedBaselineText = String(text || "");
787
+ }
788
+
789
+ function clearFileBackedBaseline() {
790
+ fileBackedBaselineText = null;
791
+ }
792
+
793
+ function hasRefreshableFilePath() {
794
+ return Boolean(sourceState && sourceState.path);
795
+ }
796
+
797
+ function editorDiffersFromFileBackedBaseline() {
798
+ if (!hasRefreshableFilePath()) return false;
799
+ if (fileBackedBaselineText === null) return true;
800
+ return sourceTextEl.value !== fileBackedBaselineText;
801
+ }
802
+
782
803
  function updateSourceBadge() {
783
804
  const label = sourceState && sourceState.label ? sourceState.label : "blank";
784
805
  sourceBadgeEl.textContent = "Editor origin: " + label;
785
806
  // Show "Set working dir" button when not file-backed
786
- var isFileBacked = sourceState.source === "file" && Boolean(sourceState.path);
807
+ var isFileBacked = hasRefreshableFilePath();
787
808
  if (isFileBacked) {
788
809
  if (resourceDirInput) resourceDirInput.value = "";
789
810
  if (resourceDirLabel) resourceDirLabel.textContent = "";
@@ -1926,12 +1947,52 @@
1926
1947
  });
1927
1948
  }
1928
1949
 
1950
+ function replaceResponsePaneWithClone() {
1951
+ const currentEl = critiqueViewEl;
1952
+ if (!currentEl || !currentEl.parentNode || typeof currentEl.cloneNode !== "function") {
1953
+ return currentEl;
1954
+ }
1955
+
1956
+ const replacement = currentEl.cloneNode(true);
1957
+ if (!replacement || replacement.nodeType !== 1) {
1958
+ return currentEl;
1959
+ }
1960
+
1961
+ currentEl.parentNode.replaceChild(replacement, currentEl);
1962
+ critiqueViewEl = replacement;
1963
+ return critiqueViewEl;
1964
+ }
1965
+
1929
1966
  function applyPendingResponseScrollReset() {
1930
1967
  if (!pendingResponseScrollReset || !critiqueViewEl) return false;
1931
1968
  if (rightView === "editor-preview") return false;
1932
- critiqueViewEl.scrollTop = 0;
1933
- critiqueViewEl.scrollLeft = 0;
1969
+
1934
1970
  pendingResponseScrollReset = false;
1971
+ let targetEl = replaceResponsePaneWithClone();
1972
+ const schedule = typeof window.requestAnimationFrame === "function"
1973
+ ? window.requestAnimationFrame.bind(window)
1974
+ : (cb) => window.setTimeout(cb, 16);
1975
+ const resetScroll = () => {
1976
+ if (!targetEl || !targetEl.isConnected) return;
1977
+ if (rightView === "editor-preview") return;
1978
+ targetEl.scrollTop = 0;
1979
+ targetEl.scrollLeft = 0;
1980
+ };
1981
+
1982
+ if (targetEl && targetEl.classList) {
1983
+ targetEl.classList.add("response-scroll-resetting");
1984
+ }
1985
+
1986
+ resetScroll();
1987
+ schedule(() => {
1988
+ resetScroll();
1989
+ schedule(() => {
1990
+ resetScroll();
1991
+ if (targetEl && targetEl.classList) {
1992
+ targetEl.classList.remove("response-scroll-resetting");
1993
+ }
1994
+ });
1995
+ });
1935
1996
  return true;
1936
1997
  }
1937
1998
 
@@ -2518,7 +2579,7 @@
2518
2579
 
2519
2580
  function getEffectiveSavePath() {
2520
2581
  // File-backed: use the original path
2521
- if (sourceState.source === "file" && sourceState.path) return sourceState.path;
2582
+ if (sourceState.path) return sourceState.path;
2522
2583
  // Upload with working dir + filename: derive path
2523
2584
  if (sourceState.source === "upload" && sourceState.label && resourceDirInput && resourceDirInput.value.trim()) {
2524
2585
  var name = sourceState.label.replace(/^upload:\s*/i, "");
@@ -2557,12 +2618,25 @@
2557
2618
  saveOverBtn.title = "Save editor is available after opening a file, setting a working dir, or using Save editor as…. Shortcut: Cmd/Ctrl+S falls back to Save editor as… when needed.";
2558
2619
  }
2559
2620
 
2621
+ function updateRefreshFromDiskTooltip() {
2622
+ if (!refreshFromDiskBtn) return;
2623
+
2624
+ if (hasRefreshableFilePath()) {
2625
+ refreshFromDiskBtn.title = "Reload the current file-backed document from disk: " + sourceState.path;
2626
+ return;
2627
+ }
2628
+
2629
+ refreshFromDiskBtn.title = "Refresh from disk is only available for documents that currently have a file path.";
2630
+ }
2631
+
2560
2632
  function syncActionButtons() {
2561
2633
  const canSaveOver = Boolean(getEffectiveSavePath());
2634
+ const canRefreshFromDisk = hasRefreshableFilePath();
2562
2635
 
2563
2636
  fileInput.disabled = uiBusy;
2564
2637
  saveAsBtn.disabled = uiBusy;
2565
2638
  saveOverBtn.disabled = uiBusy || !canSaveOver;
2639
+ if (refreshFromDiskBtn) refreshFromDiskBtn.disabled = uiBusy || !canRefreshFromDisk;
2566
2640
  sendEditorBtn.disabled = uiBusy || isEditorOnlyMode;
2567
2641
  if (getEditorBtn) getEditorBtn.disabled = uiBusy;
2568
2642
  if (loadGitDiffBtn) loadGitDiffBtn.disabled = uiBusy;
@@ -2581,6 +2655,7 @@
2581
2655
  insertHeaderBtn.disabled = uiBusy || isEditorOnlyMode;
2582
2656
  lensSelect.disabled = uiBusy || isEditorOnlyMode;
2583
2657
  updateSaveFileTooltip();
2658
+ updateRefreshFromDiskTooltip();
2584
2659
  updateHistoryControls();
2585
2660
  updateResultActionButtons();
2586
2661
  }
@@ -2598,6 +2673,9 @@
2598
2673
  label: next && next.label ? next.label : "blank",
2599
2674
  path: next && next.path ? next.path : null,
2600
2675
  };
2676
+ if (!sourceState.path) {
2677
+ clearFileBackedBaseline();
2678
+ }
2601
2679
  updateSourceBadge();
2602
2680
  syncActionButtons();
2603
2681
  }
@@ -3778,6 +3856,9 @@
3778
3856
  label: message.initialDocument.label || "blank",
3779
3857
  path: message.initialDocument.path || null,
3780
3858
  });
3859
+ if (message.initialDocument.path) {
3860
+ markFileBackedBaseline(message.initialDocument.text);
3861
+ }
3781
3862
  refreshResponseUi();
3782
3863
  if (typeof message.initialDocument.label === "string" && message.initialDocument.label.length > 0) {
3783
3864
  setStatus("Loaded " + message.initialDocument.label + ".", "success");
@@ -3978,6 +4059,8 @@
3978
4059
  if (typeof message.requestId === "string" && pendingRequestId === message.requestId) {
3979
4060
  pendingRequestId = null;
3980
4061
  pendingKind = null;
4062
+ clearArmedTitleAttention(message.requestId);
4063
+ stickyStudioKind = null;
3981
4064
  }
3982
4065
  if (message.path) {
3983
4066
  setSourceState({
@@ -3985,6 +4068,7 @@
3985
4068
  label: message.label || message.path,
3986
4069
  path: message.path,
3987
4070
  });
4071
+ markFileBackedBaseline(sourceTextEl.value);
3988
4072
  }
3989
4073
  setBusy(false);
3990
4074
  setWsState("Ready");
@@ -4032,6 +4116,15 @@
4032
4116
  return;
4033
4117
  }
4034
4118
 
4119
+ if (typeof message.requestId === "string" && pendingRequestId === message.requestId) {
4120
+ pendingRequestId = null;
4121
+ pendingKind = null;
4122
+ clearArmedTitleAttention(message.requestId);
4123
+ stickyStudioKind = null;
4124
+ setBusy(false);
4125
+ setWsState("Ready");
4126
+ }
4127
+
4035
4128
  const nextSource =
4036
4129
  nextDoc.source === "file" || nextDoc.source === "last-response"
4037
4130
  ? nextDoc.source
@@ -4045,6 +4138,9 @@
4045
4138
 
4046
4139
  setEditorText(nextDoc.text, { preserveScroll: false, preserveSelection: false });
4047
4140
  setSourceState({ source: nextSource, label: nextLabel, path: nextPath });
4141
+ if (nextPath) {
4142
+ markFileBackedBaseline(nextDoc.text);
4143
+ }
4048
4144
  refreshResponseUi();
4049
4145
  setStatus(
4050
4146
  typeof message.message === "string" && message.message.trim()
@@ -4828,6 +4924,34 @@
4828
4924
  }
4829
4925
  });
4830
4926
 
4927
+ if (refreshFromDiskBtn) {
4928
+ refreshFromDiskBtn.addEventListener("click", () => {
4929
+ if (!hasRefreshableFilePath()) {
4930
+ setStatus("Refresh from disk is only available for file-backed documents.", "warning");
4931
+ return;
4932
+ }
4933
+
4934
+ if (editorDiffersFromFileBackedBaseline()) {
4935
+ const confirmed = window.confirm("Replace current editor contents with the latest version from disk?");
4936
+ if (!confirmed) return;
4937
+ }
4938
+
4939
+ const requestId = beginUiAction("refresh_from_disk");
4940
+ if (!requestId) return;
4941
+
4942
+ const sent = sendMessage({
4943
+ type: "refresh_from_disk_request",
4944
+ requestId,
4945
+ });
4946
+
4947
+ if (!sent) {
4948
+ pendingRequestId = null;
4949
+ pendingKind = null;
4950
+ setBusy(false);
4951
+ }
4952
+ });
4953
+ }
4954
+
4831
4955
  sendEditorBtn.addEventListener("click", () => {
4832
4956
  const content = sourceTextEl.value;
4833
4957
  if (!content.trim()) {
@@ -5136,6 +5260,10 @@
5136
5260
  const file = fileInput.files && fileInput.files[0];
5137
5261
  if (!file) return;
5138
5262
 
5263
+ // Clear the input immediately so selecting the same file again will
5264
+ // still fire a future change event.
5265
+ fileInput.value = "";
5266
+
5139
5267
  const reader = new FileReader();
5140
5268
  reader.onload = () => {
5141
5269
  const text = typeof reader.result === "string" ? reader.result : "";
package/client/studio.css CHANGED
@@ -1220,6 +1220,10 @@
1220
1220
  transform: translateZ(0);
1221
1221
  }
1222
1222
 
1223
+ .panel-scroll.response-scroll-resetting {
1224
+ overflow-anchor: none;
1225
+ }
1226
+
1223
1227
  .preview-error {
1224
1228
  color: var(--warn);
1225
1229
  margin-bottom: 0.75em;
package/index.ts CHANGED
@@ -159,6 +159,11 @@ interface SaveOverRequestMessage {
159
159
  content: string;
160
160
  }
161
161
 
162
+ interface RefreshFromDiskRequestMessage {
163
+ type: "refresh_from_disk_request";
164
+ requestId: string;
165
+ }
166
+
162
167
  interface SendToEditorRequestMessage {
163
168
  type: "send_to_editor_request";
164
169
  requestId: string;
@@ -192,6 +197,7 @@ type IncomingStudioMessage =
192
197
  | CompactRequestMessage
193
198
  | SaveAsRequestMessage
194
199
  | SaveOverRequestMessage
200
+ | RefreshFromDiskRequestMessage
195
201
  | SendToEditorRequestMessage
196
202
  | GetFromEditorRequestMessage
197
203
  | LoadGitDiffRequestMessage
@@ -234,25 +240,38 @@ function buildStudioPdfTitleSpacingLength(value: string | undefined, fallback: s
234
240
  return trimmed || fallback;
235
241
  }
236
242
 
243
+ function buildStudioPdfCalloutTitleSizeCommand(options?: StudioPdfRenderOptions): string {
244
+ const sizePt = getStudioRequestedPdfFontsizePt(options);
245
+ if (sizePt && sizePt >= 14) return "\\normalsize";
246
+ if (sizePt && sizePt >= 13) return "\\small";
247
+ return "\\footnotesize";
248
+ }
249
+
237
250
  function buildStudioPdfPreamble(options?: StudioPdfRenderOptions): string {
238
251
  const sectionHeadingSize = buildStudioPdfHeadingSizeCommand(options?.sectionSize, "\\Large");
239
252
  const subsectionHeadingSize = buildStudioPdfHeadingSizeCommand(options?.subsectionSize, "\\large");
240
253
  const subsubsectionHeadingSize = buildStudioPdfHeadingSizeCommand(options?.subsubsectionSize, "\\normalsize");
254
+ const calloutTitleSize = buildStudioPdfCalloutTitleSizeCommand(options);
241
255
  const sectionSpaceBefore = buildStudioPdfTitleSpacingLength(options?.sectionSpaceBefore, "1.5ex plus 0.5ex minus 0.2ex");
242
256
  const sectionSpaceAfter = buildStudioPdfTitleSpacingLength(options?.sectionSpaceAfter, "1ex plus 0.2ex");
243
257
  const subsectionSpaceBefore = buildStudioPdfTitleSpacingLength(options?.subsectionSpaceBefore, "1.2ex plus 0.4ex minus 0.2ex");
244
258
  const subsectionSpaceAfter = buildStudioPdfTitleSpacingLength(options?.subsectionSpaceAfter, "0.6ex plus 0.1ex");
245
259
  return `\\usepackage{titlesec}
246
- \\titleformat{\\section}{${sectionHeadingSize}\\bfseries\\sffamily}{}{0pt}{}[\\vspace{3pt}\\titlerule\\vspace{12pt}]
247
- \\titleformat{\\subsection}{${subsectionHeadingSize}\\bfseries\\sffamily}{}{0pt}{}
248
- \\titleformat{\\subsubsection}{${subsubsectionHeadingSize}\\bfseries\\sffamily}{}{0pt}{}
260
+ \\titleformat{\\section}{${sectionHeadingSize}\\bfseries\\sffamily\\raggedright\\hyphenpenalty=10000\\exhyphenpenalty=10000\\relax}{}{0pt}{}[\\vspace{3pt}\\titlerule\\vspace{12pt}]
261
+ \\titleformat{\\subsection}{${subsectionHeadingSize}\\bfseries\\sffamily\\raggedright\\hyphenpenalty=10000\\exhyphenpenalty=10000\\relax}{}{0pt}{}
262
+ \\titleformat{\\subsubsection}{${subsubsectionHeadingSize}\\bfseries\\sffamily\\raggedright\\hyphenpenalty=10000\\exhyphenpenalty=10000\\relax}{}{0pt}{}
263
+ \\titleformat{\\paragraph}[runin]{\\normalsize\\bfseries\\sffamily\\raggedright\\hyphenpenalty=10000\\exhyphenpenalty=10000\\relax}{}{0pt}{}
264
+ \\titleformat{\\subparagraph}[runin]{\\small\\bfseries\\sffamily\\raggedright\\hyphenpenalty=10000\\exhyphenpenalty=10000\\relax}{}{0pt}{}
249
265
  \\titlespacing*{\\section}{0pt}{${sectionSpaceBefore}}{${sectionSpaceAfter}}
250
266
  \\titlespacing*{\\subsection}{0pt}{${subsectionSpaceBefore}}{${subsectionSpaceAfter}}
267
+ \\titlespacing*{\\paragraph}{0pt}{0.9ex plus 0.3ex minus 0.1ex}{0.8em}
268
+ \\titlespacing*{\\subparagraph}{0pt}{0.7ex plus 0.2ex minus 0.1ex}{0.7em}
251
269
  \\usepackage{xcolor}
252
270
  \\usepackage{varwidth}
253
271
  \\definecolor{StudioAnnotationBg}{HTML}{EAF3FF}
254
272
  \\definecolor{StudioAnnotationBorder}{HTML}{8CB8FF}
255
273
  \\definecolor{StudioAnnotationText}{HTML}{1F5FBF}
274
+ \\definecolor{StudioCodeBlockBg}{HTML}{F6F8FA}
256
275
  \\definecolor{StudioDiffAddText}{HTML}{1A7F37}
257
276
  \\definecolor{StudioDiffDelText}{HTML}{CF222E}
258
277
  \\definecolor{StudioDiffMetaText}{HTML}{57606A}
@@ -278,7 +297,8 @@ function buildStudioPdfPreamble(options?: StudioPdfRenderOptions): string {
278
297
  \\newcommand{\\StudioDiffMetaTok}[1]{\\textcolor{StudioDiffMetaText}{#1}}
279
298
  \\newcommand{\\StudioDiffHunkTok}[1]{\\textcolor{StudioDiffHunkText}{#1}}
280
299
  \\newcommand{\\StudioDiffHeaderTok}[1]{\\textcolor{StudioDiffHunkText}{\\textbf{#1}}}
281
- \\newenvironment{studiocallout}[4]{\\par\\vspace{0.6em}\\noindent\\begingroup\\def\\StudioCalloutBorder{#2}\\def\\StudioCalloutText{#3}\\def\\StudioCalloutLabelBg{#4}\\color{\\StudioCalloutBorder}\\hrule height 0.8pt\\relax\\vspace{0.32em}\\noindent\\colorbox{\\StudioCalloutLabelBg}{\\strut\\hspace{0.55em}{\\sffamily\\bfseries\\footnotesize\\textcolor{\\StudioCalloutText}{#1}}\\hspace{0.55em}}\\par\\vspace{0.24em}\\normalcolor\\leftskip=0.9em\\rightskip=0pt\\parindent=0pt\\parskip=0.18em}{\\par\\vspace{0.12em}\\noindent\\color{\\StudioCalloutBorder}\\hrule height 0.55pt\\par\\endgroup\\vspace{0.5em}}
300
+ \\newenvironment{studiocallout}[4]{\\par\\vspace{0.6em}\\noindent\\begingroup\\def\\StudioCalloutBorder{#2}\\def\\StudioCalloutText{#3}\\def\\StudioCalloutLabelBg{#4}\\color{\\StudioCalloutBorder}\\hrule height 0.8pt\\relax\\vspace{0.32em}\\noindent\\colorbox{\\StudioCalloutLabelBg}{\\strut\\hspace{0.55em}{${calloutTitleSize}\\sffamily\\bfseries\\textcolor{\\StudioCalloutText}{#1}}\\hspace{0.55em}}\\par\\vspace{0.24em}\\normalcolor\\leftskip=0.9em\\rightskip=0pt\\parindent=0pt\\parskip=0.18em}{\\par\\vspace{0.12em}\\noindent\\color{\\StudioCalloutBorder}\\hrule height 0.55pt\\par\\endgroup\\vspace{0.5em}}
301
+ \\usepackage{float}
282
302
  \\usepackage{caption}
283
303
  \\captionsetup[figure]{justification=raggedright,singlelinecheck=false}
284
304
  \\usepackage{enumitem}
@@ -288,9 +308,9 @@ function buildStudioPdfPreamble(options?: StudioPdfRenderOptions): string {
288
308
  \\usepackage{fvextra}
289
309
  \\makeatletter
290
310
  \\@ifundefined{Highlighting}{%
291
- \\DefineVerbatimEnvironment{Highlighting}{Verbatim}{commandchars=\\\\\\{\\},breaklines,breakanywhere}%
311
+ \\DefineVerbatimEnvironment{Highlighting}{Verbatim}{commandchars=\\\\\\{\\},breaklines,breakanywhere,bgcolor=StudioCodeBlockBg,framesep=2mm}%
292
312
  }{%
293
- \\RecustomVerbatimEnvironment{Highlighting}{Verbatim}{commandchars=\\\\\\{\\},breaklines,breakanywhere}%
313
+ \\RecustomVerbatimEnvironment{Highlighting}{Verbatim}{commandchars=\\\\\\{\\},breaklines,breakanywhere,bgcolor=StudioCodeBlockBg,framesep=2mm}%
294
314
  }
295
315
  \\makeatother
296
316
  `;
@@ -4096,11 +4116,12 @@ async function renderStudioLiteralTextPdf(text: string, title = "Studio export",
4096
4116
  \\usepackage[${literalPdfConfig.geometryOptions}]{geometry}
4097
4117
  ${literalPdfConfig.fontCommands}\\usepackage{fvextra}
4098
4118
  \\usepackage{xcolor}
4119
+ \\definecolor{StudioCodeBlockBg}{HTML}{F6F8FA}
4099
4120
  \\usepackage{upquote}
4100
4121
  \\begin{document}
4101
4122
  \\renewcommand{\\baselinestretch}{${literalPdfConfig.lineStretch}}\\selectfont
4102
4123
  ${literalPdfConfig.fontSizeCommand}\\section*{${title.replace(/[{}\\]/g, "").trim() || "Studio export"}}
4103
- \\VerbatimInput[breaklines,breakanywhere,fontsize=\\small,frame=single,rulecolor=\\color{black!15},framesep=2mm]{input.txt}
4124
+ \\VerbatimInput[breaklines,breakanywhere,fontsize=\\small,bgcolor=StudioCodeBlockBg,frame=single,rulecolor=\\color{black!15},framesep=2mm]{input.txt}
4104
4125
  \\end{document}
4105
4126
  `;
4106
4127
 
@@ -5400,6 +5421,13 @@ function parseIncomingMessage(data: RawData): IncomingStudioMessage | null {
5400
5421
  };
5401
5422
  }
5402
5423
 
5424
+ if (msg.type === "refresh_from_disk_request" && typeof msg.requestId === "string") {
5425
+ return {
5426
+ type: "refresh_from_disk_request",
5427
+ requestId: msg.requestId,
5428
+ };
5429
+ }
5430
+
5403
5431
  if (msg.type === "send_to_editor_request" && typeof msg.requestId === "string" && typeof msg.content === "string") {
5404
5432
  return {
5405
5433
  type: "send_to_editor_request",
@@ -5797,6 +5825,7 @@ ${cssVarsBlock}
5797
5825
  <div class="controls">
5798
5826
  <button id="saveAsBtn" type="button" title="Save editor content to a new file path. Cmd/Ctrl+S falls back here when no direct save path is available.">Save editor as…</button>
5799
5827
  <button id="saveOverBtn" type="button" title="Overwrite current file with editor content. Shortcut: Cmd/Ctrl+S.">Save editor</button>
5828
+ <button id="refreshFromDiskBtn" type="button" title="Reload the current file-backed document from disk.">Refresh from disk</button>
5800
5829
  <label class="file-label" title="Load a local file into editor text.">Load file content<input id="fileInput" type="file" accept=".md,.markdown,.mdx,.qmd,.js,.mjs,.cjs,.jsx,.ts,.mts,.cts,.tsx,.py,.pyw,.sh,.bash,.zsh,.json,.jsonc,.json5,.rs,.c,.h,.cpp,.cxx,.cc,.hpp,.hxx,.jl,.f90,.f95,.f03,.f,.for,.r,.R,.m,.tex,.latex,.diff,.patch,.java,.go,.rb,.swift,.html,.htm,.css,.xml,.yaml,.yml,.toml,.lua,.txt,.rst,.adoc" /></label>
5801
5830
  <button id="loadGitDiffBtn" type="button" title="Load the current git diff from the Studio context into the editor.">Load git diff</button>
5802
5831
  <button id="getEditorBtn" type="button" title="Load the current terminal editor draft into Studio.">Load from pi editor</button>
@@ -7182,6 +7211,50 @@ export default function (pi: ExtensionAPI) {
7182
7211
  return;
7183
7212
  }
7184
7213
 
7214
+ if (msg.type === "refresh_from_disk_request") {
7215
+ if (!isValidRequestId(msg.requestId)) {
7216
+ sendToClient(client, { type: "error", requestId: msg.requestId, message: "Invalid request ID." });
7217
+ return;
7218
+ }
7219
+ if (isStudioBusy()) {
7220
+ sendToClient(client, { type: "busy", requestId: msg.requestId, message: "Studio is busy." });
7221
+ return;
7222
+ }
7223
+ if (!initialStudioDocument || !initialStudioDocument.path) {
7224
+ sendToClient(client, {
7225
+ type: "error",
7226
+ requestId: msg.requestId,
7227
+ message: "Refresh from disk is only available for file-backed documents.",
7228
+ });
7229
+ return;
7230
+ }
7231
+
7232
+ const refreshed = readStudioFile(initialStudioDocument.path, studioCwd);
7233
+ if (refreshed.ok === false) {
7234
+ sendToClient(client, {
7235
+ type: "error",
7236
+ requestId: msg.requestId,
7237
+ message: refreshed.message,
7238
+ });
7239
+ return;
7240
+ }
7241
+
7242
+ initialStudioDocument = {
7243
+ text: refreshed.text,
7244
+ label: refreshed.label,
7245
+ source: "file",
7246
+ path: refreshed.resolvedPath,
7247
+ };
7248
+
7249
+ broadcast({
7250
+ type: "studio_document",
7251
+ requestId: msg.requestId,
7252
+ document: initialStudioDocument,
7253
+ message: `Reloaded ${refreshed.label} from disk.`,
7254
+ });
7255
+ return;
7256
+ }
7257
+
7185
7258
  if (msg.type === "send_to_editor_request") {
7186
7259
  if (!isValidRequestId(msg.requestId)) {
7187
7260
  sendToClient(client, { type: "error", requestId: msg.requestId, message: "Invalid request ID." });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-studio",
3
- "version": "0.5.41",
3
+ "version": "0.5.42",
4
4
  "description": "Two-pane browser workspace for pi with prompt/response editing, annotations, critiques, prompt/response history, and live Markdown/LaTeX/code preview",
5
5
  "type": "module",
6
6
  "license": "MIT",