pi-studio 0.8.3 → 0.9.0
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 +24 -0
- package/README.md +5 -3
- package/client/studio-client.js +1448 -37
- package/client/studio.css +310 -0
- package/index.ts +623 -22
- package/package.json +2 -2
package/index.ts
CHANGED
|
@@ -35,6 +35,7 @@ type StudioSourceKind = "file" | "last-response" | "blank";
|
|
|
35
35
|
type TerminalActivityPhase = "idle" | "running" | "tool" | "responding";
|
|
36
36
|
type StudioPromptMode = "response" | "run" | "effective";
|
|
37
37
|
type StudioPromptTriggerKind = "run" | "steer";
|
|
38
|
+
type StudioReplRuntime = "shell" | "python" | "ipython" | "julia" | "r" | "ghci" | "clojure";
|
|
38
39
|
|
|
39
40
|
const STUDIO_CSS_URL = new URL("./client/studio.css", import.meta.url);
|
|
40
41
|
const STUDIO_ANNOTATION_HELPERS_URL = new URL("./client/studio-annotation-helpers.js", import.meta.url);
|
|
@@ -111,6 +112,14 @@ interface StudioContextUsageSnapshot {
|
|
|
111
112
|
percent: number | null;
|
|
112
113
|
}
|
|
113
114
|
|
|
115
|
+
interface StudioReplSessionInfo {
|
|
116
|
+
sessionName: string;
|
|
117
|
+
target: string;
|
|
118
|
+
runtime: StudioReplRuntime | "unknown";
|
|
119
|
+
label: string;
|
|
120
|
+
source: "studio" | "pi-repl" | "tmux";
|
|
121
|
+
}
|
|
122
|
+
|
|
114
123
|
interface PreparedStudioPdfExport {
|
|
115
124
|
pdf: Buffer;
|
|
116
125
|
filename: string;
|
|
@@ -196,6 +205,14 @@ interface StudioTraceAssistantEntry {
|
|
|
196
205
|
stopReason: string | null;
|
|
197
206
|
}
|
|
198
207
|
|
|
208
|
+
interface StudioTraceImage {
|
|
209
|
+
id: string;
|
|
210
|
+
mimeType: string;
|
|
211
|
+
data: string;
|
|
212
|
+
byteLength: number | null;
|
|
213
|
+
label: string | null;
|
|
214
|
+
}
|
|
215
|
+
|
|
199
216
|
interface StudioTraceToolEntry {
|
|
200
217
|
id: string;
|
|
201
218
|
type: "tool";
|
|
@@ -204,6 +221,7 @@ interface StudioTraceToolEntry {
|
|
|
204
221
|
label: string | null;
|
|
205
222
|
argsSummary: string | null;
|
|
206
223
|
output: string;
|
|
224
|
+
images: StudioTraceImage[];
|
|
207
225
|
startedAt: number;
|
|
208
226
|
updatedAt: number;
|
|
209
227
|
status: StudioTraceEntryStatus;
|
|
@@ -258,6 +276,41 @@ interface SendRunRequestMessage {
|
|
|
258
276
|
text: string;
|
|
259
277
|
}
|
|
260
278
|
|
|
279
|
+
interface ReplListRequestMessage {
|
|
280
|
+
type: "repl_list_request";
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
interface ReplCaptureRequestMessage {
|
|
284
|
+
type: "repl_capture_request";
|
|
285
|
+
sessionName?: string;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
interface ReplStartRequestMessage {
|
|
289
|
+
type: "repl_start_request";
|
|
290
|
+
requestId: string;
|
|
291
|
+
runtime: StudioReplRuntime;
|
|
292
|
+
newSession?: boolean;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
interface ReplStopRequestMessage {
|
|
296
|
+
type: "repl_stop_request";
|
|
297
|
+
requestId: string;
|
|
298
|
+
sessionName: string;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
interface ReplSendRequestMessage {
|
|
302
|
+
type: "repl_send_request";
|
|
303
|
+
requestId: string;
|
|
304
|
+
sessionName: string;
|
|
305
|
+
text: string;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
interface ReplInterruptRequestMessage {
|
|
309
|
+
type: "repl_interrupt_request";
|
|
310
|
+
requestId: string;
|
|
311
|
+
sessionName: string;
|
|
312
|
+
}
|
|
313
|
+
|
|
261
314
|
interface CompactRequestMessage {
|
|
262
315
|
type: "compact_request";
|
|
263
316
|
requestId: string;
|
|
@@ -322,6 +375,12 @@ type IncomingStudioMessage =
|
|
|
322
375
|
| CritiqueRequestMessage
|
|
323
376
|
| AnnotationRequestMessage
|
|
324
377
|
| SendRunRequestMessage
|
|
378
|
+
| ReplListRequestMessage
|
|
379
|
+
| ReplCaptureRequestMessage
|
|
380
|
+
| ReplStartRequestMessage
|
|
381
|
+
| ReplStopRequestMessage
|
|
382
|
+
| ReplSendRequestMessage
|
|
383
|
+
| ReplInterruptRequestMessage
|
|
325
384
|
| CompactRequestMessage
|
|
326
385
|
| SaveAsRequestMessage
|
|
327
386
|
| SaveOverRequestMessage
|
|
@@ -345,6 +404,22 @@ const MAX_PREPARED_PDF_EXPORTS = 8;
|
|
|
345
404
|
const MAX_PREPARED_HTML_EXPORTS = 8;
|
|
346
405
|
const STUDIO_TRACE_SNAPSHOT_MAX_ENTRIES = 80;
|
|
347
406
|
const STUDIO_TRACE_SNAPSHOT_MAX_FIELD_CHARS = 20_000;
|
|
407
|
+
const STUDIO_TRACE_IMAGE_MAX_COUNT = 8;
|
|
408
|
+
const STUDIO_TRACE_IMAGE_MAX_BASE64_CHARS = 2_500_000;
|
|
409
|
+
const STUDIO_TRACE_SNAPSHOT_MAX_IMAGES = 12;
|
|
410
|
+
const STUDIO_TRACE_SNAPSHOT_MAX_IMAGE_BASE64_CHARS = 6_000_000;
|
|
411
|
+
const STUDIO_TRACE_IMAGE_SAFE_MIME_TYPES = new Set(["image/png", "image/jpeg", "image/gif", "image/webp"]);
|
|
412
|
+
const STUDIO_REPL_CAPTURE_LINES = 800;
|
|
413
|
+
const STUDIO_REPL_SEND_MAX_CHARS = 200_000;
|
|
414
|
+
const STUDIO_REPL_RUNTIME_LABELS: Record<StudioReplRuntime, string> = {
|
|
415
|
+
shell: "Shell",
|
|
416
|
+
python: "Python",
|
|
417
|
+
ipython: "IPython",
|
|
418
|
+
julia: "Julia",
|
|
419
|
+
r: "R",
|
|
420
|
+
ghci: "GHCi",
|
|
421
|
+
clojure: "Clojure",
|
|
422
|
+
};
|
|
348
423
|
const MAX_STUDIO_TRACE_SNAPSHOTS = RESPONSE_HISTORY_LIMIT;
|
|
349
424
|
const TRANSIENT_STUDIO_DOCUMENT_TTL_MS = 30 * 60 * 1000;
|
|
350
425
|
const MAX_TRANSIENT_STUDIO_DOCUMENTS = 16;
|
|
@@ -6397,6 +6472,54 @@ function parseIncomingMessage(data: RawData): IncomingStudioMessage | null {
|
|
|
6397
6472
|
};
|
|
6398
6473
|
}
|
|
6399
6474
|
|
|
6475
|
+
if (msg.type === "repl_list_request") {
|
|
6476
|
+
return { type: "repl_list_request" };
|
|
6477
|
+
}
|
|
6478
|
+
|
|
6479
|
+
if (msg.type === "repl_capture_request") {
|
|
6480
|
+
return {
|
|
6481
|
+
type: "repl_capture_request",
|
|
6482
|
+
sessionName: typeof msg.sessionName === "string" ? msg.sessionName : undefined,
|
|
6483
|
+
};
|
|
6484
|
+
}
|
|
6485
|
+
|
|
6486
|
+
if (msg.type === "repl_start_request" && typeof msg.requestId === "string") {
|
|
6487
|
+
const runtime = normalizeStudioReplRuntime(msg.runtime);
|
|
6488
|
+
if (runtime) {
|
|
6489
|
+
return {
|
|
6490
|
+
type: "repl_start_request",
|
|
6491
|
+
requestId: msg.requestId,
|
|
6492
|
+
runtime,
|
|
6493
|
+
newSession: Boolean(msg.newSession),
|
|
6494
|
+
};
|
|
6495
|
+
}
|
|
6496
|
+
}
|
|
6497
|
+
|
|
6498
|
+
if (msg.type === "repl_stop_request" && typeof msg.requestId === "string" && typeof msg.sessionName === "string") {
|
|
6499
|
+
return {
|
|
6500
|
+
type: "repl_stop_request",
|
|
6501
|
+
requestId: msg.requestId,
|
|
6502
|
+
sessionName: msg.sessionName,
|
|
6503
|
+
};
|
|
6504
|
+
}
|
|
6505
|
+
|
|
6506
|
+
if (msg.type === "repl_send_request" && typeof msg.requestId === "string" && typeof msg.sessionName === "string" && typeof msg.text === "string") {
|
|
6507
|
+
return {
|
|
6508
|
+
type: "repl_send_request",
|
|
6509
|
+
requestId: msg.requestId,
|
|
6510
|
+
sessionName: msg.sessionName,
|
|
6511
|
+
text: msg.text,
|
|
6512
|
+
};
|
|
6513
|
+
}
|
|
6514
|
+
|
|
6515
|
+
if (msg.type === "repl_interrupt_request" && typeof msg.requestId === "string" && typeof msg.sessionName === "string") {
|
|
6516
|
+
return {
|
|
6517
|
+
type: "repl_interrupt_request",
|
|
6518
|
+
requestId: msg.requestId,
|
|
6519
|
+
sessionName: msg.sessionName,
|
|
6520
|
+
};
|
|
6521
|
+
}
|
|
6522
|
+
|
|
6400
6523
|
if (
|
|
6401
6524
|
msg.type === "compact_request" &&
|
|
6402
6525
|
typeof msg.requestId === "string" &&
|
|
@@ -6649,9 +6772,44 @@ function truncateStudioTraceSnapshotText(text: string, maxChars = STUDIO_TRACE_S
|
|
|
6649
6772
|
};
|
|
6650
6773
|
}
|
|
6651
6774
|
|
|
6775
|
+
function copyStudioTraceImagesForSnapshot(
|
|
6776
|
+
images: StudioTraceImage[] | undefined,
|
|
6777
|
+
budget: { remainingImages: number; remainingBase64Chars: number },
|
|
6778
|
+
): { images: StudioTraceImage[]; omitted: number } {
|
|
6779
|
+
const copied: StudioTraceImage[] = [];
|
|
6780
|
+
let omitted = 0;
|
|
6781
|
+
for (const image of Array.isArray(images) ? images : []) {
|
|
6782
|
+
if (!image || typeof image !== "object") continue;
|
|
6783
|
+
const mimeType = normalizeStudioTraceImageMimeType(image.mimeType);
|
|
6784
|
+
const data = typeof image.data === "string" ? image.data : "";
|
|
6785
|
+
if (!data || !isStudioTraceSafeImageMimeType(mimeType)) {
|
|
6786
|
+
omitted += 1;
|
|
6787
|
+
continue;
|
|
6788
|
+
}
|
|
6789
|
+
if (budget.remainingImages <= 0 || data.length > budget.remainingBase64Chars) {
|
|
6790
|
+
omitted += 1;
|
|
6791
|
+
continue;
|
|
6792
|
+
}
|
|
6793
|
+
copied.push({
|
|
6794
|
+
id: typeof image.id === "string" && image.id.trim() ? image.id : `trace-image-snapshot-${copied.length + 1}`,
|
|
6795
|
+
mimeType,
|
|
6796
|
+
data,
|
|
6797
|
+
byteLength: typeof image.byteLength === "number" && Number.isFinite(image.byteLength) ? image.byteLength : estimateStudioTraceBase64ByteLength(data),
|
|
6798
|
+
label: typeof image.label === "string" && image.label.trim() ? image.label : null,
|
|
6799
|
+
});
|
|
6800
|
+
budget.remainingImages -= 1;
|
|
6801
|
+
budget.remainingBase64Chars -= data.length;
|
|
6802
|
+
}
|
|
6803
|
+
return { images: copied, omitted };
|
|
6804
|
+
}
|
|
6805
|
+
|
|
6652
6806
|
function createStudioTraceSnapshot(source: StudioTraceState): { traceState: StudioTraceState; truncated: boolean } {
|
|
6653
6807
|
let truncated = false;
|
|
6654
6808
|
const sourceEntries = Array.isArray(source.entries) ? source.entries : [];
|
|
6809
|
+
const imageBudget = {
|
|
6810
|
+
remainingImages: STUDIO_TRACE_SNAPSHOT_MAX_IMAGES,
|
|
6811
|
+
remainingBase64Chars: STUDIO_TRACE_SNAPSHOT_MAX_IMAGE_BASE64_CHARS,
|
|
6812
|
+
};
|
|
6655
6813
|
const entries = sourceEntries.slice(-STUDIO_TRACE_SNAPSHOT_MAX_ENTRIES).map((entry) => {
|
|
6656
6814
|
if (entry.type === "assistant") {
|
|
6657
6815
|
const thinking = truncateStudioTraceSnapshotText(entry.thinking);
|
|
@@ -6665,11 +6823,16 @@ function createStudioTraceSnapshot(source: StudioTraceState): { traceState: Stud
|
|
|
6665
6823
|
}
|
|
6666
6824
|
const argsSummary = truncateStudioTraceSnapshotText(entry.argsSummary ?? "");
|
|
6667
6825
|
const output = truncateStudioTraceSnapshotText(entry.output);
|
|
6668
|
-
|
|
6826
|
+
const snapshotImages = copyStudioTraceImagesForSnapshot(entry.images, imageBudget);
|
|
6827
|
+
truncated = truncated || argsSummary.truncated || output.truncated || snapshotImages.omitted > 0;
|
|
6828
|
+
const omittedImageNote = snapshotImages.omitted > 0
|
|
6829
|
+
? `[${snapshotImages.omitted} image preview${snapshotImages.omitted === 1 ? "" : "s"} omitted from saved Working view to keep history bounded.]`
|
|
6830
|
+
: "";
|
|
6669
6831
|
return {
|
|
6670
6832
|
...entry,
|
|
6671
6833
|
argsSummary: argsSummary.text || null,
|
|
6672
|
-
output: output.text,
|
|
6834
|
+
output: [output.text, omittedImageNote].filter(Boolean).join("\n"),
|
|
6835
|
+
images: snapshotImages.images,
|
|
6673
6836
|
};
|
|
6674
6837
|
});
|
|
6675
6838
|
if (sourceEntries.length > entries.length) truncated = true;
|
|
@@ -6706,29 +6869,102 @@ function sanitizeStudioTraceOutputText(text: string): string {
|
|
|
6706
6869
|
.replace(/\b[A-Za-z0-9+/]{3000,}={0,2}\b/g, "[base64 data omitted]");
|
|
6707
6870
|
}
|
|
6708
6871
|
|
|
6872
|
+
function normalizeStudioTraceImageMimeType(value: unknown): string {
|
|
6873
|
+
return typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
6874
|
+
}
|
|
6875
|
+
|
|
6876
|
+
function getStudioTraceImageMimeType(block: unknown): string {
|
|
6877
|
+
if (!block || typeof block !== "object") return "";
|
|
6878
|
+
const payload = block as Record<string, unknown>;
|
|
6879
|
+
const source = payload.source && typeof payload.source === "object" ? payload.source as Record<string, unknown> : null;
|
|
6880
|
+
return normalizeStudioTraceImageMimeType(
|
|
6881
|
+
payload.mimeType
|
|
6882
|
+
?? payload.mediaType
|
|
6883
|
+
?? payload.media_type
|
|
6884
|
+
?? source?.mimeType
|
|
6885
|
+
?? source?.mediaType
|
|
6886
|
+
?? source?.media_type,
|
|
6887
|
+
);
|
|
6888
|
+
}
|
|
6889
|
+
|
|
6709
6890
|
function isStudioTraceImageBlock(block: unknown): boolean {
|
|
6710
6891
|
if (!block || typeof block !== "object") return false;
|
|
6711
6892
|
const payload = block as Record<string, unknown>;
|
|
6712
6893
|
const type = typeof payload.type === "string" ? payload.type.toLowerCase() : "";
|
|
6713
6894
|
if (type.includes("image")) return true;
|
|
6714
|
-
|
|
6715
|
-
|
|
6716
|
-
|
|
6717
|
-
|
|
6895
|
+
return getStudioTraceImageMimeType(block).startsWith("image/");
|
|
6896
|
+
}
|
|
6897
|
+
|
|
6898
|
+
function isStudioTraceSafeImageMimeType(mimeType: string): boolean {
|
|
6899
|
+
return STUDIO_TRACE_IMAGE_SAFE_MIME_TYPES.has(normalizeStudioTraceImageMimeType(mimeType));
|
|
6900
|
+
}
|
|
6901
|
+
|
|
6902
|
+
function getStudioTraceImageData(block: unknown): string | null {
|
|
6903
|
+
if (!block || typeof block !== "object") return null;
|
|
6904
|
+
const payload = block as Record<string, unknown>;
|
|
6905
|
+
if (typeof payload.data === "string") return payload.data;
|
|
6718
6906
|
const source = payload.source && typeof payload.source === "object" ? payload.source as Record<string, unknown> : null;
|
|
6719
|
-
|
|
6720
|
-
return
|
|
6907
|
+
if (source && typeof source.data === "string") return source.data;
|
|
6908
|
+
return null;
|
|
6909
|
+
}
|
|
6910
|
+
|
|
6911
|
+
function normalizeStudioTraceBase64Data(data: string): string | null {
|
|
6912
|
+
const compact = String(data || "").replace(/\s+/g, "");
|
|
6913
|
+
if (!compact || !/^[A-Za-z0-9+/]*={0,2}$/.test(compact)) return null;
|
|
6914
|
+
return compact;
|
|
6721
6915
|
}
|
|
6722
6916
|
|
|
6723
|
-
function
|
|
6917
|
+
function estimateStudioTraceBase64ByteLength(data: string): number | null {
|
|
6918
|
+
const compact = normalizeStudioTraceBase64Data(data);
|
|
6919
|
+
if (!compact) return null;
|
|
6920
|
+
const padding = compact.endsWith("==") ? 2 : (compact.endsWith("=") ? 1 : 0);
|
|
6921
|
+
return Math.max(0, Math.floor((compact.length * 3) / 4) - padding);
|
|
6922
|
+
}
|
|
6923
|
+
|
|
6924
|
+
function formatStudioTraceByteSize(bytes: number | null): string {
|
|
6925
|
+
if (typeof bytes !== "number" || !Number.isFinite(bytes) || bytes < 0) return "unknown size";
|
|
6926
|
+
if (bytes < 1024) return `${Math.round(bytes)} B`;
|
|
6927
|
+
const kib = bytes / 1024;
|
|
6928
|
+
if (kib < 1024) return `${kib.toFixed(kib >= 100 ? 0 : 1).replace(/\.0$/, "")} KB`;
|
|
6929
|
+
const mib = kib / 1024;
|
|
6930
|
+
return `${mib.toFixed(mib >= 100 ? 0 : 1).replace(/\.0$/, "")} MB`;
|
|
6931
|
+
}
|
|
6932
|
+
|
|
6933
|
+
function describeStudioTraceImageBlock(block: unknown, reason?: string): string {
|
|
6934
|
+
const mime = getStudioTraceImageMimeType(block) || "image";
|
|
6935
|
+
return `[Image: ${mime}${reason ? ` ${reason}` : ""}]`;
|
|
6936
|
+
}
|
|
6937
|
+
|
|
6938
|
+
function collectStudioTraceImageBlock(block: unknown, images: StudioTraceImage[]): string {
|
|
6939
|
+
const mimeType = getStudioTraceImageMimeType(block) || "image/unknown";
|
|
6940
|
+
if (!isStudioTraceSafeImageMimeType(mimeType)) {
|
|
6941
|
+
return describeStudioTraceImageBlock(block, "omitted from Working view: unsupported image type");
|
|
6942
|
+
}
|
|
6943
|
+
if (images.length >= STUDIO_TRACE_IMAGE_MAX_COUNT) {
|
|
6944
|
+
return describeStudioTraceImageBlock(block, "omitted from Working view: image count limit reached");
|
|
6945
|
+
}
|
|
6946
|
+
const data = getStudioTraceImageData(block);
|
|
6947
|
+
const normalizedData = data ? normalizeStudioTraceBase64Data(data) : null;
|
|
6948
|
+
if (!normalizedData) {
|
|
6949
|
+
return describeStudioTraceImageBlock(block, "omitted from Working view: no base64 data");
|
|
6950
|
+
}
|
|
6951
|
+
if (normalizedData.length > STUDIO_TRACE_IMAGE_MAX_BASE64_CHARS) {
|
|
6952
|
+
const estimatedBytes = estimateStudioTraceBase64ByteLength(normalizedData);
|
|
6953
|
+
return describeStudioTraceImageBlock(block, `omitted from Working view: ${formatStudioTraceByteSize(estimatedBytes)} exceeds image preview limit`);
|
|
6954
|
+
}
|
|
6724
6955
|
const payload = (block && typeof block === "object") ? block as Record<string, unknown> : {};
|
|
6725
|
-
const
|
|
6726
|
-
const
|
|
6727
|
-
|
|
6728
|
-
|
|
6729
|
-
|
|
6730
|
-
|
|
6731
|
-
|
|
6956
|
+
const hash = createHash("sha256").update(mimeType).update(normalizedData).digest("hex").slice(0, 16);
|
|
6957
|
+
const image: StudioTraceImage = {
|
|
6958
|
+
id: `trace-image-${hash}-${images.length + 1}`,
|
|
6959
|
+
mimeType,
|
|
6960
|
+
data: normalizedData,
|
|
6961
|
+
byteLength: estimateStudioTraceBase64ByteLength(normalizedData),
|
|
6962
|
+
label: typeof payload.label === "string" && payload.label.trim()
|
|
6963
|
+
? payload.label.trim()
|
|
6964
|
+
: (typeof payload.alt === "string" && payload.alt.trim() ? payload.alt.trim() : null),
|
|
6965
|
+
};
|
|
6966
|
+
images.push(image);
|
|
6967
|
+
return "";
|
|
6732
6968
|
}
|
|
6733
6969
|
|
|
6734
6970
|
function stringifyStudioTraceObject(value: unknown): string {
|
|
@@ -6745,19 +6981,19 @@ function stringifyStudioTraceObject(value: unknown): string {
|
|
|
6745
6981
|
}
|
|
6746
6982
|
}
|
|
6747
6983
|
|
|
6748
|
-
function
|
|
6984
|
+
function formatStudioTraceOutputPart(result: unknown, images: StudioTraceImage[]): string {
|
|
6749
6985
|
if (result == null) return "";
|
|
6750
6986
|
if (typeof result === "string") return sanitizeStudioTraceOutputText(result);
|
|
6751
6987
|
if (Array.isArray(result)) {
|
|
6752
|
-
return result.map((item) =>
|
|
6988
|
+
return result.map((item) => formatStudioTraceOutputPart(item, images)).filter(Boolean).join("\n");
|
|
6753
6989
|
}
|
|
6754
6990
|
if (typeof result === "object") {
|
|
6755
|
-
if (isStudioTraceImageBlock(result)) return
|
|
6991
|
+
if (isStudioTraceImageBlock(result)) return collectStudioTraceImageBlock(result, images);
|
|
6756
6992
|
const payload = result as { content?: Array<{ type?: string; text?: string }> };
|
|
6757
6993
|
if (Array.isArray(payload.content)) {
|
|
6758
6994
|
return payload.content
|
|
6759
6995
|
.map((block) => {
|
|
6760
|
-
if (isStudioTraceImageBlock(block)) return
|
|
6996
|
+
if (isStudioTraceImageBlock(block)) return collectStudioTraceImageBlock(block, images);
|
|
6761
6997
|
if (block && block.type === "text" && typeof block.text === "string") return sanitizeStudioTraceOutputText(block.text);
|
|
6762
6998
|
return stringifyStudioTraceObject(block);
|
|
6763
6999
|
})
|
|
@@ -6769,6 +7005,18 @@ function formatStudioTraceOutput(result: unknown): string {
|
|
|
6769
7005
|
return sanitizeStudioTraceOutputText(String(result));
|
|
6770
7006
|
}
|
|
6771
7007
|
|
|
7008
|
+
function formatStudioTraceToolResult(result: unknown): { output: string; images: StudioTraceImage[] } {
|
|
7009
|
+
const images: StudioTraceImage[] = [];
|
|
7010
|
+
return {
|
|
7011
|
+
output: formatStudioTraceOutputPart(result, images),
|
|
7012
|
+
images,
|
|
7013
|
+
};
|
|
7014
|
+
}
|
|
7015
|
+
|
|
7016
|
+
function formatStudioTraceOutput(result: unknown): string {
|
|
7017
|
+
return formatStudioTraceToolResult(result).output;
|
|
7018
|
+
}
|
|
7019
|
+
|
|
6772
7020
|
function summarizeStudioTraceToolArgs(toolName: string, args: unknown): string | null {
|
|
6773
7021
|
const normalizedTool = String(toolName || "").trim().toLowerCase();
|
|
6774
7022
|
const payload = (args && typeof args === "object") ? (args as Record<string, unknown>) : {};
|
|
@@ -6793,6 +7041,218 @@ function summarizeStudioTraceToolArgs(toolName: string, args: unknown): string |
|
|
|
6793
7041
|
}
|
|
6794
7042
|
}
|
|
6795
7043
|
|
|
7044
|
+
function isStudioReplRuntime(value: unknown): value is StudioReplRuntime {
|
|
7045
|
+
return value === "shell"
|
|
7046
|
+
|| value === "python"
|
|
7047
|
+
|| value === "ipython"
|
|
7048
|
+
|| value === "julia"
|
|
7049
|
+
|| value === "r"
|
|
7050
|
+
|| value === "ghci"
|
|
7051
|
+
|| value === "clojure";
|
|
7052
|
+
}
|
|
7053
|
+
|
|
7054
|
+
function normalizeStudioReplRuntime(value: unknown): StudioReplRuntime | null {
|
|
7055
|
+
const normalized = String(value || "").trim().toLowerCase();
|
|
7056
|
+
if (normalized === "r") return "r";
|
|
7057
|
+
return isStudioReplRuntime(normalized) ? normalized : null;
|
|
7058
|
+
}
|
|
7059
|
+
|
|
7060
|
+
function getStudioReplRuntimeCommand(runtime: StudioReplRuntime): string {
|
|
7061
|
+
if (runtime === "shell") return String(process.env.SHELL || "bash").trim() || "bash";
|
|
7062
|
+
if (runtime === "python") return "python3";
|
|
7063
|
+
if (runtime === "ipython") return "ipython";
|
|
7064
|
+
if (runtime === "julia") return "julia";
|
|
7065
|
+
if (runtime === "r") return "R";
|
|
7066
|
+
if (runtime === "ghci") return "ghci";
|
|
7067
|
+
return "clojure";
|
|
7068
|
+
}
|
|
7069
|
+
|
|
7070
|
+
function getStudioReplSessionName(runtime: StudioReplRuntime): string {
|
|
7071
|
+
return `pi-studio-repl-${runtime}`;
|
|
7072
|
+
}
|
|
7073
|
+
|
|
7074
|
+
function getNewStudioReplSessionName(runtime: StudioReplRuntime): string {
|
|
7075
|
+
const suffix = `${Date.now().toString(36)}-${randomUUID().slice(0, 6)}`;
|
|
7076
|
+
return `pi-studio-repl-${runtime}-${suffix}`;
|
|
7077
|
+
}
|
|
7078
|
+
|
|
7079
|
+
function getStudioReplPaneTarget(sessionName: string): string {
|
|
7080
|
+
return `${sessionName}:0.0`;
|
|
7081
|
+
}
|
|
7082
|
+
|
|
7083
|
+
function inferStudioReplSessionRuntime(sessionName: string): { runtime: StudioReplRuntime | "unknown"; source: StudioReplSessionInfo["source"] } {
|
|
7084
|
+
const studioMatch = sessionName.match(/^pi-studio-repl-([a-z0-9-]+)$/i);
|
|
7085
|
+
if (studioMatch) {
|
|
7086
|
+
const raw = (studioMatch[1] || "").toLowerCase();
|
|
7087
|
+
const runtime = (["clojure", "python", "ipython", "julia", "shell", "ghci", "r"] as StudioReplRuntime[])
|
|
7088
|
+
.find((candidate) => raw === candidate || raw.startsWith(`${candidate}-`));
|
|
7089
|
+
return { runtime: runtime ?? "unknown", source: "studio" };
|
|
7090
|
+
}
|
|
7091
|
+
const piReplMatch = sessionName.match(/^pi-repl-([a-z0-9-]+)$/i);
|
|
7092
|
+
if (piReplMatch) {
|
|
7093
|
+
const raw = piReplMatch[1]?.toLowerCase() || "";
|
|
7094
|
+
const runtime = raw === "python" ? "python" : normalizeStudioReplRuntime(raw);
|
|
7095
|
+
return { runtime: runtime ?? "unknown", source: "pi-repl" };
|
|
7096
|
+
}
|
|
7097
|
+
return { runtime: "unknown", source: "tmux" };
|
|
7098
|
+
}
|
|
7099
|
+
|
|
7100
|
+
function shouldShowStudioReplTmuxSession(sessionName: string): boolean {
|
|
7101
|
+
return /^pi-studio-repl-/i.test(sessionName) || /^pi-repl-/i.test(sessionName);
|
|
7102
|
+
}
|
|
7103
|
+
|
|
7104
|
+
function formatStudioReplSessionLabel(sessionName: string, runtime: StudioReplRuntime | "unknown", source: StudioReplSessionInfo["source"]): string {
|
|
7105
|
+
const runtimeLabel = runtime === "unknown" ? "REPL" : STUDIO_REPL_RUNTIME_LABELS[runtime];
|
|
7106
|
+
if (source === "pi-repl") return `${runtimeLabel} (${sessionName})`;
|
|
7107
|
+
if (source === "studio") return `${runtimeLabel} (${sessionName})`;
|
|
7108
|
+
return sessionName;
|
|
7109
|
+
}
|
|
7110
|
+
|
|
7111
|
+
function isTmuxAvailable(): boolean {
|
|
7112
|
+
const result = spawnSync("tmux", ["-V"], { encoding: "utf8", timeout: 3_000 });
|
|
7113
|
+
return result.status === 0;
|
|
7114
|
+
}
|
|
7115
|
+
|
|
7116
|
+
function runStudioTmux(args: string[], options?: { cwd?: string; input?: string; timeout?: number }): { ok: true; stdout: string; stderr: string } | { ok: false; message: string; stdout: string; stderr: string } {
|
|
7117
|
+
const result = spawnSync("tmux", args, {
|
|
7118
|
+
cwd: options?.cwd,
|
|
7119
|
+
input: options?.input,
|
|
7120
|
+
encoding: "utf8",
|
|
7121
|
+
timeout: options?.timeout ?? 5_000,
|
|
7122
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
7123
|
+
});
|
|
7124
|
+
const stdout = typeof result.stdout === "string" ? result.stdout : "";
|
|
7125
|
+
const stderr = typeof result.stderr === "string" ? result.stderr : "";
|
|
7126
|
+
if (result.error) {
|
|
7127
|
+
const message = result.error.message || String(result.error);
|
|
7128
|
+
return { ok: false, message, stdout, stderr };
|
|
7129
|
+
}
|
|
7130
|
+
if (result.status !== 0) {
|
|
7131
|
+
return { ok: false, message: (stderr || stdout || `tmux exited with code ${result.status}`).trim(), stdout, stderr };
|
|
7132
|
+
}
|
|
7133
|
+
return { ok: true, stdout, stderr };
|
|
7134
|
+
}
|
|
7135
|
+
|
|
7136
|
+
function listStudioReplSessions(): { tmuxAvailable: boolean; sessions: StudioReplSessionInfo[]; error?: string } {
|
|
7137
|
+
if (!isTmuxAvailable()) return { tmuxAvailable: false, sessions: [], error: "tmux is not available." };
|
|
7138
|
+
const result = runStudioTmux(["list-sessions", "-F", "#{session_name}"], { timeout: 3_000 });
|
|
7139
|
+
if (!result.ok) {
|
|
7140
|
+
const message = result.message.toLowerCase().includes("no server running") ? "No tmux sessions are running." : result.message;
|
|
7141
|
+
return { tmuxAvailable: true, sessions: [], error: message };
|
|
7142
|
+
}
|
|
7143
|
+
const sessions = result.stdout
|
|
7144
|
+
.split(/\r?\n/)
|
|
7145
|
+
.map((line) => line.trim())
|
|
7146
|
+
.filter(Boolean)
|
|
7147
|
+
.filter(shouldShowStudioReplTmuxSession)
|
|
7148
|
+
.map((sessionName) => {
|
|
7149
|
+
const inferred = inferStudioReplSessionRuntime(sessionName);
|
|
7150
|
+
return {
|
|
7151
|
+
sessionName,
|
|
7152
|
+
target: getStudioReplPaneTarget(sessionName),
|
|
7153
|
+
runtime: inferred.runtime,
|
|
7154
|
+
label: formatStudioReplSessionLabel(sessionName, inferred.runtime, inferred.source),
|
|
7155
|
+
source: inferred.source,
|
|
7156
|
+
};
|
|
7157
|
+
});
|
|
7158
|
+
return { tmuxAvailable: true, sessions };
|
|
7159
|
+
}
|
|
7160
|
+
|
|
7161
|
+
function captureStudioReplSession(sessionName: string): { ok: true; transcript: string; session: StudioReplSessionInfo } | { ok: false; message: string } {
|
|
7162
|
+
if (!/^[-_.A-Za-z0-9]+$/.test(sessionName)) return { ok: false, message: "Invalid REPL session name." };
|
|
7163
|
+
const inferred = inferStudioReplSessionRuntime(sessionName);
|
|
7164
|
+
const session: StudioReplSessionInfo = {
|
|
7165
|
+
sessionName,
|
|
7166
|
+
target: getStudioReplPaneTarget(sessionName),
|
|
7167
|
+
runtime: inferred.runtime,
|
|
7168
|
+
label: formatStudioReplSessionLabel(sessionName, inferred.runtime, inferred.source),
|
|
7169
|
+
source: inferred.source,
|
|
7170
|
+
};
|
|
7171
|
+
const result = runStudioTmux(["capture-pane", "-p", "-t", session.target, "-S", `-${STUDIO_REPL_CAPTURE_LINES}`], { timeout: 3_000 });
|
|
7172
|
+
if (!result.ok) return { ok: false, message: result.message };
|
|
7173
|
+
return { ok: true, transcript: result.stdout.replace(/[\t ]+$/gm, "").trimEnd(), session };
|
|
7174
|
+
}
|
|
7175
|
+
|
|
7176
|
+
function startStudioReplSession(runtime: StudioReplRuntime, cwd: string, options?: { newSession?: boolean }): { ok: true; session: StudioReplSessionInfo; message: string } | { ok: false; message: string } {
|
|
7177
|
+
if (!isTmuxAvailable()) return { ok: false, message: "tmux is not available. Install tmux to use Studio REPL sessions." };
|
|
7178
|
+
const sessionName = options?.newSession ? getNewStudioReplSessionName(runtime) : getStudioReplSessionName(runtime);
|
|
7179
|
+
const existing = runStudioTmux(["has-session", "-t", sessionName], { timeout: 3_000 });
|
|
7180
|
+
if (existing.ok) {
|
|
7181
|
+
const inferred = inferStudioReplSessionRuntime(sessionName);
|
|
7182
|
+
return {
|
|
7183
|
+
ok: true,
|
|
7184
|
+
session: {
|
|
7185
|
+
sessionName,
|
|
7186
|
+
target: getStudioReplPaneTarget(sessionName),
|
|
7187
|
+
runtime: inferred.runtime,
|
|
7188
|
+
label: formatStudioReplSessionLabel(sessionName, inferred.runtime, inferred.source),
|
|
7189
|
+
source: inferred.source,
|
|
7190
|
+
},
|
|
7191
|
+
message: `${STUDIO_REPL_RUNTIME_LABELS[runtime]} REPL is already running.`,
|
|
7192
|
+
};
|
|
7193
|
+
}
|
|
7194
|
+
const command = getStudioReplRuntimeCommand(runtime);
|
|
7195
|
+
const result = runStudioTmux(["new-session", "-d", "-s", sessionName, "-c", cwd || process.cwd(), command], { timeout: 5_000 });
|
|
7196
|
+
if (!result.ok) return { ok: false, message: result.message || `Failed to start ${STUDIO_REPL_RUNTIME_LABELS[runtime]} REPL.` };
|
|
7197
|
+
return {
|
|
7198
|
+
ok: true,
|
|
7199
|
+
session: {
|
|
7200
|
+
sessionName,
|
|
7201
|
+
target: getStudioReplPaneTarget(sessionName),
|
|
7202
|
+
runtime,
|
|
7203
|
+
label: formatStudioReplSessionLabel(sessionName, runtime, "studio"),
|
|
7204
|
+
source: "studio",
|
|
7205
|
+
},
|
|
7206
|
+
message: `Started ${options?.newSession ? "new " : ""}${STUDIO_REPL_RUNTIME_LABELS[runtime]} REPL.`,
|
|
7207
|
+
};
|
|
7208
|
+
}
|
|
7209
|
+
|
|
7210
|
+
function stopStudioReplSession(sessionName: string): { ok: true; message: string } | { ok: false; message: string } {
|
|
7211
|
+
if (!/^[-_.A-Za-z0-9]+$/.test(sessionName)) return { ok: false, message: "Invalid REPL session name." };
|
|
7212
|
+
const inferred = inferStudioReplSessionRuntime(sessionName);
|
|
7213
|
+
if (inferred.source !== "studio") {
|
|
7214
|
+
return { ok: false, message: "Studio can only stop Studio-owned REPL sessions. Use tmux or pi-repl to stop external sessions." };
|
|
7215
|
+
}
|
|
7216
|
+
const result = runStudioTmux(["kill-session", "-t", sessionName], { timeout: 5_000 });
|
|
7217
|
+
if (!result.ok) return { ok: false, message: result.message || "Failed to stop REPL session." };
|
|
7218
|
+
return { ok: true, message: `Stopped ${sessionName}.` };
|
|
7219
|
+
}
|
|
7220
|
+
|
|
7221
|
+
function prepareTextForStudioReplSend(sessionName: string, source: string): string {
|
|
7222
|
+
const normalized = String(source || "").replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
7223
|
+
const runtime = inferStudioReplSessionRuntime(sessionName).runtime;
|
|
7224
|
+
if ((runtime === "python" || runtime === "ipython") && normalized.includes("\n")) {
|
|
7225
|
+
// The standard Python prompt needs a final blank line to close pasted suites
|
|
7226
|
+
// such as `for`/`if`/`def` blocks. Without it the prompt can remain in
|
|
7227
|
+
// continuation mode, making the next send look like an unexpected indent.
|
|
7228
|
+
return `${normalized.replace(/\n+$/, "")}\n\n`;
|
|
7229
|
+
}
|
|
7230
|
+
return normalized.endsWith("\n") ? normalized : `${normalized}\n`;
|
|
7231
|
+
}
|
|
7232
|
+
|
|
7233
|
+
function sendTextToStudioReplSession(sessionName: string, text: string): { ok: true; message: string } | { ok: false; message: string } {
|
|
7234
|
+
if (!/^[-_.A-Za-z0-9]+$/.test(sessionName)) return { ok: false, message: "Invalid REPL session name." };
|
|
7235
|
+
const source = String(text || "");
|
|
7236
|
+
if (!source.trim()) return { ok: false, message: "Editor text is empty." };
|
|
7237
|
+
if (source.length > STUDIO_REPL_SEND_MAX_CHARS) {
|
|
7238
|
+
return { ok: false, message: `REPL input is too large (${source.length} chars; max ${STUDIO_REPL_SEND_MAX_CHARS}).` };
|
|
7239
|
+
}
|
|
7240
|
+
const bufferName = `pi-studio-repl-${randomUUID().replace(/-/g, "")}`;
|
|
7241
|
+
const input = prepareTextForStudioReplSend(sessionName, source);
|
|
7242
|
+
const loadResult = runStudioTmux(["load-buffer", "-b", bufferName, "-"], { input, timeout: 5_000 });
|
|
7243
|
+
if (!loadResult.ok) return { ok: false, message: loadResult.message || "Failed to load text into tmux buffer." };
|
|
7244
|
+
const pasteResult = runStudioTmux(["paste-buffer", "-d", "-b", bufferName, "-t", getStudioReplPaneTarget(sessionName)], { timeout: 5_000 });
|
|
7245
|
+
if (!pasteResult.ok) return { ok: false, message: pasteResult.message || "Failed to paste text into REPL session." };
|
|
7246
|
+
return { ok: true, message: `Sent ${source.length} chars to ${sessionName}.` };
|
|
7247
|
+
}
|
|
7248
|
+
|
|
7249
|
+
function interruptStudioReplSession(sessionName: string): { ok: true; message: string } | { ok: false; message: string } {
|
|
7250
|
+
if (!/^[-_.A-Za-z0-9]+$/.test(sessionName)) return { ok: false, message: "Invalid REPL session name." };
|
|
7251
|
+
const result = runStudioTmux(["send-keys", "-t", getStudioReplPaneTarget(sessionName), "C-c"], { timeout: 5_000 });
|
|
7252
|
+
if (!result.ok) return { ok: false, message: result.message || "Failed to interrupt REPL session." };
|
|
7253
|
+
return { ok: true, message: `Interrupted ${sessionName}.` };
|
|
7254
|
+
}
|
|
7255
|
+
|
|
6796
7256
|
function isAllowedOrigin(_origin: string | undefined, _port: number): boolean {
|
|
6797
7257
|
// For local-only studio, token auth is the primary guard. In practice,
|
|
6798
7258
|
// browser origin headers can vary (or be omitted) across wrappers/browsers,
|
|
@@ -7283,6 +7743,11 @@ ${cssVarsBlock}
|
|
|
7283
7743
|
<div class="source-actions-row">
|
|
7284
7744
|
<button id="sendRunBtn" type="button" title="Run editor text. While a direct run is active, this button becomes Stop. Cmd/Ctrl+Enter queues steering from the current editor text. Stop the active request with Esc.">Run editor text</button>
|
|
7285
7745
|
<button id="queueSteerBtn" type="button" title="Queue steering is available while Run editor text is active." disabled>Queue steering</button>
|
|
7746
|
+
<button id="sendReplBtn" type="button" hidden title="Send the current selection, or the full editor text, to the active REPL session shown in the right pane.">Send to REPL</button>
|
|
7747
|
+
<select id="replSendModeSelect" hidden aria-label="REPL send mode" title="Choose how Send to REPL interprets the editor text.">
|
|
7748
|
+
<option value="scratch" selected>Scratch send</option>
|
|
7749
|
+
<option value="literate">Literate send</option>
|
|
7750
|
+
</select>
|
|
7286
7751
|
<button id="copyDraftBtn" type="button" title="Copy the current editor text to the clipboard.">Copy text</button>
|
|
7287
7752
|
<button id="openCompanionBtn" type="button" title="Open a detached copy of the current editor text in a new editor-only Studio tab.">Open new editor</button>
|
|
7288
7753
|
<button id="sendEditorBtn" type="button">Send to pi editor</button>
|
|
@@ -7425,6 +7890,7 @@ ${cssVarsBlock}
|
|
|
7425
7890
|
<option value="preview" selected>Response (Preview)</option>
|
|
7426
7891
|
<option value="editor-preview">Editor (Preview)</option>
|
|
7427
7892
|
<option value="trace">Working</option>
|
|
7893
|
+
<option value="repl">REPL</option>
|
|
7428
7894
|
</select>
|
|
7429
7895
|
</div>
|
|
7430
7896
|
<div class="section-header-actions">
|
|
@@ -7562,6 +8028,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
7562
8028
|
contextWindow: null,
|
|
7563
8029
|
percent: null,
|
|
7564
8030
|
};
|
|
8031
|
+
let studioReplActiveSessionName: string | null = null;
|
|
7565
8032
|
let compactInProgress = false;
|
|
7566
8033
|
let compactRequestId: string | null = null;
|
|
7567
8034
|
|
|
@@ -7997,6 +8464,57 @@ export default function (pi: ExtensionAPI) {
|
|
|
7997
8464
|
}
|
|
7998
8465
|
};
|
|
7999
8466
|
|
|
8467
|
+
const sendReplStateToClient = (client: WebSocket, extra?: Record<string, unknown>) => {
|
|
8468
|
+
const state = listStudioReplSessions();
|
|
8469
|
+
if (studioReplActiveSessionName && !state.sessions.some((session) => session.sessionName === studioReplActiveSessionName)) {
|
|
8470
|
+
studioReplActiveSessionName = state.sessions[0]?.sessionName ?? null;
|
|
8471
|
+
} else if (!studioReplActiveSessionName && state.sessions.length > 0) {
|
|
8472
|
+
studioReplActiveSessionName = state.sessions[0].sessionName;
|
|
8473
|
+
}
|
|
8474
|
+
sendToClient(client, {
|
|
8475
|
+
type: "repl_state",
|
|
8476
|
+
tmuxAvailable: state.tmuxAvailable,
|
|
8477
|
+
sessions: state.sessions,
|
|
8478
|
+
activeSessionName: studioReplActiveSessionName,
|
|
8479
|
+
error: state.error ?? null,
|
|
8480
|
+
...extra,
|
|
8481
|
+
});
|
|
8482
|
+
};
|
|
8483
|
+
|
|
8484
|
+
const sendReplCaptureToClient = (client: WebSocket, sessionName?: string | null, extra?: Record<string, unknown>) => {
|
|
8485
|
+
const targetSession = (typeof sessionName === "string" && sessionName.trim())
|
|
8486
|
+
? sessionName.trim()
|
|
8487
|
+
: studioReplActiveSessionName;
|
|
8488
|
+
if (!targetSession) {
|
|
8489
|
+
sendReplStateToClient(client, {
|
|
8490
|
+
transcript: "",
|
|
8491
|
+
capturedAt: Date.now(),
|
|
8492
|
+
...extra,
|
|
8493
|
+
});
|
|
8494
|
+
return;
|
|
8495
|
+
}
|
|
8496
|
+
const captured = captureStudioReplSession(targetSession);
|
|
8497
|
+
if (!captured.ok) {
|
|
8498
|
+
sendReplStateToClient(client, {
|
|
8499
|
+
activeSessionName: targetSession,
|
|
8500
|
+
transcript: "",
|
|
8501
|
+
captureError: captured.message,
|
|
8502
|
+
capturedAt: Date.now(),
|
|
8503
|
+
...extra,
|
|
8504
|
+
});
|
|
8505
|
+
return;
|
|
8506
|
+
}
|
|
8507
|
+
studioReplActiveSessionName = captured.session.sessionName;
|
|
8508
|
+
sendToClient(client, {
|
|
8509
|
+
type: "repl_capture",
|
|
8510
|
+
session: captured.session,
|
|
8511
|
+
activeSessionName: captured.session.sessionName,
|
|
8512
|
+
transcript: captured.transcript,
|
|
8513
|
+
capturedAt: Date.now(),
|
|
8514
|
+
...extra,
|
|
8515
|
+
});
|
|
8516
|
+
};
|
|
8517
|
+
|
|
8000
8518
|
const emitDebugEvent = (event: string, details?: Record<string, unknown>) => {
|
|
8001
8519
|
broadcast({
|
|
8002
8520
|
type: "debug_event",
|
|
@@ -8151,6 +8669,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
8151
8669
|
label: deriveToolActivityLabel(toolName, args),
|
|
8152
8670
|
argsSummary: summarizeStudioTraceToolArgs(toolName, args),
|
|
8153
8671
|
output: "",
|
|
8672
|
+
images: [],
|
|
8154
8673
|
startedAt: now,
|
|
8155
8674
|
updatedAt: now,
|
|
8156
8675
|
status: "pending",
|
|
@@ -8168,9 +8687,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
8168
8687
|
output: string,
|
|
8169
8688
|
status: StudioTraceEntryStatus,
|
|
8170
8689
|
isError: boolean,
|
|
8690
|
+
images?: StudioTraceImage[],
|
|
8171
8691
|
) => {
|
|
8172
8692
|
const entry = ensureStudioTraceToolEntry(toolCallId, toolName, args);
|
|
8173
8693
|
entry.output = output;
|
|
8694
|
+
if (Array.isArray(images)) entry.images = images;
|
|
8174
8695
|
entry.status = status;
|
|
8175
8696
|
entry.isError = isError;
|
|
8176
8697
|
entry.updatedAt = Date.now();
|
|
@@ -8818,6 +9339,82 @@ export default function (pi: ExtensionAPI) {
|
|
|
8818
9339
|
return;
|
|
8819
9340
|
}
|
|
8820
9341
|
|
|
9342
|
+
if (msg.type === "repl_list_request") {
|
|
9343
|
+
sendReplStateToClient(client);
|
|
9344
|
+
return;
|
|
9345
|
+
}
|
|
9346
|
+
|
|
9347
|
+
if (msg.type === "repl_capture_request") {
|
|
9348
|
+
sendReplCaptureToClient(client, msg.sessionName ?? null);
|
|
9349
|
+
return;
|
|
9350
|
+
}
|
|
9351
|
+
|
|
9352
|
+
if (msg.type === "repl_start_request") {
|
|
9353
|
+
if (!isValidRequestId(msg.requestId)) {
|
|
9354
|
+
sendToClient(client, { type: "error", requestId: msg.requestId, message: "Invalid request ID." });
|
|
9355
|
+
return;
|
|
9356
|
+
}
|
|
9357
|
+
const started = startStudioReplSession(msg.runtime, studioCwd, { newSession: msg.newSession });
|
|
9358
|
+
if (!started.ok) {
|
|
9359
|
+
sendReplStateToClient(client, { requestId: msg.requestId, replError: started.message });
|
|
9360
|
+
sendToClient(client, { type: "error", requestId: msg.requestId, message: started.message });
|
|
9361
|
+
return;
|
|
9362
|
+
}
|
|
9363
|
+
studioReplActiveSessionName = started.session.sessionName;
|
|
9364
|
+
sendReplStateToClient(client, { requestId: msg.requestId, replMessage: started.message });
|
|
9365
|
+
sendReplCaptureToClient(client, started.session.sessionName, { requestId: msg.requestId, replMessage: started.message });
|
|
9366
|
+
return;
|
|
9367
|
+
}
|
|
9368
|
+
|
|
9369
|
+
if (msg.type === "repl_stop_request") {
|
|
9370
|
+
if (!isValidRequestId(msg.requestId)) {
|
|
9371
|
+
sendToClient(client, { type: "error", requestId: msg.requestId, message: "Invalid request ID." });
|
|
9372
|
+
return;
|
|
9373
|
+
}
|
|
9374
|
+
const stopped = stopStudioReplSession(msg.sessionName);
|
|
9375
|
+
if (!stopped.ok) {
|
|
9376
|
+
sendReplStateToClient(client, { requestId: msg.requestId, replError: stopped.message });
|
|
9377
|
+
sendToClient(client, { type: "error", requestId: msg.requestId, message: stopped.message });
|
|
9378
|
+
return;
|
|
9379
|
+
}
|
|
9380
|
+
if (studioReplActiveSessionName === msg.sessionName) studioReplActiveSessionName = null;
|
|
9381
|
+
sendReplStateToClient(client, { requestId: msg.requestId, replMessage: stopped.message, transcript: "", capturedAt: Date.now() });
|
|
9382
|
+
return;
|
|
9383
|
+
}
|
|
9384
|
+
|
|
9385
|
+
if (msg.type === "repl_send_request") {
|
|
9386
|
+
if (!isValidRequestId(msg.requestId)) {
|
|
9387
|
+
sendToClient(client, { type: "error", requestId: msg.requestId, message: "Invalid request ID." });
|
|
9388
|
+
return;
|
|
9389
|
+
}
|
|
9390
|
+
const sent = sendTextToStudioReplSession(msg.sessionName, msg.text);
|
|
9391
|
+
if (!sent.ok) {
|
|
9392
|
+
sendToClient(client, { type: "error", requestId: msg.requestId, message: sent.message });
|
|
9393
|
+
sendReplCaptureToClient(client, msg.sessionName, { requestId: msg.requestId, replError: sent.message });
|
|
9394
|
+
return;
|
|
9395
|
+
}
|
|
9396
|
+
studioReplActiveSessionName = msg.sessionName;
|
|
9397
|
+
sendToClient(client, { type: "repl_send_ack", requestId: msg.requestId, sessionName: msg.sessionName, message: sent.message });
|
|
9398
|
+
setTimeout(() => sendReplCaptureToClient(client, msg.sessionName, { requestId: msg.requestId }), 150);
|
|
9399
|
+
return;
|
|
9400
|
+
}
|
|
9401
|
+
|
|
9402
|
+
if (msg.type === "repl_interrupt_request") {
|
|
9403
|
+
if (!isValidRequestId(msg.requestId)) {
|
|
9404
|
+
sendToClient(client, { type: "error", requestId: msg.requestId, message: "Invalid request ID." });
|
|
9405
|
+
return;
|
|
9406
|
+
}
|
|
9407
|
+
const interrupted = interruptStudioReplSession(msg.sessionName);
|
|
9408
|
+
if (!interrupted.ok) {
|
|
9409
|
+
sendToClient(client, { type: "error", requestId: msg.requestId, message: interrupted.message });
|
|
9410
|
+
sendReplCaptureToClient(client, msg.sessionName, { requestId: msg.requestId, replError: interrupted.message });
|
|
9411
|
+
return;
|
|
9412
|
+
}
|
|
9413
|
+
studioReplActiveSessionName = msg.sessionName;
|
|
9414
|
+
sendReplCaptureToClient(client, msg.sessionName, { requestId: msg.requestId, replMessage: interrupted.message });
|
|
9415
|
+
return;
|
|
9416
|
+
}
|
|
9417
|
+
|
|
8821
9418
|
if (msg.type === "compact_request") {
|
|
8822
9419
|
if (!isValidRequestId(msg.requestId)) {
|
|
8823
9420
|
sendToClient(client, { type: "error", requestId: msg.requestId, message: "Invalid request ID." });
|
|
@@ -10233,25 +10830,29 @@ export default function (pi: ExtensionAPI) {
|
|
|
10233
10830
|
|
|
10234
10831
|
pi.on("tool_execution_update", async (event) => {
|
|
10235
10832
|
if (!agentBusy) return;
|
|
10833
|
+
const formatted = formatStudioTraceToolResult(event.partialResult);
|
|
10236
10834
|
updateStudioTraceToolEntry(
|
|
10237
10835
|
event.toolCallId,
|
|
10238
10836
|
event.toolName,
|
|
10239
10837
|
event.args,
|
|
10240
|
-
|
|
10838
|
+
formatted.output,
|
|
10241
10839
|
"streaming",
|
|
10242
10840
|
false,
|
|
10841
|
+
formatted.images,
|
|
10243
10842
|
);
|
|
10244
10843
|
});
|
|
10245
10844
|
|
|
10246
10845
|
pi.on("tool_execution_end", async (event) => {
|
|
10247
10846
|
if (!agentBusy) return;
|
|
10847
|
+
const formatted = formatStudioTraceToolResult(event.result);
|
|
10248
10848
|
updateStudioTraceToolEntry(
|
|
10249
10849
|
event.toolCallId,
|
|
10250
10850
|
event.toolName,
|
|
10251
10851
|
undefined,
|
|
10252
|
-
|
|
10852
|
+
formatted.output,
|
|
10253
10853
|
event.isError ? "error" : "complete",
|
|
10254
10854
|
Boolean(event.isError),
|
|
10855
|
+
formatted.images,
|
|
10255
10856
|
);
|
|
10256
10857
|
emitDebugEvent("tool_execution_end", { toolName: event.toolName, activeRequestId: activeRequest?.id ?? null, activeRequestKind: activeRequest?.kind ?? null });
|
|
10257
10858
|
// Keep tool phase visible until the next tool call, assistant response phase,
|