pi-studio 0.5.36 → 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/CHANGELOG.md CHANGED
@@ -4,6 +4,14 @@ All notable changes to `pi-studio` are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.5.37] — 2026-03-29
8
+
9
+ ### Added
10
+ - Studio now includes a local persistent scratchpad for parking quick thoughts while you work. The scratchpad opens as an integrated modal, keeps its contents after closing, and provides copy / clear / insert-into-editor actions.
11
+
12
+ ### Changed
13
+ - Scratchpad UI text and actions now make the persistence semantics explicit: closing keeps the current notes unless you actively clear them.
14
+
7
15
  ## [0.5.36] — 2026-03-28
8
16
 
9
17
  ### Changed
package/README.md CHANGED
@@ -16,6 +16,7 @@ Extension for [pi](https://pi.dev) that opens a local two-pane browser workspace
16
16
 
17
17
  - Opens a two-pane browser workspace: **Editor** (left) + **Response/Thinking/Editor Preview** (right)
18
18
  - Runs editor text directly, or asks for structured critique (auto/writing/code focus)
19
+ - Includes a local persistent scratchpad for quick notes you want to keep out of the main editor until you're ready to copy or insert them
19
20
  - Browses response history (`Prev/Next/Last`) and loads either:
20
21
  - response text
21
22
  - critique notes/full critique
@@ -88,6 +88,16 @@
88
88
  const compactBtn = document.getElementById("compactBtn");
89
89
  const leftFocusBtn = document.getElementById("leftFocusBtn");
90
90
  const rightFocusBtn = document.getElementById("rightFocusBtn");
91
+ const scratchpadBtn = document.getElementById("scratchpadBtn");
92
+ const scratchpadOverlayEl = document.getElementById("scratchpadOverlay");
93
+ const scratchpadDialogEl = document.getElementById("scratchpadDialog");
94
+ const scratchpadTextEl = document.getElementById("scratchpadText");
95
+ const scratchpadMetaEl = document.getElementById("scratchpadMeta");
96
+ const scratchpadInsertBtn = document.getElementById("scratchpadInsertBtn");
97
+ const scratchpadCopyBtn = document.getElementById("scratchpadCopyBtn");
98
+ const scratchpadClearBtn = document.getElementById("scratchpadClearBtn");
99
+ const scratchpadCloseBtn = document.getElementById("scratchpadCloseBtn");
100
+ const scratchpadDoneBtn = document.getElementById("scratchpadDoneBtn");
91
101
 
92
102
  const initialSourceState = {
93
103
  source: (document.body && document.body.dataset && document.body.dataset.initialSource) || "blank",
@@ -227,6 +237,7 @@
227
237
  const RESPONSE_HIGHLIGHT_MAX_CHARS = 120_000;
228
238
  const RESPONSE_HIGHLIGHT_STORAGE_KEY = "piStudio.responseHighlightEnabled";
229
239
  const ANNOTATION_MODE_STORAGE_KEY = "piStudio.annotationsEnabled";
240
+ const SCRATCHPAD_STORAGE_KEY = "piStudio.scratchpad";
230
241
  const PREVIEW_INPUT_DEBOUNCE_MS = 0;
231
242
  const PREVIEW_PENDING_BADGE_DELAY_MS = 220;
232
243
  const previewPendingTimers = new WeakMap();
@@ -241,6 +252,8 @@
241
252
  let responseHighlightEnabled = false;
242
253
  let editorHighlightRenderRaf = null;
243
254
  let annotationsEnabled = true;
255
+ let scratchpadText = "";
256
+ let scratchpadReturnFocusEl = null;
244
257
  const PREVIEW_ANNOTATION_PLACEHOLDER_PREFIX = "PISTUDIOANNOT";
245
258
  const annotationHelpers = globalThis.PiStudioAnnotationHelpers;
246
259
  if (!annotationHelpers || typeof annotationHelpers.collectInlineAnnotationMarkers !== "function") {
@@ -864,6 +877,28 @@
864
877
  if (!event || event.defaultPrevented) return;
865
878
 
866
879
  const key = typeof event.key === "string" ? event.key : "";
880
+ const plainEscape = key === "Escape"
881
+ && !event.metaKey
882
+ && !event.ctrlKey
883
+ && !event.altKey
884
+ && !event.shiftKey;
885
+ const scratchpadOwnsEvent = Boolean(
886
+ scratchpadDialogEl
887
+ && event.target
888
+ && typeof scratchpadDialogEl.contains === "function"
889
+ && scratchpadDialogEl.contains(event.target)
890
+ );
891
+
892
+ if (isScratchpadOpen() && plainEscape) {
893
+ event.preventDefault();
894
+ closeScratchpad();
895
+ return;
896
+ }
897
+
898
+ if (scratchpadOwnsEvent) {
899
+ return;
900
+ }
901
+
867
902
  const isToggleShortcut =
868
903
  (key === "Escape" && (event.metaKey || event.ctrlKey))
869
904
  || key === "F10";
@@ -874,13 +909,7 @@
874
909
  return;
875
910
  }
876
911
 
877
- if (
878
- key === "Escape"
879
- && !event.metaKey
880
- && !event.ctrlKey
881
- && !event.altKey
882
- && !event.shiftKey
883
- ) {
912
+ if (plainEscape) {
884
913
  const activeKind = getAbortablePendingKind();
885
914
  if (activeKind === "direct" || activeKind === "critique") {
886
915
  event.preventDefault();
@@ -3209,6 +3238,126 @@
3209
3238
  persistStoredToggle(ANNOTATION_MODE_STORAGE_KEY, enabled);
3210
3239
  }
3211
3240
 
3241
+ function readStoredText(storageKey) {
3242
+ if (!window.localStorage) return null;
3243
+ try {
3244
+ const value = window.localStorage.getItem(storageKey);
3245
+ return typeof value === "string" ? value : null;
3246
+ } catch {
3247
+ return null;
3248
+ }
3249
+ }
3250
+
3251
+ function persistStoredText(storageKey, value) {
3252
+ if (!window.localStorage) return;
3253
+ try {
3254
+ window.localStorage.setItem(storageKey, String(value ?? ""));
3255
+ } catch {
3256
+ // ignore storage failures
3257
+ }
3258
+ }
3259
+
3260
+ function isScratchpadOpen() {
3261
+ return Boolean(scratchpadOverlayEl && !scratchpadOverlayEl.hidden);
3262
+ }
3263
+
3264
+ function readStoredScratchpadText() {
3265
+ return readStoredText(SCRATCHPAD_STORAGE_KEY);
3266
+ }
3267
+
3268
+ function persistScratchpadText(value) {
3269
+ persistStoredText(SCRATCHPAD_STORAGE_KEY, value);
3270
+ }
3271
+
3272
+ function updateScratchpadUi() {
3273
+ const normalized = String(scratchpadText || "");
3274
+ const hasContent = Boolean(normalized.trim());
3275
+ if (scratchpadBtn) {
3276
+ scratchpadBtn.textContent = hasContent ? "Scratchpad •" : "Scratchpad";
3277
+ scratchpadBtn.classList.toggle("has-content", hasContent);
3278
+ scratchpadBtn.title = hasContent
3279
+ ? "Open your local persistent scratchpad. Current notes persist after closing until you edit or clear them."
3280
+ : "Open a local persistent scratchpad for quick notes. Anything you type will persist after closing until you edit or clear it.";
3281
+ }
3282
+ if (scratchpadMetaEl) {
3283
+ scratchpadMetaEl.textContent = hasContent
3284
+ ? "Saved locally · persists after close · " + normalized.length + " chars"
3285
+ : "Empty · local only";
3286
+ }
3287
+ if (scratchpadInsertBtn) scratchpadInsertBtn.disabled = !hasContent;
3288
+ if (scratchpadCopyBtn) scratchpadCopyBtn.disabled = !hasContent;
3289
+ if (scratchpadClearBtn) scratchpadClearBtn.disabled = !normalized.length;
3290
+ }
3291
+
3292
+ function setScratchpadText(nextText, options) {
3293
+ scratchpadText = String(nextText || "");
3294
+ if (scratchpadTextEl && scratchpadTextEl.value !== scratchpadText) {
3295
+ scratchpadTextEl.value = scratchpadText;
3296
+ }
3297
+ if (!options || options.persist !== false) {
3298
+ persistScratchpadText(scratchpadText);
3299
+ }
3300
+ updateScratchpadUi();
3301
+ }
3302
+
3303
+ function closeScratchpad(options) {
3304
+ if (!scratchpadOverlayEl || scratchpadOverlayEl.hidden) return;
3305
+ scratchpadOverlayEl.hidden = true;
3306
+ document.body.classList.remove("scratchpad-open");
3307
+ const focusTarget = options && Object.prototype.hasOwnProperty.call(options, "focusTarget")
3308
+ ? options.focusTarget
3309
+ : (scratchpadReturnFocusEl || scratchpadBtn || sourceTextEl);
3310
+ scratchpadReturnFocusEl = null;
3311
+ if (focusTarget && typeof focusTarget.focus === "function") {
3312
+ const schedule = typeof window.requestAnimationFrame === "function"
3313
+ ? window.requestAnimationFrame.bind(window)
3314
+ : (cb) => window.setTimeout(cb, 16);
3315
+ schedule(() => focusTarget.focus());
3316
+ }
3317
+ }
3318
+
3319
+ function openScratchpad() {
3320
+ if (!scratchpadOverlayEl) return;
3321
+ scratchpadReturnFocusEl = document.activeElement && document.activeElement !== document.body
3322
+ ? document.activeElement
3323
+ : sourceTextEl;
3324
+ scratchpadOverlayEl.hidden = false;
3325
+ document.body.classList.add("scratchpad-open");
3326
+ if (scratchpadTextEl && typeof scratchpadTextEl.focus === "function") {
3327
+ const schedule = typeof window.requestAnimationFrame === "function"
3328
+ ? window.requestAnimationFrame.bind(window)
3329
+ : (cb) => window.setTimeout(cb, 16);
3330
+ schedule(() => {
3331
+ scratchpadTextEl.focus();
3332
+ if (typeof scratchpadTextEl.selectionStart === "number") {
3333
+ const end = scratchpadTextEl.value.length;
3334
+ scratchpadTextEl.setSelectionRange(end, end);
3335
+ }
3336
+ });
3337
+ }
3338
+ }
3339
+
3340
+ function insertScratchpadIntoEditor() {
3341
+ const content = String(scratchpadText || "");
3342
+ if (!content.trim()) {
3343
+ setStatus("Scratchpad is empty.", "warning");
3344
+ return;
3345
+ }
3346
+
3347
+ const current = sourceTextEl.value || "";
3348
+ const start = typeof sourceTextEl.selectionStart === "number" ? sourceTextEl.selectionStart : current.length;
3349
+ const end = typeof sourceTextEl.selectionEnd === "number" ? sourceTextEl.selectionEnd : start;
3350
+ const safeStart = Math.max(0, Math.min(start, current.length));
3351
+ const safeEnd = Math.max(safeStart, Math.min(end, current.length));
3352
+ const next = current.slice(0, safeStart) + content + current.slice(safeEnd);
3353
+ setEditorText(next, { preserveScroll: false, preserveSelection: false });
3354
+ const caret = safeStart + content.length;
3355
+ sourceTextEl.setSelectionRange(caret, caret);
3356
+ setActivePane("left");
3357
+ closeScratchpad({ focusTarget: sourceTextEl });
3358
+ setStatus("Inserted scratchpad into editor.", "success");
3359
+ }
3360
+
3212
3361
  function updateEditorHighlightState() {
3213
3362
  const enabled = editorHighlightEnabled && editorView === "markdown";
3214
3363
 
@@ -4715,6 +4864,71 @@
4715
4864
  }
4716
4865
  });
4717
4866
 
4867
+ if (scratchpadBtn) {
4868
+ scratchpadBtn.addEventListener("click", () => {
4869
+ openScratchpad();
4870
+ });
4871
+ }
4872
+
4873
+ if (scratchpadCloseBtn) {
4874
+ scratchpadCloseBtn.addEventListener("click", () => {
4875
+ closeScratchpad();
4876
+ });
4877
+ }
4878
+
4879
+ if (scratchpadDoneBtn) {
4880
+ scratchpadDoneBtn.addEventListener("click", () => {
4881
+ closeScratchpad();
4882
+ });
4883
+ }
4884
+
4885
+ if (scratchpadOverlayEl) {
4886
+ scratchpadOverlayEl.addEventListener("click", (event) => {
4887
+ if (event.target === scratchpadOverlayEl) {
4888
+ closeScratchpad();
4889
+ }
4890
+ });
4891
+ }
4892
+
4893
+ if (scratchpadTextEl) {
4894
+ scratchpadTextEl.addEventListener("input", () => {
4895
+ setScratchpadText(scratchpadTextEl.value);
4896
+ });
4897
+ }
4898
+
4899
+ if (scratchpadInsertBtn) {
4900
+ scratchpadInsertBtn.addEventListener("click", () => {
4901
+ insertScratchpadIntoEditor();
4902
+ });
4903
+ }
4904
+
4905
+ if (scratchpadCopyBtn) {
4906
+ scratchpadCopyBtn.addEventListener("click", async () => {
4907
+ if (!String(scratchpadText || "").trim()) {
4908
+ setStatus("Scratchpad is empty.", "warning");
4909
+ return;
4910
+ }
4911
+
4912
+ try {
4913
+ await navigator.clipboard.writeText(String(scratchpadText || ""));
4914
+ setStatus("Copied scratchpad text.", "success");
4915
+ } catch (error) {
4916
+ setStatus("Clipboard write failed.", "warning");
4917
+ }
4918
+ });
4919
+ }
4920
+
4921
+ if (scratchpadClearBtn) {
4922
+ scratchpadClearBtn.addEventListener("click", () => {
4923
+ if (!String(scratchpadText || "").length) return;
4924
+ const confirmed = window.confirm("Clear scratchpad text?");
4925
+ if (!confirmed) return;
4926
+ setScratchpadText("");
4927
+ if (scratchpadTextEl) scratchpadTextEl.focus();
4928
+ setStatus("Cleared scratchpad.", "success");
4929
+ });
4930
+ }
4931
+
4718
4932
  if (saveAnnotatedBtn) {
4719
4933
  saveAnnotatedBtn.addEventListener("click", () => {
4720
4934
  const content = sourceTextEl.value;
@@ -4853,6 +5067,7 @@
4853
5067
  refreshResponseUi();
4854
5068
  updateAnnotatedReplyHeaderButton();
4855
5069
  setActivePane("left");
5070
+ setScratchpadText(readStoredScratchpadText() || "", { persist: false });
4856
5071
 
4857
5072
  const storedEditorHighlightEnabled = readStoredEditorHighlightEnabled();
4858
5073
  const initialHighlightEnabled = storedEditorHighlightEnabled ?? Boolean(highlightSelect && highlightSelect.value === "on");
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;
@@ -1339,6 +1345,125 @@
1339
1345
  background: var(--panel);
1340
1346
  }
1341
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
+
1342
1467
  #status.error { color: var(--error); }
1343
1468
  #status.warning { color: var(--warn); }
1344
1469
  #status.success { color: var(--ok); }
package/index.ts CHANGED
@@ -5740,6 +5740,7 @@ ${cssVarsBlock}
5740
5740
  </div>
5741
5741
  <div class="section-header-actions">
5742
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>
5743
5744
  </div>
5744
5745
  </div>
5745
5746
  <div class="source-wrap">
@@ -5875,6 +5876,28 @@ ${cssVarsBlock}
5875
5876
  <span class="shortcut-hint">Focus pane: F10 (or Cmd/Ctrl+Esc) to toggle · Run / queue steering: Cmd/Ctrl+Enter · Stop request: Esc</span>
5876
5877
  </footer>
5877
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
+
5878
5901
  <!-- Defer sanitizer script so studio can boot/connect even if CDN is slow or blocked. -->
5879
5902
  <script defer src="https://cdn.jsdelivr.net/npm/dompurify@3.2.6/dist/purify.min.js"></script>
5880
5903
  <script>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-studio",
3
- "version": "0.5.36",
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",