pi-studio 0.5.35 → 0.5.37

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/client/studio.css CHANGED
@@ -220,6 +220,12 @@
220
220
  filter: brightness(0.95);
221
221
  }
222
222
 
223
+ #scratchpadBtn.has-content {
224
+ border-color: var(--accent);
225
+ color: var(--accent);
226
+ font-weight: 600;
227
+ }
228
+
223
229
  .section-header select {
224
230
  font-weight: 600;
225
231
  font-size: 14px;
@@ -523,6 +529,14 @@
523
529
  vertical-align: baseline;
524
530
  }
525
531
 
532
+ .annotation-preview-marker code {
533
+ font-family: var(--mono-font, ui-monospace, SFMono-Regular, Menlo, monospace);
534
+ font-size: 0.95em;
535
+ background: rgba(0, 0, 0, 0.08);
536
+ border-radius: 3px;
537
+ padding: 0 0.2em;
538
+ }
539
+
526
540
  #sourcePreview {
527
541
  flex: 1 1 auto;
528
542
  min-height: 0;
@@ -1331,6 +1345,125 @@
1331
1345
  background: var(--panel);
1332
1346
  }
1333
1347
 
1348
+ body.scratchpad-open {
1349
+ overflow: hidden;
1350
+ }
1351
+
1352
+ .scratchpad-overlay {
1353
+ position: fixed;
1354
+ inset: 0;
1355
+ z-index: 50;
1356
+ display: flex;
1357
+ align-items: center;
1358
+ justify-content: center;
1359
+ padding: 24px;
1360
+ background: rgba(0, 0, 0, 0.48);
1361
+ backdrop-filter: blur(2px);
1362
+ }
1363
+
1364
+ .scratchpad-overlay[hidden] {
1365
+ display: none !important;
1366
+ }
1367
+
1368
+ .scratchpad-dialog {
1369
+ width: min(860px, 100%);
1370
+ max-height: min(82vh, 900px);
1371
+ border: 1px solid var(--border);
1372
+ border-radius: 14px;
1373
+ background: var(--panel);
1374
+ box-shadow: 0 18px 50px rgba(0, 0, 0, 0.28);
1375
+ display: flex;
1376
+ flex-direction: column;
1377
+ overflow: hidden;
1378
+ }
1379
+
1380
+ .scratchpad-header {
1381
+ display: flex;
1382
+ align-items: flex-start;
1383
+ justify-content: space-between;
1384
+ gap: 12px;
1385
+ padding: 16px 18px 12px;
1386
+ border-bottom: 1px solid var(--border-muted);
1387
+ background: var(--panel-2);
1388
+ }
1389
+
1390
+ .scratchpad-header > div {
1391
+ flex: 1 1 auto;
1392
+ min-width: 0;
1393
+ }
1394
+
1395
+ .scratchpad-header h2 {
1396
+ margin: 0;
1397
+ font-size: 17px;
1398
+ font-weight: 600;
1399
+ }
1400
+
1401
+ .scratchpad-description {
1402
+ margin: 6px 0 0;
1403
+ font-size: 12px;
1404
+ line-height: 1.45;
1405
+ color: var(--muted);
1406
+ max-width: none;
1407
+ }
1408
+
1409
+ .scratchpad-close-btn {
1410
+ padding: 6px 10px;
1411
+ line-height: 1;
1412
+ flex: 0 0 auto;
1413
+ }
1414
+
1415
+ .scratchpad-textarea {
1416
+ width: 100%;
1417
+ min-height: 280px;
1418
+ flex: 1 1 auto;
1419
+ border: 0;
1420
+ border-bottom: 1px solid var(--border-muted);
1421
+ border-radius: 0;
1422
+ margin: 0;
1423
+ padding: 16px 18px;
1424
+ background: var(--panel);
1425
+ color: var(--text);
1426
+ font-family: var(--font-mono);
1427
+ font-size: 13px;
1428
+ line-height: 1.55;
1429
+ resize: vertical;
1430
+ outline: none;
1431
+ }
1432
+
1433
+ .scratchpad-footer {
1434
+ display: flex;
1435
+ align-items: center;
1436
+ justify-content: space-between;
1437
+ gap: 12px;
1438
+ flex-wrap: wrap;
1439
+ padding: 12px 18px 16px;
1440
+ background: var(--panel);
1441
+ }
1442
+
1443
+ .scratchpad-meta {
1444
+ font-size: 12px;
1445
+ color: var(--muted);
1446
+ }
1447
+
1448
+ .scratchpad-actions {
1449
+ display: inline-flex;
1450
+ align-items: center;
1451
+ gap: 8px;
1452
+ flex-wrap: wrap;
1453
+ justify-content: flex-end;
1454
+ }
1455
+
1456
+ #scratchpadDoneBtn:not(:disabled) {
1457
+ background: var(--accent);
1458
+ border-color: var(--accent);
1459
+ color: var(--accent-contrast);
1460
+ font-weight: 600;
1461
+ }
1462
+
1463
+ #scratchpadDoneBtn:not(:disabled):hover {
1464
+ filter: brightness(0.95);
1465
+ }
1466
+
1334
1467
  #status.error { color: var(--error); }
