pi-studio 0.9.57 → 0.9.59

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.
@@ -121,6 +121,7 @@
121
121
  const refreshFromDiskBtn = document.getElementById("refreshFromDiskBtn");
122
122
  const clearWorkspaceBtn = document.getElementById("clearWorkspaceBtn");
123
123
  const sendEditorBtn = document.getElementById("sendEditorBtn");
124
+ const clearPiEditorBtn = document.getElementById("clearPiEditorBtn");
124
125
  const openCompanionBtn = document.getElementById("openCompanionBtn");
125
126
  const getEditorBtn = document.getElementById("getEditorBtn");
126
127
  const studioHeaderEl = document.getElementById("studioHeader");
@@ -148,6 +149,8 @@
148
149
  const stripAnnotationsBtn = document.getElementById("stripAnnotationsBtn");
149
150
  const highlightSelect = document.getElementById("highlightSelect");
150
151
  const lineNumbersSelect = document.getElementById("lineNumbersSelect");
152
+ const studioPaneLayoutSelect = document.getElementById("studioPaneLayoutSelect");
153
+ const activityTrackingSelect = document.getElementById("activityTrackingSelect");
151
154
  const editorFontSizeSelect = document.getElementById("editorFontSizeSelect");
152
155
  const annotationModeSelect = document.getElementById("annotationModeSelect");
153
156
  const compactBtn = document.getElementById("compactBtn");
@@ -359,6 +362,9 @@
359
362
  const pendingSaveOperations = new Map();
360
363
  const pendingCompanionLaunches = new Map();
361
364
  const activeStudioTabLaunches = new Set();
365
+ // Deliberately page-memory-only: never persist Pi input-draft handoff fingerprints.
366
+ const pendingPiEditorDraftSnapshots = new Map();
367
+ let linkedPiEditorDraftSnapshot = null;
362
368
  let sourceOriginSummaryEl = null;
363
369
  let sourceResetOriginBtn = null;
364
370
  let sourceOpenCurrentFileTabBtn = null;
@@ -427,8 +433,8 @@
427
433
  rightViewSelect.title = isWatchedFilePreview
428
434
  ? "Read-only watched preview follows this file on disk."
429
435
  : (isEditorOnlyMode
430
- ? "Editor-only views: Editor Preview, contextual Quarto Preview for .qmd/.md/.markdown files, Changes, Files, REPL, or Side questions. F7 cycles; Cmd/Ctrl+Alt+3/5/6/7/8 switch directly to numbered right-pane views, and Cmd/Ctrl+Alt+F/Q open Files/Side questions."
431
- : "Right pane view mode. F7 cycles, including contextual Quarto Preview for file-backed .qmd, .md, and .markdown documents; Cmd/Ctrl+Alt+1–8 switches directly between the numbered views. Cmd/Ctrl+Alt+P/E/W/F/Q keep mnemonic shortcuts for Preview, Editor Preview, Working, Files, and Side questions.");
436
+ ? "Editor-only views: Editor Preview, contextual Quarto Preview for .qmd/.md/.markdown files, Changes, Files, REPL, or Side questions. F7 cycles; Cmd/Ctrl+Alt+3/5/6/7/8 switch directly to numbered right-pane views, and Cmd/Ctrl+Alt+F/R/Q open Files/REPL/Side questions."
437
+ : "Right pane view mode. F7 cycles, including contextual Quarto Preview for file-backed .qmd, .md, and .markdown documents; Cmd/Ctrl+Alt+1–8 switches directly between the numbered views. Cmd/Ctrl+Alt+P/E/W/F/R/Q keep mnemonic shortcuts for Preview, Editor Preview, Working, Files, REPL, and Side questions.");
432
438
  }
433
439
 
434
440
  function getInitialRightView(source) {
@@ -481,6 +487,8 @@
481
487
  const REPL_TRANSCRIPT_MAX_CHARS = 200_000;
482
488
  const REPL_JOURNAL_OUTPUT_MAX_CHARS = 80_000;
483
489
  const REPL_JOURNAL_MAX_ENTRIES = 80;
490
+ const REPL_QUICK_DRAFT_MAX_CHARS = 20_000;
491
+ const REPL_QUICK_DRAFT_MAX_SESSIONS = 12;
484
492
  const PDF_EXPORT_FETCH_TIMEOUT_MS = 180_000;
485
493
  const HTML_EXPORT_FETCH_TIMEOUT_MS = 180_000;
486
494
  const HTML_ARTIFACT_MATH_RENDER_FETCH_TIMEOUT_MS = 30_000;
@@ -588,6 +596,10 @@
588
596
  let replFollow = true;
589
597
  let replPollTimer = null;
590
598
  let replBusy = false;
599
+ let replPendingRequestId = "";
600
+ const replQuickDrafts = new Map();
601
+ let replQuickPending = null;
602
+ let replQuickFocusRequested = false;
591
603
  let replSendMode = (() => {
592
604
  try {
593
605
  const stored = window.localStorage && window.localStorage.getItem("piStudio.replSendMode");
@@ -622,7 +634,7 @@
622
634
  mode: typeof entry.mode === "string" ? entry.mode : "raw",
623
635
  prose: typeof entry.prose === "string" ? entry.prose : "",
624
636
  code: typeof entry.code === "string" ? entry.code : "",
625
- output: typeof entry.output === "string" ? entry.output : "",
637
+ output: typeof entry.output === "string" ? stripStudioReplSubmissionEcho(entry.output) : "",
626
638
  beforeTranscript: "",
627
639
  status: typeof entry.status === "string" ? entry.status : "sent",
628
640
  skippedChunks: Math.max(0, Math.floor(Number(entry.skippedChunks) || 0)),
@@ -1302,6 +1314,10 @@
1302
1314
  return {
1303
1315
  sessionName,
1304
1316
  target: typeof session.target === "string" ? session.target : (sessionName + ":0.0"),
1317
+ tmuxSessionId: typeof session.tmuxSessionId === "string" ? session.tmuxSessionId : "",
1318
+ tmuxSessionCreatedAt: typeof session.tmuxSessionCreatedAt === "number" && Number.isFinite(session.tmuxSessionCreatedAt)
1319
+ ? session.tmuxSessionCreatedAt
1320
+ : 0,
1305
1321
  runtime: typeof session.runtime === "string" ? session.runtime : "unknown",
1306
1322
  label: typeof session.label === "string" && session.label.trim() ? session.label.trim() : sessionName,
1307
1323
  source: typeof session.source === "string" ? session.source : "tmux",
@@ -1430,6 +1446,63 @@
1430
1446
  : "Display short submissions in full, truncating after 6 lines or 600 characters, with compact anchors and a plain output divider.");
1431
1447
  }
1432
1448
 
1449
+ function getReplQuickDraftKey(session) {
1450
+ const normalized = normalizeReplSession(session);
1451
+ if (!normalized || !normalized.tmuxSessionId || normalized.tmuxSessionCreatedAt <= 0) return "";
1452
+ return [normalizeReplRuntime(normalized.runtime), normalized.tmuxSessionId, normalized.tmuxSessionCreatedAt].join("\u001f");
1453
+ }
1454
+
1455
+ function getReplQuickDraft(session) {
1456
+ const key = getReplQuickDraftKey(session);
1457
+ return key ? String(replQuickDrafts.get(key) || "") : "";
1458
+ }
1459
+
1460
+ function setReplQuickDraft(session, value) {
1461
+ const key = getReplQuickDraftKey(session);
1462
+ if (!key) return "";
1463
+ const draft = String(value || "").slice(0, REPL_QUICK_DRAFT_MAX_CHARS);
1464
+ if (!draft) {
1465
+ replQuickDrafts.delete(key);
1466
+ return "";
1467
+ }
1468
+ if (!replQuickDrafts.has(key) && replQuickDrafts.size >= REPL_QUICK_DRAFT_MAX_SESSIONS) {
1469
+ const oldestKey = replQuickDrafts.keys().next().value;
1470
+ if (oldestKey) replQuickDrafts.delete(oldestKey);
1471
+ }
1472
+ replQuickDrafts.set(key, draft);
1473
+ return draft;
1474
+ }
1475
+
1476
+ function settleReplQuickPending(requestId, submitted) {
1477
+ if (!replQuickPending || !requestId || replQuickPending.requestId !== requestId) return false;
1478
+ const pending = replQuickPending;
1479
+ replQuickPending = null;
1480
+ if (replPendingRequestId === requestId) replPendingRequestId = "";
1481
+ replBusy = Boolean(replPendingRequestId);
1482
+ if (submitted && replQuickDrafts.get(pending.key) === pending.text) {
1483
+ replQuickDrafts.delete(pending.key);
1484
+ }
1485
+ return true;
1486
+ }
1487
+
1488
+ function reconcileReplQuickPending() {
1489
+ if (!replQuickPending) return false;
1490
+ const entry = replJournalEntries.find((candidate) => candidate.requestId === replQuickPending.requestId);
1491
+ if (!entry || entry.status === "sending") return false;
1492
+ return settleReplQuickPending(entry.requestId, entry.status !== "error");
1493
+ }
1494
+
1495
+ function updateReplQuickComposerActions() {
1496
+ if (!critiqueViewEl || rightView !== "repl") return;
1497
+ const session = getActiveReplSessionForCurrentRuntime();
1498
+ const draft = getReplQuickDraft(session);
1499
+ const pending = Boolean(replQuickPending && replQuickPending.key === getReplQuickDraftKey(session));
1500
+ const sendButton = critiqueViewEl.querySelector("[data-repl-action='quick-send']");
1501
+ const clearButton = critiqueViewEl.querySelector("[data-repl-action='quick-clear']");
1502
+ if (sendButton) sendButton.disabled = !session || replTmuxAvailable === false || pending || replBusy || wsState === "Disconnected" || !draft.trim();
1503
+ if (clearButton) clearButton.disabled = pending || !draft;
1504
+ }
1505
+
1433
1506
  function setReplJournalCollapsed(collapsed) {
1434
1507
  replJournalCollapsed = Boolean(collapsed);
1435
1508
  try {
@@ -1460,6 +1533,8 @@
1460
1533
  runtime: session.runtime,
1461
1534
  source: session.source,
1462
1535
  target: session.target,
1536
+ tmuxSessionId: session.tmuxSessionId,
1537
+ tmuxSessionCreatedAt: session.tmuxSessionCreatedAt,
1463
1538
  })));
1464
1539
  }
1465
1540
 
@@ -1579,7 +1654,53 @@
1579
1654
 
