pi-studio 0.5.57 → 0.5.58

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 CHANGED
@@ -4,6 +4,12 @@ All notable changes to `pi-studio` are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.5.58] — 2026-04-24
8
+
9
+ ### Added
10
+ - Rendered Markdown/editor previews now add small copy controls to code/fenced-content blocks so block contents can be copied directly from the preview.
11
+ - The local comments rail now includes a **Delete all** action for clearing all comments attached to the current document or draft without removing inline `[an: ...]` annotations from the editor text.
12
+
7
13
  ## [0.5.57] — 2026-04-20
8
14
 
9
15
  ### Changed
@@ -133,6 +133,7 @@
133
133
  const reviewNotesEmptyStateEl = document.getElementById("reviewNotesEmptyState");
134
134
  const reviewNotesAddBtn = document.getElementById("reviewNotesAddBtn");
135
135
  const reviewNotesInlineAllBtn = document.getElementById("reviewNotesInlineAllBtn");
136
+ const reviewNotesDeleteAllBtn = document.getElementById("reviewNotesDeleteAllBtn");
136
137
  const reviewNotesCloseBtn = document.getElementById("reviewNotesCloseBtn");
137
138
  const reviewNotesDoneBtn = document.getElementById("reviewNotesDoneBtn");
138
139
 
@@ -459,16 +460,92 @@
459
460
  return "working";
460
461
  }
461
462
 
463
+ async function writeTextToClipboard(text) {
464
+ const content = String(text || "");
465
+
466
+ try {
467
+ await fetchStudioJson("/clipboard", {
468
+ method: "POST",
469
+ body: JSON.stringify({ text: content }),
470
+ });
471
+ return true;
472
+ } catch {
473
+ // Fall back to browser clipboard APIs. The server-side clipboard path
474
+ // is most reliable for local Studio, but may be unavailable over SSH
475
+ // or on systems without a clipboard command.
476
+ }
477
+
478
+ // Prefer a copy-event payload first. It runs synchronously inside the
479
+ // user's click gesture and avoids browser quirks where copying a hidden
480
+ // textarea reports success but leaves the system clipboard unchanged.
481
+ if (document.execCommand && typeof document.addEventListener === "function") {
482
+ let handled = false;
483
+ const handleCopy = (event) => {
484
+ if (!event || !event.clipboardData) return;
485
+ event.clipboardData.setData("text/plain", content);
486
+ event.preventDefault();
487
+ handled = true;
488
+ };
489
+ try {
490
+ document.addEventListener("copy", handleCopy, true);
491
+ const ok = document.execCommand("copy");
492
+ if (ok && handled) return true;
493
+ } catch {
494
+ // Fall through to the other clipboard paths.
495
+ } finally {
496
+ document.removeEventListener("copy", handleCopy, true);
497
+ }
498
+ }
499
+
500
+ if (navigator.clipboard && typeof navigator.clipboard.writeText === "function") {
501
+ try {
502
+ await navigator.clipboard.writeText(content);
503
+ return true;
504
+ } catch {
505
+ // Fall through to the selection-based legacy path.
506
+ }
507
+ }
508
+
509
+ const textarea = document.createElement("textarea");
510
+ textarea.value = content;
511
+ textarea.setAttribute("readonly", "");
512
+ textarea.style.position = "fixed";
513
+ textarea.style.top = "0";
514
+ textarea.style.left = "0";
515
+ textarea.style.width = "1px";
516
+ textarea.style.height = "1px";
517
+ textarea.style.opacity = "0";
518
+ document.body.appendChild(textarea);
519
+ const activeEl = document.activeElement;
520
+ textarea.focus();
521
+ textarea.select();
522
+ textarea.setSelectionRange(0, textarea.value.length);
523
+ let ok = false;
524
+ try {
525
+ ok = document.execCommand && document.execCommand("copy");
526
+ } catch {
527
+ ok = false;
528
+ }
529
+ textarea.remove();
530
+ if (activeEl && typeof activeEl.focus === "function") {
531
+ try {
532
+ activeEl.focus();
533
+ } catch {
534
+ // Ignore focus restore failures.
535
+ }
536
+ }
537
+ return Boolean(ok);
538
+ }
539
+
462
540
  async function copyVisibleWorkingToClipboard() {
463
541
  const content = buildVisibleWorkingText();
464
542
  if (!content.trim()) {
465
543
  setStatus("No visible working details to copy yet.", "warning");
466
544
  return;
467
545
  }
468
- try {
469
- await navigator.clipboard.writeText(content);
546
+ if (await writeTextToClipboard(content)) {
470
547
  setStatus("Copied visible working text.", "success");
471
- } catch {
548
+ } else {
472
549
  setStatus("Clipboard write failed.", "warning");
473
550
  }
474
551
  }
@@ -2837,6 +2914,107 @@
2837
2914
  }
