pi-studio 0.5.44 → 0.5.46
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 +2 -1
- package/client/studio-client.js +2521 -189
- package/client/studio.css +411 -6
- package/index.ts +447 -19
- package/package.json +1 -1
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,26 @@ 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
|
+
selectedDisplayText?: string;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
interface StudioPersistentState {
|
|
128
|
+
version: 2;
|
|
129
|
+
scratchpadsByDocument: Record<string, string>;
|
|
130
|
+
reviewNotesByDocument: Record<string, PersistedStudioReviewNote[]>;
|
|
110
131
|
}
|
|
111
132
|
|
|
112
133
|
interface HelloMessage {
|
|
@@ -217,6 +238,173 @@ const CMUX_STUDIO_STATUS_KEY = "pi_studio";
|
|
|
217
238
|
const CMUX_STUDIO_STATUS_COLOR_DARK = "#5ea1ff";
|
|
218
239
|
const CMUX_STUDIO_STATUS_COLOR_LIGHT = "#0047ab";
|
|
219
240
|
const STUDIO_PROMPT_METADATA_CUSTOM_TYPE = "pi-studio/direct-prompt";
|
|
241
|
+
const STUDIO_DEFAULT_SCRATCHPAD_DOCUMENT_KEY = "doc:blank:blank";
|
|
242
|
+
const STUDIO_PERSISTENT_STATE_DIR = join(getAgentDir(), "pi-studio");
|
|
243
|
+
const STUDIO_PERSISTENT_STATE_PATH = join(STUDIO_PERSISTENT_STATE_DIR, "local-state.json");
|
|
244
|
+
|
|
245
|
+
let studioPersistentStateCache: StudioPersistentState | null = null;
|
|
246
|
+
let studioPersistentStateQueue: Promise<void> = Promise.resolve();
|
|
247
|
+
|
|
248
|
+
function createEmptyStudioPersistentState(): StudioPersistentState {
|
|
249
|
+
return {
|
|
250
|
+
version: 2,
|
|
251
|
+
scratchpadsByDocument: {},
|
|
252
|
+
reviewNotesByDocument: {},
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function normalizePersistedStudioReviewNote(value: unknown): PersistedStudioReviewNote | null {
|
|
257
|
+
if (!value || typeof value !== "object") return null;
|
|
258
|
+
const candidate = value as Partial<PersistedStudioReviewNote>;
|
|
259
|
+
if (typeof candidate.id !== "string" || !candidate.id.trim()) return null;
|
|
260
|
+
if (typeof candidate.text !== "string") return null;
|
|
261
|
+
const createdAt = typeof candidate.createdAt === "number" && Number.isFinite(candidate.createdAt)
|
|
262
|
+
? candidate.createdAt
|
|
263
|
+
: Date.now();
|
|
264
|
+
const updatedAt = typeof candidate.updatedAt === "number" && Number.isFinite(candidate.updatedAt)
|
|
265
|
+
? candidate.updatedAt
|
|
266
|
+
: createdAt;
|
|
267
|
+
const selectionStart = typeof candidate.selectionStart === "number" && Number.isFinite(candidate.selectionStart)
|
|
268
|
+
? Math.max(0, Math.floor(candidate.selectionStart))
|
|
269
|
+
: 0;
|
|
270
|
+
const selectionEnd = typeof candidate.selectionEnd === "number" && Number.isFinite(candidate.selectionEnd)
|
|
271
|
+
? Math.max(selectionStart, Math.floor(candidate.selectionEnd))
|
|
272
|
+
: selectionStart;
|
|
273
|
+
const lineStart = typeof candidate.lineStart === "number" && Number.isFinite(candidate.lineStart)
|
|
274
|
+
? Math.max(1, Math.floor(candidate.lineStart))
|
|
275
|
+
: 1;
|
|
276
|
+
const lineEnd = typeof candidate.lineEnd === "number" && Number.isFinite(candidate.lineEnd)
|
|
277
|
+
? Math.max(lineStart, Math.floor(candidate.lineEnd))
|
|
278
|
+
: lineStart;
|
|
279
|
+
return {
|
|
280
|
+
id: candidate.id,
|
|
281
|
+
text: candidate.text,
|
|
282
|
+
createdAt,
|
|
283
|
+
updatedAt,
|
|
284
|
+
selectionStart,
|
|
285
|
+
selectionEnd,
|
|
286
|
+
lineStart,
|
|
287
|
+
lineEnd,
|
|
288
|
+
selectedText: typeof candidate.selectedText === "string" ? candidate.selectedText : "",
|
|
289
|
+
selectedDisplayText: typeof candidate.selectedDisplayText === "string" ? candidate.selectedDisplayText : "",
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function normalizeStudioPersistentState(value: unknown): StudioPersistentState {
|
|
294
|
+
const fallback = createEmptyStudioPersistentState();
|
|
295
|
+
if (!value || typeof value !== "object") return fallback;
|
|
296
|
+
const candidate = value as Partial<StudioPersistentState> & {
|
|
297
|
+
reviewNotesByDocument?: unknown;
|
|
298
|
+
scratchpadsByDocument?: unknown;
|
|
299
|
+
scratchpadText?: unknown;
|
|
300
|
+
};
|
|
301
|
+
const reviewNotesByDocument: Record<string, PersistedStudioReviewNote[]> = {};
|
|
302
|
+
if (candidate.reviewNotesByDocument && typeof candidate.reviewNotesByDocument === "object") {
|
|
303
|
+
for (const [documentKey, rawNotes] of Object.entries(candidate.reviewNotesByDocument as Record<string, unknown>)) {
|
|
304
|
+
if (typeof documentKey !== "string" || !documentKey.trim() || !Array.isArray(rawNotes)) continue;
|
|
305
|
+
const normalizedNotes = rawNotes
|
|
306
|
+
.map((note) => normalizePersistedStudioReviewNote(note))
|
|
307
|
+
.filter((note): note is PersistedStudioReviewNote => Boolean(note));
|
|
308
|
+
if (normalizedNotes.length > 0) {
|
|
309
|
+
reviewNotesByDocument[documentKey] = normalizedNotes;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
const scratchpadsByDocument: Record<string, string> = {};
|
|
314
|
+
if (candidate.scratchpadsByDocument && typeof candidate.scratchpadsByDocument === "object") {
|
|
315
|
+
for (const [documentKey, rawText] of Object.entries(candidate.scratchpadsByDocument as Record<string, unknown>)) {
|
|
316
|
+
if (typeof documentKey !== "string" || !documentKey.trim() || typeof rawText !== "string") continue;
|
|
317
|
+
scratchpadsByDocument[documentKey] = rawText;
|
|
318
|
+
}
|
|
319
|
+
} else if (typeof candidate.scratchpadText === "string" && candidate.scratchpadText.length > 0) {
|
|
320
|
+
scratchpadsByDocument[STUDIO_DEFAULT_SCRATCHPAD_DOCUMENT_KEY] = candidate.scratchpadText;
|
|
321
|
+
}
|
|
322
|
+
return {
|
|
323
|
+
version: 2,
|
|
324
|
+
scratchpadsByDocument,
|
|
325
|
+
reviewNotesByDocument,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async function loadStudioPersistentState(): Promise<StudioPersistentState> {
|
|
330
|
+
if (studioPersistentStateCache) return studioPersistentStateCache;
|
|
331
|
+
try {
|
|
332
|
+
const raw = await readFile(STUDIO_PERSISTENT_STATE_PATH, "utf-8");
|
|
333
|
+
studioPersistentStateCache = normalizeStudioPersistentState(JSON.parse(raw));
|
|
334
|
+
} catch (error) {
|
|
335
|
+
if (!(error && typeof error === "object" && "code" in error && (error as { code?: unknown }).code === "ENOENT")) {
|
|
336
|
+
// Ignore parse/read errors and fall back to a fresh local state blob.
|
|
337
|
+
}
|
|
338
|
+
studioPersistentStateCache = createEmptyStudioPersistentState();
|
|
339
|
+
}
|
|
340
|
+
return studioPersistentStateCache;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
async function saveStudioPersistentState(state: StudioPersistentState): Promise<void> {
|
|
344
|
+
await mkdir(STUDIO_PERSISTENT_STATE_DIR, { recursive: true });
|
|
345
|
+
await writeFile(STUDIO_PERSISTENT_STATE_PATH, `${JSON.stringify(state, null, 2)}\n`, "utf-8");
|
|
346
|
+
studioPersistentStateCache = state;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async function mutateStudioPersistentState(mutator: (state: StudioPersistentState) => void): Promise<void> {
|
|
350
|
+
const run = studioPersistentStateQueue.catch(() => undefined).then(async () => {
|
|
351
|
+
const state = normalizeStudioPersistentState(await loadStudioPersistentState());
|
|
352
|
+
mutator(state);
|
|
353
|
+
await saveStudioPersistentState(state);
|
|
354
|
+
});
|
|
355
|
+
studioPersistentStateQueue = run.then(() => undefined, () => undefined);
|
|
356
|
+
await run;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async function readPersistedStudioScratchpadText(documentKey: string): Promise<string> {
|
|
360
|
+
const key = String(documentKey ?? "").trim();
|
|
361
|
+
if (!key) return "";
|
|
362
|
+
const state = await loadStudioPersistentState();
|
|
363
|
+
const value = state.scratchpadsByDocument[key];
|
|
364
|
+
return typeof value === "string" ? value : "";
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async function writePersistedStudioScratchpadText(documentKey: string, text: string): Promise<void> {
|
|
368
|
+
const key = String(documentKey ?? "").trim();
|
|
369
|
+
if (!key) return;
|
|
370
|
+
await mutateStudioPersistentState((state) => {
|
|
371
|
+
const normalized = String(text ?? "");
|
|
372
|
+
if (normalized.length === 0) {
|
|
373
|
+
delete state.scratchpadsByDocument[key];
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
state.scratchpadsByDocument[key] = normalized;
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function clonePersistedStudioReviewNotes(notes: PersistedStudioReviewNote[]): PersistedStudioReviewNote[] {
|
|
381
|
+
return notes.map((note) => ({ ...note }));
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
async function readPersistedStudioReviewNotes(documentKey: string): Promise<PersistedStudioReviewNote[]> {
|
|
385
|
+
const key = String(documentKey ?? "").trim();
|
|
386
|
+
if (!key) return [];
|
|
387
|
+
const state = await loadStudioPersistentState();
|
|
388
|
+
const notes = state.reviewNotesByDocument[key];
|
|
389
|
+
return Array.isArray(notes) ? clonePersistedStudioReviewNotes(notes) : [];
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
async function writePersistedStudioReviewNotes(documentKey: string, notes: PersistedStudioReviewNote[]): Promise<void> {
|
|
393
|
+
const key = String(documentKey ?? "").trim();
|
|
394
|
+
if (!key) return;
|
|
395
|
+
const normalizedNotes = Array.isArray(notes)
|
|
396
|
+
? notes
|
|
397
|
+
.map((note) => normalizePersistedStudioReviewNote(note))
|
|
398
|
+
.filter((note): note is PersistedStudioReviewNote => Boolean(note))
|
|
399
|
+
: [];
|
|
400
|
+
await mutateStudioPersistentState((state) => {
|
|
401
|
+
if (normalizedNotes.length === 0) {
|
|
402
|
+
delete state.reviewNotesByDocument[key];
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
state.reviewNotesByDocument[key] = clonePersistedStudioReviewNotes(normalizedNotes);
|
|
406
|
+
});
|
|
407
|
+
}
|
|
220
408
|
|
|
221
409
|
function scaleStudioPdfLength(length: string, factor: number): string | null {
|
|
222
410
|
const match = String(length ?? "").trim().match(/^(\d+(?:\.\d+)?)(pt|bp|mm|cm|in|pc)$/i);
|
|
@@ -951,6 +1139,10 @@ function createSessionToken(): string {
|
|
|
951
1139
|
return randomUUID();
|
|
952
1140
|
}
|
|
953
1141
|
|
|
1142
|
+
function createStudioDraftId(): string {
|
|
1143
|
+
return `draft_${randomUUID().replace(/[^a-zA-Z0-9_-]/g, "_")}`;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
954
1146
|
function rawDataToString(data: RawData): string {
|
|
955
1147
|
if (typeof data === "string") return data;
|
|
956
1148
|
if (data instanceof Buffer) return data.toString("utf-8");
|
|
@@ -5608,12 +5800,65 @@ function normalizeStudioUiMode(raw: string | null | undefined): StudioUiMode {
|
|
|
5608
5800
|
return raw === "editor-only" ? "editor-only" : "full";
|
|
5609
5801
|
}
|
|
5610
5802
|
|
|
5611
|
-
function buildStudioUrl(
|
|
5803
|
+
function buildStudioUrl(
|
|
5804
|
+
port: number,
|
|
5805
|
+
token: string,
|
|
5806
|
+
mode: StudioUiMode = "full",
|
|
5807
|
+
doc?: InitialStudioDocument | null,
|
|
5808
|
+
): string {
|
|
5612
5809
|
const params = new URLSearchParams({ token });
|
|
5613
5810
|
if (mode !== "full") params.set("mode", mode);
|
|
5811
|
+
if (doc?.source) params.set("docSource", doc.source);
|
|
5812
|
+
if (doc?.label) params.set("docLabel", doc.label);
|
|
5813
|
+
if (doc?.path) params.set("docPath", doc.path);
|
|
5814
|
+
if (doc?.draftId) params.set("draftId", doc.draftId);
|
|
5614
5815
|
return `http://127.0.0.1:${port}/?${params.toString()}`;
|
|
5615
5816
|
}
|
|
5616
5817
|
|
|
5818
|
+
function resolveRequestedStudioDocumentFromUrl(
|
|
5819
|
+
requestUrl: URL,
|
|
5820
|
+
fallback: InitialStudioDocument | null,
|
|
5821
|
+
studioCwd: string,
|
|
5822
|
+
latestResponse?: LastStudioResponse | null,
|
|
5823
|
+
): InitialStudioDocument | null {
|
|
5824
|
+
const requestedPath = (requestUrl.searchParams.get("docPath") ?? "").trim();
|
|
5825
|
+
const requestedSourceRaw = (requestUrl.searchParams.get("docSource") ?? "").trim();
|
|
5826
|
+
const requestedLabel = (requestUrl.searchParams.get("docLabel") ?? "").trim();
|
|
5827
|
+
const requestedDraftId = (requestUrl.searchParams.get("draftId") ?? "").trim();
|
|
5828
|
+
|
|
5829
|
+
if (requestedPath) {
|
|
5830
|
+
const file = readStudioFile(requestedPath, studioCwd);
|
|
5831
|
+
if (file.ok !== false) {
|
|
5832
|
+
return {
|
|
5833
|
+
text: file.text,
|
|
5834
|
+
label: requestedLabel || file.label,
|
|
5835
|
+
source: "file",
|
|
5836
|
+
path: file.resolvedPath,
|
|
5837
|
+
};
|
|
5838
|
+
}
|
|
5839
|
+
}
|
|
5840
|
+
|
|
5841
|
+
if (requestedSourceRaw === "last-response") {
|
|
5842
|
+
return {
|
|
5843
|
+
text: latestResponse?.markdown ?? (fallback?.source === "last-response" ? fallback.text : ""),
|
|
5844
|
+
label: requestedLabel || "last model response",
|
|
5845
|
+
source: "last-response",
|
|
5846
|
+
draftId: requestedDraftId || undefined,
|
|
5847
|
+
};
|
|
5848
|
+
}
|
|
5849
|
+
|
|
5850
|
+
if (requestedSourceRaw || requestedLabel || requestedDraftId) {
|
|
5851
|
+
return {
|
|
5852
|
+
text: fallback?.source === "blank" ? fallback.text : "",
|
|
5853
|
+
label: requestedLabel || requestedSourceRaw || "blank",
|
|
5854
|
+
source: "blank",
|
|
5855
|
+
draftId: requestedDraftId || undefined,
|
|
5856
|
+
};
|
|
5857
|
+
}
|
|
5858
|
+
|
|
5859
|
+
return fallback;
|
|
5860
|
+
}
|
|
5861
|
+
|
|
5617
5862
|
function formatModelLabel(model: { provider?: string; id?: string } | undefined): string {
|
|
5618
5863
|
const provider = typeof model?.provider === "string" ? model.provider.trim() : "";
|
|
5619
5864
|
const id = typeof model?.id === "string" ? model.id.trim() : "";
|
|
@@ -5751,6 +5996,7 @@ function buildStudioHtml(
|
|
|
5751
5996
|
const initialSource = initialDocument?.source ?? "blank";
|
|
5752
5997
|
const initialLabel = escapeHtmlForInline(initialDocument?.label ?? "blank");
|
|
5753
5998
|
const initialPath = escapeHtmlForInline(initialDocument?.path ?? "");
|
|
5999
|
+
const initialDraftId = escapeHtmlForInline(initialDocument?.draftId ?? "");
|
|
5754
6000
|
const initialModel = escapeHtmlForInline(initialModelLabel ?? "none");
|
|
5755
6001
|
const initialTerminal = escapeHtmlForInline(initialTerminalLabel ?? "unknown");
|
|
5756
6002
|
const initialContextTokens =
|
|
@@ -5819,7 +6065,7 @@ ${cssVarsBlock}
|
|
|
5819
6065
|
</style>
|
|
5820
6066
|
<link rel="stylesheet" href="${stylesheetHref}" />
|
|
5821
6067
|
</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}">
|
|
6068
|
+
<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
6069
|
<header>
|
|
5824
6070
|
<h1><span class="app-logo" aria-hidden="true">π</span> Studio <span class="app-subtitle">${appSubtitle}</span></h1>
|
|
5825
6071
|
<div class="controls">
|
|
@@ -5843,13 +6089,14 @@ ${cssVarsBlock}
|
|
|
5843
6089
|
</div>
|
|
5844
6090
|
<div class="section-header-actions">
|
|
5845
6091
|
<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="
|
|
6092
|
+
<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>
|
|
6093
|
+
<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
6094
|
</div>
|
|
5848
6095
|
</div>
|
|
5849
6096
|
<div class="source-wrap">
|
|
5850
6097
|
<div class="source-meta">
|
|
5851
6098
|
<div class="badge-row">
|
|
5852
|
-
<
|
|
6099
|
+
<button id="sourceBadge" type="button" class="source-badge source-badge-button">Editor origin: ${initialLabel}</button>
|
|
5853
6100
|
<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
6101
|
<span id="resourceDirLabel" class="source-badge resource-dir-label" hidden title="Click to change working directory"></span>
|
|
5855
6102
|
<span id="resourceDirInputWrap" class="resource-dir-input-wrap">
|
|
@@ -5867,9 +6114,9 @@ ${cssVarsBlock}
|
|
|
5867
6114
|
</div>
|
|
5868
6115
|
<div class="source-actions-row">
|
|
5869
6116
|
<button id="insertHeaderBtn" type="button" title="Insert annotated-reply protocol header (source metadata, [an: ...] syntax hint, precedence note, and end marker).">Annotation header</button>
|
|
5870
|
-
<select id="annotationModeSelect" aria-label="
|
|
5871
|
-
<option value="on" selected>
|
|
5872
|
-
<option value="off">
|
|
6117
|
+
<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.">
|
|
6118
|
+
<option value="on" selected>Inline annotations: On</option>
|
|
6119
|
+
<option value="off">Inline annotations: Hide</option>
|
|
5873
6120
|
</select>
|
|
5874
6121
|
<button id="stripAnnotationsBtn" type="button" title="Destructively remove all [an: ...] markers from editor text.">Strip annotations…</button>
|
|
5875
6122
|
<button id="saveAnnotatedBtn" type="button" title="Save full editor content (including [an: ...] markers) as a .annotated.md file.">Save .annotated.md</button>
|
|
@@ -5910,21 +6157,52 @@ ${cssVarsBlock}
|
|
|
5910
6157
|
<option value="yaml">Syntax highlight: YAML</option>
|
|
5911
6158
|
</select>
|
|
5912
6159
|
<select id="lineNumbersSelect" aria-label="Editor line numbers">
|
|
5913
|
-
<option value="off"
|
|
5914
|
-
<option value="on">Line numbers: On</option>
|
|
6160
|
+
<option value="off">Line numbers: Off</option>
|
|
6161
|
+
<option value="on" selected>Line numbers: On</option>
|
|
5915
6162
|
</select>
|
|
5916
6163
|
</div>
|
|
5917
6164
|
</div>
|
|
5918
6165
|
</div>
|
|
5919
|
-
<div
|
|
5920
|
-
<div
|
|
5921
|
-
<div id="
|
|
6166
|
+
<div class="source-body">
|
|
6167
|
+
<div class="source-primary">
|
|
6168
|
+
<div id="sourceEditorWrap" class="editor-highlight-wrap">
|
|
6169
|
+
<div id="reviewNoteGutter" class="editor-review-note-gutter" hidden aria-hidden="true">
|
|
6170
|
+
<div id="reviewNoteGutterContent" class="editor-review-note-gutter-content"></div>
|
|
6171
|
+
</div>
|
|
6172
|
+
<div id="lineNumberGutter" class="editor-line-number-gutter" hidden aria-hidden="true">
|
|
6173
|
+
<div id="lineNumberGutterContent" class="editor-line-number-gutter-content"></div>
|
|
6174
|
+
</div>
|
|
6175
|
+
<div id="lineNumberMeasure" class="editor-line-number-measure" aria-hidden="true"></div>
|
|
6176
|
+
<pre id="sourceHighlight" class="editor-highlight" aria-hidden="true"></pre>
|
|
6177
|
+
<textarea id="sourceText" placeholder="Paste or edit text here.">${initialText}</textarea>
|
|
6178
|
+
<button id="editorSelectionCommentBtn" type="button" class="editor-selection-comment-btn" hidden title="Create a new local comment from the current editor selection.">Comment</button>
|
|
6179
|
+
</div>
|
|
6180
|
+
<div id="sourcePreview" class="panel-scroll rendered-markdown" hidden><pre class="plain-markdown"></pre></div>
|
|
5922
6181
|
</div>
|
|
5923
|
-
<
|
|
5924
|
-
|
|
5925
|
-
|
|
6182
|
+
<aside id="reviewNotesOverlay" class="review-notes-dock-wrap" hidden>
|
|
6183
|
+
<div id="reviewNotesDialog" class="review-notes-dock" role="complementary" aria-labelledby="reviewNotesTitle">
|
|
6184
|
+
<div class="scratchpad-header">
|
|
6185
|
+
<div>
|
|
6186
|
+
<h2 id="reviewNotesTitle">Comments</h2>
|
|
6187
|
+
<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>
|
|
6188
|
+
</div>
|
|
6189
|
+
<button id="reviewNotesCloseBtn" type="button" class="scratchpad-close-btn" aria-label="Hide comments" title="Hide comments">✕</button>
|
|
6190
|
+
</div>
|
|
6191
|
+
<div class="review-notes-toolbar">
|
|
6192
|
+
<span id="reviewNotesMeta" class="scratchpad-meta">No comments</span>
|
|
6193
|
+
</div>
|
|
6194
|
+
<div id="reviewNotesEmptyState" class="review-notes-empty">No comments yet for this document. Select text in <strong>Editor (Raw)</strong> or <strong>Editor (Preview)</strong> and use <em>Comment</em>, or use <em>Line comment</em> in <strong>Editor (Raw)</strong>.</div>
|
|
6195
|
+
<div id="reviewNotesList" class="review-notes-list" aria-live="polite"></div>
|
|
6196
|
+
<div class="review-notes-dock-footer">
|
|
6197
|
+
<div class="scratchpad-actions">
|
|
6198
|
+
<button id="reviewNotesAddBtn" type="button" title="Create a new local comment on the current editor line.">Line comment</button>
|
|
6199
|
+
<button id="reviewNotesInlineAllBtn" type="button" title="Toggle inline annotations for all non-empty comments.">All inline: Off</button>
|
|
6200
|
+
<button id="reviewNotesDoneBtn" type="button" title="Hide the comments rail.">Hide</button>
|
|
6201
|
+
</div>
|
|
6202
|
+
</div>
|
|
6203
|
+
</div>
|
|
6204
|
+
</aside>
|
|
5926
6205
|
</div>
|
|
5927
|
-
<div id="sourcePreview" class="panel-scroll rendered-markdown" hidden><pre class="plain-markdown"></pre></div>
|
|
5928
6206
|
</div>
|
|
5929
6207
|
</section>
|
|
5930
6208
|
|
|
@@ -5989,7 +6267,7 @@ ${cssVarsBlock}
|
|
|
5989
6267
|
<div class="scratchpad-header">
|
|
5990
6268
|
<div>
|
|
5991
6269
|
<h2 id="scratchpadTitle">Scratchpad</h2>
|
|
5992
|
-
<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>
|
|
6270
|
+
<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>
|
|
5993
6271
|
</div>
|
|
5994
6272
|
<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>
|
|
5995
6273
|
</div>
|
|
@@ -7431,6 +7709,120 @@ export default function (pi: ExtensionAPI) {
|
|
|
7431
7709
|
res.end(prepared.pdf);
|
|
7432
7710
|
};
|
|
7433
7711
|
|
|
7712
|
+
const handleScratchpadStateRequest = async (req: IncomingMessage, res: ServerResponse, requestUrl: URL) => {
|
|
7713
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
7714
|
+
if (method === "GET") {
|
|
7715
|
+
const documentKey = (requestUrl.searchParams.get("documentKey") ?? "").trim();
|
|
7716
|
+
if (!documentKey) {
|
|
7717
|
+
respondJson(res, 400, { ok: false, error: "Missing documentKey query parameter." });
|
|
7718
|
+
return;
|
|
7719
|
+
}
|
|
7720
|
+
respondJson(res, 200, { ok: true, text: await readPersistedStudioScratchpadText(documentKey) });
|
|
7721
|
+
return;
|
|
7722
|
+
}
|
|
7723
|
+
if (method !== "POST") {
|
|
7724
|
+
res.setHeader("Allow", "GET, POST");
|
|
7725
|
+
respondJson(res, 405, { ok: false, error: "Method not allowed. Use GET or POST." });
|
|
7726
|
+
return;
|
|
7727
|
+
}
|
|
7728
|
+
|
|
7729
|
+
let rawBody = "";
|
|
7730
|
+
try {
|
|
7731
|
+
rawBody = await readRequestBody(req, REQUEST_BODY_MAX_BYTES);
|
|
7732
|
+
} catch (error) {
|
|
7733
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
7734
|
+
const status = message.includes("exceeds") ? 413 : 400;
|
|
7735
|
+
respondJson(res, status, { ok: false, error: message });
|
|
7736
|
+
return;
|
|
7737
|
+
}
|
|
7738
|
+
|
|
7739
|
+
let parsedBody: unknown;
|
|
7740
|
+
try {
|
|
7741
|
+
parsedBody = rawBody ? JSON.parse(rawBody) : {};
|
|
7742
|
+
} catch {
|
|
7743
|
+
respondJson(res, 400, { ok: false, error: "Invalid JSON body." });
|
|
7744
|
+
return;
|
|
7745
|
+
}
|
|
7746
|
+
|
|
7747
|
+
const documentKey =
|
|
7748
|
+
parsedBody && typeof parsedBody === "object" && typeof (parsedBody as { documentKey?: unknown }).documentKey === "string"
|
|
7749
|
+
? (parsedBody as { documentKey: string }).documentKey.trim()
|
|
7750
|
+
: "";
|
|
7751
|
+
if (!documentKey) {
|
|
7752
|
+
respondJson(res, 400, { ok: false, error: "Missing documentKey in request body." });
|
|
7753
|
+
return;
|
|
7754
|
+
}
|
|
7755
|
+
|
|
7756
|
+
const text =
|
|
7757
|
+
parsedBody && typeof parsedBody === "object" && typeof (parsedBody as { text?: unknown }).text === "string"
|
|
7758
|
+
? (parsedBody as { text: string }).text
|
|
7759
|
+
: null;
|
|
7760
|
+
if (text === null) {
|
|
7761
|
+
respondJson(res, 400, { ok: false, error: "Missing scratchpad text in request body." });
|
|
7762
|
+
return;
|
|
7763
|
+
}
|
|
7764
|
+
|
|
7765
|
+
await writePersistedStudioScratchpadText(documentKey, text);
|
|
7766
|
+
respondJson(res, 200, { ok: true });
|
|
7767
|
+
};
|
|
7768
|
+
|
|
7769
|
+
const handleReviewNotesRequest = async (req: IncomingMessage, res: ServerResponse, requestUrl: URL) => {
|
|
7770
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
7771
|
+
if (method === "GET") {
|
|
7772
|
+
const documentKey = (requestUrl.searchParams.get("documentKey") ?? "").trim();
|
|
7773
|
+
if (!documentKey) {
|
|
7774
|
+
respondJson(res, 400, { ok: false, error: "Missing documentKey query parameter." });
|
|
7775
|
+
return;
|
|
7776
|
+
}
|
|
7777
|
+
respondJson(res, 200, { ok: true, notes: await readPersistedStudioReviewNotes(documentKey) });
|
|
7778
|
+
return;
|
|
7779
|
+
}
|
|
7780
|
+
if (method !== "POST") {
|
|
7781
|
+
res.setHeader("Allow", "GET, POST");
|
|
7782
|
+
respondJson(res, 405, { ok: false, error: "Method not allowed. Use GET or POST." });
|
|
7783
|
+
return;
|
|
7784
|
+
}
|
|
7785
|
+
|
|
7786
|
+
let rawBody = "";
|
|
7787
|
+
try {
|
|
7788
|
+
rawBody = await readRequestBody(req, REQUEST_BODY_MAX_BYTES);
|
|
7789
|
+
} catch (error) {
|
|
7790
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
7791
|
+
const status = message.includes("exceeds") ? 413 : 400;
|
|
7792
|
+
respondJson(res, status, { ok: false, error: message });
|
|
7793
|
+
return;
|
|
7794
|
+
}
|
|
7795
|
+
|
|
7796
|
+
let parsedBody: unknown;
|
|
7797
|
+
try {
|
|
7798
|
+
parsedBody = rawBody ? JSON.parse(rawBody) : {};
|
|
7799
|
+
} catch {
|
|
7800
|
+
respondJson(res, 400, { ok: false, error: "Invalid JSON body." });
|
|
7801
|
+
return;
|
|
7802
|
+
}
|
|
7803
|
+
|
|
7804
|
+
const documentKey =
|
|
7805
|
+
parsedBody && typeof parsedBody === "object" && typeof (parsedBody as { documentKey?: unknown }).documentKey === "string"
|
|
7806
|
+
? (parsedBody as { documentKey: string }).documentKey.trim()
|
|
7807
|
+
: "";
|
|
7808
|
+
if (!documentKey) {
|
|
7809
|
+
respondJson(res, 400, { ok: false, error: "Missing documentKey in request body." });
|
|
7810
|
+
return;
|
|
7811
|
+
}
|
|
7812
|
+
|
|
7813
|
+
const notes =
|
|
7814
|
+
parsedBody && typeof parsedBody === "object" && Array.isArray((parsedBody as { notes?: unknown }).notes)
|
|
7815
|
+
? (parsedBody as { notes: PersistedStudioReviewNote[] }).notes
|
|
7816
|
+
: null;
|
|
7817
|
+
if (!notes) {
|
|
7818
|
+
respondJson(res, 400, { ok: false, error: "Missing notes array in request body." });
|
|
7819
|
+
return;
|
|
7820
|
+
}
|
|
7821
|
+
|
|
7822
|
+
await writePersistedStudioReviewNotes(documentKey, notes);
|
|
7823
|
+
respondJson(res, 200, { ok: true });
|
|
7824
|
+
};
|
|
7825
|
+
|
|
7434
7826
|
const handleRenderPreviewRequest = async (req: IncomingMessage, res: ServerResponse) => {
|
|
7435
7827
|
let rawBody = "";
|
|
7436
7828
|
try {
|
|
@@ -7673,6 +8065,36 @@ export default function (pi: ExtensionAPI) {
|
|
|
7673
8065
|
return;
|
|
7674
8066
|
}
|
|
7675
8067
|
|
|
8068
|
+
if (requestUrl.pathname === "/scratchpad-state") {
|
|
8069
|
+
const token = requestUrl.searchParams.get("token") ?? "";
|
|
8070
|
+
if (token !== serverState.token) {
|
|
8071
|
+
respondJson(res, 403, { ok: false, error: "Invalid or expired studio token. Re-run /studio." });
|
|
8072
|
+
return;
|
|
8073
|
+
}
|
|
8074
|
+
void handleScratchpadStateRequest(req, res, requestUrl).catch((error) => {
|
|
8075
|
+
respondJson(res, 500, {
|
|
8076
|
+
ok: false,
|
|
8077
|
+
error: `Scratchpad persistence failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
8078
|
+
});
|
|
8079
|
+
});
|
|
8080
|
+
return;
|
|
8081
|
+
}
|
|
8082
|
+
|
|
8083
|
+
if (requestUrl.pathname === "/review-notes") {
|
|
8084
|
+
const token = requestUrl.searchParams.get("token") ?? "";
|
|
8085
|
+
if (token !== serverState.token) {
|
|
8086
|
+
respondJson(res, 403, { ok: false, error: "Invalid or expired studio token. Re-run /studio." });
|
|
8087
|
+
return;
|
|
8088
|
+
}
|
|
8089
|
+
void handleReviewNotesRequest(req, res, requestUrl).catch((error) => {
|
|
8090
|
+
respondJson(res, 500, {
|
|
8091
|
+
ok: false,
|
|
8092
|
+
error: `Review-note persistence failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
8093
|
+
});
|
|
8094
|
+
});
|
|
8095
|
+
return;
|
|
8096
|
+
}
|
|
8097
|
+
|
|
7676
8098
|
if (requestUrl.pathname === "/render-preview") {
|
|
7677
8099
|
const token = requestUrl.searchParams.get("token") ?? "";
|
|
7678
8100
|
if (token !== serverState.token) {
|
|
@@ -7749,7 +8171,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
7749
8171
|
});
|
|
7750
8172
|
refreshContextUsage();
|
|
7751
8173
|
const studioMode = normalizeStudioUiMode(requestUrl.searchParams.get("mode"));
|
|
7752
|
-
|
|
8174
|
+
const requestInitialDocument = resolveRequestedStudioDocumentFromUrl(requestUrl, initialStudioDocument, studioCwd, lastStudioResponse);
|
|
8175
|
+
res.end(buildStudioHtml(requestInitialDocument, serverState.token, lastCommandCtx?.ui.theme, currentModelLabel, terminalSessionLabel, contextUsageSnapshot, studioMode));
|
|
7753
8176
|
};
|
|
7754
8177
|
|
|
7755
8178
|
const ensureServer = async (): Promise<StudioServerState> => {
|
|
@@ -8229,12 +8652,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
8229
8652
|
text: latestAssistant,
|
|
8230
8653
|
label: "last model response",
|
|
8231
8654
|
source: "last-response",
|
|
8655
|
+
draftId: createStudioDraftId(),
|
|
8232
8656
|
};
|
|
8233
8657
|
}
|
|
8234
8658
|
return {
|
|
8235
8659
|
text: "",
|
|
8236
8660
|
label: "blank",
|
|
8237
8661
|
source: "blank",
|
|
8662
|
+
draftId: createStudioDraftId(),
|
|
8238
8663
|
};
|
|
8239
8664
|
}
|
|
8240
8665
|
|
|
@@ -8243,6 +8668,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
8243
8668
|
text: "",
|
|
8244
8669
|
label: "blank",
|
|
8245
8670
|
source: "blank",
|
|
8671
|
+
draftId: createStudioDraftId(),
|
|
8246
8672
|
};
|
|
8247
8673
|
}
|
|
8248
8674
|
|
|
@@ -8253,12 +8679,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
8253
8679
|
text: "",
|
|
8254
8680
|
label: "blank",
|
|
8255
8681
|
source: "blank",
|
|
8682
|
+
draftId: createStudioDraftId(),
|
|
8256
8683
|
};
|
|
8257
8684
|
}
|
|
8258
8685
|
return {
|
|
8259
8686
|
text: latestAssistant,
|
|
8260
8687
|
label: "last model response",
|
|
8261
8688
|
source: "last-response",
|
|
8689
|
+
draftId: createStudioDraftId(),
|
|
8262
8690
|
};
|
|
8263
8691
|
}
|
|
8264
8692
|
|
|
@@ -8331,7 +8759,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
8331
8759
|
initialStudioDocument = selected;
|
|
8332
8760
|
|
|
8333
8761
|
const state = await ensureServer();
|
|
8334
|
-
const url = buildStudioUrl(state.port, state.token, mode);
|
|
8762
|
+
const url = buildStudioUrl(state.port, state.token, mode, selected);
|
|
8335
8763
|
const openedLabel = mode === "editor-only" ? "pi Studio editor-only view" : "pi Studio";
|
|
8336
8764
|
|
|
8337
8765
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-studio",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.46",
|
|
4
4
|
"description": "Two-pane browser workspace for pi with prompt/response editing, annotations, critiques, prompt/response history, and live Markdown/LaTeX/code preview",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|