1580
1655
  function isReplControlFocused() {
1581
1656
  const activeEl = document.activeElement;
1582
- return activeEl instanceof Element && Boolean(activeEl.closest(".repl-controls"));
1657
+ return activeEl instanceof Element && Boolean(activeEl.closest(".repl-controls, .repl-quick-composer"));
1658
+ }
1659
+
1660
+ function focusReplQuickComposer() {
1661
+ replQuickFocusRequested = true;
1662
+ window.setTimeout(() => {
1663
+ if (rightView !== "repl" || !critiqueViewEl) {
1664
+ replQuickFocusRequested = false;
1665
+ return;
1666
+ }
1667
+ const composer = critiqueViewEl.querySelector("[data-repl-quick-draft]");
1668
+ if (composer instanceof HTMLTextAreaElement) {
1669
+ if (!composer.disabled) {
1670
+ try {
1671
+ composer.focus({ preventScroll: true });
1672
+ } catch {
1673
+ try { composer.focus(); } catch {}
1674
+ }
1675
+ if (document.activeElement === composer) replQuickFocusRequested = false;
1676
+ }
1677
+ if (typeof composer.scrollIntoView === "function") {
1678
+ try {
1679
+ composer.scrollIntoView({ block: "nearest", inline: "nearest" });
1680
+ } catch {
1681
+ try { composer.scrollIntoView(); } catch {}
1682
+ }
1683
+ }
1684
+ }
1685
+ }, 0);
1686
+ }
1687
+
1688
+ function focusReplSessionControls() {
1689
+ if (rightView !== "repl" || !critiqueViewEl) return;
1690
+ critiqueViewEl.scrollTop = 0;
1691
+ const sessionSelect = critiqueViewEl.querySelector("[data-repl-session]");
1692
+ const runtimeSelect = critiqueViewEl.querySelector("[data-repl-runtime]");
1693
+ const target = sessionSelect instanceof HTMLSelectElement && !sessionSelect.disabled
1694
+ ? sessionSelect
1695
+ : runtimeSelect;
1696
+ if (target instanceof HTMLElement && typeof target.focus === "function") {
1697
+ try {
1698
+ target.focus({ preventScroll: true });
1699
+ } catch {
1700
+ try { target.focus(); } catch {}
1701
+ }
1702
+ }
1703
+ setStatus("REPL session controls focused.");
1583
1704
  }
1584
1705
 
1585
1706
  function renderReplViewIfActive(options) {
@@ -1589,6 +1710,7 @@
1589
1710
  traceRenderRaf = window.requestAnimationFrame(() => {
1590
1711
  traceRenderRaf = null;
1591
1712
  refreshResponseUi();
1713
+ if (replQuickFocusRequested) focusReplQuickComposer();
1592
1714
  });
1593
1715
  }
1594
1716
 
@@ -2020,9 +2142,11 @@
2020
2142
  let value = String(delta || "").replace(/^\s+/, "");
2021
2143
  // The raw mirror below remains raw; Studio record cards hide only the
2022
2144
  // temp-file wrapper used to submit multiline snippets safely. Match
2023
- // both legacy long Studio roots and compact private control roots.
2145
+ // both legacy long Studio roots and compact private control roots. Use
2146
+ // the Python wrapper's final double close because IPython may wrap in
2147
+ // the middle of the word `globals` and add continuation prompts.
2024
2148
  const submissionEchoPatterns = [
2025
- /^.*exec\(open\([\s\S]*?(?:pi-studio-re|pi-rc-)[\s\S]*?globals\(\)\)\s*$/gm,
2149
+ /^.*exec\(open\([\s\S]*?(?:pi-studio-re|pi-rc-)[\s\S]*?\)\)\s*$/gm,
2026
2150
  /^.*include\([\s\S]*?(?:pi-studio-re|pi-rc-)[\s\S]*?\.jl"\)\s*$/gm,
2027
2151
  /^.*source\([\s\S]*?(?:pi-studio-re|pi-rc-)[\s\S]*?local\s*=\s*\.GlobalEnv\)\s*$/gm,
2028
2152
  /^.*:script\s+[\s\S]*?(?:pi-studio-re|pi-rc-)[\s\S]*?\.ghci"?\s*$/gm,
@@ -2218,11 +2342,15 @@
2218
2342
  const session = getActiveReplSessionForCurrentRuntime();
2219
2343
  if (!session) {
2220
2344
  setStatus("Start or select a " + getReplRuntimeLabel(replRuntime) + " REPL session first.", "warning");
2221
- return;
2345
+ return null;
2346
+ }
2347
+ if (replBusy) {
2348
+ setStatus("A REPL request is already in progress.", "warning");
2349
+ return null;
2222
2350
  }
2223
2351
  if (!payload || payload.error) {
2224
2352
  setStatus(payload && payload.error ? payload.error : "Nothing to send to REPL.", "warning");
2225
- return;
2353
+ return null;
2226
2354
  }
2227
2355
  if (payload.noteOnly) {
2228
2356
  if (String(payload.prose || "").trim()) {
@@ -2241,12 +2369,12 @@
2241
2369
  } else {
2242
2370
  setStatus("No code or prose found to send.", "warning");
2243
2371
  }
2244
- return;
2372
+ return null;
2245
2373
  }
2246
2374
  const text = String(payload.text || "");
2247
2375
  if (!text.trim()) {
2248
- setStatus("Editor text is empty.", "warning");
2249
- return;
2376
+ setStatus("REPL submission is empty.", "warning");
2377
+ return null;
2250
2378
  }
2251
2379
  const requestId = makeRequestId();
2252
2380
  const journalEntry = addReplJournalEntry({
@@ -2262,6 +2390,7 @@
2262
2390
  skippedChunks: payload.skippedChunks,
2263
2391
  });
2264
2392
  activeReplJournalEntryId = journalEntry.id;
2393
+ replPendingRequestId = requestId;
2265
2394
  replBusy = true;
2266
2395
  syncActionButtons();
2267
2396
  renderReplViewIfActive({ force: true });
@@ -2280,11 +2409,15 @@
2280
2409
  prose: journalEntry.prose,
2281
2410
  skippedChunks: journalEntry.skippedChunks,
2282
2411
  })) {
2283
- replBusy = false;
2412
+ if (replPendingRequestId === requestId) replPendingRequestId = "";
2413
+ replBusy = Boolean(replPendingRequestId);
2284
2414
  replJournalEntries = replJournalEntries.map((entry) => entry.id === journalEntry.id ? { ...entry, status: "error" } : entry);
2285
2415
  persistReplJournalEntries();
2286
2416
  syncActionButtons();
2417
+ renderReplViewIfActive({ force: true });
2418
+ return null;
2287
2419
  }
2420
+ return requestId;
2288
2421
  }
2289
2422
 
2290
2423
  function sendEditorTextToRepl(options) {
@@ -2300,6 +2433,36 @@
2300
2433
  sendReplPayload(replSendMode === "literate" ? buildLiterateReplSendPayload() : buildRawReplSendPayload());
2301
2434
  }
2302
2435
 
2436
+ function submitReplQuickDraft() {
2437
+ const session = getActiveReplSessionForCurrentRuntime();
2438
+ if (!session) {
2439
+ setStatus("Start or select a " + getReplRuntimeLabel(replRuntime) + " REPL session first.", "warning");
2440
+ return false;
2441
+ }
2442
+ const key = getReplQuickDraftKey(session);
2443
+ const text = getReplQuickDraft(session);
2444
+ if (!text.trim()) {
2445
+ setStatus("Quick send is empty.", "warning");
2446
+ return false;
2447
+ }
2448
+ if (replQuickPending && replQuickPending.key === key) {
2449
+ setStatus("Quick send is already being submitted to this REPL.", "warning");
2450
+ return false;
2451
+ }
2452
+ const requestId = sendReplPayload({
2453
+ text,
2454
+ prose: "",
2455
+ label: "quick send",
2456
+ mode: "raw",
2457
+ noteOnly: false,
2458
+ skippedChunks: 0,
2459
+ });
2460
+ if (!requestId) return false;
2461
+ replQuickPending = { requestId, key, text };
2462
+ renderReplViewIfActive({ force: true });
2463
+ return true;
2464
+ }
2465
+
2303
2466
  function renderTraceViewIfActive() {
2304
2467
  if (rightView !== "trace") return;
2305
2468
  if (traceRenderRaf !== null) return;
@@ -2341,9 +2504,15 @@
2341
2504
  let paneFocusTarget = initialPaneFocusTarget;
2342
2505
  let paneSplitPercent = 50;
2343
2506
  const PANE_SPLIT_STORAGE_KEY = "piStudio.paneSplitPercent";
2507
+ const PANE_LAYOUT_STORAGE_KEY = "piStudio.paneLayout";
2508
+ const ACTIVITY_TRACKING_STORAGE_KEY = "piStudio.trackActivity";
2344
2509
  const PANE_SPLIT_MIN_PERCENT = 20;
2345
2510
  const PANE_SPLIT_MAX_PERCENT = 80;
2346
2511
  const PANE_SPLIT_SNAP_TO_CENTER_PERCENT = 1;
2512
+ let studioPaneLayout = readStudioPaneLayout();
2513
+ let activityTrackingEnabled = readActivityTrackingEnabled();
2514
+ let activityTrackingOwnsWorkingView = false;
2515
+ let activityTrackingRequestId = "";
2347
2516
  const STUDIO_WORKSPACE_MAX_TEXT_CHARS = 900_000;
2348
2517
  const STUDIO_WORKSPACE_PERSIST_INTERVAL_MS = 300;
2349
2518
  const STUDIO_WORKSPACE_RECOVERY_FETCH_TIMEOUT_MS = 1_500;
@@ -2879,7 +3048,11 @@
2879
3048
  : "Off";
2880
3049
  const lineLabel = lineNumbersEnabled ? "Lines on" : "Lines off";
2881
3050
  const editorSizeLabel = formatStudioFontSizeLabel(editorFontSize);
2882
- setStudioUiRefreshButtonText(studioUiRefreshUi.viewButton, "View: " + syntaxLabel + " · " + lineLabel + " · " + editorSizeLabel);
3051
+ const extras = [];
3052
+ if (studioPaneLayout === "editor-top") extras.push("Editor above");
3053
+ if (studioPaneLayout === "response-top") extras.push("Response above");
3054
+ if (activityTrackingEnabled) extras.push("Following activity");
3055
+ setStudioUiRefreshButtonText(studioUiRefreshUi.viewButton, "View: " + syntaxLabel + " · " + lineLabel + " · " + editorSizeLabel + (extras.length ? " · " + extras.join(" · ") : ""));
2883
3056
  }
2884
3057
  syncStudioUiRefreshReviewTrigger();
2885
3058
  }
@@ -3017,7 +3190,9 @@
3017
3190
  if (sendEditorBtn) sendEditorBtn.textContent = "Send current text to Pi editor";
3018
3191
  appendStudioUiRefreshMenuSection(contextMenu.menu, "Document", [sourceOriginSummaryEl, sourceResetOriginBtn, sourceOpenCurrentFileTabBtn, sourceOpenCurrentTextCopyTabBtn]);
3019
3192
  appendStudioUiRefreshMenuSection(contextMenu.menu, "Working directory", [resourceDirBtn, resourceDirLabel, resourceDirInputWrap]);