2838
2915
  }
2839
2916
 
2917
+ function normalizeCopyableBlockText(text) {
2918
+ return String(text || "").replace(/\r\n/g, "\n").replace(/\u200b/g, "");
2919
+ }
2920
+
2921
+ function getCopyablePreviewBlockText(blockEl) {
2922
+ if (!blockEl || typeof blockEl.querySelectorAll !== "function") return "";
2923
+ if (blockEl.classList && blockEl.classList.contains("preview-code-lines")) {
2924
+ return normalizeCopyableBlockText(
2925
+ Array.from(blockEl.querySelectorAll(".preview-code-line-content"))
2926
+ .map((lineEl) => lineEl && typeof lineEl.textContent === "string" ? lineEl.textContent : "")
2927
+ .join("\n"),
2928
+ );
2929
+ }
2930
+
2931
+ const codeEl = typeof blockEl.querySelector === "function"
2932
+ ? blockEl.querySelector("pre code, code")
2933
+ : null;
2934
+ if (codeEl && typeof codeEl.textContent === "string") {
2935
+ return normalizeCopyableBlockText(codeEl.textContent);
2936
+ }
2937
+
2938
+ const clone = typeof blockEl.cloneNode === "function" ? blockEl.cloneNode(true) : null;
2939
+ if (clone && typeof clone.querySelectorAll === "function") {
2940
+ Array.from(clone.querySelectorAll(".studio-copy-block-btn")).forEach((buttonEl) => {
2941
+ if (buttonEl && buttonEl.parentNode) buttonEl.parentNode.removeChild(buttonEl);
2942
+ });
2943
+ return normalizeCopyableBlockText(clone.textContent || "");
2944
+ }
2945
+
2946
+ return normalizeCopyableBlockText(blockEl.textContent || "");
2947
+ }
2948
+
2949
+ async function handleCopyPreviewBlockButtonClick(event) {
2950
+ const target = event && event.target;
2951
+ const copyBtn = target instanceof Element ? target.closest(".studio-copy-block-btn") : null;
2952
+ if (!copyBtn) return;
2953
+ event.preventDefault();
2954
+ event.stopPropagation();
2955
+ if (typeof event.stopImmediatePropagation === "function") {
2956
+ event.stopImmediatePropagation();
2957
+ }
2958
+
2959
+ const blockEl = copyBtn.closest(".studio-copyable-block");
2960
+ if (!blockEl) {
2961
+ setStatus("Could not find the block to copy.", "warning");
2962
+ return;
2963
+ }
2964
+
2965
+ const text = getCopyablePreviewBlockText(blockEl);
2966
+ if (!text.trim()) {
2967
+ setStatus("Nothing to copy from this block.", "warning");
2968
+ return;
2969
+ }
2970
+
2971
+ if (copyBtn.dataset && copyBtn.dataset.studioCopyBusy === "1") return;
2972
+ if (copyBtn.dataset) copyBtn.dataset.studioCopyBusy = "1";
2973
+ const ok = await writeTextToClipboard(text);
2974
+ if (ok) {
2975
+ setStatus("Copied block to clipboard.", "success");
2976
+ } else {
2977
+ setStatus("Clipboard write failed.", "warning");
2978
+ }
2979
+ if (copyBtn.dataset) {
2980
+ window.setTimeout(() => {
2981
+ if (copyBtn.dataset) copyBtn.dataset.studioCopyBusy = "0";
2982
+ }, 150);
2983
+ }
2984
+ }
2985
+
2986
+ function decorateCopyablePreviewBlocks(targetEl) {
2987
+ if (!targetEl || typeof targetEl.querySelectorAll !== "function") return;
2988
+ const blocks = Array.from(targetEl.querySelectorAll("div.sourceCode, pre, .preview-code-lines"));
2989
+ blocks.forEach((blockEl) => {
2990
+ if (!blockEl || !(blockEl instanceof Element)) return;
2991
+ if (blockEl.dataset && blockEl.dataset.studioCopyDecorated === "1") return;
2992
+ if (blockEl.matches && blockEl.matches("pre") && blockEl.closest("div.sourceCode")) return;
2993
+ if (blockEl.closest && blockEl.closest("button, .studio-copy-block-btn")) return;
2994
+
2995
+ const initialText = getCopyablePreviewBlockText(blockEl);
2996
+ if (!initialText.trim()) return;
2997
+
2998
+ blockEl.classList.add("studio-copyable-block");
2999
+ if (blockEl.dataset) blockEl.dataset.studioCopyDecorated = "1";
3000
+
3001
+ const copyBtn = document.createElement("button");
3002
+ copyBtn.type = "button";
3003
+ copyBtn.className = "studio-copy-block-btn";
3004
+ copyBtn.textContent = "Copy";
3005
+ copyBtn.title = "Copy this block to the clipboard.";
3006
+ copyBtn.setAttribute("aria-label", "Copy this block to the clipboard");
3007
+ copyBtn.addEventListener("pointerdown", (event) => {
3008
+ event.stopPropagation();
3009
+ });
3010
+ copyBtn.addEventListener("mousedown", (event) => {
3011
+ event.stopPropagation();
3012
+ });
3013
+
3014
+ blockEl.appendChild(copyBtn);
3015
+ });
3016
+ }
3017
+
2840
3018
  async function applyRenderedMarkdown(targetEl, markdown, pane, nonce) {
2841
3019
  const previewPrepared = annotationsEnabled
2842
3020
  ? prepareMarkdownForPandocPreview(markdown)
@@ -2879,6 +3057,7 @@
2879
3057
  if (shouldDecoratePreviewComments) {
2880
3058
  decorateRenderedEditorPreviewComments(targetEl, sourceTextEl.value || "");
2881
3059
  }
3060
+ decorateCopyablePreviewBlocks(targetEl);
2882
3061
 
2883
3062
  // Warn if relative images are present but unlikely to resolve (non-file-backed content)
2884
3063
  if (!sourceState.path && !(resourceDirInput && resourceDirInput.value.trim())) {
@@ -4281,6 +4460,7 @@
4281
4460
  targetEl.innerHTML = buildCodePreviewHtmlWithCommentBlocks(text, editorLanguage || "");
4282
4461
  ensurePreviewSelectionActions(targetEl);
4283
4462
  updatePreviewCommentBlocksForElement(targetEl);
4463
+ decorateCopyablePreviewBlocks(targetEl);
4284
4464
  if (pane === "response") {
4285
4465
  applyPendingResponseScrollReset();
4286
4466
  scheduleResponsePaneRepaintNudge();
@@ -8066,6 +8246,12 @@
8066
8246
  ? "Inline annotations derived from all non-empty comments are currently on. Click to remove them."
8067
8247
  : "Inline annotations derived from all non-empty comments are currently off. Click to add them.";
8068
8248
  }
8249
+ if (reviewNotesDeleteAllBtn) {
8250
+ reviewNotesDeleteAllBtn.disabled = uiBusy || !hasNotes;
8251
+ reviewNotesDeleteAllBtn.title = hasNotes
8252
+ ? "Delete all local comments for this document or draft. Existing inline [an: ...] annotations in the editor text are left unchanged."
8253
+ : "No local comments to delete.";
8254
+ }
8069
8255
  if (reviewNotesDoneBtn) {
8070
8256
  reviewNotesDoneBtn.disabled = !isOpen;
8071
8257
  }
@@ -8417,6 +8603,21 @@
8417
8603
  setStatus("Deleted local comment.", "success");
8418
8604
  }
8419
8605
 
8606
+ function deleteAllReviewNotes() {
8607
+ if (!reviewNotes.length) {
8608
+ setStatus("No local comments to delete.", "warning");
8609
+ return;
8610
+ }
8611
+ const count = reviewNotes.length;
8612
+ const confirmed = window.confirm(
8613
+ "Delete all " + count + " local comment" + (count === 1 ? "" : "s") + " for this document?\n\n"
8614
+ + "Existing inline [an: ...] annotations in the editor text will not be removed.",
8615
+ );
8616
+ if (!confirmed) return;
8617
+ setReviewNotes([]);
8618
+ setStatus("Deleted all local comments.", "success");
8619
+ }
8620
+
8420
8621
  function convertReviewNoteToAnnotation(noteId) {
8421
8622
  if (uiBusy) {
8422
8623
  setStatus("Wait until the current Studio action finishes before toggling inline annotation state.", "warning");
@@ -10078,7 +10279,7 @@
10078
10279
  }
10079
10280
 
10080
10281
  try {
10081
- await navigator.clipboard.writeText(content);
10282
+ await writeTextToClipboard(content);
10082
10283
  setStatus("Copied response text.", "success");
10083
10284
  } catch (error) {
10084
10285
  setStatus("Clipboard write failed.", "warning");
@@ -10302,7 +10503,7 @@
10302
10503
  }
10303
10504
 
10304
10505
  try {
10305
- await navigator.clipboard.writeText(content);
10506
+ await writeTextToClipboard(content);
10306
10507
  setStatus("Copied editor text.", "success");
10307
10508
  } catch (error) {
10308
10509
  setStatus("Clipboard write failed.", "warning");
@@ -10386,6 +10587,12 @@
10386
10587
  });
10387
10588
  }
