pi-studio 0.5.43 → 0.5.45
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 +21 -0
- package/README.md +2 -1
- package/client/studio-client.js +1509 -68
- package/client/studio.css +372 -4
- package/index.ts +481 -51
- package/package.json +9 -2
package/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionCommandContext, SessionEntry, Theme } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { getAgentDir } from "@mariozechner/pi-coding-agent";
|
|
2
3
|
import { spawn, spawnSync } from "node:child_process";
|
|
3
4
|
import { randomUUID } from "node:crypto";
|
|
4
5
|
import { readFileSync, statSync, writeFileSync } from "node:fs";
|
|
@@ -107,6 +108,25 @@ interface InitialStudioDocument {
|
|
|
107
108
|
label: string;
|
|
108
109
|
source: StudioSourceKind;
|
|
109
110
|
path?: string;
|
|
111
|
+
draftId?: string;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
interface PersistedStudioReviewNote {
|
|
115
|
+
id: string;
|
|
116
|
+
text: string;
|
|
117
|
+
createdAt: number;
|
|
118
|
+
updatedAt: number;
|
|
119
|
+
selectionStart: number;
|
|
120
|
+
selectionEnd: number;
|
|
121
|
+
lineStart: number;
|
|
122
|
+
lineEnd: number;
|
|
123
|
+
selectedText: string;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
interface StudioPersistentState {
|
|
127
|
+
version: 2;
|
|
128
|
+
scratchpadsByDocument: Record<string, string>;
|
|
129
|
+
reviewNotesByDocument: Record<string, PersistedStudioReviewNote[]>;
|
|
110
130
|
}
|
|
111
131
|
|
|
112
132
|
interface HelloMessage {
|
|
@@ -217,6 +237,172 @@ const CMUX_STUDIO_STATUS_KEY = "pi_studio";
|
|
|
217
237
|
const CMUX_STUDIO_STATUS_COLOR_DARK = "#5ea1ff";
|
|
218
238
|
const CMUX_STUDIO_STATUS_COLOR_LIGHT = "#0047ab";
|
|
219
239
|
const STUDIO_PROMPT_METADATA_CUSTOM_TYPE = "pi-studio/direct-prompt";
|
|
240
|
+
const STUDIO_DEFAULT_SCRATCHPAD_DOCUMENT_KEY = "doc:blank:blank";
|
|
241
|
+
const STUDIO_PERSISTENT_STATE_DIR = join(getAgentDir(), "pi-studio");
|
|
242
|
+
const STUDIO_PERSISTENT_STATE_PATH = join(STUDIO_PERSISTENT_STATE_DIR, "local-state.json");
|
|
243
|
+
|
|
244
|
+
let studioPersistentStateCache: StudioPersistentState | null = null;
|
|
245
|
+
let studioPersistentStateQueue: Promise<void> = Promise.resolve();
|
|
246
|
+
|
|
247
|
+
function createEmptyStudioPersistentState(): StudioPersistentState {
|
|
248
|
+
return {
|
|
249
|
+
version: 2,
|
|
250
|
+
scratchpadsByDocument: {},
|
|
251
|
+
reviewNotesByDocument: {},
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function normalizePersistedStudioReviewNote(value: unknown): PersistedStudioReviewNote | null {
|
|
256
|
+
if (!value || typeof value !== "object") return null;
|
|
257
|
+
const candidate = value as Partial<PersistedStudioReviewNote>;
|
|
258
|
+
if (typeof candidate.id !== "string" || !candidate.id.trim()) return null;
|
|
259
|
+
if (typeof candidate.text !== "string") return null;
|
|
260
|
+
const createdAt = typeof candidate.createdAt === "number" && Number.isFinite(candidate.createdAt)
|
|
261
|
+
? candidate.createdAt
|
|
262
|
+
: Date.now();
|
|
263
|
+
const updatedAt = typeof candidate.updatedAt === "number" && Number.isFinite(candidate.updatedAt)
|
|
264
|
+
? candidate.updatedAt
|
|
265
|
+
: createdAt;
|
|
266
|
+
const selectionStart = typeof candidate.selectionStart === "number" && Number.isFinite(candidate.selectionStart)
|
|
267
|
+
? Math.max(0, Math.floor(candidate.selectionStart))
|
|
268
|
+
: 0;
|
|
269
|
+
const selectionEnd = typeof candidate.selectionEnd === "number" && Number.isFinite(candidate.selectionEnd)
|
|
270
|
+
? Math.max(selectionStart, Math.floor(candidate.selectionEnd))
|
|
271
|
+
: selectionStart;
|
|
272
|
+
const lineStart = typeof candidate.lineStart === "number" && Number.isFinite(candidate.lineStart)
|
|
273
|
+
? Math.max(1, Math.floor(candidate.lineStart))
|
|
274
|
+
: 1;
|
|
275
|
+
const lineEnd = typeof candidate.lineEnd === "number" && Number.isFinite(candidate.lineEnd)
|
|
276
|
+
? Math.max(lineStart, Math.floor(candidate.lineEnd))
|
|
277
|
+
: lineStart;
|
|
278
|
+
return {
|
|
279
|
+
id: candidate.id,
|
|
280
|
+
text: candidate.text,
|
|
281
|
+
createdAt,
|
|
282
|
+
updatedAt,
|
|
283
|
+
selectionStart,
|
|
284
|
+
selectionEnd,
|
|
285
|
+
lineStart,
|
|
286
|
+
lineEnd,
|
|
287
|
+
selectedText: typeof candidate.selectedText === "string" ? candidate.selectedText : "",
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function normalizeStudioPersistentState(value: unknown): StudioPersistentState {
|
|
292
|
+
const fallback = createEmptyStudioPersistentState();
|
|
293
|
+
if (!value || typeof value !== "object") return fallback;
|
|
294
|
+
const candidate = value as Partial<StudioPersistentState> & {
|
|
295
|
+
reviewNotesByDocument?: unknown;
|
|
296
|
+
scratchpadsByDocument?: unknown;
|
|
297
|
+
scratchpadText?: unknown;
|
|
298
|
+
};
|
|
299
|
+
const reviewNotesByDocument: Record<string, PersistedStudioReviewNote[]> = {};
|
|
300
|
+
if (candidate.reviewNotesByDocument && typeof candidate.reviewNotesByDocument === "object") {
|
|
301
|
+
for (const [documentKey, rawNotes] of Object.entries(candidate.reviewNotesByDocument as Record<string, unknown>)) {
|
|
302
|
+
if (typeof documentKey !== "string" || !documentKey.trim() || !Array.isArray(rawNotes)) continue;
|
|
303
|
+
const normalizedNotes = rawNotes
|
|
304
|
+
.map((note) => normalizePersistedStudioReviewNote(note))
|
|
305
|
+
.filter((note): note is PersistedStudioReviewNote => Boolean(note));
|
|
306
|
+
if (normalizedNotes.length > 0) {
|
|
307
|
+
reviewNotesByDocument[documentKey] = normalizedNotes;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
const scratchpadsByDocument: Record<string, string> = {};
|
|
312
|
+
if (candidate.scratchpadsByDocument && typeof candidate.scratchpadsByDocument === "object") {
|
|
313
|
+
for (const [documentKey, rawText] of Object.entries(candidate.scratchpadsByDocument as Record<string, unknown>)) {
|
|
314
|
+
if (typeof documentKey !== "string" || !documentKey.trim() || typeof rawText !== "string") continue;
|
|
315
|
+
scratchpadsByDocument[documentKey] = rawText;
|
|
316
|
+
}
|
|
317
|
+
} else if (typeof candidate.scratchpadText === "string" && candidate.scratchpadText.length > 0) {
|
|
318
|
+
scratchpadsByDocument[STUDIO_DEFAULT_SCRATCHPAD_DOCUMENT_KEY] = candidate.scratchpadText;
|
|
319
|
+
}
|
|
320
|
+
return {
|
|
321
|
+
version: 2,
|
|
322
|
+
scratchpadsByDocument,
|
|
323
|
+
reviewNotesByDocument,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async function loadStudioPersistentState(): Promise<StudioPersistentState> {
|
|
328
|
+
if (studioPersistentStateCache) return studioPersistentStateCache;
|
|
329
|
+
try {
|
|
330
|
+
const raw = await readFile(STUDIO_PERSISTENT_STATE_PATH, "utf-8");
|
|
331
|
+
studioPersistentStateCache = normalizeStudioPersistentState(JSON.parse(raw));
|
|
332
|
+
} catch (error) {
|
|
333
|
+
if (!(error && typeof error === "object" && "code" in error && (error as { code?: unknown }).code === "ENOENT")) {
|
|
334
|
+
// Ignore parse/read errors and fall back to a fresh local state blob.
|
|
335
|
+
}
|
|
336
|
+
studioPersistentStateCache = createEmptyStudioPersistentState();
|
|
337
|
+
}
|
|
338
|
+
return studioPersistentStateCache;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
async function saveStudioPersistentState(state: StudioPersistentState): Promise<void> {
|
|
342
|
+
await mkdir(STUDIO_PERSISTENT_STATE_DIR, { recursive: true });
|
|
343
|
+
await writeFile(STUDIO_PERSISTENT_STATE_PATH, `${JSON.stringify(state, null, 2)}\n`, "utf-8");
|
|
344
|
+
studioPersistentStateCache = state;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async function mutateStudioPersistentState(mutator: (state: StudioPersistentState) => void): Promise<void> {
|
|
348
|
+
const run = studioPersistentStateQueue.catch(() => undefined).then(async () => {
|
|
349
|
+
const state = normalizeStudioPersistentState(await loadStudioPersistentState());
|
|
350
|
+
mutator(state);
|
|
351
|
+
await saveStudioPersistentState(state);
|
|
352
|
+
});
|
|
353
|
+
studioPersistentStateQueue = run.then(() => undefined, () => undefined);
|
|
354
|
+
await run;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
async function readPersistedStudioScratchpadText(documentKey: string): Promise<string> {
|
|
358
|
+
const key = String(documentKey ?? "").trim();
|
|
359
|
+
if (!key) return "";
|
|
360
|
+
const state = await loadStudioPersistentState();
|
|
361
|
+
const value = state.scratchpadsByDocument[key];
|
|
362
|
+
return typeof value === "string" ? value : "";
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async function writePersistedStudioScratchpadText(documentKey: string, text: string): Promise<void> {
|
|
366
|
+
const key = String(documentKey ?? "").trim();
|
|
367
|
+
if (!key) return;
|
|
368
|
+
await mutateStudioPersistentState((state) => {
|
|
369
|
+
const normalized = String(text ?? "");
|
|
370
|
+
if (normalized.length === 0) {
|
|
371
|
+
delete state.scratchpadsByDocument[key];
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
state.scratchpadsByDocument[key] = normalized;
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function clonePersistedStudioReviewNotes(notes: PersistedStudioReviewNote[]): PersistedStudioReviewNote[] {
|
|
379
|
+
return notes.map((note) => ({ ...note }));
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
async function readPersistedStudioReviewNotes(documentKey: string): Promise<PersistedStudioReviewNote[]> {
|
|
383
|
+
const key = String(documentKey ?? "").trim();
|
|
384
|
+
if (!key) return [];
|
|
385
|
+
const state = await loadStudioPersistentState();
|
|
386
|
+
const notes = state.reviewNotesByDocument[key];
|
|
387
|
+
return Array.isArray(notes) ? clonePersistedStudioReviewNotes(notes) : [];
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
async function writePersistedStudioReviewNotes(documentKey: string, notes: PersistedStudioReviewNote[]): Promise<void> {
|
|
391
|
+
const key = String(documentKey ?? "").trim();
|
|
392
|
+
if (!key) return;
|
|
393
|
+
const normalizedNotes = Array.isArray(notes)
|
|
394
|
+
? notes
|
|
395
|
+
.map((note) => normalizePersistedStudioReviewNote(note))
|
|
396
|
+
.filter((note): note is PersistedStudioReviewNote => Boolean(note))
|
|
397
|
+
: [];
|
|
398
|
+
await mutateStudioPersistentState((state) => {
|
|
399
|
+
if (normalizedNotes.length === 0) {
|
|
400
|
+
delete state.reviewNotesByDocument[key];
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
state.reviewNotesByDocument[key] = clonePersistedStudioReviewNotes(normalizedNotes);
|
|
404
|
+
});
|
|
405
|
+
}
|
|
220
406
|
|
|
221
407
|
function scaleStudioPdfLength(length: string, factor: number): string | null {
|
|
222
408
|
const match = String(length ?? "").trim().match(/^(\d+(?:\.\d+)?)(pt|bp|mm|cm|in|pc)$/i);
|
|
@@ -951,11 +1137,15 @@ function createSessionToken(): string {
|
|
|
951
1137
|
return randomUUID();
|
|
952
1138
|
}
|
|
953
1139
|
|
|
1140
|
+
function createStudioDraftId(): string {
|
|
1141
|
+
return `draft_${randomUUID().replace(/[^a-zA-Z0-9_-]/g, "_")}`;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
954
1144
|
function rawDataToString(data: RawData): string {
|
|
955
1145
|
if (typeof data === "string") return data;
|
|
956
1146
|
if (data instanceof Buffer) return data.toString("utf-8");
|
|
957
1147
|
if (Array.isArray(data)) return Buffer.concat(data).toString("utf-8");
|
|
958
|
-
return Buffer.from(data).toString("utf-8");
|
|
1148
|
+
return Buffer.from(data as ArrayBuffer).toString("utf-8");
|
|
959
1149
|
}
|
|
960
1150
|
|
|
961
1151
|
function isValidRequestId(id: string): boolean {
|
|
@@ -3259,7 +3449,7 @@ function renderStudioAnnotationPdfLatex(text: string): string {
|
|
|
3259
3449
|
function replaceStudioAnnotationMarkersForPdfInSegment(text: string): string {
|
|
3260
3450
|
const replaced = replaceStudioInlineAnnotationMarkers(
|
|
3261
3451
|
String(text ?? ""),
|
|
3262
|
-
(marker) => {
|
|
3452
|
+
(marker: { body: string }) => {
|
|
3263
3453
|
const cleaned = renderStudioAnnotationPdfLatex(marker.body);
|
|
3264
3454
|
if (!cleaned) return "";
|
|
3265
3455
|
return `\\studioannotation{${cleaned}}`;
|
|
@@ -3276,7 +3466,7 @@ function replaceStudioAnnotationMarkersForPdfInSegment(text: string): string {
|
|
|
3276
3466
|
|
|
3277
3467
|
function replaceStudioAnnotationMarkersForPdf(markdown: string): string {
|
|
3278
3468
|
if (!hasStudioMarkdownAnnotationMarkers(markdown)) return String(markdown ?? "");
|
|
3279
|
-
return transformStudioMarkdownOutsideFences(markdown, (segment) => replaceStudioAnnotationMarkersForPdfInSegment(segment));
|
|
3469
|
+
return transformStudioMarkdownOutsideFences(markdown, (segment: string) => replaceStudioAnnotationMarkersForPdfInSegment(segment));
|
|
3280
3470
|
}
|
|
3281
3471
|
|
|
3282
3472
|
interface StudioPdfRenderOptions {
|
|
@@ -4325,13 +4515,13 @@ function replaceStudioAnnotationMarkersInDiffTokenLine(line: string, macroName:
|
|
|
4325
4515
|
const wrapText = (text: string): string => text ? `\\${macroName}{${text}}` : "";
|
|
4326
4516
|
const rewritten = replaceStudioInlineAnnotationMarkers(
|
|
4327
4517
|
body,
|
|
4328
|
-
(marker) => {
|
|
4518
|
+
(marker: { body: string }) => {
|
|
4329
4519
|
const markerText = decodeStudioGeneratedCodeLatexText(normalizeStudioAnnotationText(marker.body));
|
|
4330
4520
|
const cleaned = makeStudioHighlightingMathScriptsVerbatimSafe(renderStudioAnnotationPdfLatex(markerText));
|
|
4331
4521
|
if (!cleaned) return "";
|
|
4332
4522
|
return `\\studioannotation{${cleaned}}`;
|
|
4333
4523
|
},
|
|
4334
|
-
(segment) => wrapText(segment),
|
|
4524
|
+
(segment: string) => wrapText(segment),
|
|
4335
4525
|
);
|
|
4336
4526
|
|
|
4337
4527
|
return rewritten === body ? line : (rewritten || wrapText(body));
|
|
@@ -5608,12 +5798,65 @@ function normalizeStudioUiMode(raw: string | null | undefined): StudioUiMode {
|
|
|
5608
5798
|
return raw === "editor-only" ? "editor-only" : "full";
|
|
5609
5799
|
}
|
|
5610
5800
|
|
|
5611
|
-
function buildStudioUrl(
|
|
5801
|
+
function buildStudioUrl(
|
|
5802
|
+
port: number,
|
|
5803
|
+
token: string,
|
|
5804
|
+
mode: StudioUiMode = "full",
|
|
5805
|
+
doc?: InitialStudioDocument | null,
|
|
5806
|
+
): string {
|
|
5612
5807
|
const params = new URLSearchParams({ token });
|
|
5613
5808
|
if (mode !== "full") params.set("mode", mode);
|
|
5809
|
+
if (doc?.source) params.set("docSource", doc.source);
|
|
5810
|
+
if (doc?.label) params.set("docLabel", doc.label);
|
|
5811
|
+
if (doc?.path) params.set("docPath", doc.path);
|
|
5812
|
+
if (doc?.draftId) params.set("draftId", doc.draftId);
|
|
5614
5813
|
return `http://127.0.0.1:${port}/?${params.toString()}`;
|
|
5615
5814
|
}
|
|
5616
5815
|
|
|
5816
|
+
function resolveRequestedStudioDocumentFromUrl(
|
|
5817
|
+
requestUrl: URL,
|
|
5818
|
+
fallback: InitialStudioDocument | null,
|
|
5819
|
+
studioCwd: string,
|
|
5820
|
+
latestResponse?: LastStudioResponse | null,
|
|
5821
|
+
): InitialStudioDocument | null {
|
|
5822
|
+
const requestedPath = (requestUrl.searchParams.get("docPath") ?? "").trim();
|
|
5823
|
+
const requestedSourceRaw = (requestUrl.searchParams.get("docSource") ?? "").trim();
|
|
5824
|
+
const requestedLabel = (requestUrl.searchParams.get("docLabel") ?? "").trim();
|
|
5825
|
+
const requestedDraftId = (requestUrl.searchParams.get("draftId") ?? "").trim();
|
|
5826
|
+
|
|
5827
|
+
if (requestedPath) {
|
|
5828
|
+
const file = readStudioFile(requestedPath, studioCwd);
|
|
5829
|
+
if (file.ok !== false) {
|
|
5830
|
+
return {
|
|
5831
|
+
text: file.text,
|
|
5832
|
+
label: requestedLabel || file.label,
|
|
5833
|
+
source: "file",
|
|
5834
|
+
path: file.resolvedPath,
|
|
5835
|
+
};
|
|
5836
|
+
}
|
|
5837
|
+
}
|
|
5838
|
+
|
|
5839
|
+
if (requestedSourceRaw === "last-response") {
|
|
5840
|
+
return {
|
|
5841
|
+
text: latestResponse?.markdown ?? (fallback?.source === "last-response" ? fallback.text : ""),
|
|
5842
|
+
label: requestedLabel || "last model response",
|
|
5843
|
+
source: "last-response",
|
|
5844
|
+
draftId: requestedDraftId || undefined,
|
|
5845
|
+
};
|
|
5846
|
+
}
|
|
5847
|
+
|
|
5848
|
+
if (requestedSourceRaw || requestedLabel || requestedDraftId) {
|
|
5849
|
+
return {
|
|
5850
|
+
text: fallback?.source === "blank" ? fallback.text : "",
|
|
5851
|
+
label: requestedLabel || requestedSourceRaw || "blank",
|
|
5852
|
+
source: "blank",
|
|
5853
|
+
draftId: requestedDraftId || undefined,
|
|
5854
|
+
};
|
|
5855
|
+
}
|
|
5856
|
+
|
|
5857
|
+
return fallback;
|
|
5858
|
+
}
|
|
5859
|
+
|
|
5617
5860
|
function formatModelLabel(model: { provider?: string; id?: string } | undefined): string {
|
|
5618
5861
|
const provider = typeof model?.provider === "string" ? model.provider.trim() : "";
|
|
5619
5862
|
const id = typeof model?.id === "string" ? model.id.trim() : "";
|
|
@@ -5751,6 +5994,7 @@ function buildStudioHtml(
|
|
|
5751
5994
|
const initialSource = initialDocument?.source ?? "blank";
|
|
5752
5995
|
const initialLabel = escapeHtmlForInline(initialDocument?.label ?? "blank");
|
|
5753
5996
|
const initialPath = escapeHtmlForInline(initialDocument?.path ?? "");
|
|
5997
|
+
const initialDraftId = escapeHtmlForInline(initialDocument?.draftId ?? "");
|
|
5754
5998
|
const initialModel = escapeHtmlForInline(initialModelLabel ?? "none");
|
|
5755
5999
|
const initialTerminal = escapeHtmlForInline(initialTerminalLabel ?? "unknown");
|
|
5756
6000
|
const initialContextTokens =
|
|
@@ -5819,7 +6063,7 @@ ${cssVarsBlock}
|
|
|
5819
6063
|
</style>
|
|
5820
6064
|
<link rel="stylesheet" href="${stylesheetHref}" />
|
|
5821
6065
|
</head>
|
|
5822
|
-
<body data-initial-source="${initialSource}" data-initial-label="${initialLabel}" data-initial-path="${initialPath}" data-model-label="${initialModel}" data-terminal-label="${initialTerminal}" data-context-tokens="${initialContextTokens}" data-context-window="${initialContextWindow}" data-context-percent="${initialContextPercent}" data-studio-mode="${studioMode}">
|
|
6066
|
+
<body data-initial-source="${initialSource}" data-initial-label="${initialLabel}" data-initial-path="${initialPath}" data-initial-draft-id="${initialDraftId}" data-model-label="${initialModel}" data-terminal-label="${initialTerminal}" data-context-tokens="${initialContextTokens}" data-context-window="${initialContextWindow}" data-context-percent="${initialContextPercent}" data-studio-mode="${studioMode}">
|
|
5823
6067
|
<header>
|
|
5824
6068
|
<h1><span class="app-logo" aria-hidden="true">π</span> Studio <span class="app-subtitle">${appSubtitle}</span></h1>
|
|
5825
6069
|
<div class="controls">
|
|
@@ -5843,13 +6087,14 @@ ${cssVarsBlock}
|
|
|
5843
6087
|
</div>
|
|
5844
6088
|
<div class="section-header-actions">
|
|
5845
6089
|
<button id="leftFocusBtn" class="pane-focus-btn" type="button" title="Show only the editor pane. Shortcut: F10 or Cmd/Ctrl+Esc.">Focus pane</button>
|
|
5846
|
-
<button id="
|
|
6090
|
+
<button id="reviewNotesBtn" type="button" title="Toggle local comments beside the current editor document or draft. Comments stay outside the document text and can later be converted into [an: ...] annotations.">Comments</button>
|
|
6091
|
+
<button id="scratchpadBtn" type="button" title="Open a local persistent scratchpad for the current editor document or draft. Scratchpad text is never run, critiqued, or exported unless you explicitly insert it into the editor.">Scratchpad</button>
|
|
5847
6092
|
</div>
|
|
5848
6093
|
</div>
|
|
5849
6094
|
<div class="source-wrap">
|
|
5850
6095
|
<div class="source-meta">
|
|
5851
6096
|
<div class="badge-row">
|
|
5852
|
-
<
|
|
6097
|
+
<button id="sourceBadge" type="button" class="source-badge source-badge-button">Editor origin: ${initialLabel}</button>
|
|
5853
6098
|
<button id="resourceDirBtn" type="button" class="resource-dir-btn" hidden title="Set working directory for resolving relative paths in preview">Set working dir</button>
|
|
5854
6099
|
<span id="resourceDirLabel" class="source-badge resource-dir-label" hidden title="Click to change working directory"></span>
|
|
5855
6100
|
<span id="resourceDirInputWrap" class="resource-dir-input-wrap">
|
|
@@ -5866,60 +6111,95 @@ ${cssVarsBlock}
|
|
|
5866
6111
|
<button id="sendEditorBtn" type="button">Send to pi editor</button>
|
|
5867
6112
|
</div>
|
|
5868
6113
|
<div class="source-actions-row">
|
|
5869
|
-
<button id="insertHeaderBtn" type="button" title="Insert annotated-reply protocol header (source metadata, [an: ...] syntax hint, precedence note, and end marker).">
|
|
5870
|
-
<select id="annotationModeSelect" aria-label="
|
|
5871
|
-
<option value="on" selected>
|
|
5872
|
-
<option value="off">
|
|
6114
|
+
<button id="insertHeaderBtn" type="button" title="Insert annotated-reply protocol header (source metadata, [an: ...] syntax hint, precedence note, and end marker).">Annotation header</button>
|
|
6115
|
+
<select id="annotationModeSelect" aria-label="Inline annotation visibility mode" title="On: keep and send [an: ...] markers. Hide: keep markers in the editor, hide them in preview, and strip before Run/Critique.">
|
|
6116
|
+
<option value="on" selected>Inline annotations: On</option>
|
|
6117
|
+
<option value="off">Inline annotations: Hide</option>
|
|
5873
6118
|
</select>
|
|
5874
6119
|
<button id="stripAnnotationsBtn" type="button" title="Destructively remove all [an: ...] markers from editor text.">Strip annotations…</button>
|
|
5875
6120
|
<button id="saveAnnotatedBtn" type="button" title="Save full editor content (including [an: ...] markers) as a .annotated.md file.">Save .annotated.md</button>
|
|
5876
6121
|
</div>
|
|
5877
6122
|
<div class="source-actions-row">
|
|
5878
6123
|
<select id="lensSelect" aria-label="Critique focus">
|
|
5879
|
-
<option value="auto" selected>Critique
|
|
5880
|
-
<option value="writing">Critique
|
|
5881
|
-
<option value="code">Critique
|
|
6124
|
+
<option value="auto" selected>Critique: Auto</option>
|
|
6125
|
+
<option value="writing">Critique: Writing</option>
|
|
6126
|
+
<option value="code">Critique: Code</option>
|
|
5882
6127
|
</select>
|
|
5883
|
-
<button id="critiqueBtn" type="button">Critique
|
|
6128
|
+
<button id="critiqueBtn" type="button">Critique text</button>
|
|
5884
6129
|
<select id="highlightSelect" aria-label="Editor syntax highlighting">
|
|
5885
6130
|
<option value="off">Syntax highlight: Off</option>
|
|
5886
|
-
<option value="
|
|
6131
|
+
<option value="bash">Syntax highlight: Bash</option>
|
|
6132
|
+
<option value="c">Syntax highlight: C</option>
|
|
6133
|
+
<option value="cpp">Syntax highlight: C++</option>
|
|
6134
|
+
<option value="css">Syntax highlight: CSS</option>
|
|
6135
|
+
<option value="diff">Syntax highlight: Diff</option>
|
|
6136
|
+
<option value="fortran">Syntax highlight: Fortran</option>
|
|
6137
|
+
<option value="go">Syntax highlight: Go</option>
|
|
6138
|
+
<option value="html">Syntax highlight: HTML</option>
|
|
6139
|
+
<option value="java">Syntax highlight: Java</option>
|
|
6140
|
+
<option value="javascript">Syntax highlight: JavaScript</option>
|
|
6141
|
+
<option value="json">Syntax highlight: JSON</option>
|
|
6142
|
+
<option value="julia">Syntax highlight: Julia</option>
|
|
6143
|
+
<option value="latex">Syntax highlight: LaTeX</option>
|
|
6144
|
+
<option value="lua">Syntax highlight: Lua</option>
|
|
6145
|
+
<option value="markdown" selected>Syntax highlight: Markdown</option>
|
|
6146
|
+
<option value="matlab">Syntax highlight: MATLAB</option>
|
|
6147
|
+
<option value="text">Syntax highlight: Plain Text</option>
|
|
6148
|
+
<option value="python">Syntax highlight: Python</option>
|
|
6149
|
+
<option value="r">Syntax highlight: R</option>
|
|
6150
|
+
<option value="rust">Syntax highlight: Rust</option>
|
|
6151
|
+
<option value="swift">Syntax highlight: Swift</option>
|
|
6152
|
+
<option value="toml">Syntax highlight: TOML</option>
|
|
6153
|
+
<option value="typescript">Syntax highlight: TypeScript</option>
|
|
6154
|
+
<option value="xml">Syntax highlight: XML</option>
|
|
6155
|
+
<option value="yaml">Syntax highlight: YAML</option>
|
|
5887
6156
|
</select>
|
|
5888
|
-
<select id="
|
|
5889
|
-
<option value="
|
|
5890
|
-
<option value="
|
|
5891
|
-
<option value="cpp">Lang: C++</option>
|
|
5892
|
-
<option value="css">Lang: CSS</option>
|
|
5893
|
-
<option value="diff">Lang: Diff</option>
|
|
5894
|
-
<option value="fortran">Lang: Fortran</option>
|
|
5895
|
-
<option value="go">Lang: Go</option>
|
|
5896
|
-
<option value="html">Lang: HTML</option>
|
|
5897
|
-
<option value="java">Lang: Java</option>
|
|
5898
|
-
<option value="javascript">Lang: JavaScript</option>
|
|
5899
|
-
<option value="json">Lang: JSON</option>
|
|
5900
|
-
<option value="julia">Lang: Julia</option>
|
|
5901
|
-
<option value="latex">Lang: LaTeX</option>
|
|
5902
|
-
<option value="lua">Lang: Lua</option>
|
|
5903
|
-
<option value="markdown" selected>Lang: Markdown</option>
|
|
5904
|
-
<option value="matlab">Lang: MATLAB</option>
|
|
5905
|
-
<option value="text">Lang: Plain Text</option>
|
|
5906
|
-
<option value="python">Lang: Python</option>
|
|
5907
|
-
<option value="r">Lang: R</option>
|
|
5908
|
-
<option value="rust">Lang: Rust</option>
|
|
5909
|
-
<option value="swift">Lang: Swift</option>
|
|
5910
|
-
<option value="toml">Lang: TOML</option>
|
|
5911
|
-
<option value="typescript">Lang: TypeScript</option>
|
|
5912
|
-
<option value="xml">Lang: XML</option>
|
|
5913
|
-
<option value="yaml">Lang: YAML</option>
|
|
6157
|
+
<select id="lineNumbersSelect" aria-label="Editor line numbers">
|
|
6158
|
+
<option value="off">Line numbers: Off</option>
|
|
6159
|
+
<option value="on" selected>Line numbers: On</option>
|
|
5914
6160
|
</select>
|
|
5915
6161
|
</div>
|
|
5916
6162
|
</div>
|
|
5917
6163
|
</div>
|
|
5918
|
-
<div
|
|
5919
|
-
<
|
|
5920
|
-
|
|
6164
|
+
<div class="source-body">
|
|
6165
|
+
<div class="source-primary">
|
|
6166
|
+
<div id="sourceEditorWrap" class="editor-highlight-wrap">
|
|
6167
|
+
<div id="reviewNoteGutter" class="editor-review-note-gutter" hidden aria-hidden="true">
|
|
6168
|
+
<div id="reviewNoteGutterContent" class="editor-review-note-gutter-content"></div>
|
|
6169
|
+
</div>
|
|
6170
|
+
<div id="lineNumberGutter" class="editor-line-number-gutter" hidden aria-hidden="true">
|
|
6171
|
+
<div id="lineNumberGutterContent" class="editor-line-number-gutter-content"></div>
|
|
6172
|
+
</div>
|
|
6173
|
+
<div id="lineNumberMeasure" class="editor-line-number-measure" aria-hidden="true"></div>
|
|
6174
|
+
<pre id="sourceHighlight" class="editor-highlight" aria-hidden="true"></pre>
|
|
6175
|
+
<textarea id="sourceText" placeholder="Paste or edit text here.">${initialText}</textarea>
|
|
6176
|
+
</div>
|
|
6177
|
+
<div id="sourcePreview" class="panel-scroll rendered-markdown" hidden><pre class="plain-markdown"></pre></div>
|
|
6178
|
+
</div>
|
|
6179
|
+
<aside id="reviewNotesOverlay" class="review-notes-dock-wrap" hidden>
|
|
6180
|
+
<div id="reviewNotesDialog" class="review-notes-dock" role="complementary" aria-labelledby="reviewNotesTitle">
|
|
6181
|
+
<div class="scratchpad-header">
|
|
6182
|
+
<div>
|
|
6183
|
+
<h2 id="reviewNotesTitle">Comments</h2>
|
|
6184
|
+
<p class="scratchpad-description">Local comments for editor text. Stay out of the text, anchored to selections or lines, and can be converted into inline <span class="review-notes-inline-token">[an: ...]</span> annotations.</p>
|
|
6185
|
+
</div>
|
|
6186
|
+
<button id="reviewNotesCloseBtn" type="button" class="scratchpad-close-btn" aria-label="Hide comments" title="Hide comments">✕</button>
|
|
6187
|
+
</div>
|
|
6188
|
+
<div class="review-notes-toolbar">
|
|
6189
|
+
<span id="reviewNotesMeta" class="scratchpad-meta">No comments</span>
|
|
6190
|
+
</div>
|
|
6191
|
+
<div id="reviewNotesEmptyState" class="review-notes-empty">No comments yet for this document. Select text (or just place the caret on a line) in <strong>Editor (Raw)</strong>, then choose <em>New comment from selection</em>.</div>
|
|
6192
|
+
<div id="reviewNotesList" class="review-notes-list" aria-live="polite"></div>
|
|
6193
|
+
<div class="review-notes-dock-footer">
|
|
6194
|
+
<div class="scratchpad-actions">
|
|
6195
|
+
<button id="reviewNotesAddBtn" type="button" title="Create a new local comment from the current editor selection, or from the current line if nothing is selected.">New comment from selection</button>
|
|
6196
|
+
<button id="reviewNotesInlineAllBtn" type="button" title="Toggle inline annotations for all non-empty comments.">All inline: Off</button>
|
|
6197
|
+
<button id="reviewNotesDoneBtn" type="button" title="Hide the comments rail.">Hide</button>
|
|
6198
|
+
</div>
|
|
6199
|
+
</div>
|
|
6200
|
+
</div>
|
|
6201
|
+
</aside>
|
|
5921
6202
|
</div>
|
|
5922
|
-
<div id="sourcePreview" class="panel-scroll rendered-markdown" hidden><pre class="plain-markdown"></pre></div>
|
|
5923
6203
|
</div>
|
|
5924
6204
|
</section>
|
|
5925
6205
|
|
|
@@ -5984,7 +6264,7 @@ ${cssVarsBlock}
|
|
|
5984
6264
|
<div class="scratchpad-header">
|
|
5985
6265
|
<div>
|
|
5986
6266
|
<h2 id="scratchpadTitle">Scratchpad</h2>
|
|
5987
|
-
<p class="scratchpad-description">Local persistent notes for thoughts you want to park while working. Closing the scratchpad does not clear it: notes persist locally until you edit or clear them. Scratchpad text is not run, critiqued, sent, or exported unless you explicitly insert it into the editor.</p>
|
|
6267
|
+
<p class="scratchpad-description">Local persistent notes for thoughts you want to park while working on the current Studio document or draft. Closing the scratchpad does not clear it: notes persist locally for this document identity until you edit or clear them. File-backed documents reliably come back across Pi restarts; unsaved drafts stay with their own draft instance until you save them or discard them. Scratchpad text is not run, critiqued, sent, or exported unless you explicitly insert it into the editor.</p>
|
|
5988
6268
|
</div>
|
|
5989
6269
|
<button id="scratchpadCloseBtn" type="button" class="scratchpad-close-btn" aria-label="Keep current scratchpad text and close scratchpad" title="Keep current scratchpad text and close scratchpad">✕</button>
|
|
5990
6270
|
</div>
|
|
@@ -7426,6 +7706,120 @@ export default function (pi: ExtensionAPI) {
|
|
|
7426
7706
|
res.end(prepared.pdf);
|
|
7427
7707
|
};
|
|
7428
7708
|
|
|
7709
|
+
const handleScratchpadStateRequest = async (req: IncomingMessage, res: ServerResponse, requestUrl: URL) => {
|
|
7710
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
7711
|
+
if (method === "GET") {
|
|
7712
|
+
const documentKey = (requestUrl.searchParams.get("documentKey") ?? "").trim();
|
|
7713
|
+
if (!documentKey) {
|
|
7714
|
+
respondJson(res, 400, { ok: false, error: "Missing documentKey query parameter." });
|
|
7715
|
+
return;
|
|
7716
|
+
}
|
|
7717
|
+
respondJson(res, 200, { ok: true, text: await readPersistedStudioScratchpadText(documentKey) });
|
|
7718
|
+
return;
|
|
7719
|
+
}
|
|
7720
|
+
if (method !== "POST") {
|
|
7721
|
+
res.setHeader("Allow", "GET, POST");
|
|
7722
|
+
respondJson(res, 405, { ok: false, error: "Method not allowed. Use GET or POST." });
|
|
7723
|
+
return;
|
|
7724
|
+
}
|
|
7725
|
+
|
|
7726
|
+
let rawBody = "";
|
|
7727
|
+
try {
|
|
7728
|
+
rawBody = await readRequestBody(req, REQUEST_BODY_MAX_BYTES);
|
|
7729
|
+
} catch (error) {
|
|
7730
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
7731
|
+
const status = message.includes("exceeds") ? 413 : 400;
|
|
7732
|
+
respondJson(res, status, { ok: false, error: message });
|
|
7733
|
+
return;
|
|
7734
|
+
}
|
|
7735
|
+
|
|
7736
|
+
let parsedBody: unknown;
|
|
7737
|
+
try {
|
|
7738
|
+
parsedBody = rawBody ? JSON.parse(rawBody) : {};
|
|
7739
|
+
} catch {
|
|
7740
|
+
respondJson(res, 400, { ok: false, error: "Invalid JSON body." });
|
|
7741
|
+
return;
|
|
7742
|
+
}
|
|
7743
|
+
|
|
7744
|
+
const documentKey =
|
|
7745
|
+
parsedBody && typeof parsedBody === "object" && typeof (parsedBody as { documentKey?: unknown }).documentKey === "string"
|
|
7746
|
+
? (parsedBody as { documentKey: string }).documentKey.trim()
|
|
7747
|
+
: "";
|
|
7748
|
+
if (!documentKey) {
|
|
7749
|
+
respondJson(res, 400, { ok: false, error: "Missing documentKey in request body." });
|
|
7750
|
+
return;
|
|
7751
|
+
}
|
|
7752
|
+
|
|
7753
|
+
const text =
|
|
7754
|
+
parsedBody && typeof parsedBody === "object" && typeof (parsedBody as { text?: unknown }).text === "string"
|
|
7755
|
+
? (parsedBody as { text: string }).text
|
|
7756
|
+
: null;
|
|
7757
|
+
if (text === null) {
|
|
7758
|
+
respondJson(res, 400, { ok: false, error: "Missing scratchpad text in request body." });
|
|
7759
|
+
return;
|
|
7760
|
+
}
|
|
7761
|
+
|
|
7762
|
+
await writePersistedStudioScratchpadText(documentKey, text);
|
|
7763
|
+
respondJson(res, 200, { ok: true });
|
|
7764
|
+
};
|
|
7765
|
+
|
|
7766
|
+
const handleReviewNotesRequest = async (req: IncomingMessage, res: ServerResponse, requestUrl: URL) => {
|
|
7767
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
7768
|
+
if (method === "GET") {
|
|
7769
|
+
const documentKey = (requestUrl.searchParams.get("documentKey") ?? "").trim();
|
|
7770
|
+
if (!documentKey) {
|
|
7771
|
+
respondJson(res, 400, { ok: false, error: "Missing documentKey query parameter." });
|
|
7772
|
+
return;
|
|
7773
|
+
}
|
|
7774
|
+
respondJson(res, 200, { ok: true, notes: await readPersistedStudioReviewNotes(documentKey) });
|
|
7775
|
+
return;
|
|
7776
|
+
}
|
|
7777
|
+
if (method !== "POST") {
|
|
7778
|
+
res.setHeader("Allow", "GET, POST");
|
|
7779
|
+
respondJson(res, 405, { ok: false, error: "Method not allowed. Use GET or POST." });
|
|
7780
|
+
return;
|
|
7781
|
+
}
|
|
7782
|
+
|
|
7783
|
+
let rawBody = "";
|
|
7784
|
+
try {
|
|
7785
|
+
rawBody = await readRequestBody(req, REQUEST_BODY_MAX_BYTES);
|
|
7786
|
+
} catch (error) {
|
|
7787
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
7788
|
+
const status = message.includes("exceeds") ? 413 : 400;
|
|
7789
|
+
respondJson(res, status, { ok: false, error: message });
|
|
7790
|
+
return;
|
|
7791
|
+
}
|
|
7792
|
+
|
|
7793
|
+
let parsedBody: unknown;
|
|
7794
|
+
try {
|
|
7795
|
+
parsedBody = rawBody ? JSON.parse(rawBody) : {};
|
|
7796
|
+
} catch {
|
|
7797
|
+
respondJson(res, 400, { ok: false, error: "Invalid JSON body." });
|
|
7798
|
+
return;
|
|
7799
|
+
}
|
|
7800
|
+
|
|
7801
|
+
const documentKey =
|
|
7802
|
+
parsedBody && typeof parsedBody === "object" && typeof (parsedBody as { documentKey?: unknown }).documentKey === "string"
|
|
7803
|
+
? (parsedBody as { documentKey: string }).documentKey.trim()
|
|
7804
|
+
: "";
|
|
7805
|
+
if (!documentKey) {
|
|
7806
|
+
respondJson(res, 400, { ok: false, error: "Missing documentKey in request body." });
|
|
7807
|
+
return;
|
|
7808
|
+
}
|
|
7809
|
+
|
|
7810
|
+
const notes =
|
|
7811
|
+
parsedBody && typeof parsedBody === "object" && Array.isArray((parsedBody as { notes?: unknown }).notes)
|
|
7812
|
+
? (parsedBody as { notes: PersistedStudioReviewNote[] }).notes
|
|
7813
|
+
: null;
|
|
7814
|
+
if (!notes) {
|
|
7815
|
+
respondJson(res, 400, { ok: false, error: "Missing notes array in request body." });
|
|
7816
|
+
return;
|
|
7817
|
+
}
|
|
7818
|
+
|
|
7819
|
+
await writePersistedStudioReviewNotes(documentKey, notes);
|
|
7820
|
+
respondJson(res, 200, { ok: true });
|
|
7821
|
+
};
|
|
7822
|
+
|
|
7429
7823
|
const handleRenderPreviewRequest = async (req: IncomingMessage, res: ServerResponse) => {
|
|
7430
7824
|
let rawBody = "";
|
|
7431
7825
|
try {
|
|
@@ -7668,6 +8062,36 @@ export default function (pi: ExtensionAPI) {
|
|
|
7668
8062
|
return;
|
|
7669
8063
|
}
|
|
7670
8064
|
|
|
8065
|
+
if (requestUrl.pathname === "/scratchpad-state") {
|
|
8066
|
+
const token = requestUrl.searchParams.get("token") ?? "";
|
|
8067
|
+
if (token !== serverState.token) {
|
|
8068
|
+
respondJson(res, 403, { ok: false, error: "Invalid or expired studio token. Re-run /studio." });
|
|
8069
|
+
return;
|
|
8070
|
+
}
|
|
8071
|
+
void handleScratchpadStateRequest(req, res, requestUrl).catch((error) => {
|
|
8072
|
+
respondJson(res, 500, {
|
|
8073
|
+
ok: false,
|
|
8074
|
+
error: `Scratchpad persistence failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
8075
|
+
});
|
|
8076
|
+
});
|
|
8077
|
+
return;
|
|
8078
|
+
}
|
|
8079
|
+
|
|
8080
|
+
if (requestUrl.pathname === "/review-notes") {
|
|
8081
|
+
const token = requestUrl.searchParams.get("token") ?? "";
|
|
8082
|
+
if (token !== serverState.token) {
|
|
8083
|
+
respondJson(res, 403, { ok: false, error: "Invalid or expired studio token. Re-run /studio." });
|
|
8084
|
+
return;
|
|
8085
|
+
}
|
|
8086
|
+
void handleReviewNotesRequest(req, res, requestUrl).catch((error) => {
|
|
8087
|
+
respondJson(res, 500, {
|
|
8088
|
+
ok: false,
|
|
8089
|
+
error: `Review-note persistence failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
8090
|
+
});
|
|
8091
|
+
});
|
|
8092
|
+
return;
|
|
8093
|
+
}
|
|
8094
|
+
|
|
7671
8095
|
if (requestUrl.pathname === "/render-preview") {
|
|
7672
8096
|
const token = requestUrl.searchParams.get("token") ?? "";
|
|
7673
8097
|
if (token !== serverState.token) {
|
|
@@ -7744,7 +8168,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
7744
8168
|
});
|
|
7745
8169
|
refreshContextUsage();
|
|
7746
8170
|
const studioMode = normalizeStudioUiMode(requestUrl.searchParams.get("mode"));
|
|
7747
|
-
|
|
8171
|
+
const requestInitialDocument = resolveRequestedStudioDocumentFromUrl(requestUrl, initialStudioDocument, studioCwd, lastStudioResponse);
|
|
8172
|
+
res.end(buildStudioHtml(requestInitialDocument, serverState.token, lastCommandCtx?.ui.theme, currentModelLabel, terminalSessionLabel, contextUsageSnapshot, studioMode));
|
|
7748
8173
|
};
|
|
7749
8174
|
|
|
7750
8175
|
const ensureServer = async (): Promise<StudioServerState> => {
|
|
@@ -8224,12 +8649,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
8224
8649
|
text: latestAssistant,
|
|
8225
8650
|
label: "last model response",
|
|
8226
8651
|
source: "last-response",
|
|
8652
|
+
draftId: createStudioDraftId(),
|
|
8227
8653
|
};
|
|
8228
8654
|
}
|
|
8229
8655
|
return {
|
|
8230
8656
|
text: "",
|
|
8231
8657
|
label: "blank",
|
|
8232
8658
|
source: "blank",
|
|
8659
|
+
draftId: createStudioDraftId(),
|
|
8233
8660
|
};
|
|
8234
8661
|
}
|
|
8235
8662
|
|
|
@@ -8238,6 +8665,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
8238
8665
|
text: "",
|
|
8239
8666
|
label: "blank",
|
|
8240
8667
|
source: "blank",
|
|
8668
|
+
draftId: createStudioDraftId(),
|
|
8241
8669
|
};
|
|
8242
8670
|
}
|
|
8243
8671
|
|
|
@@ -8248,12 +8676,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
8248
8676
|
text: "",
|
|
8249
8677
|
label: "blank",
|
|
8250
8678
|
source: "blank",
|
|
8679
|
+
draftId: createStudioDraftId(),
|
|
8251
8680
|
};
|
|
8252
8681
|
}
|
|
8253
8682
|
return {
|
|
8254
8683
|
text: latestAssistant,
|
|
8255
8684
|
label: "last model response",
|
|
8256
8685
|
source: "last-response",
|
|
8686
|
+
draftId: createStudioDraftId(),
|
|
8257
8687
|
};
|
|
8258
8688
|
}
|
|
8259
8689
|
|
|
@@ -8326,7 +8756,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
8326
8756
|
initialStudioDocument = selected;
|
|
8327
8757
|
|
|
8328
8758
|
const state = await ensureServer();
|
|
8329
|
-
const url = buildStudioUrl(state.port, state.token, mode);
|
|
8759
|
+
const url = buildStudioUrl(state.port, state.token, mode, selected);
|
|
8330
8760
|
const openedLabel = mode === "editor-only" ? "pi Studio editor-only view" : "pi Studio";
|
|
8331
8761
|
|
|
8332
8762
|
try {
|