3020
- if (!isEditorOnlyMode && sendEditorBtn) appendStudioUiRefreshMenuSection(contextMenu.menu, "Pi editor", [sendEditorBtn]);
3193
+ if (!isEditorOnlyMode) {
3194
+ appendStudioUiRefreshMenuSection(contextMenu.menu, "Pi editor", [sendEditorBtn, clearPiEditorBtn]);
3195
+ }
3021
3196
  const cursorContextBtn = makeStudioUiRefreshElement("button", "completion-context-option", "Editor only");
3022
3197
  cursorContextBtn.type = "button";
3023
3198
  cursorContextBtn.setAttribute("data-completion-context-mode", "cursor");
@@ -3150,6 +3325,8 @@
3150
3325
  const viewButton = makeStudioUiRefreshElement("button", "", "View");
3151
3326
  const viewMenu = makeStudioUiRefreshMenu(viewButton, "view", "studio-refresh-view-anchor");
3152
3327
  appendStudioUiRefreshMenuSection(viewMenu.menu, "Display", [highlightSelect, lineNumbersSelect, editorFontSizeSelect]);
3328
+ if (activityTrackingSelect) activityTrackingSelect.hidden = isEditorOnlyMode || isWatchedFilePreview;
3329
+ appendStudioUiRefreshMenuSection(viewMenu.menu, "Workspace", [studioPaneLayoutSelect, (isEditorOnlyMode || isWatchedFilePreview) ? null : activityTrackingSelect]);
3153
3330
  stateEl.appendChild(annotationsMenu.anchor);
3154
3331
  stateEl.appendChild(viewMenu.anchor);
3155
3332
 
@@ -3374,6 +3551,7 @@
3374
3551
  if (kind === "compact") return "compacting context";
3375
3552
  if (kind === "send_to_editor") return "sending to pi editor";
3376
3553
  if (kind === "get_from_editor") return "loading from pi editor";
3554
+ if (kind === "clear_pi_editor") return "clearing Pi editor text";
3377
3555
  if (kind === "load_git_diff") return "loading git diff";
3378
3556
  if (kind === "open_editor_only") return "opening companion editor";
3379
3557
  if (kind === "refresh_from_disk") return "refreshing from disk";
@@ -3632,6 +3810,7 @@
3632
3810
  if (kind === "compact") return "Compacting…";
3633
3811
  if (kind === "send_to_editor") return "Sending to editor…";
3634
3812
  if (kind === "get_from_editor") return "Loading from editor…";
3813
+ if (kind === "clear_pi_editor") return "Clearing Pi editor text…";
3635
3814
  if (kind === "load_git_diff") return "Loading git diff…";
3636
3815
  if (kind === "refresh_from_disk") return "Refreshing from disk…";
3637
3816
  if (kind === "save_as" || kind === "save_over") return "Saving…";
@@ -4293,6 +4472,58 @@
4293
4472
  return /^sha256:[a-f0-9]{64}$/.test(revision) ? revision : null;
4294
4473
  }
4295
4474
 
4475
+ function normalizePiEditorDraftSnapshot(value) {
4476
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
4477
+ const fingerprint = normalizeStudioDiskRevision(value.fingerprint);
4478
+ const byteLength = value.byteLength;
4479
+ if (!fingerprint || typeof byteLength !== "number" || !Number.isSafeInteger(byteLength) || byteLength < 0) return null;
4480
+ return { fingerprint, byteLength };
4481
+ }
4482
+
4483
+ function piEditorDraftSnapshotsMatch(left, right) {
4484
+ const normalizedLeft = normalizePiEditorDraftSnapshot(left);
4485
+ const normalizedRight = normalizePiEditorDraftSnapshot(right);
4486
+ return Boolean(
4487
+ normalizedLeft
4488
+ && normalizedRight
4489
+ && normalizedLeft.fingerprint === normalizedRight.fingerprint
4490
+ && normalizedLeft.byteLength === normalizedRight.byteLength
4491
+ );
4492
+ }
4493
+
4494
+ function setLinkedPiEditorDraftSnapshot(value, hasContent) {
4495
+ linkedPiEditorDraftSnapshot = hasContent ? normalizePiEditorDraftSnapshot(value) : null;
4496
+ }
4497
+
4498
+ function clearLinkedPiEditorDraftSnapshot() {
4499
+ linkedPiEditorDraftSnapshot = null;
4500
+ }
4501
+
4502
+ function reserveLinkedPiEditorDraftSnapshot(requestId) {
4503
+ const snapshot = normalizePiEditorDraftSnapshot(linkedPiEditorDraftSnapshot);
4504
+ if (!snapshot || typeof requestId !== "string" || !requestId) return null;
4505
+ linkedPiEditorDraftSnapshot = null;
4506
+ pendingPiEditorDraftSnapshots.set(requestId, snapshot);
4507
+ return snapshot;
4508
+ }
4509
+
4510
+ function restoreReservedPiEditorDraftSnapshot(requestId) {
4511
+ if (typeof requestId !== "string" || !requestId) return;
4512
+ const snapshot = pendingPiEditorDraftSnapshots.get(requestId);
4513
+ if (!snapshot) return;
4514
+ pendingPiEditorDraftSnapshots.delete(requestId);
4515
+ if (!linkedPiEditorDraftSnapshot) linkedPiEditorDraftSnapshot = snapshot;
4516
+ }
4517
+
4518
+ function settleReservedPiEditorDraftSnapshot(requestId, reportedSnapshot) {
4519
+ if (typeof requestId !== "string" || !requestId) return false;
4520
+ const snapshot = pendingPiEditorDraftSnapshots.get(requestId);
4521
+ if (!snapshot) return false;
4522
+ if (reportedSnapshot && !piEditorDraftSnapshotsMatch(snapshot, reportedSnapshot)) return false;
4523
+ pendingPiEditorDraftSnapshots.delete(requestId);
4524
+ return true;
4525
+ }
4526
+
4296
4527
  function markFileBackedBaseline(text, diskRevision) {
4297
4528
  fileBackedBaselineText = String(text || "");
4298
4529
  fileBackedDiskRevision = normalizeStudioDiskRevision(diskRevision);
@@ -4390,6 +4621,127 @@
4390
4621
  setStatus(descriptor.fileBacked ? "Detached editor from file origin into a new draft." : "Reset editor origin to a new draft.", "success");
4391
4622
  }
4392
4623
 