10388
10589
 
10590
+ if (reviewNotesDeleteAllBtn) {
10591
+ reviewNotesDeleteAllBtn.addEventListener("click", () => {
10592
+ deleteAllReviewNotes();
10593
+ });
10594
+ }
10595
+
10389
10596
  if (reviewNoteGutterContentEl) {
10390
10597
  reviewNoteGutterContentEl.addEventListener("click", (event) => {
10391
10598
  const target = event.target;
@@ -10397,6 +10604,20 @@
10397
10604
  });
10398
10605
  }
10399
10606
 
10607
+ document.addEventListener("click", (event) => {
10608
+ const target = event.target;
10609
+ const copyBtn = target instanceof Element ? target.closest(".studio-copy-block-btn") : null;
10610
+ if (!copyBtn) return;
10611
+ void handleCopyPreviewBlockButtonClick(event);
10612
+ }, true);
10613
+
10614
+ document.addEventListener("pointerup", (event) => {
10615
+ const target = event.target;
10616
+ const copyBtn = target instanceof Element ? target.closest(".studio-copy-block-btn") : null;
10617
+ if (!copyBtn) return;
10618
+ void handleCopyPreviewBlockButtonClick(event);
10619
+ }, true);
10620
+
10400
10621
  function handlePreviewCommentActionMouseDown(event) {
10401
10622
  const target = event.target;
10402
10623
  const actionBtn = target instanceof Element ? target.closest(".preview-comment-add, .preview-comment-jump, .preview-comment-summary") : null;
@@ -10483,7 +10704,7 @@
10483
10704
  }
