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 +10 -0
- package/client/studio-client.js +120 -42
- package/client/studio.css +15 -2
- package/index.ts +68 -10
- package/package.json +1 -1
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
|
package/client/studio-client.js
CHANGED
|
@@ -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
|
-
|
|
864
|
-
|
|
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)
|
|
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
|
-
|
|
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 (
|
|
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
|
|
8385
|
-
: "
|
|
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
|
|
8761
|
-
|
|
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) + "'>
|
|
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"
|
|
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
|
-
|
|
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 (
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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: "
|
|
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("
|
|
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:
|
|
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:
|
|
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 (
|
|
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="
|
|
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")
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
12311
|
+
sendToClient(client, {
|
|
12256
12312
|
type: "studio_document",
|
|
12257
12313
|
requestId: msg.requestId,
|
|
12258
|
-
document:
|
|
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.
|
|
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",
|