4624
+ function normalizeStudioPaneLayout(value) {
4625
+ const normalized = String(value || "").trim().toLowerCase();
4626
+ return normalized === "editor-top" || normalized === "response-top" ? normalized : "side-by-side";
4627
+ }
4628
+
4629
+ function readStudioPaneLayout() {
4630
+ try {
4631
+ return normalizeStudioPaneLayout(window.localStorage && window.localStorage.getItem(PANE_LAYOUT_STORAGE_KEY));
4632
+ } catch {
4633
+ return "side-by-side";
4634
+ }
4635
+ }
4636
+
4637
+ function readActivityTrackingEnabled() {
4638
+ try {
4639
+ return Boolean(window.localStorage && window.localStorage.getItem(ACTIVITY_TRACKING_STORAGE_KEY) === "on");
4640
+ } catch {
4641
+ return false;
4642
+ }
4643
+ }
4644
+
4645
+ function isStackedStudioPaneLayout() {
4646
+ return studioPaneLayout === "editor-top" || studioPaneLayout === "response-top";
4647
+ }
4648
+
4649
+ function isResponseFirstStudioPaneLayout() {
4650
+ return studioPaneLayout === "response-top";
4651
+ }
4652
+
4653
+ function getStudioPaneLayoutLabel(value) {
4654
+ if (value === "editor-top") return "Editor above response";
4655
+ if (value === "response-top") return "Response above editor";
4656
+ return "Side by side";
4657
+ }
4658
+
4659
+ function setStudioPaneLayout(value, options) {
4660
+ studioPaneLayout = normalizeStudioPaneLayout(value);
4661
+ document.body.dataset.studioLayout = studioPaneLayout;
4662
+ if (studioPaneLayoutSelect) studioPaneLayoutSelect.value = studioPaneLayout;
4663
+ const mainEl = paneResizeHandleEl && typeof paneResizeHandleEl.closest === "function"
4664
+ ? paneResizeHandleEl.closest("main")
4665
+ : null;
4666
+ if (mainEl && leftPaneEl && rightPaneEl && paneResizeHandleEl) {
4667
+ if (isResponseFirstStudioPaneLayout()) {
4668
+ mainEl.append(rightPaneEl, paneResizeHandleEl, leftPaneEl);
4669
+ } else {
4670
+ mainEl.append(leftPaneEl, paneResizeHandleEl, rightPaneEl);
4671
+ }
4672
+ }
4673
+ applyPaneSplitPercent(paneSplitPercent, { persist: false });
4674
+ if (!options || options.persist !== false) {
4675
+ try {
4676
+ if (window.localStorage) window.localStorage.setItem(PANE_LAYOUT_STORAGE_KEY, studioPaneLayout);
4677
+ } catch {
4678
+ // Ignore localStorage failures.
4679
+ }
4680
+ }
4681
+ syncStudioUiRefreshSummaries();
4682
+ if (!options || !options.silent) {
4683
+ setStatus("Pane layout: " + getStudioPaneLayoutLabel(studioPaneLayout) + ".");
4684
+ }
4685
+ return studioPaneLayout;
4686
+ }
4687
+
4688
+ function setActivityTrackingEnabled(enabled, options) {
4689
+ activityTrackingEnabled = Boolean(enabled) && !isEditorOnlyMode && !isWatchedFilePreview;
4690
+ if (activityTrackingSelect) {
4691
+ activityTrackingSelect.value = activityTrackingEnabled ? "on" : "off";
4692
+ activityTrackingSelect.hidden = isEditorOnlyMode || isWatchedFilePreview;
4693
+ }
4694
+ if (!activityTrackingEnabled) {
4695
+ activityTrackingOwnsWorkingView = false;
4696
+ activityTrackingRequestId = "";
4697
+ }
4698
+ if (!options || options.persist !== false) {
4699
+ try {
4700
+ if (window.localStorage) window.localStorage.setItem(ACTIVITY_TRACKING_STORAGE_KEY, activityTrackingEnabled ? "on" : "off");
4701
+ } catch {
4702
+ // Ignore localStorage failures.
4703
+ }
4704
+ }
4705
+ syncStudioUiRefreshSummaries();
4706
+ if (activityTrackingEnabled && agentBusyFromServer) {
4707
+ beginTrackedStudioActivity(pendingRequestId || "active");
4708
+ }
4709
+ if (!options || !options.silent) {
4710
+ setStatus(activityTrackingEnabled
4711
+ ? "Studio will show Working during main Pi activity, then return to Response Preview."
4712
+ : "Activity following disabled.");
4713
+ }
4714
+ return activityTrackingEnabled;
4715
+ }
4716
+
4717
+ function beginTrackedStudioActivity(requestId) {
4718
+ if (!activityTrackingEnabled || isEditorOnlyMode || isWatchedFilePreview) return false;
4719
+ const normalizedRequestId = String(requestId || "active");
4720
+ if (!activityTrackingOwnsWorkingView && activityTrackingRequestId
4721
+ && (activityTrackingRequestId === normalizedRequestId || activityTrackingRequestId === "active")) {
4722
+ if (activityTrackingRequestId === "active" && normalizedRequestId !== "active") {
4723
+ activityTrackingRequestId = normalizedRequestId;
4724
+ }
4725
+ return false;
4726
+ }
4727
+ if (activityTrackingOwnsWorkingView && activityTrackingRequestId === normalizedRequestId && rightView === "trace") return false;
4728
+ activityTrackingRequestId = normalizedRequestId;
4729
+ activityTrackingOwnsWorkingView = true;
4730
+ if (rightView !== "trace") setRightView("trace", { activityTracking: true });
4731
+ return true;
4732
+ }
4733
+
4734
+ function finishTrackedStudioActivity(requestId) {
4735
+ if (!activityTrackingRequestId && !activityTrackingOwnsWorkingView) return false;
4736
+ const normalizedRequestId = String(requestId || "");
4737
+ if (normalizedRequestId && activityTrackingRequestId !== normalizedRequestId) return false;
4738
+ const shouldReturn = activityTrackingEnabled && activityTrackingOwnsWorkingView && rightView === "trace";
4739
+ activityTrackingOwnsWorkingView = false;
4740
+ activityTrackingRequestId = "";
4741
+ if (shouldReturn) setRightView("preview", { activityTracking: true });
4742
+ return shouldReturn;
4743
+ }
4744
+
4393
4745
  function clampPaneSplitPercent(value) {
4394
4746
  const numeric = Number(value);
4395
4747
  if (!Number.isFinite(numeric)) return 50;
@@ -4400,13 +4752,19 @@
4400
4752
  function applyPaneSplitPercent(percent, options) {
4401
4753
  paneSplitPercent = clampPaneSplitPercent(percent);
4402
4754
  const rightPercent = Math.round((100 - paneSplitPercent) * 10) / 10;
4755
+ const dividerPercent = isResponseFirstStudioPaneLayout() ? rightPercent : paneSplitPercent;
4403
4756
  document.documentElement.style.setProperty("--studio-left-pane-fr", paneSplitPercent + "fr");
4404
4757
  document.documentElement.style.setProperty("--studio-right-pane-fr", rightPercent + "fr");
4405
4758
  if (paneResizeHandleEl) {
4759
+ const stacked = isStackedStudioPaneLayout();
4760
+ paneResizeHandleEl.setAttribute("aria-orientation", stacked ? "horizontal" : "vertical");
4406
4761
  paneResizeHandleEl.setAttribute("aria-valuemin", String(PANE_SPLIT_MIN_PERCENT));
4407
4762
  paneResizeHandleEl.setAttribute("aria-valuemax", String(PANE_SPLIT_MAX_PERCENT));
4408
- paneResizeHandleEl.setAttribute("aria-valuenow", String(Math.round(paneSplitPercent)));
4763
+ paneResizeHandleEl.setAttribute("aria-valuenow", String(Math.round(dividerPercent)));
4409
4764
  paneResizeHandleEl.setAttribute("aria-valuetext", "Editor " + Math.round(paneSplitPercent) + " percent, response " + Math.round(rightPercent) + " percent");
4765
+ paneResizeHandleEl.title = stacked
4766
+ ? "Drag to resize the stacked panes. Double-click to reset."
4767
+ : "Drag to resize the panes. Double-click to reset.";
4410
4768
  }
4411
4769
  if (!options || options.persist !== false) {
4412
4770
  try {
@@ -4438,17 +4796,23 @@
4438
4796
  : null;
4439
4797
  if (!mainEl || typeof mainEl.getBoundingClientRect !== "function") return paneSplitPercent;
4440
4798
  const rect = mainEl.getBoundingClientRect();
4441
- if (!rect.width) return paneSplitPercent;
4442
- const x = typeof event.clientX === "number" ? event.clientX : (rect.left + rect.width / 2);
4443
- return ((x - rect.left) / rect.width) * 100;
4799
+ const stacked = isStackedStudioPaneLayout();
4800
+ const extent = stacked ? rect.height : rect.width;
4801
+ if (!extent) return paneSplitPercent;
4802
+ const coordinate = stacked
4803
+ ? ((typeof event.clientY === "number" ? event.clientY : (rect.top + rect.height / 2)) - rect.top)
4804
+ : ((typeof event.clientX === "number" ? event.clientX : (rect.left + rect.width / 2)) - rect.left);
4805
+ const dividerPercent = (coordinate / extent) * 100;
4806
+ return isResponseFirstStudioPaneLayout() ? 100 - dividerPercent : dividerPercent;
4444
4807
  }
4445
4808
 
4446
4809
  function setupPaneResizeHandle() {
4447
4810
  if (!paneResizeHandleEl) return;
4811
+ setStudioPaneLayout(studioPaneLayout, { persist: false, silent: true });
4448
4812
  loadPaneSplitPercent();
4449
4813
  let dragging = false;
4450
4814
  let movedDuringDrag = false;
4451
- let pointerStartX = 0;
4815
+ let pointerStartPosition = 0;
4452
4816
  let activePaneResizePointerId = null;
4453
4817
  const finishDrag = () => {
4454
4818
  if (!dragging) return;
@@ -4473,7 +4837,9 @@
4473
4837
  event.stopPropagation();
4474
4838
  dragging = true;
4475
4839
  movedDuringDrag = false;
4476
- pointerStartX = typeof event.clientX === "number" ? event.clientX : 0;
4840
+ pointerStartPosition = isStackedStudioPaneLayout()
4841
+ ? (typeof event.clientY === "number" ? event.clientY : 0)
4842
+ : (typeof event.clientX === "number" ? event.clientX : 0);
4477
4843
  activePaneResizePointerId = event.pointerId;
4478
4844
  if (document.body && document.body.classList) document.body.classList.add("pane-resizing");
4479
4845
  try {
@@ -4489,7 +4855,10 @@
4489
4855
  });
4490
4856
  paneResizeHandleEl.addEventListener("pointermove", (event) => {
4491
4857
  if (!dragging) return;
4492
- const movement = typeof event.clientX === "number" ? Math.abs(event.clientX - pointerStartX) : 0;
4858
+ const currentPosition = isStackedStudioPaneLayout()
4859
+ ? (typeof event.clientY === "number" ? event.clientY : pointerStartPosition)
4860
+ : (typeof event.clientX === "number" ? event.clientX : pointerStartPosition);
4861
+ const movement = Math.abs(currentPosition - pointerStartPosition);
4493
4862
  if (!movedDuringDrag && movement < 3) return;
4494
4863
  movedDuringDrag = true;
4495
4864
  event.preventDefault();
@@ -4503,25 +4872,26 @@
4503
4872
  resetPaneSplitPercent();
4504
4873
  });
4505
4874
  paneResizeHandleEl.addEventListener("keydown", (event) => {
4875
+ const stacked = isStackedStudioPaneLayout();
4876
+ const decreaseKey = stacked ? "ArrowUp" : "ArrowLeft";
4877
+ const increaseKey = stacked ? "ArrowDown" : "ArrowRight";
4878
+ let dividerPercent = isResponseFirstStudioPaneLayout() ? 100 - paneSplitPercent : paneSplitPercent;
4506
4879
  if (event.key === "Home") {
4507
- event.preventDefault();
4508
- applyPaneSplitPercent(PANE_SPLIT_MIN_PERCENT);
4509
- return;
4510
- }
4511
- if (event.key === "End") {
4512
- event.preventDefault();
4513
- applyPaneSplitPercent(PANE_SPLIT_MAX_PERCENT);
4514
- return;
4515
- }
4516
- if (event.key === "Enter" || event.key === " " || event.key === "Spacebar") {
4880
+ dividerPercent = PANE_SPLIT_MIN_PERCENT;
4881
+ } else if (event.key === "End") {
4882
+ dividerPercent = PANE_SPLIT_MAX_PERCENT;
4883
+ } else if (event.key === "Enter" || event.key === " " || event.key === "Spacebar") {
4517
4884
  event.preventDefault();
4518
4885
  resetPaneSplitPercent();
4519
4886
  return;
4887
+ } else if (event.key === decreaseKey || event.key === increaseKey) {
4888
+ const step = event.shiftKey ? 10 : 5;
4889
+ dividerPercent += event.key === decreaseKey ? -step : step;
4890
+ } else {
4891
+ return;
4520
4892
  }
4521
- if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return;
4522
4893
  event.preventDefault();
4523
- const step = event.shiftKey ? 10 : 5;
4524
- applyPaneSplitPercent(paneSplitPercent + (event.key === "ArrowLeft" ? -step : step));
4894
+ applyPaneSplitPercent(isResponseFirstStudioPaneLayout() ? 100 - dividerPercent : dividerPercent);
4525
4895
  });
4526
4896
  }
4527
4897
 
@@ -4598,7 +4968,7 @@
4598
4968
  function shouldPreserveScrollForPaneActivationEvent(event) {
4599
4969
  const target = event && event.target;
4600
4970
  if (!(target instanceof Element)) return true;
4601
- if (target.closest("button, select, input, a, [role='button'], .studio-copy-block-btn, .preview-comment-add, .preview-comment-jump")) {
4971
+ if (target.closest("button, select, input, textarea, a, [role='button'], .studio-copy-block-btn, .preview-comment-add, .preview-comment-jump")) {
4602
4972
  return false;
4603
4973
  }
4604
4974
  return true;
@@ -4702,8 +5072,10 @@
4702
5072
  setStatus(label + " is unavailable in editor-only Studio views.", "warning");
4703
5073
  return false;
4704
5074
  }
4705
- const snapshot = snapshotStudioScrollablePositions();
4706
- setRightView(requestedView);
5075
+ const snapshot = snapshotStudioScrollablePositions().filter((entry) => (
5076
+ !(options && options.focusReplComposer && entry.el === critiqueViewEl)
5077
+ ));
5078
+ setRightView(requestedView, options);
4707
5079
  scheduleStudioScrollablePositionRestore(snapshot);
4708
5080
  if (!options || options.announce !== false) {
4709
5081
  setStatus("Right pane view: " + label + ".");
@@ -4786,6 +5158,20 @@
4786
5158
  return true;
4787
5159
  }
4788
5160
 
5161
+ function triggerLoadFromPiEditorShortcut() {
5162
+ if (isWatchedFilePreview) {
5163
+ setStatus("This preview is read-only. Open full Studio to load the Pi draft.", "warning");
5164
+ return false;
5165
+ }
5166
+ if (!getEditorBtn || getEditorBtn.hidden) return false;
5167
+ if (getEditorBtn.disabled) {
5168
+ setStatus("Load from Pi editor is unavailable while Studio is busy.", "warning");
5169
+ return false;
5170
+ }
5171
+ getEditorBtn.click();
5172
+ return true;
5173
+ }
5174
+
4789
5175
  function triggerEditorSaveShortcut() {
4790
5176
  if (hasRefreshableFilePath() && saveOverBtn && !saveOverBtn.disabled && !saveOverBtn.hidden) {
4791
5177
  saveOverBtn.click();
@@ -5089,6 +5475,16 @@
5089
5475
  return;
5090
5476
  }
5091
5477
 
5478
+ const isReplShortcut = (key.toLowerCase() === "r" || code === "KeyR")
5479
+ && (event.metaKey || event.ctrlKey)
5480
+ && event.altKey
5481
+ && !event.shiftKey;
5482
+ if (isReplShortcut) {
5483
+ event.preventDefault();
5484
+ switchRightPaneToView("repl", { focusReplComposer: true });
5485
+ return;
5486
+ }
5487
+
5092
5488
  const isSideQuestionsShortcut = (key.toLowerCase() === "q" || code === "KeyQ")
5093
5489
  && (event.metaKey || event.ctrlKey)
5094
5490
  && event.altKey
@@ -5137,6 +5533,18 @@
5137
5533
  return;
5138
5534
  }
5139
5535
 
5536
+ const isLoadFromPiEditorShortcut =
5537
+ (key.toLowerCase() === "l" || code === "KeyL")
5538
+ && (event.metaKey || event.ctrlKey)
5539
+ && !event.altKey
5540
+ && event.shiftKey;
5541
+
5542
+ if (isLoadFromPiEditorShortcut) {
5543
+ event.preventDefault();
5544
+ triggerLoadFromPiEditorShortcut();
5545
+ return;
5546
+ }
5547
+
5140
5548
  const isSaveAsShortcut =
5141
5549
  key.toLowerCase() === "s"
5142
5550
  && (event.metaKey || event.ctrlKey)
@@ -7585,7 +7993,7 @@
7585
7993
  refreshBtn.type = "button";
7586
7994
  refreshBtn.className = "studio-pdf-focus-btn studio-pdf-focus-refresh";
7587
7995
  refreshBtn.textContent = "Refresh";
7588
- refreshBtn.title = "Reload this PDF preview from disk. Shortcut: Cmd/Ctrl+Alt+R.";
7996
+ refreshBtn.title = "Reload this PDF preview from disk. Shortcut: Cmd/Ctrl+Alt+Shift+R.";
7589
7997
  refreshBtn.setAttribute("aria-label", "Refresh PDF preview from disk");
7590
7998
  refreshBtn.addEventListener("click", () => refreshStudioPdfFocusViewer());
7591
7999
  actions.appendChild(refreshBtn);
@@ -8148,7 +8556,7 @@
8148
8556
  const matches = (key === "r" || code === "KeyR")
8149
8557
  && (event.metaKey || event.ctrlKey)
8150
8558
  && event.altKey
8151
- && !event.shiftKey;
8559
+ && event.shiftKey;
8152
8560
  if (!matches) return false;
8153
8561
  if (!refreshVisibleStudioPdfPreviews()) return false;
8154
8562
  event.preventDefault();
@@ -8904,7 +9312,7 @@
8904
9312
  refreshBtn.type = "button";
8905
9313
  refreshBtn.className = "studio-pdf-card-action studio-pdf-card-refresh";
8906
9314
  refreshBtn.textContent = "Refresh";
8907
- refreshBtn.title = "Reload this PDF preview from disk. Shortcut: Cmd/Ctrl+Alt+R.";
9315
+ refreshBtn.title = "Reload this PDF preview from disk. Shortcut: Cmd/Ctrl+Alt+Shift+R.";
8908
9316
  refreshBtn.addEventListener("click", (event) => {
8909
9317
  event.preventDefault();
8910
9318
  event.stopPropagation();
@@ -9870,10 +10278,26 @@
9870
10278
  if (!actionBtn) return;
9871
10279
  event.preventDefault();
9872
10280
  const action = actionBtn.getAttribute("data-repl-action") || "";
10281
+ if (action === "quick-send") {
10282
+ submitReplQuickDraft();
10283
+ return;
10284
+ }
10285
+ if (action === "jump-controls") {
10286
+ focusReplSessionControls();
10287
+ return;
10288
+ }
10289
+ if (action === "quick-clear") {
10290
+ const session = getActiveReplSessionForCurrentRuntime();
10291
+ if (session) setReplQuickDraft(session, "");
10292
+ renderReplViewIfActive({ force: true });
10293
+ focusReplQuickComposer();
10294
+ return;
10295
+ }
9873
10296
  if (action === "start" || action === "new-session") {
9874
10297
  const requestId = makeRequestId();
9875
10298
  const command = getCurrentReplStartCommandFromDom();
9876
10299
  setReplCommandOverride(replRuntime, command);
10300
+ replPendingRequestId = requestId;
9877
10301
  replBusy = true;
9878
10302
  replError = "";
9879
10303
  replMessage = (action === "new-session" ? "Starting new " : "Starting ") + getReplRuntimeLabel(replRuntime) + " session" + (command ? " with custom command" : "") + "…";
@@ -9882,8 +10306,10 @@
9882
10306
  const message = { type: "repl_start_request", requestId, runtime: replRuntime, newSession: action === "new-session" };
9883
10307
  if (command) message.command = command;
9884
10308
  if (!sendMessage(message)) {
9885
- replBusy = false;
10309
+ if (replPendingRequestId === requestId) replPendingRequestId = "";
10310
+ replBusy = Boolean(replPendingRequestId);
9886
10311
  syncActionButtons();
10312
+ renderReplViewIfActive({ force: true });
9887
10313
  }
9888
10314
  return;
9889
10315
  }
@@ -9894,14 +10320,17 @@
9894
10320
  return;
9895
10321
  }
9896
10322
  const requestId = makeRequestId();
10323
+ replPendingRequestId = requestId;
9897
10324
  replBusy = true;
9898
10325
  replError = "";
9899
10326
  replMessage = "Stopping " + session.sessionName + "…";
9900
10327
  syncActionButtons();
9901
10328
  renderReplViewIfActive({ force: true });
9902
10329
  if (!sendMessage({ type: "repl_stop_request", requestId, sessionName: session.sessionName })) {
9903
- replBusy = false;
10330
+ if (replPendingRequestId === requestId) replPendingRequestId = "";
10331
+ replBusy = Boolean(replPendingRequestId);
9904
10332
  syncActionButtons();
10333
+ renderReplViewIfActive({ force: true });
9905
10334
  }
9906
10335
  return;
9907
10336
  }
@@ -9912,14 +10341,17 @@
9912
10341
  return;
9913
10342
  }
9914
10343
  const requestId = makeRequestId();
10344
+ replPendingRequestId = requestId;
9915
10345
  replBusy = true;
9916
10346
  replError = "";
9917
10347
  replMessage = "Interrupting REPL…";
9918
10348
  syncActionButtons();
9919
10349
  renderReplViewIfActive({ force: true });
9920
10350
  if (!sendMessage({ type: "repl_interrupt_request", requestId, sessionName: session.sessionName })) {
9921
- replBusy = false;
10351
+ if (replPendingRequestId === requestId) replPendingRequestId = "";
10352
+ replBusy = Boolean(replPendingRequestId);
9922
10353
  syncActionButtons();
10354
+ renderReplViewIfActive({ force: true });
9923
10355
  }
9924
10356
  return;
9925
10357
  }
@@ -9979,6 +10411,39 @@
9979
10411
  }
9980
10412
  }