10484
10705
 
10485
10706
  try {
10486
- await navigator.clipboard.writeText(String(scratchpadText || ""));
10707
+ await writeTextToClipboard(String(scratchpadText || ""));
10487
10708
  setStatus("Copied scratchpad text.", "success");
10488
10709
  } catch (error) {
10489
10710
  setStatus("Clipboard write failed.", "warning");
package/client/studio.css CHANGED
@@ -1133,6 +1133,43 @@
1133
1133
  white-space: inherit;
1134
1134
  }
1135
1135
 
1136
+ .rendered-markdown .studio-copyable-block {
1137
+ position: relative;
1138
+ }
1139
+
1140
+ .rendered-markdown .studio-copy-block-btn {
1141
+ position: absolute;
1142
+ top: 8px;
1143
+ right: 8px;
1144
+ z-index: 4;
1145
+ padding: 3px 8px;
1146
+ border-radius: 999px;
1147
+ border: 1px solid var(--border-muted);
1148
+ background: var(--panel);
1149
+ color: var(--muted);
1150
+ font-family: var(--font-ui);
1151
+ font-size: 11px;
1152
+ font-weight: 700;
1153
+ line-height: 1.4;
1154
+ opacity: 0.25;
1155
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.16);
1156
+ cursor: pointer;
1157
+ pointer-events: auto;
1158
+ user-select: none;
1159
+ }
1160
+
1161
+ .rendered-markdown .studio-copy-block-btn:active {
1162
+ transform: translateY(1px);
1163
+ }
1164
+
1165
+ .rendered-markdown .studio-copyable-block:hover > .studio-copy-block-btn,
1166
+ .rendered-markdown .studio-copy-block-btn:focus-visible {
1167
+ opacity: 1;
1168
+ color: var(--text);
1169
+ border-color: var(--accent-soft-strong);
1170
+ background: var(--panel);
1171
+ }
1172
+
1136
1173
  .rendered-markdown :not(pre) > code {
1137
1174
  background: rgba(127, 127, 127, 0.13);
1138
1175
  border: 1px solid var(--md-codeblock-border);
@@ -2226,7 +2263,8 @@
2226
2263
  background: var(--panel-2);
2227
2264
  }