1335
1468
  #status.warning { color: var(--warn); }
1336
1469
  #status.success { color: var(--ok); }
package/index.ts CHANGED
@@ -8,6 +8,16 @@ import { homedir, tmpdir } from "node:os";
8
8
  import { basename, dirname, extname, isAbsolute, join, resolve } from "node:path";
9
9
  import { URL, pathToFileURL } from "node:url";
10
10
  import { WebSocketServer, WebSocket, type RawData } from "ws";
11
+ import {
12
+ collectStudioInlineAnnotationMarkers,
13
+ hasStudioMarkdownAnnotationMarkers,
14
+ isStudioAnnotationWordChar,
15
+ normalizeStudioAnnotationText,
16
+ readStudioAnnotationProtectedTokenAt,
17
+ replaceStudioInlineAnnotationMarkers,
18
+ transformStudioMarkdownOutsideFences,
19
+ } from "./shared/studio-annotation-scanner.js";
20
+ import { escapeStudioPdfLatexTextFragment } from "./shared/studio-pdf-escape.js";
11
21
 
12
22
  type Lens = "writing" | "code";
13
23
  type RequestedLens = Lens | "auto";
@@ -18,6 +28,7 @@ type StudioPromptMode = "response" | "run" | "effective";
18
28
  type StudioPromptTriggerKind = "run" | "steer";
19
29
 
20
30
  const STUDIO_CSS_URL = new URL("./client/studio.css", import.meta.url);
31
+ const STUDIO_ANNOTATION_HELPERS_URL = new URL("./client/studio-annotation-helpers.js", import.meta.url);
21
32
  const STUDIO_CLIENT_URL = new URL("./client/studio-client.js", import.meta.url);
22
33
 
23
34
  interface StudioServerState {
@@ -3023,14 +3034,6 @@ function inferStudioPdfLanguage(markdown: string, editorLanguage?: string): stri
3023
3034
  return undefined;
3024
3035
  }
3025
3036
 