9981
10413
 
10414
+ function handleReplPaneFocusIn(event) {
10415
+ if (!replQuickFocusRequested || rightView !== "repl") return;
10416
+ const target = event && event.target;
10417
+ if (target instanceof Element && !target.hasAttribute("data-repl-quick-draft")) {
10418
+ replQuickFocusRequested = false;
10419
+ }
10420
+ }
10421
+
10422
+ function handleReplPaneInput(event) {
10423
+ if (rightView !== "repl") return;
10424
+ const target = event && event.target;
10425
+ if (!(target instanceof HTMLTextAreaElement) || !target.hasAttribute("data-repl-quick-draft")) return;
10426
+ const session = getActiveReplSessionForCurrentRuntime();
10427
+ if (!session) return;
10428
+ const draft = setReplQuickDraft(session, target.value);
10429
+ if (target.value !== draft) target.value = draft;
10430
+ updateReplQuickComposerActions();
10431
+ }
10432
+
10433
+ function handleReplPaneKeydown(event) {
10434
+ if (rightView !== "repl" || !event || event.isComposing) return;
10435
+ const target = event.target;
10436
+ if (!(target instanceof HTMLTextAreaElement) || !target.hasAttribute("data-repl-quick-draft")) return;
10437
+ const submitShortcut = event.key === "Enter"
10438
+ && (event.metaKey || event.ctrlKey)
10439
+ && !event.altKey
10440
+ && !event.shiftKey;
10441
+ if (!submitShortcut) return;
10442
+ event.preventDefault();
10443
+ event.stopPropagation();
10444
+ submitReplQuickDraft();
10445
+ }
10446
+
9982
10447
  function handleReplPaneChange(event) {
9983
10448
  if (rightView !== "repl") return;
9984
10449
  const target = event.target;
@@ -10026,8 +10491,11 @@
10026
10491
  critiqueViewEl.addEventListener("click", handleGitChangesPaneClick);
10027
10492
  critiqueViewEl.addEventListener("click", handleStudioQuartoPreviewClick);
10028
10493
  critiqueViewEl.addEventListener("click", (event) => { void handleSideQuestionClick(event); });
10494
+ critiqueViewEl.addEventListener("focusin", handleReplPaneFocusIn);
10029
10495
  critiqueViewEl.addEventListener("input", handleSideQuestionInput);
10496
+ critiqueViewEl.addEventListener("input", handleReplPaneInput);
10030
10497
  critiqueViewEl.addEventListener("keydown", handleSideQuestionKeydown);
10498
+ critiqueViewEl.addEventListener("keydown", handleReplPaneKeydown);
10031
10499
  critiqueViewEl.addEventListener("change", handleSideQuestionChange);
10032
10500
  critiqueViewEl.addEventListener("change", handleReplPaneChange);
10033
10501
  critiqueViewEl.addEventListener("change", (event) => {
@@ -11763,6 +12231,28 @@
11763
12231
  return "tmux attach -t " + String(session.sessionName || "");
11764
12232
  }
11765
12233
 
12234
+ function buildReplQuickComposerHtml(activeSession, canSendToActiveSession) {
12235
+ const key = getReplQuickDraftKey(activeSession);
12236
+ const draft = getReplQuickDraft(activeSession);
12237
+ const pending = Boolean(replQuickPending && replQuickPending.key === key);
12238
+ const runtimeLabel = activeSession ? getReplRuntimeLabel(activeSession.runtime) : getReplRuntimeLabel(replRuntime);
12239
+ const tmuxTargetLabel = activeSession && activeSession.target ? ("tmux " + activeSession.target) : "";
12240
+ const targetDetails = activeSession
12241
+ ? ([runtimeLabel, activeSession.sessionName, tmuxTargetLabel].filter(Boolean).join(" · "))
12242
+ : (runtimeLabel + " · no session selected");
12243
+ const visibleTargetLabel = activeSession ? activeSession.sessionName : "No session selected";
12244
+ const inputDisabled = !activeSession || !key || pending || replBusy;
12245
+ const sendDisabled = !key || !canSendToActiveSession || wsState === "Disconnected" || pending || !draft.trim();
12246
+ return "<section class='repl-quick-composer' aria-labelledby='replQuickComposerTitle'>"
12247
+ + "<div class='repl-quick-header'><div><strong id='replQuickComposerTitle'>Quick send</strong><button type='button' class='repl-quick-target' data-repl-action='jump-controls' title='Target: " + escapeHtml(targetDetails) + ". Jump to REPL session controls.' aria-label='Target: " + escapeHtml(targetDetails) + ". Jump to REPL session controls.'><span class='repl-quick-target-label'>" + escapeHtml(visibleTargetLabel) + "</span><span class='repl-quick-target-arrow' aria-hidden='true'>↑</span></button></div>"
12248
+ + "<span class='repl-quick-hint'>Enter adds a line · Cmd/Ctrl+Enter sends</span></div>"
12249
+ + "<textarea data-repl-quick-draft rows='3' maxlength='" + REPL_QUICK_DRAFT_MAX_CHARS + "' aria-label='Quick code for " + escapeHtml(targetDetails) + "' aria-keyshortcuts='Meta+Enter Control+Enter' placeholder='" + (activeSession ? (key ? "Type a short command or snippet…" : "Exact tmux identity unavailable; refresh the session list") : "Start or select a REPL session first") + "'" + (inputDisabled ? " disabled" : "") + ">" + escapeHtml(draft) + "</textarea>"
12250
+ + "<div class='repl-quick-actions'><span>Raw send through Studio’s safe submission and Shared REPL Record path.</span><div>"
12251
+ + "<button type='button' data-repl-action='quick-clear'" + (pending || !draft ? " disabled" : "") + ">Clear</button>"
12252
+ + "<button class='repl-quick-send-btn' type='button' data-repl-action='quick-send'" + (sendDisabled ? " disabled" : "") + ">" + (pending ? "Sending…" : "Send") + "</button>"
12253
+ + "</div></div></section>";
12254
+ }
12255
+
11766
12256
  function buildReplPanelHtml() {
11767
12257
  const runtimeOptions = [
11768
12258
  ["shell", "Shell"],
@@ -11814,6 +12304,7 @@
11814
12304
  + (replError ? "<div class='repl-notice repl-notice-error'>" + escapeHtml(replError) + "</div>" : "")
11815
12305
  + buildReplJournalHtml(transcript)
11816
12306
  + buildReplStudioActionsHtml()
12307
+ + buildReplQuickComposerHtml(activeSession, canSendToActiveSession)
11817
12308
  + buildReplMirrorHtml(body, transcript)
11818
12309
  + "</div>";
11819
12310
  }
@@ -13483,7 +13974,7 @@
13483
13974
  const safeStart = Math.max(0, Math.min(start, current.length));
13484
13975
  const safeEnd = Math.max(safeStart, Math.min(end, current.length));
13485
13976
  const next = current.slice(0, safeStart) + answer.text + current.slice(safeEnd);
13486
- setEditorText(next, { preserveScroll: false, preserveSelection: false });
13977
+ setEditorText(next, { preserveScroll: false, preserveSelection: false, preservePiEditorDraftLink: true });
13487
13978
  const caret = safeStart + answer.text.length;
13488
13979
  sourceTextEl.setSelectionRange(caret, caret);
13489
13980
  setActivePane("left");
@@ -14099,6 +14590,7 @@
14099
14590
  if (refreshFromDiskBtn) refreshFromDiskBtn.disabled = uiBusy || isWatchedFilePreview || !canRefreshFromDisk;
14100
14591
  if (clearWorkspaceBtn) clearWorkspaceBtn.disabled = uiBusy || isWatchedFilePreview;
14101
14592
  sendEditorBtn.disabled = uiBusy || isEditorOnlyMode;
14593
+ if (clearPiEditorBtn) clearPiEditorBtn.disabled = uiBusy || isEditorOnlyMode;
14102
14594
  if (getEditorBtn) getEditorBtn.disabled = uiBusy || isWatchedFilePreview;
14103
14595
  if (watchedOpenEditableBtn) {
14104
14596
  watchedOpenEditableBtn.hidden = !isWatchedFilePreview;
@@ -14480,6 +14972,9 @@
14480
14972
  const value = String(nextText || "");
14481
14973
  const preserveScroll = Boolean(options && options.preserveScroll);
14482
14974
  const preserveSelection = Boolean(options && options.preserveSelection);
14975
+ if (!(options && options.preservePiEditorDraftLink === true)) {
14976
+ clearLinkedPiEditorDraftSnapshot();
14977
+ }
14483
14978
  if (activePreviewCommentSelection) {
14484
14979
  clearPreviewCommentSelection();
14485
14980
  }
@@ -15062,9 +15557,14 @@
15062
15557
  scheduleWorkspacePersistence();
15063
15558
  }
15064
15559
 
15065
- function setRightView(nextView) {
15560
+ function setRightView(nextView, options) {
15561
+ const automatedActivityChange = Boolean(options && options.activityTracking);
15562
+ if (activityTrackingOwnsWorkingView && !automatedActivityChange) {
15563
+ activityTrackingOwnsWorkingView = false;
15564
+ }
15066
15565
  const previousView = rightView;
15067
15566
  rightView = normalizeRightViewValue(nextView);
15567
+ if (rightView !== "repl") replQuickFocusRequested = false;
15068
15568
  syncRightViewModeOptions();
15069
15569
  rightViewSelect.value = rightView;
15070
15570
  if (rightView === "trace" && previousView !== "trace") {
@@ -15103,6 +15603,9 @@
15103
15603
  if (composer instanceof HTMLTextAreaElement) composer.focus({ preventScroll: true });
15104
15604
  }, 0);
15105
15605
  }
15606
+ if (rightView === "repl" && (previousView !== "repl" || (options && options.focusReplComposer))) {
15607
+ focusReplQuickComposer();
15608
+ }
15106
15609
  scheduleWorkspacePersistence();
15107
15610
  }
15108
15611
 
@@ -21974,7 +22477,7 @@
21974
22477
  ? current.slice(0, inlineState.range.end) + current.slice(inlineState.range.end + inlineState.markerText.length)
21975
22478
  : current.slice(0, inlineState.range.end) + inlineState.markerText + current.slice(inlineState.range.end);
21976
22479
  setEditorView("markdown");
21977
- setEditorText(next, { preserveScroll: true, preserveSelection: true });
22480
+ setEditorText(next, { preserveScroll: true, preserveSelection: true, preservePiEditorDraftLink: true });
21978
22481
  pendingReviewNoteInlineFocusId = note.id;
21979
22482
  renderReviewNotesList();
21980
22483
  updateReviewNotesUi();
@@ -22019,7 +22522,7 @@
22019
22522
  }
22020
22523
 
22021
22524
  setEditorView("markdown");
22022
- setEditorText(currentText, { preserveScroll: true, preserveSelection: true });
22525
+ setEditorText(currentText, { preserveScroll: true, preserveSelection: true, preservePiEditorDraftLink: true });
22023
22526
  renderReviewNotesList();
22024
22527
  updateReviewNotesUi();
22025
22528
  if (reviewNotesInlineAllBtn && typeof reviewNotesInlineAllBtn.focus === "function") {
@@ -22205,7 +22708,7 @@
22205
22708
  const safeStart = Math.max(0, Math.min(start, current.length));
22206
22709
  const safeEnd = Math.max(safeStart, Math.min(end, current.length));
22207
22710
  const next = current.slice(0, safeStart) + content + current.slice(safeEnd);
22208
- setEditorText(next, { preserveScroll: false, preserveSelection: false });
22711
+ setEditorText(next, { preserveScroll: false, preserveSelection: false, preservePiEditorDraftLink: true });
22209
22712
  const caret = safeStart + content.length;
22210
22713
  sourceTextEl.setSelectionRange(caret, caret);
22211
22714
  setActivePane("left");
@@ -22408,11 +22911,14 @@
22408
22911
  const replHint = rightView === "repl" && getActiveReplSessionForCurrentRuntime()
22409
22912
  ? " Sends text to Pi, not the REPL; use Send chunk/selection or Send to REPL to execute code in the active REPL."
22410
22913
  : "";
22914
+ const piEditorDraftHint = linkedPiEditorDraftSnapshot
22915
+ ? " The linked Pi terminal draft will be cleared after Pi accepts this run only if that draft remains unchanged."
22916
+ : "";
22411
22917
  sendRunBtn.title = directIsStop
22412
22918
  ? "Stop the active run. Shortcut: Esc."
22413
22919
  : (annotationsEnabled
22414
- ? "Run editor text as-is (includes [an: ...] markers). Shortcut: Cmd/Ctrl+Enter. Stop the active request with Esc." + replHint
22415
- : "Run editor text with [an: ...] markers stripped. Shortcut: Cmd/Ctrl+Enter. Stop the active request with Esc." + replHint);
22920
+ ? "Run editor text as-is (includes [an: ...] markers). Shortcut: Cmd/Ctrl+Enter. Stop the active request with Esc." + piEditorDraftHint + replHint
22921
+ : "Run editor text with [an: ...] markers stripped. Shortcut: Cmd/Ctrl+Enter. Stop the active request with Esc." + piEditorDraftHint + replHint);
22416
22922
  }