2228
2265
 
2229
- .review-note-delete-btn:not(:disabled) {
2266
+ .review-note-delete-btn:not(:disabled),
2267
+ #reviewNotesDeleteAllBtn:not(:disabled) {
2230
2268
  border-color: var(--warn-border);
2231
2269
  color: var(--warn);
2232
2270
  }
package/index.ts CHANGED
@@ -4078,6 +4078,86 @@ async function preprocessStudioMermaidForPdf(markdown: string, workDir: string):
4078
4078
  };
4079
4079
  }
4080
4080
 
4081
+ interface StudioClipboardCommand {
4082
+ command: string;
4083
+ args: string[];
4084
+ label: string;
4085
+ }
4086
+
4087
+ function getStudioClipboardCommands(): StudioClipboardCommand[] {
4088
+ if (process.platform === "darwin") {
4089
+ return [{ command: "pbcopy", args: [], label: "pbcopy" }];
4090
+ }
4091
+ if (process.platform === "win32") {
4092
+ return [{ command: "cmd.exe", args: ["/c", "clip"], label: "clip" }];
4093
+ }
4094
+ return [
4095
+ { command: "wl-copy", args: [], label: "wl-copy" },
4096
+ { command: "xclip", args: ["-selection", "clipboard"], label: "xclip" },
4097
+ { command: "xsel", args: ["--clipboard", "--input"], label: "xsel" },
4098
+ ];
4099
+ }
4100
+
4101
+ function writeStudioClipboardWithCommand(spec: StudioClipboardCommand, text: string): Promise<void> {
4102
+ return new Promise((resolve, reject) => {
4103
+ const child = spawn(spec.command, spec.args, { stdio: ["pipe", "ignore", "pipe"] });
4104
+ const stderrChunks: Buffer[] = [];
4105
+ let settled = false;
4106
+ const timer = setTimeout(() => {
4107
+ if (settled) return;
4108
+ settled = true;
4109
+ try {
4110
+ child.kill();
4111
+ } catch {
4112
+ // Ignore kill failures.
4113
+ }
4114
+ reject(new Error(`${spec.label} timed out.`));
4115
+ }, 3000);
4116
+
4117
+ const fail = (error: Error) => {
4118
+ if (settled) return;
4119
+ settled = true;
4120
+ clearTimeout(timer);
4121
+ reject(error);
4122
+ };
4123
+
4124
+ child.stderr.on("data", (chunk: Buffer | string) => {
4125
+ stderrChunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
4126
+ });
4127
+
4128
+ child.once("error", (error) => {
4129
+ fail(error instanceof Error ? error : new Error(String(error)));
4130
+ });
4131
+
4132
+ child.once("close", (code) => {
4133
+ if (settled) return;
4134
+ settled = true;
4135
+ clearTimeout(timer);
4136
+ if (code === 0) {
4137
+ resolve();
4138
+ return;
4139
+ }
4140
+ const stderr = Buffer.concat(stderrChunks).toString("utf-8").trim();
4141
+ reject(new Error(`${spec.label} exited with code ${code}${stderr ? `: ${stderr}` : ""}`));
4142
+ });
4143
+
4144
+ child.stdin.end(text, "utf-8");
4145
+ });
4146
+ }
4147
+
4148
+ async function writeStudioSystemClipboard(text: string): Promise<{ ok: true; method: string } | { ok: false; error: string }> {
4149
+ const errors: string[] = [];
4150
+ for (const spec of getStudioClipboardCommands()) {
4151
+ try {
4152
+ await writeStudioClipboardWithCommand(spec, text);
4153
+ return { ok: true, method: spec.label };
4154
+ } catch (error) {
4155
+ errors.push(`${spec.label}: ${error instanceof Error ? error.message : String(error)}`);
4156
+ }
4157
+ }
4158
+ return { ok: false, error: errors.join("; ") || "No system clipboard command is available." };
4159
+ }
4160
+
4081
4161
  async function renderStudioMarkdownWithPandoc(markdown: string, isLatex?: boolean, resourcePath?: string, sourcePath?: string): Promise<string> {
4082
4162
  const pandocCommand = process.env.PANDOC_PATH?.trim() || "pandoc";
4083
4163
  const markdownWithoutHtmlComments = isLatex ? markdown : stripStudioMarkdownHtmlComments(markdown);
@@ -6182,6 +6262,7 @@ ${cssVarsBlock}
6182
6262
  <div class="scratchpad-actions">
6183
6263
  <button id="reviewNotesAddBtn" type="button" title="Create a new local comment on the current editor line.">Line comment</button>
6184
6264
  <button id="reviewNotesInlineAllBtn" type="button" title="Toggle inline annotations for all non-empty comments.">All inline: Off</button>
6265
+ <button id="reviewNotesDeleteAllBtn" type="button" title="Delete all local comments for this document or draft.">Delete all</button>
6185
6266
  <button id="reviewNotesDoneBtn" type="button" title="Hide the comments rail.">Hide</button>
6186
6267
  </div>
6187
6268
  </div>
@@ -7902,6 +7983,49 @@ export default function (pi: ExtensionAPI) {
7902
7983
  respondJson(res, 200, { ok: true });
7903
7984
  };
7904
7985
 
7986
+ const handleClipboardRequest = async (req: IncomingMessage, res: ServerResponse) => {
7987
+ const method = (req.method ?? "GET").toUpperCase();
7988
+ if (method !== "POST") {
7989
+ res.setHeader("Allow", "POST");
7990
+ respondJson(res, 405, { ok: false, error: "Method not allowed. Use POST." });
7991
+ return;
7992
+ }
7993
+
7994
+ let rawBody = "";
7995
+ try {
7996
+ rawBody = await readRequestBody(req, REQUEST_BODY_MAX_BYTES);
7997
+ } catch (error) {
7998
+ const message = error instanceof Error ? error.message : String(error);
7999
+ const status = message.includes("exceeds") ? 413 : 400;
8000
+ respondJson(res, status, { ok: false, error: message });
8001
+ return;
8002
+ }
8003
+
8004
+ let parsedBody: unknown;
8005
+ try {
8006
+ parsedBody = rawBody ? JSON.parse(rawBody) : {};
8007
+ } catch {
8008
+ respondJson(res, 400, { ok: false, error: "Invalid JSON body." });
8009
+ return;
8010
+ }
8011
+
8012
+ const text =
8013
+ parsedBody && typeof parsedBody === "object" && typeof (parsedBody as { text?: unknown }).text === "string"
8014
+ ? (parsedBody as { text: string }).text
8015
+ : null;
8016
+ if (text === null) {
8017
+ respondJson(res, 400, { ok: false, error: "Missing clipboard text in request body." });
8018
+ return;
8019
+ }
8020
+
8021
+ const result = await writeStudioSystemClipboard(text);
8022
+ if (result.ok) {
8023
+ respondJson(res, 200, { ok: true, method: result.method });
8024
+ return;
8025
+ }
8026
+ respondJson(res, 500, { ok: false, error: result.error });
8027
+ };
8028
+
7905
8029
  const handleReviewNotesRequest = async (req: IncomingMessage, res: ServerResponse, requestUrl: URL) => {
7906
8030
  const method = (req.method ?? "GET").toUpperCase();
7907
8031
  if (method === "GET") {
@@ -8240,6 +8364,21 @@ export default function (pi: ExtensionAPI) {
8240
8364
  return;
8241
8365
  }
8242
8366
 
8367
+ if (requestUrl.pathname === "/clipboard") {
8368
+ const token = requestUrl.searchParams.get("token") ?? "";
8369
+ if (token !== serverState.token) {
8370
+ respondJson(res, 403, { ok: false, error: "Invalid or expired studio token. Re-run /studio." });
8371
+ return;
8372
+ }
8373
+ void handleClipboardRequest(req, res).catch((error) => {
8374
+ respondJson(res, 500, {
8375
+ ok: false,
8376
+ error: `Clipboard write failed: ${error instanceof Error ? error.message : String(error)}`,
8377
+ });
8378
+ });
8379
+ return;
8380
+ }
8381
+
8243
8382
  if (requestUrl.pathname === "/render-preview") {
8244
8383
  const token = requestUrl.searchParams.get("token") ?? "";
8245
8384
  if (token !== serverState.token) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-studio",
3
- "version": "0.5.57",
3
+ "version": "0.5.58",
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",