3026
- function escapeStudioPdfLatexTextFragment(text: string): string {
3027
- return String(text ?? "")
3028
- .replace(/\\/g, "\\textbackslash{}")
3029
- .replace(/([{}%#$&_])/g, "\\$1")
3030
- .replace(/~/g, "\\textasciitilde{}")
3031
- .replace(/\^/g, "\\textasciicircum{}");
3032
- }
3033
-
3034
3037
  function escapeStudioPdfLatexText(text: string): string {
3035
3038
  const normalized = String(text ?? "")
3036
3039
  .replace(/\r\n/g, "\n")
@@ -3085,71 +3088,158 @@ function escapeStudioPdfLatexText(text: string): string {
3085
3088
  return out.trim();
3086
3089
  }
3087
3090
 
3088
- function replaceStudioAnnotationMarkersForPdfInSegment(text: string): string {
3089
- return String(text ?? "")
3090
- .replace(/\[an:\s*([^\]]+?)\]/gi, (_match, markerText: string) => {
3091
- const cleaned = escapeStudioPdfLatexText(markerText);
3092
- if (!cleaned) return "";
3093
- return `\\studioannotation{${cleaned}}`;
3094
- })
3095
- .replace(/\{\[\}\s*an:\s*([\s\S]*?)\s*\{\]\}/gi, (_match, markerText: string) => {
3096
- const cleaned = escapeStudioPdfLatexText(markerText);
3097
- if (!cleaned) return "";
3098
- return `\\studioannotation{${cleaned}}`;
3099
- });
3091
+ function renderStudioAnnotationCodeSpanPdfLatex(rawToken: string): string {
3092
+ const raw = String(rawToken ?? "");
3093
+ if (!raw || raw[0] !== "`") return escapeStudioPdfLatexTextFragment(raw);
3094
+
3095
+ let fenceLength = 1;
3096
+ while (raw[fenceLength] === "`") fenceLength += 1;
3097
+ const fence = "`".repeat(fenceLength);
3098
+ if (raw.length < fenceLength * 2 || raw.slice(raw.length - fenceLength) !== fence) {
3099
+ return escapeStudioPdfLatexTextFragment(raw);
3100
+ }
3101
+
3102
+ return `\\texttt{${escapeStudioPdfLatexTextFragment(raw.slice(fenceLength, raw.length - fenceLength))}}`;
3100
3103
  }
3101
3104
 
3102
- function replaceStudioAnnotationMarkersForPdf(markdown: string): string {
3103
- const lines = String(markdown ?? "").split("\n");
3104
- const out: string[] = [];
3105
- let plainBuffer: string[] = [];
3106
- let inFence = false;
3107
- let fenceChar: "`" | "~" | undefined;
3108
- let fenceLength = 0;
3105
+ function canOpenStudioAnnotationEmphasisDelimiter(source: string, startIndex: number, delimiter: string): boolean {
3106
+ if (source.slice(startIndex, startIndex + delimiter.length) !== delimiter) return false;
3107
+ const prev = startIndex > 0 ? source[startIndex - 1] ?? "" : "";
3108
+ const next = source[startIndex + delimiter.length] ?? "";
3109
+ if (!next || /\s/.test(next)) return false;
3110
+ return !isStudioAnnotationWordChar(prev);
3111
+ }
3109
3112
 
3110
- const flushPlain = () => {
3111
- if (plainBuffer.length === 0) return;
3112
- out.push(replaceStudioAnnotationMarkersForPdfInSegment(plainBuffer.join("\n")));
3113
- plainBuffer = [];
3114
- };
3113
+ function canCloseStudioAnnotationEmphasisDelimiter(source: string, startIndex: number, delimiter: string): boolean {
3114
+ if (source.slice(startIndex, startIndex + delimiter.length) !== delimiter) return false;
3115
+ const prev = startIndex > 0 ? source[startIndex - 1] ?? "" : "";
3116
+ const next = source[startIndex + delimiter.length] ?? "";
3117
+ if (!prev || /\s/.test(prev)) return false;
3118
+ return !isStudioAnnotationWordChar(next);
3119
+ }
3115
3120
 
3116
- for (const line of lines) {
3117
- const trimmed = line.trimStart();
3118
- const fenceMatch = trimmed.match(/^(`{3,}|~{3,})/);
3121
+ function renderStudioAnnotationPdfLatexContent(text: string): string {
3122
+ const source = String(text ?? "");
3123
+ let out = "";
3124
+ let plainStart = 0;
3125
+ let index = 0;
3119
3126
 
3120
- if (fenceMatch) {
3121
- const marker = fenceMatch[1]!;
3122
- const markerChar = marker[0] as "`" | "~";
3123
- const markerLength = marker.length;
3127
+ while (index < source.length) {
3128
+ const token = readStudioAnnotationProtectedTokenAt(source, index);
3129
+ if (!token) {
3130
+ index += 1;
3131
+ continue;
3132
+ }
3124
3133
 
3125
- if (!inFence) {
3126
- flushPlain();
3127
- inFence = true;
3128
- fenceChar = markerChar;
3129
- fenceLength = markerLength;
3130
- out.push(line);
3131
- continue;
3132
- }
3134
+ if (index > plainStart) {
3135
+ out += renderStudioAnnotationPlainTextPdfLatex(source.slice(plainStart, index));
3136
+ }
3133
3137
 
3134
- if (fenceChar === markerChar && markerLength >= fenceLength) {
3135
- inFence = false;
3136
- fenceChar = undefined;
3137
- fenceLength = 0;
3138
- }
3138
+ if (token.type === "code") {
3139
+ out += renderStudioAnnotationCodeSpanPdfLatex(token.raw);
3140
+ } else if (token.type === "math") {
3141
+ out += escapeStudioPdfLatexText(token.raw);
3142
+ } else {
3143
+ out += escapeStudioPdfLatexTextFragment(token.raw);
3144
+ }
3139
3145
 
3140
- out.push(line);
3146
+ index = token.end;
3147
+ plainStart = index;
3148
+ }
3149
+
3150
+ if (plainStart < source.length) {
3151
+ out += renderStudioAnnotationPlainTextPdfLatex(source.slice(plainStart));
3152
+ }
3153
+
3154
+ return out;
3155
+ }
3156
+
3157
+ function readStudioAnnotationPdfEmphasisSpanAt(source: string, startIndex: number, delimiter: string, commandName: string): { end: number; latex: string } | null {
3158
+ if (!canOpenStudioAnnotationEmphasisDelimiter(source, startIndex, delimiter)) return null;
3159
+
3160
+ let index = startIndex + delimiter.length;
3161
+ while (index < source.length) {
3162
+ if (source[index] === "\\") {
3163
+ index = Math.min(source.length, index + 2);
3141
3164
  continue;
3142
3165
  }
3143
3166
 
3144
- if (inFence) {
3145
- out.push(line);
3146
- } else {
3147
- plainBuffer.push(line);
3167
+ const protectedToken = readStudioAnnotationProtectedTokenAt(source, index);
3168
+ if (protectedToken) {
3169
+ index = protectedToken.end;
3170
+ continue;
3171
+ }
3172
+
3173
+ if (canCloseStudioAnnotationEmphasisDelimiter(source, index, delimiter)) {
3174
+ const inner = source.slice(startIndex + delimiter.length, index);
3175
+ return {
3176
+ end: index + delimiter.length,
3177
+ latex: `\\${commandName}{${renderStudioAnnotationPdfLatexContent(inner)}}`,
3178
+ };
3148
3179
  }
3180
+
3181
+ index += 1;
3149
3182
  }
3150
3183
 
3151
- flushPlain();
3152
- return out.join("\n");
3184
+ return null;
3185
+ }
3186
+
3187
+ function renderStudioAnnotationPlainTextPdfLatex(text: string): string {
3188
+ const source = String(text ?? "");
3189
+ let out = "";
3190
+ let index = 0;
3191
+
3192
+ while (index < source.length) {
3193
+ const strongMatch = readStudioAnnotationPdfEmphasisSpanAt(source, index, "**", "textbf")
3194
+ ?? readStudioAnnotationPdfEmphasisSpanAt(source, index, "__", "textbf");
3195
+ if (strongMatch) {
3196
+ out += strongMatch.latex;
3197
+ index = strongMatch.end;
3198
+ continue;
3199
+ }
3200
+
3201
+ const emphasisMatch = readStudioAnnotationPdfEmphasisSpanAt(source, index, "*", "emph")
3202
+ ?? readStudioAnnotationPdfEmphasisSpanAt(source, index, "_", "emph");
3203
+ if (emphasisMatch) {
3204
+ out += emphasisMatch.latex;
3205
+ index = emphasisMatch.end;
3206
+ continue;
3207
+ }
3208
+
3209
+ out += escapeStudioPdfLatexTextFragment(source[index] ?? "");
3210
+ index += 1;
3211
+ }
3212
+
3213
+ return out;
3214
+ }
3215
+
3216
+ function renderStudioAnnotationPdfLatex(text: string): string {
3217
+ const normalized = normalizeStudioAnnotationText(text);
3218
+ if (!normalized) return "";
3219
+ return renderStudioAnnotationPdfLatexContent(normalized).trim();
3220
+ }
3221
+
3222
+ function replaceStudioAnnotationMarkersForPdfInSegment(text: string): string {
3223
+ const replaced = replaceStudioInlineAnnotationMarkers(
3224
+ String(text ?? ""),
3225
+ (marker) => {
3226
+ const cleaned = renderStudioAnnotationPdfLatex(marker.body);
3227
+ if (!cleaned) return "";
3228
+ return `\\studioannotation{${cleaned}}`;
3229
+ },
3230
+ );
3231
+
3232
+ return String(replaced ?? "")
3233
+ .replace(/\{\[\}\s*an:\s*([\s\S]*?)\s*\{\]\}/gi, (_match, markerText: string) => {
3234
+ const cleaned = renderStudioAnnotationPdfLatex(markerText);
3235
+ if (!cleaned) return "";
3236
+ return `\\studioannotation{${cleaned}}`;
3237
+ });
3238
+ }
3239
+
3240
+ function replaceStudioAnnotationMarkersForPdf(markdown: string): string {
3241
+ if (!hasStudioMarkdownAnnotationMarkers(markdown)) return String(markdown ?? "");
3242
+ return transformStudioMarkdownOutsideFences(markdown, (segment) => replaceStudioAnnotationMarkersForPdfInSegment(segment));
3153
3243
  }
3154
3244
 
3155
3245
  interface StudioPdfRenderOptions {
@@ -4146,38 +4236,19 @@ function replaceStudioAnnotationMarkersInDiffTokenLine(line: string, macroName:
4146
4236
  if (!tokenMatch) return line;
4147
4237
 
4148
4238
  const body = tokenMatch[1] ?? "";
4149
- const markerPattern = /\[an:\s*([^\]]+?)\]/gi;
4150
- let lastIndex = 0;
4151
- let rewritten = "";
4152
- let match: RegExpExecArray | null;
4153
-
4154
4239
  const wrapText = (text: string): string => text ? `\\${macroName}{${text}}` : "";
4240
+ const rewritten = replaceStudioInlineAnnotationMarkers(
4241
+ body,
4242
+ (marker) => {
4243
+ const markerText = decodeStudioGeneratedCodeLatexText(normalizeStudioAnnotationText(marker.body));
4244
+ const cleaned = makeStudioHighlightingMathScriptsVerbatimSafe(renderStudioAnnotationPdfLatex(markerText));
4245
+ if (!cleaned) return "";
4246
+ return `\\studioannotation{${cleaned}}`;
4247
+ },
4248
+ (segment) => wrapText(segment),
4249
+ );
4155
4250
 
4156
- while ((match = markerPattern.exec(body)) !== null) {
4157
- const token = match[0] ?? "";
4158
- const start = match.index;
4159
- if (start > lastIndex) {
4160
- rewritten += wrapText(body.slice(lastIndex, start));
4161
- }
4162
-
4163
- const markerText = decodeStudioGeneratedCodeLatexText((match[1] ?? "").replace(/\s{2,}/g, " ").trim());
4164
- const cleaned = makeStudioHighlightingMathScriptsVerbatimSafe(escapeStudioPdfLatexText(markerText));
4165
- if (cleaned) {
4166
- rewritten += `\\studioannotation{${cleaned}}`;
4167
- }
4168
-
4169
- lastIndex = start + token.length;
4170
- if (token.length === 0) {
4171
- markerPattern.lastIndex += 1;
4172
- }
4173
- }
4174
-
4175
- if (lastIndex === 0) return line;
4176
- if (lastIndex < body.length) {
4177
- rewritten += wrapText(body.slice(lastIndex));
4178
- }
4179
-
4180
- return rewritten || wrapText(body);
4251
+ return rewritten === body ? line : (rewritten || wrapText(body));
4181
4252
  }
4182
4253
 
4183
4254
  function rewriteStudioGeneratedDiffHighlighting(latex: string): string {
@@ -4496,7 +4567,7 @@ async function renderStudioPdfWithPandoc(
4496
4567
  }
4497
4568
  };
4498
4569
 
4499
- if (isLatex && (latexSubfigurePdfTransform.groups.length > 0 || /\[an:\s*[^\]]+\]/i.test(sourceWithResolvedRefs))) {
4570
+ if (isLatex && (latexSubfigurePdfTransform.groups.length > 0 || collectStudioInlineAnnotationMarkers(sourceWithResolvedRefs).length > 0)) {
4500
4571
  return await renderStudioPdfFromGeneratedLatex(
4501
4572
  sourceWithResolvedRefs,
4502
4573
  pandocCommand,
@@ -5627,6 +5698,7 @@ function buildStudioHtml(
5627
5698
  };
5628
5699
  const cssVarsBlock = Object.entries(vars).map(([k, v]) => ` ${k}: ${v};`).join("\n");
5629
5700
  const stylesheetHref = `/studio.css?token=${encodeURIComponent(studioToken ?? "")}`;
5701
+ const annotationHelpersScriptHref = `/studio-annotation-helpers.js?token=${encodeURIComponent(studioToken ?? "")}`;
5630
5702
  const clientScriptHref = `/studio-client.js?token=${encodeURIComponent(studioToken ?? "")}`;
5631
5703
  const faviconHref = buildStudioFaviconDataUri(style);
5632
5704
  const bootConfigJson = JSON.stringify({ mermaidConfig }).replace(/</g, "\\u003c");
@@ -5668,6 +5740,7 @@ ${cssVarsBlock}
5668
5740
  </div>
5669
5741
  <div class="section-header-actions">
5670
5742
  <button id="leftFocusBtn" class="pane-focus-btn" type="button" title="Show only the editor pane. Shortcut: F10 or Cmd/Ctrl+Esc.">Focus pane</button>
5743
+ <button id="scratchpadBtn" type="button" title="Open a local persistent scratchpad for quick notes. Scratchpad text is never run, critiqued, or exported unless you explicitly insert it into the editor.">Scratchpad</button>
5671
5744
  </div>
5672
5745
  </div>
5673
5746
  <div class="source-wrap">
@@ -5803,11 +5876,34 @@ ${cssVarsBlock}
5803
5876
  <span class="shortcut-hint">Focus pane: F10 (or Cmd/Ctrl+Esc) to toggle · Run / queue steering: Cmd/Ctrl+Enter · Stop request: Esc</span>
5804
5877
  </footer>
5805
5878
 
5879
+ <div id="scratchpadOverlay" class="scratchpad-overlay" hidden>
5880
+ <div id="scratchpadDialog" class="scratchpad-dialog" role="dialog" aria-modal="true" aria-labelledby="scratchpadTitle">
5881
+ <div class="scratchpad-header">
5882
+ <div>
5883
+ <h2 id="scratchpadTitle">Scratchpad</h2>
5884
+ <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>
5885
+ </div>
5886
+ <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>
5887
+ </div>
5888
+ <textarea id="scratchpadText" class="scratchpad-textarea" placeholder="Jot quick thoughts, TODOs, or prompt ideas here..."></textarea>
5889
+ <div class="scratchpad-footer">
5890
+ <span id="scratchpadMeta" class="scratchpad-meta">Empty · local only</span>
5891
+ <div class="scratchpad-actions">
5892
+ <button id="scratchpadInsertBtn" type="button" title="Insert the scratchpad text into the editor at the current selection, or append it if no editor selection is available.">Insert into editor</button>
5893
+ <button id="scratchpadCopyBtn" type="button" title="Copy scratchpad text to the clipboard.">Copy</button>
5894
+ <button id="scratchpadClearBtn" type="button" title="Clear scratchpad text.">Clear</button>
5895
+ <button id="scratchpadDoneBtn" type="button" title="Keep the current scratchpad text and close the scratchpad.">Keep and close</button>
5896
+ </div>
5897
+ </div>
5898
+ </div>
5899
+ </div>
5900
+
5806
5901
  <!-- Defer sanitizer script so studio can boot/connect even if CDN is slow or blocked. -->
5807
5902
  <script defer src="https://cdn.jsdelivr.net/npm/dompurify@3.2.6/dist/purify.min.js"></script>
5808
5903
  <script>
5809
5904
  window.__PI_STUDIO_BOOT__ = ${bootConfigJson};
5810
5905
  </script>
5906
+ <script src="${annotationHelpersScriptHref}"></script>
5811
5907
  <script src="${clientScriptHref}"></script>
5812
5908
  </body>
5813
5909
  </html>`;
@@ -7353,7 +7449,7 @@ export default function (pi: ExtensionAPI) {
7353
7449
  return;
7354
7450
  }
7355
7451
 
7356
- if (requestUrl.pathname === "/studio-client.js") {
7452
+ if (requestUrl.pathname === "/studio-annotation-helpers.js" || requestUrl.pathname === "/studio-client.js") {
7357
7453
  const token = requestUrl.searchParams.get("token") ?? "";
7358
7454
  if (token !== serverState.token) {
7359
7455
  respondText(res, 403, "Invalid or expired studio token. Re-run /studio.");
@@ -7367,8 +7463,15 @@ export default function (pi: ExtensionAPI) {
7367
7463
  return;
7368
7464
  }
7369
7465
 
7466
+ const targetUrl = requestUrl.pathname === "/studio-annotation-helpers.js"
7467
+ ? STUDIO_ANNOTATION_HELPERS_URL
7468
+ : STUDIO_CLIENT_URL;
7469
+ const targetLabel = requestUrl.pathname === "/studio-annotation-helpers.js"
7470
+ ? "studio annotation helper script"
7471
+ : "studio client script";
7472
+
7370
7473
  try {
7371
- const clientScript = readFileSync(STUDIO_CLIENT_URL, "utf-8");
7474
+ const clientScript = readFileSync(targetUrl, "utf-8");
7372
7475
  res.writeHead(200, {
7373
7476
  "Content-Type": "application/javascript; charset=utf-8",
7374
7477
  "Cache-Control": "no-store",
@@ -7377,7 +7480,7 @@ export default function (pi: ExtensionAPI) {
7377
7480
  });
7378
7481
  res.end(clientScript);
7379
7482
  } catch (error) {
7380
- respondText(res, 500, `Failed to load studio client script: ${error instanceof Error ? error.message : String(error)}`);
7483
+ respondText(res, 500, `Failed to load ${targetLabel}: ${error instanceof Error ? error.message : String(error)}`);
7381
7484
  }
7382
7485
  return;
7383
7486
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-studio",
3
- "version": "0.5.35",
3
+ "version": "0.5.37",
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",
@@ -18,11 +18,15 @@
18
18
  "files": [
19
19
  "index.ts",
20
20
  "client",
21
+ "shared",
21
22
  "README.md",
22
23
  "CHANGELOG.md",
23
24
  "WORKFLOW.md",
24
25
  "assets/screenshots"
25
26
  ],
27
+ "scripts": {
28
+ "test": "node --test"
29
+ },
26
30
  "pi": {
27
31
  "extensions": [
28
32
  "./index.ts"