22417
22923
 
22418
22924
  if (queueSteerBtn) {
@@ -22978,6 +23484,11 @@
22978
23484
  }
22979
23485
  if (rightView === "side-questions") renderSideQuestionView();
22980
23486
  syncAskAsideButton();
23487
+ if (agentBusyFromServer) {
23488
+ beginTrackedStudioActivity(pendingRequestId || "active");
23489
+ } else {
23490
+ finishTrackedStudioActivity();
23491
+ }
22981
23492
 
22982
23493
  if (pendingRequestId) {
22983
23494
  if (busy) {
@@ -23054,18 +23565,25 @@
23054
23565
  const previousError = replError;
23055
23566
  const previousMessage = replMessage;
23056
23567
  const wasBusy = replBusy;
23568
+ const responseRequestId = typeof message.requestId === "string" ? message.requestId : "";
23569
+ const settlesPendingRequest = Boolean(responseRequestId && responseRequestId === replPendingRequestId);
23570
+ const previousActiveLifetimeKey = getReplQuickDraftKey(getActiveReplSession());
23057
23571
  replTmuxAvailable = typeof message.tmuxAvailable === "boolean" ? message.tmuxAvailable : replTmuxAvailable;
23058
23572
  const sessionsChanged = setReplSessions(message.sessions);
23059
23573
  if (typeof message.activeSessionName === "string" && message.activeSessionName.trim()) {
23060
23574
  setActiveReplSessionForCurrentRuntime(message.activeSessionName);
23061
23575
  }
23062
23576
  const journalChanged = mergeReplJournalEntries(message.journalEntries);
23577
+ reconcileReplQuickPending();
23063
23578
  maybeImportLegacyReplJournalEntries(replActiveSessionName);
23064
23579
  if (typeof message.transcript === "string") replTranscript = trimReplTranscript(message.transcript);
23065
23580
  if (typeof message.capturedAt === "number") replCapturedAt = message.capturedAt;
23066
23581
  replError = typeof message.replError === "string" ? message.replError : (typeof message.captureError === "string" ? message.captureError : "");
23067
23582
  replMessage = typeof message.replMessage === "string" ? message.replMessage : "";
23068
- replBusy = false;
23583
+ if (settlesPendingRequest && replPendingRequestId === responseRequestId) replPendingRequestId = "";
23584
+ replBusy = Boolean(replPendingRequestId);
23585
+ const activeSessionIdentityChanged = Boolean(previousActiveLifetimeKey)
23586
+ && previousActiveLifetimeKey !== getReplQuickDraftKey(getActiveReplSession());
23069
23587
  const controlsChanged = wasBusy
23070
23588
  || sessionsChanged
23071
23589
  || previousTmuxAvailable !== replTmuxAvailable
@@ -23077,7 +23595,7 @@
23077
23595
  || previousMessage !== replMessage
23078
23596
  || journalChanged
23079
23597
  || (!previousCapturedAt && replCapturedAt);
23080
- if (viewChanged) renderReplViewIfActive();
23598
+ if (viewChanged) renderReplViewIfActive({ force: activeSessionIdentityChanged });
23081
23599
  updateReferenceBadge();
23082
23600
  return;
23083
23601
  }
@@ -23102,18 +23620,28 @@
23102
23620
  const previousError = replError;
23103
23621
  const previousMessage = replMessage;
23104
23622
  const wasBusy = replBusy;
23623
+ const responseRequestId = typeof message.requestId === "string" ? message.requestId : "";
23624
+ const settlesPendingRequest = Boolean(responseRequestId && responseRequestId === replPendingRequestId);
23625
+ const previousActiveLifetimeKey = getReplQuickDraftKey(getActiveReplSession());
23105
23626
  let sessionsChanged = false;
23106
23627
  if (message.session) {
23107
23628
  const session = normalizeReplSession(message.session);
23108
- if (session && !replSessions.some((candidate) => candidate.sessionName === session.sessionName)) {
23109
- replSessions = [...replSessions, session];
23110
- sessionsChanged = true;
23629
+ if (session) {
23630
+ const existingIndex = replSessions.findIndex((candidate) => candidate.sessionName === session.sessionName);
23631
+ if (existingIndex < 0) {
23632
+ replSessions = [...replSessions, session];
23633
+ sessionsChanged = true;
23634
+ } else if (serializeReplSessionsForCompare([replSessions[existingIndex]]) !== serializeReplSessionsForCompare([session])) {
23635
+ replSessions = replSessions.map((candidate, index) => index === existingIndex ? session : candidate);
23636
+ sessionsChanged = true;
23637
+ }
23111
23638
  }
23112
23639
  }
23113
23640
  if (typeof message.activeSessionName === "string" && message.activeSessionName.trim()) {
23114
23641
  setActiveReplSessionForCurrentRuntime(message.activeSessionName);
23115
23642
  }
23116
23643
  let journalChanged = mergeReplJournalEntries(message.journalEntries);
23644
+ reconcileReplQuickPending();
23117
23645
  maybeImportLegacyReplJournalEntries(replActiveSessionName);
23118
23646
  if (typeof message.transcript === "string") {
23119
23647
  replTranscript = trimReplTranscript(message.transcript);
@@ -23125,7 +23653,10 @@
23125
23653
  if (typeof message.capturedAt === "number") replCapturedAt = message.capturedAt;
23126
23654
  replError = typeof message.replError === "string" ? message.replError : "";
23127
23655
  if (typeof message.replMessage === "string") replMessage = message.replMessage;
23128
- replBusy = false;
23656
+ if (settlesPendingRequest && replPendingRequestId === responseRequestId) replPendingRequestId = "";
23657
+ replBusy = Boolean(replPendingRequestId);
23658
+ const activeSessionIdentityChanged = Boolean(previousActiveLifetimeKey)
23659
+ && previousActiveLifetimeKey !== getReplQuickDraftKey(getActiveReplSession());
23129
23660
  const controlsChanged = wasBusy || sessionsChanged || previousActiveSessionName !== replActiveSessionName;
23130
23661
  if (controlsChanged) syncActionButtons();
23131
23662
  const viewChanged = controlsChanged
@@ -23134,7 +23665,7 @@
23134
23665
  || previousMessage !== replMessage
23135
23666
  || journalChanged
23136
23667
  || (!previousCapturedAt && replCapturedAt);
23137
- if (viewChanged) renderReplViewIfActive();
23668
+ if (viewChanged) renderReplViewIfActive({ force: activeSessionIdentityChanged });
23138
23669
  updateReferenceBadge();
23139
23670
  return;
23140
23671
  }
@@ -23153,9 +23684,12 @@
23153
23684
  }
23154
23685
 
23155
23686
  if (message.type === "repl_send_ack") {
23156
- replBusy = false;
23687
+ const responseRequestId = typeof message.requestId === "string" ? message.requestId : "";
23688
+ if (responseRequestId && replPendingRequestId === responseRequestId) replPendingRequestId = "";
23689
+ replBusy = Boolean(replPendingRequestId);
23157
23690
  replMessage = "";
23158
23691
  replError = "";
23692
+ if (responseRequestId) settleReplQuickPending(responseRequestId, true);
23159
23693
  mergeReplJournalEntries(message.journalEntries);
23160
23694
  if (typeof message.requestId === "string") {
23161
23695
  replJournalEntries = replJournalEntries.map((entry) => entry.requestId === message.requestId ? { ...entry, status: "sent", updatedAt: Date.now() } : entry);
@@ -23171,6 +23705,8 @@
23171
23705
  pendingRequestId = typeof message.requestId === "string" ? message.requestId : pendingRequestId;
23172
23706
  pendingKind = typeof message.kind === "string" ? message.kind : "unknown";
23173
23707
  stickyStudioKind = pendingKind;
23708
+ agentBusyFromServer = true;
23709
+ beginTrackedStudioActivity(pendingRequestId || "active");
23174
23710
  if (pendingKind === "direct") {
23175
23711
  studioRunChainActive = true;
23176
23712
  }
@@ -23201,6 +23737,7 @@
23201
23737
  const busy = Boolean(message.busy);
23202
23738
  setBusy(busy);
23203
23739
  setWsState(busy ? "Submitting" : "Ready");
23740
+ finishTrackedStudioActivity(typeof message.requestId === "string" ? message.requestId : "");
23204
23741
  setStatus(typeof message.message === "string" ? message.message : "Compaction completed.", "success");
23205
23742
  return;
23206
23743
  }
@@ -23215,6 +23752,7 @@
23215
23752
  const busy = Boolean(message.busy);
23216
23753
  setBusy(busy);
23217
23754
  setWsState(busy ? "Submitting" : "Ready");
23755
+ finishTrackedStudioActivity(typeof message.requestId === "string" ? message.requestId : "");
23218
23756
  setStatus(typeof message.message === "string" ? message.message : "Compaction failed.", "error");
23219
23757
  return;
23220
23758
  }
@@ -23225,6 +23763,7 @@
23225
23763
  }
