pi-studio 0.9.18 → 0.9.19

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,16 @@ All notable changes to `pi-studio` are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.9.19] — 2026-05-27
8
+
9
+ ### Changed
10
+ - Renamed the browser file picker to **Import file copy…**, clarified that browser-imported files are unsaved copies until saved, and renamed Files-view tab actions to **Open file tab**, **Convert tab**, or **Preview tab** depending on file type.
11
+
12
+ ### Fixed
13
+ - Working-view tool inputs now keep a bounded full input copy so long shell commands and REPL snippets can be expanded/copied instead of only showing the short activity summary.
14
+ - Editor-only and editor-preview panes now fall back to the file extension language while the stored editor language is still the default, avoiding Julia/code files being briefly or persistently rendered as Markdown prose.
15
+ - Clarified refresh-from-disk behaviour for detached drafts, kept the resource directory visible for file-backed tabs, made refresh requests preserve/use the client tab's current file path so files opened from the Files view can be refreshed reliably, and made Working tool inputs appear as soon as Pi announces a tool call.
16
+
7
17
  ## [0.9.18] — 2026-05-26
8
18
 
9
19
  ### Added
@@ -580,6 +580,7 @@
580
580
  toolName: typeof entry.toolName === "string" ? entry.toolName : "tool",
581
581
  label: parseNonEmptyString(entry.label),
582
582
  argsSummary: parseNonEmptyString(entry.argsSummary),
583
+ args: parseNonEmptyString(entry.args),
583
584
  output: typeof entry.output === "string" ? entry.output : "",
584
585
  images: Array.isArray(entry.images)
585
586
  ? entry.images.map((image, imageIndex) => normalizeTraceImage(image, imageIndex)).filter(Boolean)
@@ -860,8 +861,9 @@
860
861
  ? ("Tool: " + String(entry.toolName || "tool") + " — " + entry.label)
861
862
  : ("Tool: " + String(entry.toolName || "tool"));
862
863
  const parts = [header];
863
- if (String(entry.argsSummary || "").trim()) {
864
- parts.push("Input:\n" + String(entry.argsSummary || "").trim());
864
+ const inputText = String(entry.args || entry.argsSummary || "").trim();
865
+ if (inputText) {
866
+ parts.push("Input:\n" + inputText);
865
867
  }
866
868
  if (String(entry.output || "").trim()) {
867
869
  parts.push("Output:\n" + String(entry.output || "").trim());
@@ -2609,6 +2611,9 @@
2609
2611
  }
2610
2612
 
2611
2613
  function getIdleStatus() {
2614
+ if (isEditorOnlyMode) {
2615
+ return "Editor-only mode: edit, load, annotate, preview, save, suggest, or refresh file-backed text.";
2616
+ }
2612
2617
  return "Edit, load, or annotate text, then run, save, send to pi editor, or critique.";
2613
2618
  }
2614
2619
 
@@ -3176,7 +3181,7 @@
3176
3181
 
3177
3182
  function updateSourceBadge() {
3178
3183
  const label = sourceState && sourceState.label ? sourceState.label : "blank";
3179
- sourceBadgeEl.textContent = (studioUiRefreshEnabled ? "Origin: " : "Editor origin: ") + label;
3184
+ sourceBadgeEl.textContent = (studioUiRefreshEnabled ? "Origin: " : "Editor origin: ") + label + (hasRefreshableFilePath() ? " · file" : "");
3180
3185
  const descriptor = getCurrentStudioDocumentDescriptor();
3181
3186
  if (sourceBadgeEl) {
3182
3187
  sourceBadgeEl.title = descriptor.fileBacked
@@ -3188,9 +3193,11 @@
3188
3193
  if (isFileBacked) {
3189
3194
  var fileBackedResourceDir = getCurrentResourceDirValue() || dirnameForDisplayPath(sourceState.path);
3190
3195
  if (resourceDirInput) resourceDirInput.value = fileBackedResourceDir;
3191
- if (resourceDirLabel) resourceDirLabel.textContent = "";
3196
+ if (resourceDirLabel) {
3197
+ resourceDirLabel.textContent = fileBackedResourceDir ? ("Resource dir: " + fileBackedResourceDir) : "Resource dir: file directory";
3198
+ resourceDirLabel.hidden = false;
3199
+ }
3192
3200
  if (resourceDirBtn) resourceDirBtn.hidden = true;
3193
- if (resourceDirLabel) resourceDirLabel.hidden = true;
3194
3201
  if (resourceDirInputWrap) resourceDirInputWrap.classList.remove("visible");
3195
3202
  } else {
3196
3203
  // Restore to label if dir is set, otherwise show button
@@ -4480,8 +4487,8 @@
4480
4487
  return lines.join("\n");
4481
4488
  }
4482
4489
 
4483
- function renderDelimitedTextPreview(targetEl, text, pane) {
4484
- const html = buildDelimitedTextPreviewHtml(text, editorLanguage || "");
4490
+ function renderDelimitedTextPreview(targetEl, text, pane, language) {
4491
+ const html = buildDelimitedTextPreviewHtml(text, language || editorLanguage || "");
4485
4492
  if (!html || !targetEl) return false;
4486
4493
  if (pane === "source") {
4487
4494
  sourcePreviewRenderNonce += 1;
@@ -8236,15 +8243,16 @@
8236
8243
  function renderSourcePreviewNow() {
8237
8244
  if (editorView !== "preview") return;
8238
8245
  const text = prepareEditorTextForPreview(sourceTextEl.value || "");
8239
- if (isHtmlArtifactPreviewText(text, editorLanguage)) {
8246
+ const previewLanguage = getEditorLanguageForPreview();
8247
+ if (isHtmlArtifactPreviewText(text, previewLanguage)) {
8240
8248
  renderHtmlArtifactPreview(sourcePreviewEl, text, "source", { title: "Editor HTML preview", ...getHtmlPreviewResourceContextOptions() });
8241
8249
  return;
8242
8250
  }
8243
- if (renderDelimitedTextPreview(sourcePreviewEl, text, "source")) {
8251
+ if (renderDelimitedTextPreview(sourcePreviewEl, text, "source", previewLanguage)) {
8244
8252
  return;
8245
8253
  }
8246
- if (supportsCodePreviewCommentsForCurrentEditor()) {
8247
- renderCodePreviewWithCommentBlocks(sourcePreviewEl, text, "source");
8254
+ if (supportsCodePreviewCommentsForLanguage(previewLanguage)) {
8255
+ renderCodePreviewWithCommentBlocks(sourcePreviewEl, text, "source", previewLanguage);
8248
8256
  return;
8249
8257
  }
8250
8258
  const nonce = ++sourcePreviewRenderNonce;
@@ -8368,9 +8376,12 @@
8368
8376
  return { text: preview, truncated: true, hiddenChars, hiddenLines };
8369
8377
  }
8370
8378
 
8371
- function renderTraceOutput(text, outputKey) {
8379
+ function renderTraceOutput(text, outputKey, options) {
8372
8380
  const value = String(text || "");
8373
8381
  const key = String(outputKey || "trace-output");
8382
+ const label = options && typeof options.label === "string" && options.label.trim()
8383
+ ? options.label.trim()
8384
+ : "Output";
8374
8385
  const isExpanded = traceExpandedOutputs.has(key);
8375
8386
  const preview = getTraceOutputPreview(value);
8376
8387
  const visibleText = isExpanded || !preview.truncated ? value : preview.text;
@@ -8380,10 +8391,11 @@
8380
8391
  const hiddenParts = [];
8381
8392
  if (preview.hiddenLines > 0) hiddenParts.push(preview.hiddenLines + " more line" + (preview.hiddenLines === 1 ? "" : "s"));
8382
8393
  if (preview.hiddenChars > 0) hiddenParts.push(formatCompactNumber(preview.hiddenChars) + " chars hidden");
8394
+ const labelLower = label.toLowerCase();
8383
8395
  const summary = isExpanded
8384
- ? "Showing full output (" + formatTraceOutputSize(value) + ")."
8385
- : "Output truncated — " + (hiddenParts.join(", ") || "more hidden") + ".";
8386
- const buttonLabel = isExpanded ? "Collapse" : "Show full";
8396
+ ? "Showing full " + labelLower + " (" + formatTraceOutputSize(value) + ")."
8397
+ : label + " truncated — " + (hiddenParts.join(", ") || "more hidden") + ".";
8398
+ const buttonLabel = isExpanded ? "Collapse " + labelLower : "Show full " + labelLower;
8387
8399
  return "<div class='trace-output-wrap" + (isExpanded ? " is-expanded" : " is-truncated") + "'>"
8388
8400
  + body
8389
8401
  + "<div class='trace-output-truncation'>"
@@ -8757,15 +8769,16 @@
8757
8769
  }
8758
8770
 
8759
8771
  const title = entry.label || entry.toolName || "tool";
8760
- const argsSummary = entry.argsSummary
8761
- ? "<div class='trace-section'><div class='trace-section-label'>Input</div>" + renderTraceOutput(entry.argsSummary, entry.id + ":input") + "</div>"
8772
+ const inputText = entry.args || entry.argsSummary || "";
8773
+ const argsSummary = inputText
8774
+ ? "<div class='trace-section trace-section-input'><div class='trace-section-label'>Input</div>" + renderTraceOutput(inputText, entry.id + ":input", { label: "Input" }) + "</div>"
8762
8775
  : "";
8763
8776
  const imageOutput = renderTraceImages(entry.images);
8764
8777
  const outputPieces = [];
8765
- if (entry.output) outputPieces.push(renderTraceOutput(entry.output, entry.id + ":output"));
8778
+ if (entry.output) outputPieces.push(renderTraceOutput(entry.output, entry.id + ":output", { label: "Output" }));
8766
8779
  if (imageOutput) outputPieces.push(imageOutput);
8767
8780
  const output = outputPieces.length
8768
- ? "<div class='trace-section'><div class='trace-section-label'>Output</div>" + outputPieces.join("") + "</div>"
8781
+ ? "<div class='trace-section trace-section-output'><div class='trace-section-label'>Output</div>" + outputPieces.join("") + "</div>"
8769
8782
  : "<div class='trace-empty-inline'>No output yet.</div>";
8770
8783
  const toolStatusLabel = entry.isError
8771
8784
  ? "Error"
@@ -8874,8 +8887,11 @@
8874
8887
  const newTabAction = kind === "text" || kind === "office"
8875
8888
  ? "open-new"
8876
8889
  : ((kind === "pdf" || kind === "image") ? "open-preview-new" : "");
8890
+ const newTabLabel = kind === "text"
8891
+ ? "Open file tab"
8892
+ : (kind === "office" ? "Convert tab" : ((kind === "pdf" || kind === "image") ? "Preview tab" : "New tab"));
8877
8893
  const textActions = newTabAction
8878
- ? "<button type='button' data-files-action='" + escapeHtml(newTabAction) + "' data-files-path='" + escapeHtml(entry.path) + "'>New tab</button>"
8894
+ ? "<button type='button' data-files-action='" + escapeHtml(newTabAction) + "' data-files-path='" + escapeHtml(entry.path) + "'>" + escapeHtml(newTabLabel) + "</button>"
8879
8895
  : "";
8880
8896
  const openTitle = type === "directory"
8881
8897
  ? "Open folder"
@@ -8998,10 +9014,38 @@
8998
9014
  }
8999
9015
  }
9000
9016
 
9017
+ function basenameForStudioPath(path) {
9018
+ const value = stripPreviewLocalLinkUrlSuffix(path || "").replace(/\\/g, "/");
9019
+ const parts = value.split("/");
9020
+ return parts.pop() || value || "file";
9021
+ }
9022
+
9023
+ function ensureCurrentEditorFileBackedFromFilesPath(path) {
9024
+ const cleanPath = stripPreviewLocalLinkUrlSuffix(path || "").trim();
9025
+ if (!isLikelyAbsoluteStudioPath(cleanPath)) return;
9026
+ if (sourceState && sourceState.path === cleanPath) return;
9027
+ const resourceDir = normalizeStudioResourceDirValue(fileBrowserState.rootDir || getCurrentResourceDirValue() || dirnameForDisplayPath(cleanPath));
9028
+ if (resourceDirInput && resourceDir) resourceDirInput.value = resourceDir;
9029
+ setSourceState({
9030
+ source: "file",
9031
+ label: sourceState && sourceState.label && sourceState.label !== "blank" ? sourceState.label : basenameForStudioPath(cleanPath),
9032
+ path: cleanPath,
9033
+ });
9034
+ markFileBackedBaseline(sourceTextEl.value);
9035
+ }
9036
+
9001
9037
  async function openFileBrowserEntry(path, kind) {
9002
9038
  const context = getFileBrowserLocalLinkContext();
9003
- if (kind === "text" || kind === "office") {
9004
- await openPreviewDocumentHere(path, context);
9039
+ if (kind === "text") {
9040
+ await openPreviewDocumentHere(path, context, { fallbackPath: path, fileBackedIntent: true });
9041
+ ensureCurrentEditorFileBackedFromFilesPath(path);
9042
+ if (sourceState && sourceState.path) {
9043
+ setStatus("Opened file-backed document in editor: " + (sourceState.label || sourceState.path), "success");
9044
+ }
9045
+ return;
9046
+ }
9047
+ if (kind === "office") {
9048
+ await openPreviewDocumentHere(path, context, { fallbackPath: path });
9005
9049
  return;
9006
9050
  }
9007
9051
  if (kind === "pdf") {
@@ -9103,15 +9147,16 @@
9103
9147
  scheduleResponsePaneRepaintNudge();
9104
9148
  return;
9105
9149
  }
9106
- if (isHtmlArtifactPreviewText(editorText, editorLanguage)) {
9150
+ const previewLanguage = getEditorLanguageForPreview();
9151
+ if (isHtmlArtifactPreviewText(editorText, previewLanguage)) {
9107
9152
  renderHtmlArtifactPreview(critiqueViewEl, editorText, "response", { title: "Editor HTML preview", ...getHtmlPreviewResourceContextOptions() });
9108
9153
  return;
9109
9154
  }
9110
- if (renderDelimitedTextPreview(critiqueViewEl, editorText, "response")) {
9155
+ if (renderDelimitedTextPreview(critiqueViewEl, editorText, "response", previewLanguage)) {
9111
9156
  return;
9112
9157
  }
9113
- if (supportsCodePreviewCommentsForCurrentEditor()) {
9114
- renderCodePreviewWithCommentBlocks(critiqueViewEl, editorText, "response");
9158
+ if (supportsCodePreviewCommentsForLanguage(previewLanguage)) {
9159
+ renderCodePreviewWithCommentBlocks(critiqueViewEl, editorText, "response", previewLanguage);
9115
9160
  return;
9116
9161
  }
9117
9162
  const nonce = ++responsePreviewRenderNonce;
@@ -9296,13 +9341,17 @@
9296
9341
  return resourceDirInput ? normalizeStudioResourceDirValue(resourceDirInput.value) : "";
9297
9342
  }
9298
9343
 
9344
+ function stripImportedFileLabel(label) {
9345
+ return String(label || "").replace(/^(?:upload|imported copy):\s*/i, "");
9346
+ }
9347
+
9299
9348
  function getEffectiveSavePath() {
9300
9349
  // File-backed: use the original path
9301
9350
  if (sourceState.path) return sourceState.path;
9302
- // Upload with working dir + filename: derive path
9351
+ // Browser-imported copy with working dir + filename: derive path
9303
9352
  const resourceDir = getCurrentResourceDirValue();
9304
9353
  if (sourceState.source === "upload" && sourceState.label && resourceDir) {
9305
- var name = sourceState.label.replace(/^upload:\s*/i, "");
9354
+ var name = stripImportedFileLabel(sourceState.label);
9306
9355
  if (name) return resourceDir.replace(/\/$/, "") + "/" + name;
9307
9356
  }
9308
9357
  return null;
@@ -9327,7 +9376,7 @@
9327
9376
  return dir + stem + ".annotated.md";
9328
9377
  }
9329
9378
 
9330
- const rawLabel = sourceState.label ? sourceState.label.replace(/^upload:\s*/i, "") : "draft.md";
9379
+ const rawLabel = sourceState.label ? stripImportedFileLabel(sourceState.label) : "draft.md";
9331
9380
  const stem = rawLabel.replace(/\.[^.]+$/, "") || "draft";
9332
9381
  const suggestedDir = getCurrentResourceDirValue()
9333
9382
  ? getCurrentResourceDirValue().replace(/\/$/, "") + "/"
@@ -9355,7 +9404,7 @@
9355
9404
  return;
9356
9405
  }
9357
9406
 
9358
- refreshFromDiskBtn.title = "Refresh from disk is only available for documents that currently have a file path.";
9407
+ refreshFromDiskBtn.title = "Refresh from disk is available after opening a file from disk. Use Files → Open here, Files → Open file tab, or /studio-editor-only <path> for a refreshable editor tab.";
9359
9408
  }
9360
9409
 
9361
9410
  function syncActionButtons() {
@@ -10726,21 +10775,34 @@
10726
10775
  return confirmed;
10727
10776
  }
10728
10777
 
10729
- async function openPreviewDocumentHere(href, contextOverride) {
10778
+ function isLikelyAbsoluteStudioPath(path) {
10779
+ const value = stripPreviewLocalLinkUrlSuffix(path || "").trim();
10780
+ return Boolean(value && (/^\//.test(value) || /^[A-Za-z]:[\\/]/.test(value)));
10781
+ }
10782
+
10783
+ async function openPreviewDocumentHere(href, contextOverride, options) {
10730
10784
  if (!confirmPreviewOfficeConversion(href, "here")) return;
10731
10785
  if (editorHasPotentialUnsavedContent()) {
10732
- const confirmed = window.confirm("Replace the current editor contents with this linked file? Unsaved editor changes may be lost.");
10786
+ const kind = getPreviewLocalLinkKind(href);
10787
+ const prompt = kind === "office"
10788
+ ? "Replace the current editor contents with this converted Markdown copy? Unsaved editor changes may be lost."
10789
+ : "Open this file-backed document in the current editor?\n\nThis will replace the current editor contents and attach the editor to the file on disk, so Save editor and Refresh from disk use that file. Unsaved editor changes may be lost.";
10790
+ const confirmed = window.confirm(prompt);
10733
10791
  if (!confirmed) return;
10734
10792
  }
10735
10793
  const payload = await fetchPreviewLocalLink("document", href, contextOverride);
10736
10794
  if (typeof payload.text !== "string") throw new Error("Studio did not return document text.");
10737
- const path = typeof payload.path === "string" ? payload.path : "";
10795
+ const responsePath = typeof payload.path === "string" ? payload.path : "";
10796
+ const fallbackPath = options && typeof options.fallbackPath === "string" && isLikelyAbsoluteStudioPath(options.fallbackPath)
10797
+ ? stripPreviewLocalLinkUrlSuffix(options.fallbackPath).trim()
10798
+ : "";
10799
+ const path = responsePath || fallbackPath;
10738
10800
  const label = typeof payload.label === "string" && payload.label.trim() ? payload.label.trim() : (path || "linked file");
10739
10801
  const nextResourceDir = typeof payload.resourceDir === "string" ? normalizeStudioResourceDirValue(payload.resourceDir) : "";
10740
10802
  const converted = payload && payload.converted === true;
10741
10803
  if (resourceDirInput && nextResourceDir) resourceDirInput.value = nextResourceDir;
10742
10804
  setEditorText(payload.text, { preserveScroll: false, preserveSelection: false });
10743
- if (converted) {
10805
+ if (converted || !path) {
10744
10806
  setSourceState({ source: "blank", label, path: null });
10745
10807
  } else {
10746
10808
  setSourceState({ source: "file", label, path });
@@ -10750,7 +10812,9 @@
10750
10812
  if (detected) setEditorLanguage(detected);
10751
10813
  setEditorView("markdown");
10752
10814
  setActivePane("left");
10753
- setStatus(converted ? ("Converted document into editor: " + label) : ("Opened linked file in editor: " + label), "success");
10815
+ setStatus(converted
10816
+ ? ("Converted document into editor: " + label)
10817
+ : (path ? ("Opened file-backed document in editor: " + label) : ("Opened linked file copy in editor: " + label)), "success");
10754
10818
  }
10755
10819
 
10756
10820
  async function openPreviewDocumentInNewEditor(href, pendingWindow, contextOverride) {
@@ -12145,8 +12209,21 @@
12145
12209
  return out.join("<br>");
12146
12210
  }
12147
12211
 
12212
+ function getEditorLanguageForPreview() {
12213
+ const detected = detectLanguageFromName((sourceState && (sourceState.path || sourceState.label)) || "");
12214
+ if (detected && (!editorLanguage || editorLanguage === "markdown" || editorLanguage === "text")) {
12215
+ return detected;
12216
+ }
12217
+ return editorLanguage || detected || "";
12218
+ }
12219
+
12220
+ function supportsCodePreviewCommentsForLanguage(language) {
12221
+ const lang = normalizeFenceLanguage(language || "");
12222
+ return Boolean(lang) && lang !== "markdown" && lang !== "latex" && !getDelimitedTextPreviewConfig(lang);
12223
+ }
12224
+
12148
12225
  function supportsCodePreviewCommentsForCurrentEditor() {
12149
- return Boolean(editorLanguage) && editorLanguage !== "markdown" && editorLanguage !== "latex" && !getDelimitedTextPreviewConfig(editorLanguage);
12226
+ return supportsCodePreviewCommentsForLanguage(getEditorLanguageForPreview());
12150
12227
  }
12151
12228
 
12152
12229
  function getCodePreviewCommentKind(language) {
@@ -12191,11 +12268,11 @@
12191
12268
  return "<div class='response-markdown-highlight preview-code-lines'>" + html.join("") + "</div>";
12192
12269
  }
12193
12270
 
12194
- function renderCodePreviewWithCommentBlocks(targetEl, text, pane) {
12271
+ function renderCodePreviewWithCommentBlocks(targetEl, text, pane, language) {
12195
12272
  if (!targetEl) return;
12196
12273
  clearPreviewJumpHighlight(targetEl);
12197
12274
  finishPreviewRender(targetEl);
12198
- targetEl.innerHTML = buildCodePreviewHtmlWithCommentBlocks(text, editorLanguage || "");
12275
+ targetEl.innerHTML = buildCodePreviewHtmlWithCommentBlocks(text, language || editorLanguage || "");
12199
12276
  ensurePreviewSelectionActions(targetEl);
12200
12277
  updatePreviewCommentBlocksForElement(targetEl);
12201
12278
  decorateCopyablePreviewBlocks(targetEl);
@@ -18562,7 +18639,7 @@
18562
18639
  return;
18563
18640
  }
18564
18641
 
18565
- var suggestedName = sourceState.label ? sourceState.label.replace(/^upload:\s*/i, "") : "draft.md";
18642
+ var suggestedName = sourceState.label ? stripImportedFileLabel(sourceState.label) : "draft.md";
18566
18643
  var suggestedDir = getCurrentResourceDirValue() ? getCurrentResourceDirValue().replace(/\/$/, "") + "/" : "./";
18567
18644
  const suggested = sourceState.path || (suggestedDir + suggestedName);
18568
18645
  const path = window.prompt("Save editor content as:", suggested);
@@ -18617,7 +18694,7 @@
18617
18694
  if (refreshFromDiskBtn) {
18618
18695
  refreshFromDiskBtn.addEventListener("click", () => {
18619
18696
  if (!hasRefreshableFilePath()) {
18620
- setStatus("Refresh from disk is only available for file-backed documents.", "warning");
18697
+ setStatus("Refresh from disk needs a file path. Use Files → Open here, Files → Open file tab, or /studio-editor-only <path> for a refreshable editor tab.", "warning");
18621
18698
  return;
18622
18699
  }
18623
18700
 
@@ -18632,6 +18709,7 @@
18632
18709
  const sent = sendMessage({
18633
18710
  type: "refresh_from_disk_request",
18634
18711
  requestId,
18712
+ path: sourceState.path,
18635
18713
  });
18636
18714
 
18637
18715
  if (!sent) {
@@ -19252,7 +19330,7 @@
19252
19330
  setEditorText(text, { preserveScroll: false, preserveSelection: false });
19253
19331
  setSourceState({
19254
19332
  source: "upload",
19255
- label: "upload: " + file.name,
19333
+ label: "imported copy: " + file.name,
19256
19334
  path: null,
19257
19335
  });
19258
19336
  refreshResponseUi();
@@ -19260,7 +19338,7 @@
19260
19338
  if (detectedLang) {
19261
19339
  setEditorLanguage(detectedLang);
19262
19340
  }
19263
- setStatus("Loaded file " + file.name + ".", "success");
19341
+ setStatus("Imported file copy: " + file.name + ".", "success");
19264
19342
  };
19265
19343
  reader.onerror = () => {
19266
19344
  setStatus("Failed to read file.", "error");
package/client/studio.css CHANGED
@@ -3153,7 +3153,7 @@
3153
3153
  .trace-card {
3154
3154
  display: flex;
3155
3155
  flex-direction: column;
3156
- gap: 8px;
3156
+ gap: 10px;
3157
3157
  padding: 10px 12px;
3158
3158
  border: 1px solid var(--panel-border);
3159
3159
  border-radius: 10px;
@@ -3179,10 +3179,23 @@
3179
3179
  .trace-section {
3180
3180
  display: flex;
3181
3181
  flex-direction: column;
3182
- gap: 4px;
3182
+ gap: 6px;
3183
+ padding: 8px;
3184
+ border: 1px solid var(--border-subtle);
3185
+ border-radius: 10px;
3186
+ background: var(--panel);
3187
+ }
3188
+
3189
+ .trace-section + .trace-section {
3190
+ margin-top: 2px;
3183
3191
  }
3184
3192
 
3185
3193
  .trace-section-label {
3194
+ align-self: flex-start;
3195
+ padding: 2px 7px;
3196
+ border: 1px solid var(--border-subtle);
3197
+ border-radius: 999px;
3198
+ background: var(--panel-2);
3186
3199
  font-size: 11px;
3187
3200
  font-weight: 600;
3188
3201
  color: var(--muted);
package/index.ts CHANGED
@@ -257,6 +257,7 @@ interface StudioTraceToolEntry {
257
257
  toolName: string;
258
258
  label: string | null;
259
259
  argsSummary: string | null;
260
+ args: string | null;
260
261
  output: string;
261
262
  images: StudioTraceImage[];
262
263
  startedAt: number;
@@ -429,6 +430,7 @@ interface SaveOverRequestMessage {
429
430
  interface RefreshFromDiskRequestMessage {
430
431
  type: "refresh_from_disk_request";
431
432
  requestId: string;
433
+ path?: string;
432
434
  }
433
435
 
434
436
  interface SendToEditorRequestMessage {
@@ -517,6 +519,7 @@ const MAX_PREPARED_PDF_EXPORTS = 8;
517
519
  const MAX_PREPARED_HTML_EXPORTS = 8;
518
520
  const STUDIO_TRACE_SNAPSHOT_MAX_ENTRIES = 80;
519
521
  const STUDIO_TRACE_SNAPSHOT_MAX_FIELD_CHARS = 20_000;
522
+ const STUDIO_TRACE_TOOL_ARGS_MAX_CHARS = 20_000;
520
523
  const STUDIO_TRACE_IMAGE_MAX_COUNT = 8;
521
524
  const STUDIO_TRACE_IMAGE_MAX_BASE64_CHARS = 2_500_000;
522
525
  const STUDIO_TRACE_SNAPSHOT_MAX_IMAGES = 12;
@@ -8140,10 +8143,15 @@ function parseIncomingMessage(data: RawData): IncomingStudioMessage | null {
8140
8143
  };
8141
8144
  }
8142
8145
 
8143
- if (msg.type === "refresh_from_disk_request" && typeof msg.requestId === "string") {
8146
+ if (
8147
+ msg.type === "refresh_from_disk_request"
8148
+ && typeof msg.requestId === "string"
8149
+ && (msg.path === undefined || typeof msg.path === "string")
8150
+ ) {
8144
8151
  return {
8145
8152
  type: "refresh_from_disk_request",
8146
8153
  requestId: msg.requestId,
8154
+ path: typeof msg.path === "string" ? msg.path : undefined,
8147
8155
  };
8148
8156
  }
8149
8157
 
@@ -8567,15 +8575,17 @@ function createStudioTraceSnapshot(source: StudioTraceState): { traceState: Stud
8567
8575
  };
8568
8576
  }
8569
8577
  const argsSummary = truncateStudioTraceSnapshotText(entry.argsSummary ?? "");
8578
+ const args = truncateStudioTraceSnapshotText(entry.args ?? entry.argsSummary ?? "");
8570
8579
  const output = truncateStudioTraceSnapshotText(entry.output);
8571
8580
  const snapshotImages = copyStudioTraceImagesForSnapshot(entry.images, imageBudget);
8572
- truncated = truncated || argsSummary.truncated || output.truncated || snapshotImages.omitted > 0;
8581
+ truncated = truncated || argsSummary.truncated || args.truncated || output.truncated || snapshotImages.omitted > 0;
8573
8582
  const omittedImageNote = snapshotImages.omitted > 0
8574
8583
  ? `[${snapshotImages.omitted} image preview${snapshotImages.omitted === 1 ? "" : "s"} omitted from saved Working view to keep history bounded.]`
8575
8584
  : "";
8576
8585
  return {
8577
8586
  ...entry,
8578
8587
  argsSummary: argsSummary.text || null,
8588
+ args: args.text || null,
8579
8589
  output: [output.text, omittedImageNote].filter(Boolean).join("\n"),
8580
8590
  images: snapshotImages.images,
8581
8591
  };
@@ -8786,6 +8796,34 @@ function summarizeStudioTraceToolArgs(toolName: string, args: unknown): string |
8786
8796
  }
8787
8797
  }
8788
8798
 
8799
+ function truncateStudioTraceToolArgs(text: string): string {
8800
+ const value = sanitizeStudioTraceOutputText(String(text || "").trim());
8801
+ if (!value || value.length <= STUDIO_TRACE_TOOL_ARGS_MAX_CHARS) return value;
8802
+ const keepHead = Math.max(1_000, Math.floor(STUDIO_TRACE_TOOL_ARGS_MAX_CHARS * 0.65));
8803
+ const keepTail = Math.max(1_000, STUDIO_TRACE_TOOL_ARGS_MAX_CHARS - keepHead - 160);
8804
+ const omitted = value.length - keepHead - keepTail;
8805
+ return `${value.slice(0, keepHead)}\n\n… ${omitted} chars omitted from tool input …\n\n${value.slice(value.length - keepTail)}`;
8806
+ }
8807
+
8808
+ function formatStudioTraceToolArgs(toolName: string, args: unknown): string | null {
8809
+ const normalizedTool = String(toolName || "").trim().toLowerCase();
8810
+ const payload = (args && typeof args === "object") ? (args as Record<string, unknown>) : {};
8811
+ let raw = "";
8812
+ if (normalizedTool === "bash" && typeof payload.command === "string") {
8813
+ raw = payload.command;
8814
+ } else if ((normalizedTool === "repl_send" || normalizedTool === "studio_repl_send") && typeof payload.code === "string") {
8815
+ raw = payload.code;
8816
+ } else {
8817
+ try {
8818
+ raw = JSON.stringify(args, null, 2);
8819
+ } catch {
8820
+ raw = String(args ?? "");
8821
+ }
8822
+ }
8823
+ const truncated = truncateStudioTraceToolArgs(raw);
8824
+ return truncated ? truncated : null;
8825
+ }
8826
+
8789
8827
  function isStudioReplRuntime(value: unknown): value is StudioReplRuntime {
8790
8828
  return value === "shell"
8791
8829
  || value === "python"
@@ -9827,7 +9865,7 @@ ${cssVarsBlock}
9827
9865
  <button id="saveOverBtn" type="button" title="Overwrite current file with editor content. Shortcut: Cmd/Ctrl+S.">Save editor</button>
9828
9866
  <button id="refreshFromDiskBtn" type="button" title="Reload the current file-backed document from disk.">Refresh from disk</button>
9829
9867
  <button id="clearWorkspaceBtn" type="button" title="Clear editor text and reset this tab to a fresh blank draft. Saved files and responses are not changed.">Reset editor</button>
9830
- <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>
9868
+ <label class="file-label" title="Import a browser-selected text file into the editor as an unsaved copy. It will not be refreshable from disk until you save it.">Import file copy…<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>
9831
9869
  <button id="loadGitDiffBtn" type="button" title="Load the current git diff from the Studio context into the editor.">Load git diff</button>
9832
9870
  <button id="getEditorBtn" type="button" title="Load the current terminal editor draft into Studio.">Load from pi editor</button>
9833
9871
  <button id="zenModeBtn" class="zen-mode-btn" type="button" title="Hide secondary Studio controls. Shortcut: F9.">Zen</button>
@@ -11040,7 +11078,17 @@ export default function (pi: ExtensionAPI) {
11040
11078
  const existingId = studioTraceToolEntryIds.get(toolCallId);
11041
11079
  if (existingId) {
11042
11080
  const existing = studioTraceState.entries.find((entry) => entry.id === existingId);
11043
- if (existing && existing.type === "tool") return existing;
11081
+ if (existing && existing.type === "tool") {
11082
+ if (args !== undefined) {
11083
+ existing.toolName = toolName;
11084
+ existing.label = deriveToolActivityLabel(toolName, args);
11085
+ existing.argsSummary = summarizeStudioTraceToolArgs(toolName, args);
11086
+ existing.args = formatStudioTraceToolArgs(toolName, args);
11087
+ existing.updatedAt = Date.now();
11088
+ upsertStudioTraceEntry(existing);
11089
+ }
11090
+ return existing;
11091
+ }
11044
11092
  }
11045
11093
  if (studioTraceState.runId == null || studioTraceState.status === "idle") {
11046
11094
  resetStudioTraceForRun();
@@ -11053,6 +11101,7 @@ export default function (pi: ExtensionAPI) {
11053
11101
  toolName,
11054
11102
  label: deriveToolActivityLabel(toolName, args),
11055
11103
  argsSummary: summarizeStudioTraceToolArgs(toolName, args),
11104
+ args: formatStudioTraceToolArgs(toolName, args),
11056
11105
  output: "",
11057
11106
  images: [],
11058
11107
  startedAt: now,
@@ -11075,6 +11124,8 @@ export default function (pi: ExtensionAPI) {
11075
11124
  images?: StudioTraceImage[],
11076
11125
  ) => {
11077
11126
  const entry = ensureStudioTraceToolEntry(toolCallId, toolName, args);
11127
+ if (!entry.argsSummary) entry.argsSummary = summarizeStudioTraceToolArgs(toolName, args);
11128
+ if (!entry.args) entry.args = formatStudioTraceToolArgs(toolName, args);
11078
11129
  entry.output = output;
11079
11130
  if (Array.isArray(images)) entry.images = images;
11080
11131
  entry.status = status;
@@ -12225,16 +12276,18 @@ export default function (pi: ExtensionAPI) {
12225
12276
  sendToClient(client, { type: "busy", requestId: msg.requestId, message: "Studio is busy." });
12226
12277
  return;
12227
12278
  }
12228
- if (!initialStudioDocument || !initialStudioDocument.path) {
12279
+ const requestedPath = typeof msg.path === "string" && msg.path.trim() ? msg.path.trim() : "";
12280
+ const refreshPath = requestedPath || initialStudioDocument?.path || "";
12281
+ if (!refreshPath) {
12229
12282
  sendToClient(client, {
12230
12283
  type: "error",
12231
12284
  requestId: msg.requestId,
12232
- message: "Refresh from disk is only available for file-backed documents.",
12285
+ message: "Refresh from disk needs a file path. Use Files → Open here, Files → Open file tab, or /studio-editor-only <path> for a refreshable editor tab.",
12233
12286
  });
12234
12287
  return;
12235
12288
  }
12236
12289
 
12237
- const refreshed = readStudioFile(initialStudioDocument.path, studioCwd);
12290
+ const refreshed = readStudioFile(refreshPath, studioCwd);
12238
12291
  if (refreshed.ok === false) {
12239
12292
  sendToClient(client, {
12240
12293
  type: "error",
@@ -12244,18 +12297,21 @@ export default function (pi: ExtensionAPI) {
12244
12297
  return;
12245
12298
  }
12246
12299
 
12247
- initialStudioDocument = {
12300
+ const refreshedDocument: InitialStudioDocument = {
12248
12301
  text: refreshed.text,
12249
12302
  label: refreshed.label,
12250
12303
  source: "file",
12251
12304
  path: refreshed.resolvedPath,
12252
12305
  resourceDir: dirname(refreshed.resolvedPath),
12253
12306
  };
12307
+ if (!requestedPath || initialStudioDocument?.path === refreshed.resolvedPath) {
12308
+ initialStudioDocument = refreshedDocument;
12309
+ }
12254
12310
 
12255
- broadcast({
12311
+ sendToClient(client, {
12256
12312
  type: "studio_document",
12257
12313
  requestId: msg.requestId,
12258
- document: initialStudioDocument,
12314
+ document: refreshedDocument,
12259
12315
  message: `Reloaded ${refreshed.label} from disk.`,
12260
12316
  });
12261
12317
  return;
@@ -13666,7 +13722,9 @@ export default function (pi: ExtensionAPI) {
13666
13722
  if (!agentBusy) return;
13667
13723
  const toolName = typeof event.toolName === "string" ? event.toolName : "";
13668
13724
  const input = (event as { input?: unknown }).input;
13725
+ const toolCallId = typeof event.toolCallId === "string" ? event.toolCallId : "";
13669
13726
  const label = deriveToolActivityLabel(toolName, input);
13727
+ if (toolCallId) ensureStudioTraceToolEntry(toolCallId, toolName, input);
13670
13728
  emitDebugEvent("tool_call", { toolName, label, activeRequestId: activeRequest?.id ?? null, activeRequestKind: activeRequest?.kind ?? null });
13671
13729
  setTerminalActivity("tool", toolName, label);
13672
13730
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-studio",
3
- "version": "0.9.18",
3
+ "version": "0.9.19",
4
4
  "description": "Two-pane browser workspace for pi with prompt/response editing, annotations, critiques, active quiz, prompt/response history, live previews, and tmux-backed REPL/literate REPL workflows",
5
5
  "type": "module",
6
6
  "license": "MIT",