23226
23764
 
23227
23765
  const completedRequestId = typeof message.requestId === "string" ? message.requestId : pendingRequestId;
23766
+ if (completedRequestId) pendingPiEditorDraftSnapshots.delete(completedRequestId);
23228
23767
  const responseKind =
23229
23768
  typeof message.kind === "string"
23230
23769
  ? message.kind
@@ -23234,6 +23773,7 @@
23234
23773
  pendingRequestId = null;
23235
23774
  pendingKind = null;
23236
23775
  queuedLatestResponse = null;
23776
+ agentBusyFromServer = false;
23237
23777
  setBusy(false);
23238
23778
  setWsState("Ready");
23239
23779
 
@@ -23250,6 +23790,7 @@
23250
23790
  if (!appliedFromHistory && typeof message.markdown === "string") {
23251
23791
  handleIncomingResponse(message.markdown, responseKind, message.timestamp, message.thinking);
23252
23792
  }
23793
+ finishTrackedStudioActivity(completedRequestId || "");
23253
23794
 
23254
23795
  if (responseKind === "critique") {
23255
23796
  setStatus("Critique ready.", "success");
@@ -23357,17 +23898,46 @@
23357
23898
  return;
23358
23899
  }
23359
23900
 
23901
+ if (message.type === "pi_editor_draft_result") {
23902
+ const requestId = typeof message.requestId === "string" ? message.requestId : "";
23903
+ if (!settleReservedPiEditorDraftSnapshot(requestId, message.piEditorDraftSnapshot)) return;
23904
+ const outcome = String(message.outcome || "");
23905
+ syncActionButtons();
23906
+ if (outcome === "cleared") {
23907
+ setStatus("Submission accepted; cleared the unchanged Pi draft.", "success");
23908
+ } else if (outcome === "already-empty") {
23909
+ setStatus("Submission accepted; the Pi draft was already empty.", "success");
23910
+ } else if (outcome !== "changed") {
23911
+ setStatus("Submission accepted, but Studio could not confirm Pi-draft cleanup; check the terminal draft.", "warning");
23912
+ }
23913
+ return;
23914
+ }
23915
+
23360
23916
  if (message.type === "editor_loaded") {
23361
23917
  if (typeof message.requestId === "string" && pendingRequestId === message.requestId) {
23362
23918
  pendingRequestId = null;
23363
23919
  pendingKind = null;
23364
23920
  }
23921
+ const snapshot = normalizePiEditorDraftSnapshot(message.piEditorDraftSnapshot);
23922
+ setLinkedPiEditorDraftSnapshot(snapshot, Boolean(snapshot && snapshot.byteLength > 0));
23365
23923
  setBusy(false);
23366
23924
  setWsState("Ready");
23367
23925
  setStatus(typeof message.message === "string" ? message.message : "Loaded into pi editor.", "success");
23368
23926
  return;
23369
23927
  }
23370
23928
 
23929
+ if (message.type === "pi_editor_cleared") {
23930
+ if (typeof message.requestId === "string" && pendingRequestId === message.requestId) {
23931
+ pendingRequestId = null;
23932
+ pendingKind = null;
23933
+ }
23934
+ clearLinkedPiEditorDraftSnapshot();
23935
+ setBusy(false);
23936
+ setWsState("Ready");
23937
+ setStatus(message.cleared ? "Cleared Pi editor text." : "Pi editor text was already empty.", "success");
23938
+ return;
23939
+ }
23940
+
23371
23941
  if (message.type === "editor_snapshot") {
23372
23942
  if (isWatchedFilePreview) {
23373
23943
  setStatus("Ignored editor snapshot because this preview follows its watched file on disk.", "warning");
@@ -23383,12 +23953,14 @@
23383
23953
 
23384
23954
  const content = typeof message.content === "string" ? message.content : "";
23385
23955
  setEditorText(content, { preserveScroll: false, preserveSelection: false });
23956
+ const snapshot = normalizePiEditorDraftSnapshot(message.piEditorDraftSnapshot);
23957
+ setLinkedPiEditorDraftSnapshot(snapshot, Boolean(content.length > 0 && snapshot));
23386
23958
  setSourceState({ source: "pi-editor", label: "pi editor draft", path: null });
23387
23959
  setBusy(false);
23388
23960
  setWsState("Ready");
23389
23961
  setStatus(
23390
23962
  content.trim()
23391
- ? "Loaded draft from pi editor."
23963
+ ? "Loaded draft from pi editor. A Studio Run will clear it if the Pi draft remains unchanged."
23392
23964
  : "pi editor is empty. Loaded blank text.",
23393
23965
  content.trim() ? "success" : "warning",
23394
23966
  );
@@ -23552,6 +24124,11 @@
23552
24124
 
23553
24125
  setBusy(busy);
23554
24126
  setWsState(busy ? "Submitting" : "Ready");
24127
+ if (agentBusyFromServer) {
24128
+ beginTrackedStudioActivity(pendingRequestId || "active");
24129
+ } else {
24130
+ finishTrackedStudioActivity();
24131
+ }
23555
24132
 
23556
24133
  if (pendingRequestId) {
23557
24134
  if (busy) {
@@ -23580,6 +24157,7 @@
23580
24157
 
23581
24158
  if (message.type === "busy") {
23582
24159
  if (typeof message.requestId === "string") {
24160
+ restoreReservedPiEditorDraftSnapshot(message.requestId);
23583
24161
  pendingSaveOperations.delete(message.requestId);
23584
24162
  failPendingCompanionLaunch(message.requestId, "Studio could not start the companion editor because another request was busy.");
23585
24163
  }
@@ -23594,6 +24172,7 @@
23594
24172
  clearArmedTitleAttention(message.requestId);
23595
24173
  }
23596
24174
  stickyStudioKind = null;
24175
+ finishTrackedStudioActivity(typeof message.requestId === "string" ? message.requestId : "");
23597
24176
  setBusy(false);
23598
24177
  setWsState("Ready");
23599
24178
  setStatus(typeof message.message === "string" ? message.message : "Studio is busy.", "warning");
@@ -23602,6 +24181,7 @@
23602
24181
 
23603
24182
  if (message.type === "error") {
23604
24183
  if (typeof message.requestId === "string") {
24184
+ restoreReservedPiEditorDraftSnapshot(message.requestId);
23605
24185
  pendingSaveOperations.delete(message.requestId);
23606
24186
  failPendingCompanionLaunch(message.requestId, "Studio could not prepare the companion editor. Return to the originating Studio page for details.");
23607
24187
  }
@@ -23615,9 +24195,14 @@
23615
24195
  if (typeof message.requestId === "string") {
23616
24196
  clearArmedTitleAttention(message.requestId);
23617
24197
  }
23618
- if (replBusy) {
24198
+ const failedReplRequestId = typeof message.requestId === "string" && message.requestId === replPendingRequestId
24199
+ ? message.requestId
24200
+ : "";
24201
+ if (failedReplRequestId) {
24202
+ replPendingRequestId = "";
23619
24203
  replBusy = false;
23620
24204
  replError = typeof message.message === "string" ? message.message : "REPL request failed.";
24205
+ settleReplQuickPending(failedReplRequestId, false);
23621
24206
  if (typeof message.requestId === "string") {
23622
24207
  replJournalEntries = replJournalEntries.map((entry) => entry.requestId === message.requestId ? { ...entry, status: "error", output: replError, updatedAt: Date.now() } : entry);
23623
24208
  persistReplJournalEntries();
@@ -23625,6 +24210,7 @@
23625
24210
  renderReplViewIfActive({ force: true });
23626
24211
  }
23627
24212
  stickyStudioKind = null;
24213
+ finishTrackedStudioActivity(typeof message.requestId === "string" ? message.requestId : "");
23628
24214
  setBusy(false);
23629
24215
  setWsState("Ready");
23630
24216
  setStatus(typeof message.message === "string" ? message.message : "Request failed.", "error");
@@ -23744,6 +24330,7 @@
23744
24330
  quartoPreviewCheckSourcePath = "";
23745
24331
  quartoPreviewActionRequestId = null;
23746
24332
  sideQuestionMarkdownExportRequest = null;
24333
+ pendingPiEditorDraftSnapshots.clear();
23747
24334
  failAllPendingCompanionLaunches("The originating Studio connection was lost before the companion editor was ready.");
23748
24335
  if (rightView === "editor-quarto-preview") renderQuartoPreviewView();
23749
24336
  setBusy(true);
@@ -24014,7 +24601,7 @@
24014
24601
 
24015
24602
  if (stripped.hadHeader) {
24016
24603
  const updated = stripped.body;
24017
- setEditorText(updated, { preserveScroll: true, preserveSelection: true });
24604
+ setEditorText(updated, { preserveScroll: true, preserveSelection: true, preservePiEditorDraftLink: true });
24018
24605
  updateResultActionButtons();
24019
24606
  setStatus("Removed annotated reply header.", "success");
24020
24607
  return;
@@ -24027,7 +24614,7 @@
24027
24614
  return;
24028
24615
  }
24029
24616
 
24030
- setEditorText(updated, { preserveScroll: true, preserveSelection: true });
24617
+ setEditorText(updated, { preserveScroll: true, preserveSelection: true, preservePiEditorDraftLink: true });
24031
24618
  updateResultActionButtons();
24032
24619
  setStatus("Inserted annotated reply header.", "success");
24033
24620
  }
@@ -24160,6 +24747,18 @@
24160
24747
  });
24161
24748
  }
24162
24749
 
24750
+ if (studioPaneLayoutSelect) {
24751
+ studioPaneLayoutSelect.addEventListener("change", () => {
24752
+ setStudioPaneLayout(studioPaneLayoutSelect.value);
24753
+ });
24754
+ }
24755
+
24756
+ if (activityTrackingSelect) {
24757
+ activityTrackingSelect.addEventListener("change", () => {
24758
+ setActivityTrackingEnabled(activityTrackingSelect.value === "on");
24759
+ });
24760
+ }
24761
+
24163
24762
  if (annotationModeSelect) {
24164
24763
  annotationModeSelect.addEventListener("change", () => {
24165
24764
  setAnnotationsEnabled(annotationModeSelect.value !== "off");
@@ -24885,6 +25484,29 @@
24885
25484
  }
24886
25485
  });
24887
25486
 
25487
+ if (clearPiEditorBtn) {
25488
+ clearPiEditorBtn.addEventListener("click", async () => {
25489
+ closeStudioUiRefreshMenus();
25490
+ const confirmed = await requestStudioConfirmation(
25491
+ "Clear the current Pi input draft? This removes text waiting in the terminal editor, including context added from Neovim. Studio text and Pi conversation history will not change.",
25492
+ { title: "Clear Pi editor text?", confirmLabel: "Clear text", destructive: true },
25493
+ );
25494
+ if (!confirmed) {
25495
+ setStatus("Pi editor text was not cleared.");
25496
+ return;
25497
+ }
25498
+
25499
+ const requestId = beginUiAction("clear_pi_editor");
25500
+ if (!requestId) return;
25501
+ const sent = sendMessage({ type: "clear_pi_editor_request", requestId });
25502
+ if (!sent) {
25503
+ pendingRequestId = null;
25504
+ pendingKind = null;
25505
+ setBusy(false);
25506
+ }
25507
+ });
25508
+ }
25509
+
24888
25510
  if (openCompanionBtn) {
24889
25511
  openCompanionBtn.addEventListener("click", () => {
24890
25512
  requestOpenEditorOnlyDocument("", {
@@ -24952,14 +25574,17 @@
24952
25574
 
24953
25575
  const requestId = beginUiAction("direct");
24954
25576
  if (!requestId) return;
25577
+ const piEditorDraftSnapshot = reserveLinkedPiEditorDraftSnapshot(requestId);
24955
25578
 
24956
25579
  const sent = sendMessage({
24957
25580
  type: "send_run_request",
24958
25581
  requestId,
24959
25582
  text: prepared,
25583
+ piEditorDraftSnapshot: piEditorDraftSnapshot || undefined,
24960
25584
  });
24961
25585
 
24962
25586
  if (!sent) {
25587
+ restoreReservedPiEditorDraftSnapshot(requestId);
24963
25588
  pendingRequestId = null;
24964
25589
  pendingKind = null;
24965
25590
  setBusy(false);
@@ -24980,12 +25605,17 @@
24980
25605
 
24981
25606
  const requestId = makeRequestId();
24982
25607
  clearTitleAttention();
25608
+ const piEditorDraftSnapshot = reserveLinkedPiEditorDraftSnapshot(requestId);
24983
25609
  const sent = sendMessage({
24984
25610
  type: "send_run_request",
24985
25611
  requestId,
24986
25612
  text: prepared,
25613
+ piEditorDraftSnapshot: piEditorDraftSnapshot || undefined,
24987
25614
  });
24988
- if (!sent) return;
25615
+ if (!sent) {
25616
+ restoreReservedPiEditorDraftSnapshot(requestId);
25617
+ return;
25618
+ }
24989
25619
  setStatus("Queueing steering…", "warning");
24990
25620
  });
24991
25621
  }
@@ -25421,7 +26051,7 @@
25421
26051
  if (!confirmed) return;
25422
26052
 
25423
26053
  const strippedContent = stripAnnotationMarkers(content);
25424
- setEditorText(strippedContent, { preserveScroll: true, preserveSelection: false });
26054
+ setEditorText(strippedContent, { preserveScroll: true, preserveSelection: false, preservePiEditorDraftLink: true });
25425
26055
  setStatus("Removed annotation markers from editor text.", "success");
25426
26056
  });
25427
26057
  }
@@ -25628,6 +26258,7 @@
25628
26258
  const storedLineNumbersEnabled = readStoredEditorLineNumbersEnabled();
25629
26259
  const initialLineNumbersEnabled = storedLineNumbersEnabled ?? Boolean(lineNumbersSelect && lineNumbersSelect.value === "on");
25630
26260
  setLineNumbersEnabled(initialLineNumbersEnabled);
26261
+ setActivityTrackingEnabled(activityTrackingEnabled, { persist: false, silent: true });
25631
26262
 
25632
26263
  const storedResponseHighlightEnabled = readStoredResponseHighlightEnabled();
25633
26264
  const initialResponseHighlightEnabled = storedResponseHighlightEnabled ?? Boolean(responseHighlightSelect && responseHighlightSelect.value === "on");