pi-studio 0.9.56 → 0.9.58

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -132,6 +132,7 @@
132
132
  const queueSteerBtn = document.getElementById("queueSteerBtn");
133
133
  const sendReplBtn = document.getElementById("sendReplBtn");
134
134
  const replSendModeSelect = document.getElementById("replSendModeSelect");
135
+ const replEchoModeSelect = document.getElementById("replEchoModeSelect");
135
136
  const copyDraftBtn = document.getElementById("copyDraftBtn");
136
137
  const suggestCompletionBtn = document.getElementById("suggestCompletionBtn");
137
138
  const suggestCompletionOptionsBtn = document.getElementById("suggestCompletionOptionsBtn");
@@ -147,6 +148,8 @@
147
148
  const stripAnnotationsBtn = document.getElementById("stripAnnotationsBtn");
148
149
  const highlightSelect = document.getElementById("highlightSelect");
149
150
  const lineNumbersSelect = document.getElementById("lineNumbersSelect");
151
+ const studioPaneLayoutSelect = document.getElementById("studioPaneLayoutSelect");
152
+ const activityTrackingSelect = document.getElementById("activityTrackingSelect");
150
153
  const editorFontSizeSelect = document.getElementById("editorFontSizeSelect");
151
154
  const annotationModeSelect = document.getElementById("annotationModeSelect");
152
155
  const compactBtn = document.getElementById("compactBtn");
@@ -426,8 +429,8 @@
426
429
  rightViewSelect.title = isWatchedFilePreview
427
430
  ? "Read-only watched preview follows this file on disk."
428
431
  : (isEditorOnlyMode
429
- ? "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."
430
- : "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.");
432
+ ? "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."
433
+ : "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.");
431
434
  }
432
435
 
433
436
  function getInitialRightView(source) {
@@ -480,6 +483,8 @@
480
483
  const REPL_TRANSCRIPT_MAX_CHARS = 200_000;
481
484
  const REPL_JOURNAL_OUTPUT_MAX_CHARS = 80_000;
482
485
  const REPL_JOURNAL_MAX_ENTRIES = 80;
486
+ const REPL_QUICK_DRAFT_MAX_CHARS = 20_000;
487
+ const REPL_QUICK_DRAFT_MAX_SESSIONS = 12;
483
488
  const PDF_EXPORT_FETCH_TIMEOUT_MS = 180_000;
484
489
  const HTML_EXPORT_FETCH_TIMEOUT_MS = 180_000;
485
490
  const HTML_ARTIFACT_MATH_RENDER_FETCH_TIMEOUT_MS = 30_000;
@@ -587,6 +592,10 @@
587
592
  let replFollow = true;
588
593
  let replPollTimer = null;
589
594
  let replBusy = false;
595
+ let replPendingRequestId = "";
596
+ const replQuickDrafts = new Map();
597
+ let replQuickPending = null;
598
+ let replQuickFocusRequested = false;
590
599
  let replSendMode = (() => {
591
600
  try {
592
601
  const stored = window.localStorage && window.localStorage.getItem("piStudio.replSendMode");
@@ -595,6 +604,14 @@
595
604
  return "raw";
596
605
  }
597
606
  })();
607
+ let replEchoMode = (() => {
608
+ try {
609
+ const stored = String((window.localStorage && window.localStorage.getItem("piStudio.replEchoMode.v2")) || "").trim().toLowerCase();
610
+ return stored === "summary" || stored === "full" ? stored : "off";
611
+ } catch {
612
+ return "off";
613
+ }
614
+ })();
598
615
  function normalizeReplJournalEntry(entry) {
599
616
  if (!entry || typeof entry !== "object") return null;
600
617
  const hasCompatibleOrigin = entry.origin === "pi-repl" || entry.origin === "pi-studio";
@@ -613,7 +630,7 @@
613
630
  mode: typeof entry.mode === "string" ? entry.mode : "raw",
614
631
  prose: typeof entry.prose === "string" ? entry.prose : "",
615
632
  code: typeof entry.code === "string" ? entry.code : "",
616
- output: typeof entry.output === "string" ? entry.output : "",
633
+ output: typeof entry.output === "string" ? stripStudioReplSubmissionEcho(entry.output) : "",
617
634
  beforeTranscript: "",
618
635
  status: typeof entry.status === "string" ? entry.status : "sent",
619
636
  skippedChunks: Math.max(0, Math.floor(Number(entry.skippedChunks) || 0)),
@@ -1293,6 +1310,10 @@
1293
1310
  return {
1294
1311
  sessionName,
1295
1312
  target: typeof session.target === "string" ? session.target : (sessionName + ":0.0"),
1313
+ tmuxSessionId: typeof session.tmuxSessionId === "string" ? session.tmuxSessionId : "",
1314
+ tmuxSessionCreatedAt: typeof session.tmuxSessionCreatedAt === "number" && Number.isFinite(session.tmuxSessionCreatedAt)
1315
+ ? session.tmuxSessionCreatedAt
1316
+ : 0,
1296
1317
  runtime: typeof session.runtime === "string" ? session.runtime : "unknown",
1297
1318
  label: typeof session.label === "string" && session.label.trim() ? session.label.trim() : sessionName,
1298
1319
  source: typeof session.source === "string" ? session.source : "tmux",
@@ -1353,6 +1374,11 @@
1353
1374
  return String(value || "").trim().toLowerCase() === "literate" ? "literate" : "raw";
1354
1375
  }
1355
1376
 
1377
+ function normalizeReplEchoMode(value) {
1378
+ const normalized = String(value || "").trim().toLowerCase();
1379
+ return normalized === "summary" || normalized === "full" ? normalized : "off";
1380
+ }
1381
+
1356
1382
  function isMacShortcutPlatform() {
1357
1383
  try {
1358
1384
  const platform = String((navigator && navigator.platform) || "");
@@ -1394,6 +1420,85 @@
1394
1420
  }
1395
1421
  }
1396
1422
 
1423
+ function setReplEchoMode(mode) {
1424
+ replEchoMode = normalizeReplEchoMode(mode);
1425
+ if (replEchoModeSelect) replEchoModeSelect.value = replEchoMode;
1426
+ try {
1427
+ if (window.localStorage) window.localStorage.setItem("piStudio.replEchoMode.v2", replEchoMode);
1428
+ } catch {
1429
+ // Ignore storage failures.
1430
+ }
1431
+ }
1432
+
1433
+ function syncReplEchoModeSelect(visible) {
1434
+ if (!replEchoModeSelect) return;
1435
+ replEchoModeSelect.hidden = !visible;
1436
+ replEchoModeSelect.disabled = !visible || wsState === "Disconnected" || uiBusy || replBusy;
1437
+ replEchoModeSelect.value = replEchoMode;
1438
+ replEchoModeSelect.title = replEchoMode === "off"
1439
+ ? "Do not add a submitted-code display or alignment anchors to the raw REPL pane."
1440
+ : (replEchoMode === "full"
1441
+ ? "Display up to 40 lines or 4,000 characters of submitted code; source remains in raw terminal history."
1442
+ : "Display short submissions in full, truncating after 6 lines or 600 characters, with compact anchors and a plain output divider.");
1443
+ }
1444
+
1445
+ function getReplQuickDraftKey(session) {
1446
+ const normalized = normalizeReplSession(session);
1447
+ if (!normalized || !normalized.tmuxSessionId || normalized.tmuxSessionCreatedAt <= 0) return "";
1448
+ return [normalizeReplRuntime(normalized.runtime), normalized.tmuxSessionId, normalized.tmuxSessionCreatedAt].join("\u001f");
1449
+ }
1450
+
1451
+ function getReplQuickDraft(session) {
1452
+ const key = getReplQuickDraftKey(session);
1453
+ return key ? String(replQuickDrafts.get(key) || "") : "";
1454
+ }
1455
+
1456
+ function setReplQuickDraft(session, value) {
1457
+ const key = getReplQuickDraftKey(session);
1458
+ if (!key) return "";
1459
+ const draft = String(value || "").slice(0, REPL_QUICK_DRAFT_MAX_CHARS);
1460
+ if (!draft) {
1461
+ replQuickDrafts.delete(key);
1462
+ return "";
1463
+ }
1464
+ if (!replQuickDrafts.has(key) && replQuickDrafts.size >= REPL_QUICK_DRAFT_MAX_SESSIONS) {
1465
+ const oldestKey = replQuickDrafts.keys().next().value;
1466
+ if (oldestKey) replQuickDrafts.delete(oldestKey);
1467
+ }
1468
+ replQuickDrafts.set(key, draft);
1469
+ return draft;
1470
+ }
1471
+
1472
+ function settleReplQuickPending(requestId, submitted) {
1473
+ if (!replQuickPending || !requestId || replQuickPending.requestId !== requestId) return false;
1474
+ const pending = replQuickPending;
1475
+ replQuickPending = null;
1476
+ if (replPendingRequestId === requestId) replPendingRequestId = "";
1477
+ replBusy = Boolean(replPendingRequestId);
1478
+ if (submitted && replQuickDrafts.get(pending.key) === pending.text) {
1479
+ replQuickDrafts.delete(pending.key);
1480
+ }
1481
+ return true;
1482
+ }
1483
+
1484
+ function reconcileReplQuickPending() {
1485
+ if (!replQuickPending) return false;
1486
+ const entry = replJournalEntries.find((candidate) => candidate.requestId === replQuickPending.requestId);
1487
+ if (!entry || entry.status === "sending") return false;
1488
+ return settleReplQuickPending(entry.requestId, entry.status !== "error");
1489
+ }
1490
+
1491
+ function updateReplQuickComposerActions() {
1492
+ if (!critiqueViewEl || rightView !== "repl") return;
1493
+ const session = getActiveReplSessionForCurrentRuntime();
1494
+ const draft = getReplQuickDraft(session);
1495
+ const pending = Boolean(replQuickPending && replQuickPending.key === getReplQuickDraftKey(session));
1496
+ const sendButton = critiqueViewEl.querySelector("[data-repl-action='quick-send']");
1497
+ const clearButton = critiqueViewEl.querySelector("[data-repl-action='quick-clear']");
1498
+ if (sendButton) sendButton.disabled = !session || replTmuxAvailable === false || pending || replBusy || wsState === "Disconnected" || !draft.trim();
1499
+ if (clearButton) clearButton.disabled = pending || !draft;
1500
+ }
1501
+
1397
1502
  function setReplJournalCollapsed(collapsed) {
1398
1503
  replJournalCollapsed = Boolean(collapsed);
1399
1504
  try {
@@ -1424,6 +1529,8 @@
1424
1529
  runtime: session.runtime,
1425
1530
  source: session.source,
1426
1531
  target: session.target,
1532
+ tmuxSessionId: session.tmuxSessionId,
1533
+ tmuxSessionCreatedAt: session.tmuxSessionCreatedAt,
1427
1534
  })));
1428
1535
  }
1429
1536
 
@@ -1543,7 +1650,53 @@
1543
1650
 
1544
1651
  function isReplControlFocused() {
1545
1652
  const activeEl = document.activeElement;
1546
- return activeEl instanceof Element && Boolean(activeEl.closest(".repl-controls"));
1653
+ return activeEl instanceof Element && Boolean(activeEl.closest(".repl-controls, .repl-quick-composer"));
1654
+ }
1655
+
1656
+ function focusReplQuickComposer() {
1657
+ replQuickFocusRequested = true;
1658
+ window.setTimeout(() => {
1659
+ if (rightView !== "repl" || !critiqueViewEl) {
1660
+ replQuickFocusRequested = false;
1661
+ return;
1662
+ }
1663
+ const composer = critiqueViewEl.querySelector("[data-repl-quick-draft]");
1664
+ if (composer instanceof HTMLTextAreaElement) {
1665
+ if (!composer.disabled) {
1666
+ try {
1667
+ composer.focus({ preventScroll: true });
1668
+ } catch {
1669
+ try { composer.focus(); } catch {}
1670
+ }
1671
+ if (document.activeElement === composer) replQuickFocusRequested = false;
1672
+ }
1673
+ if (typeof composer.scrollIntoView === "function") {
1674
+ try {
1675
+ composer.scrollIntoView({ block: "nearest", inline: "nearest" });
1676
+ } catch {
1677
+ try { composer.scrollIntoView(); } catch {}
1678
+ }
1679
+ }
1680
+ }
1681
+ }, 0);
1682
+ }
1683
+
1684
+ function focusReplSessionControls() {
1685
+ if (rightView !== "repl" || !critiqueViewEl) return;
1686
+ critiqueViewEl.scrollTop = 0;
1687
+ const sessionSelect = critiqueViewEl.querySelector("[data-repl-session]");
1688
+ const runtimeSelect = critiqueViewEl.querySelector("[data-repl-runtime]");
1689
+ const target = sessionSelect instanceof HTMLSelectElement && !sessionSelect.disabled
1690
+ ? sessionSelect
1691
+ : runtimeSelect;
1692
+ if (target instanceof HTMLElement && typeof target.focus === "function") {
1693
+ try {
1694
+ target.focus({ preventScroll: true });
1695
+ } catch {
1696
+ try { target.focus(); } catch {}
1697
+ }
1698
+ }
1699
+ setStatus("REPL session controls focused.");
1547
1700
  }
1548
1701
 
1549
1702
  function renderReplViewIfActive(options) {
@@ -1553,6 +1706,7 @@
1553
1706
  traceRenderRaf = window.requestAnimationFrame(() => {
1554
1707
  traceRenderRaf = null;
1555
1708
  refreshResponseUi();
1709
+ if (replQuickFocusRequested) focusReplQuickComposer();
1556
1710
  });
1557
1711
  }
1558
1712
 
@@ -1983,14 +2137,17 @@
1983
2137
  function stripStudioReplSubmissionEcho(delta) {
1984
2138
  let value = String(delta || "").replace(/^\s+/, "");
1985
2139
  // The raw mirror below remains raw; Studio record cards hide only the
1986
- // temp-file wrapper used to submit multiline snippets safely. The
1987
- // pi-studio-re fragment catches IPython's wrapped pi-studio-repl paths.
2140
+ // temp-file wrapper used to submit multiline snippets safely. Match
2141
+ // both legacy long Studio roots and compact private control roots. Use
2142
+ // the Python wrapper's final double close because IPython may wrap in
2143
+ // the middle of the word `globals` and add continuation prompts.
1988
2144
  const submissionEchoPatterns = [
1989
- /^.*exec\(open\([\s\S]*?pi-studio-re[\s\S]*?globals\(\)\)\s*$/gm,
1990
- /^.*include\([\s\S]*?pi-studio-re[\s\S]*?\.jl"\)\s*$/gm,
1991
- /^.*source\([\s\S]*?pi-studio-re[\s\S]*?local\s*=\s*\.GlobalEnv\)\s*$/gm,
1992
- /^.*:script\s+[\s\S]*?pi-studio-re[\s\S]*?\.ghci"?\s*$/gm,
1993
- /^.*\(do\s+\(load-file\s+[\s\S]*?pi-studio-re[\s\S]*?:pi-studio\/silent\)\s*$/gm,
2145
+ /^.*exec\(open\([\s\S]*?(?:pi-studio-re|pi-rc-)[\s\S]*?\)\)\s*$/gm,
2146
+ /^.*include\([\s\S]*?(?:pi-studio-re|pi-rc-)[\s\S]*?\.jl"\)\s*$/gm,
2147
+ /^.*source\([\s\S]*?(?:pi-studio-re|pi-rc-)[\s\S]*?local\s*=\s*\.GlobalEnv\)\s*$/gm,
2148
+ /^.*:script\s+[\s\S]*?(?:pi-studio-re|pi-rc-)[\s\S]*?\.ghci"?\s*$/gm,
2149
+ /^.*\(do\s+\(load-file\s+[\s\S]*?(?:pi-studio-re|pi-rc-)[\s\S]*?:pi-studio\/silent\)\s*$/gm,
2150
+ /^.*\.\s+[\s\S]*?(?:pi-studio-re|pi-rc-)[\s\S]*?\.sh[\s\S]*?(?:done\.flag|[a-f0-9]{16}\.done).*$/gm,
1994
2151
  ];
1995
2152
  for (const pattern of submissionEchoPatterns) {
1996
2153
  value = value.replace(pattern, "");
@@ -2026,6 +2183,10 @@
2026
2183
  if (entryIndex < 0) return false;
2027
2184
  const entry = replJournalEntries[entryIndex];
2028
2185
  if (entry.sessionName && sessionName && entry.sessionName !== sessionName) return false;
2186
+ if (!entry.legacyLocal && (entry.status === "captured" || entry.status === "timeout" || entry.status === "error")) {
2187
+ activeReplJournalEntryId = "";
2188
+ return false;
2189
+ }
2029
2190
  const delta = cleanReplCapturedOutput(extractReplTranscriptDelta(entry.beforeTranscript, transcript), entry);
2030
2191
  if (!delta.trim()) return false;
2031
2192
  if (entry.output === delta && entry.status === "captured") return false;
@@ -2177,11 +2338,15 @@
2177
2338
  const session = getActiveReplSessionForCurrentRuntime();
2178
2339
  if (!session) {
2179
2340
  setStatus("Start or select a " + getReplRuntimeLabel(replRuntime) + " REPL session first.", "warning");
2180
- return;
2341
+ return null;
2342
+ }
2343
+ if (replBusy) {
2344
+ setStatus("A REPL request is already in progress.", "warning");
2345
+ return null;
2181
2346
  }
2182
2347
  if (!payload || payload.error) {
2183
2348
  setStatus(payload && payload.error ? payload.error : "Nothing to send to REPL.", "warning");
2184
- return;
2349
+ return null;
2185
2350
  }
2186
2351
  if (payload.noteOnly) {
2187
2352
  if (String(payload.prose || "").trim()) {
@@ -2200,12 +2365,12 @@
2200
2365
  } else {
2201
2366
  setStatus("No code or prose found to send.", "warning");
2202
2367
  }
2203
- return;
2368
+ return null;
2204
2369
  }
2205
2370
  const text = String(payload.text || "");
2206
2371
  if (!text.trim()) {
2207
- setStatus("Editor text is empty.", "warning");
2208
- return;
2372
+ setStatus("REPL submission is empty.", "warning");
2373
+ return null;
2209
2374
  }
2210
2375
  const requestId = makeRequestId();
2211
2376
  const journalEntry = addReplJournalEntry({
@@ -2221,6 +2386,7 @@
2221
2386
  skippedChunks: payload.skippedChunks,
2222
2387
  });
2223
2388
  activeReplJournalEntryId = journalEntry.id;
2389
+ replPendingRequestId = requestId;
2224
2390
  replBusy = true;
2225
2391
  syncActionButtons();
2226
2392
  renderReplViewIfActive({ force: true });
@@ -2231,6 +2397,7 @@
2231
2397
  requestId,
2232
2398
  sessionName: session.sessionName,
2233
2399
  text,
2400
+ echoMode: replEchoMode,
2234
2401
  journalEntryId: journalEntry.id,
2235
2402
  createdAt: journalEntry.createdAt,
2236
2403
  label: journalEntry.label,
@@ -2238,11 +2405,15 @@
2238
2405
  prose: journalEntry.prose,
2239
2406
  skippedChunks: journalEntry.skippedChunks,
2240
2407
  })) {
2241
- replBusy = false;
2408
+ if (replPendingRequestId === requestId) replPendingRequestId = "";
2409
+ replBusy = Boolean(replPendingRequestId);
2242
2410
  replJournalEntries = replJournalEntries.map((entry) => entry.id === journalEntry.id ? { ...entry, status: "error" } : entry);
2243
2411
  persistReplJournalEntries();
2244
2412
  syncActionButtons();
2413
+ renderReplViewIfActive({ force: true });
2414
+ return null;
2245
2415
  }
2416
+ return requestId;
2246
2417
  }
2247
2418
 
2248
2419
  function sendEditorTextToRepl(options) {
@@ -2258,6 +2429,36 @@
2258
2429
  sendReplPayload(replSendMode === "literate" ? buildLiterateReplSendPayload() : buildRawReplSendPayload());
2259
2430
  }
2260
2431
 
2432
+ function submitReplQuickDraft() {
2433
+ const session = getActiveReplSessionForCurrentRuntime();
2434
+ if (!session) {
2435
+ setStatus("Start or select a " + getReplRuntimeLabel(replRuntime) + " REPL session first.", "warning");
2436
+ return false;
2437
+ }
2438
+ const key = getReplQuickDraftKey(session);
2439
+ const text = getReplQuickDraft(session);
2440
+ if (!text.trim()) {
2441
+ setStatus("Quick send is empty.", "warning");
2442
+ return false;
2443
+ }
2444
+ if (replQuickPending && replQuickPending.key === key) {
2445
+ setStatus("Quick send is already being submitted to this REPL.", "warning");
2446
+ return false;
2447
+ }
2448
+ const requestId = sendReplPayload({
2449
+ text,
2450
+ prose: "",
2451
+ label: "quick send",
2452
+ mode: "raw",
2453
+ noteOnly: false,
2454
+ skippedChunks: 0,
2455
+ });
2456
+ if (!requestId) return false;
2457
+ replQuickPending = { requestId, key, text };
2458
+ renderReplViewIfActive({ force: true });
2459
+ return true;
2460
+ }
2461
+
2261
2462
  function renderTraceViewIfActive() {
2262
2463
  if (rightView !== "trace") return;
2263
2464
  if (traceRenderRaf !== null) return;
@@ -2299,9 +2500,15 @@
2299
2500
  let paneFocusTarget = initialPaneFocusTarget;
2300
2501
  let paneSplitPercent = 50;
2301
2502
  const PANE_SPLIT_STORAGE_KEY = "piStudio.paneSplitPercent";
2503
+ const PANE_LAYOUT_STORAGE_KEY = "piStudio.paneLayout";
2504
+ const ACTIVITY_TRACKING_STORAGE_KEY = "piStudio.trackActivity";
2302
2505
  const PANE_SPLIT_MIN_PERCENT = 20;
2303
2506
  const PANE_SPLIT_MAX_PERCENT = 80;
2304
2507
  const PANE_SPLIT_SNAP_TO_CENTER_PERCENT = 1;
2508
+ let studioPaneLayout = readStudioPaneLayout();
2509
+ let activityTrackingEnabled = readActivityTrackingEnabled();
2510
+ let activityTrackingOwnsWorkingView = false;
2511
+ let activityTrackingRequestId = "";
2305
2512
  const STUDIO_WORKSPACE_MAX_TEXT_CHARS = 900_000;
2306
2513
  const STUDIO_WORKSPACE_PERSIST_INTERVAL_MS = 300;
2307
2514
  const STUDIO_WORKSPACE_RECOVERY_FETCH_TIMEOUT_MS = 1_500;
@@ -2837,7 +3044,11 @@
2837
3044
  : "Off";
2838
3045
  const lineLabel = lineNumbersEnabled ? "Lines on" : "Lines off";
2839
3046
  const editorSizeLabel = formatStudioFontSizeLabel(editorFontSize);
2840
- setStudioUiRefreshButtonText(studioUiRefreshUi.viewButton, "View: " + syntaxLabel + " · " + lineLabel + " · " + editorSizeLabel);
3047
+ const extras = [];
3048
+ if (studioPaneLayout === "editor-top") extras.push("Editor above");
3049
+ if (studioPaneLayout === "response-top") extras.push("Response above");
3050
+ if (activityTrackingEnabled) extras.push("Following activity");
3051
+ setStudioUiRefreshButtonText(studioUiRefreshUi.viewButton, "View: " + syntaxLabel + " · " + lineLabel + " · " + editorSizeLabel + (extras.length ? " · " + extras.join(" · ") : ""));
2841
3052
  }
2842
3053
  syncStudioUiRefreshReviewTrigger();
2843
3054
  }
@@ -3108,6 +3319,8 @@
3108
3319
  const viewButton = makeStudioUiRefreshElement("button", "", "View");
3109
3320
  const viewMenu = makeStudioUiRefreshMenu(viewButton, "view", "studio-refresh-view-anchor");
3110
3321
  appendStudioUiRefreshMenuSection(viewMenu.menu, "Display", [highlightSelect, lineNumbersSelect, editorFontSizeSelect]);
3322
+ if (activityTrackingSelect) activityTrackingSelect.hidden = isEditorOnlyMode || isWatchedFilePreview;
3323
+ appendStudioUiRefreshMenuSection(viewMenu.menu, "Workspace", [studioPaneLayoutSelect, (isEditorOnlyMode || isWatchedFilePreview) ? null : activityTrackingSelect]);
3111
3324
  stateEl.appendChild(annotationsMenu.anchor);
3112
3325
  stateEl.appendChild(viewMenu.anchor);
3113
3326
 
@@ -4348,6 +4561,127 @@
4348
4561
  setStatus(descriptor.fileBacked ? "Detached editor from file origin into a new draft." : "Reset editor origin to a new draft.", "success");
4349
4562
  }
4350
4563
 
4564
+ function normalizeStudioPaneLayout(value) {
4565
+ const normalized = String(value || "").trim().toLowerCase();
4566
+ return normalized === "editor-top" || normalized === "response-top" ? normalized : "side-by-side";
4567
+ }
4568
+
4569
+ function readStudioPaneLayout() {
4570
+ try {
4571
+ return normalizeStudioPaneLayout(window.localStorage && window.localStorage.getItem(PANE_LAYOUT_STORAGE_KEY));
4572
+ } catch {
4573
+ return "side-by-side";
4574
+ }
4575
+ }
4576
+
4577
+ function readActivityTrackingEnabled() {
4578
+ try {
4579
+ return Boolean(window.localStorage && window.localStorage.getItem(ACTIVITY_TRACKING_STORAGE_KEY) === "on");
4580
+ } catch {
4581
+ return false;
4582
+ }
4583
+ }
4584
+
4585
+ function isStackedStudioPaneLayout() {
4586
+ return studioPaneLayout === "editor-top" || studioPaneLayout === "response-top";
4587
+ }
4588
+
4589
+ function isResponseFirstStudioPaneLayout() {
4590
+ return studioPaneLayout === "response-top";
4591
+ }
4592
+
4593
+ function getStudioPaneLayoutLabel(value) {
4594
+ if (value === "editor-top") return "Editor above response";
4595
+ if (value === "response-top") return "Response above editor";
4596
+ return "Side by side";
4597
+ }
4598
+
4599
+ function setStudioPaneLayout(value, options) {
4600
+ studioPaneLayout = normalizeStudioPaneLayout(value);
4601
+ document.body.dataset.studioLayout = studioPaneLayout;
4602
+ if (studioPaneLayoutSelect) studioPaneLayoutSelect.value = studioPaneLayout;
4603
+ const mainEl = paneResizeHandleEl && typeof paneResizeHandleEl.closest === "function"
4604
+ ? paneResizeHandleEl.closest("main")
4605
+ : null;
4606
+ if (mainEl && leftPaneEl && rightPaneEl && paneResizeHandleEl) {
4607
+ if (isResponseFirstStudioPaneLayout()) {
4608
+ mainEl.append(rightPaneEl, paneResizeHandleEl, leftPaneEl);
4609
+ } else {
4610
+ mainEl.append(leftPaneEl, paneResizeHandleEl, rightPaneEl);
4611
+ }
4612
+ }
4613
+ applyPaneSplitPercent(paneSplitPercent, { persist: false });
4614
+ if (!options || options.persist !== false) {
4615
+ try {
4616
+ if (window.localStorage) window.localStorage.setItem(PANE_LAYOUT_STORAGE_KEY, studioPaneLayout);
4617
+ } catch {
4618
+ // Ignore localStorage failures.
4619
+ }
4620
+ }
4621
+ syncStudioUiRefreshSummaries();
4622
+ if (!options || !options.silent) {
4623
+ setStatus("Pane layout: " + getStudioPaneLayoutLabel(studioPaneLayout) + ".");
4624
+ }
4625
+ return studioPaneLayout;
4626
+ }
4627
+
4628
+ function setActivityTrackingEnabled(enabled, options) {
4629
+ activityTrackingEnabled = Boolean(enabled) && !isEditorOnlyMode && !isWatchedFilePreview;
4630
+ if (activityTrackingSelect) {
4631
+ activityTrackingSelect.value = activityTrackingEnabled ? "on" : "off";
4632
+ activityTrackingSelect.hidden = isEditorOnlyMode || isWatchedFilePreview;
4633
+ }
4634
+ if (!activityTrackingEnabled) {
4635
+ activityTrackingOwnsWorkingView = false;
4636
+ activityTrackingRequestId = "";
4637
+ }
4638
+ if (!options || options.persist !== false) {
4639
+ try {
4640
+ if (window.localStorage) window.localStorage.setItem(ACTIVITY_TRACKING_STORAGE_KEY, activityTrackingEnabled ? "on" : "off");
4641
+ } catch {
4642
+ // Ignore localStorage failures.
4643
+ }
4644
+ }
4645
+ syncStudioUiRefreshSummaries();
4646
+ if (activityTrackingEnabled && agentBusyFromServer) {
4647
+ beginTrackedStudioActivity(pendingRequestId || "active");
4648
+ }
4649
+ if (!options || !options.silent) {
4650
+ setStatus(activityTrackingEnabled
4651
+ ? "Studio will show Working during main Pi activity, then return to Response Preview."
4652
+ : "Activity following disabled.");
4653
+ }
4654
+ return activityTrackingEnabled;
4655
+ }
4656
+
4657
+ function beginTrackedStudioActivity(requestId) {
4658
+ if (!activityTrackingEnabled || isEditorOnlyMode || isWatchedFilePreview) return false;
4659
+ const normalizedRequestId = String(requestId || "active");
4660
+ if (!activityTrackingOwnsWorkingView && activityTrackingRequestId
4661
+ && (activityTrackingRequestId === normalizedRequestId || activityTrackingRequestId === "active")) {
4662
+ if (activityTrackingRequestId === "active" && normalizedRequestId !== "active") {
4663
+ activityTrackingRequestId = normalizedRequestId;
4664
+ }
4665
+ return false;
4666
+ }
4667
+ if (activityTrackingOwnsWorkingView && activityTrackingRequestId === normalizedRequestId && rightView === "trace") return false;
4668
+ activityTrackingRequestId = normalizedRequestId;
4669
+ activityTrackingOwnsWorkingView = true;
4670
+ if (rightView !== "trace") setRightView("trace", { activityTracking: true });
4671
+ return true;
4672
+ }
4673
+
4674
+ function finishTrackedStudioActivity(requestId) {
4675
+ if (!activityTrackingRequestId && !activityTrackingOwnsWorkingView) return false;
4676
+ const normalizedRequestId = String(requestId || "");
4677
+ if (normalizedRequestId && activityTrackingRequestId !== normalizedRequestId) return false;
4678
+ const shouldReturn = activityTrackingEnabled && activityTrackingOwnsWorkingView && rightView === "trace";
4679
+ activityTrackingOwnsWorkingView = false;
4680
+ activityTrackingRequestId = "";
4681
+ if (shouldReturn) setRightView("preview", { activityTracking: true });
4682
+ return shouldReturn;
4683
+ }
4684
+
4351
4685
  function clampPaneSplitPercent(value) {
4352
4686
  const numeric = Number(value);
4353
4687
  if (!Number.isFinite(numeric)) return 50;
@@ -4358,13 +4692,19 @@
4358
4692
  function applyPaneSplitPercent(percent, options) {
4359
4693
  paneSplitPercent = clampPaneSplitPercent(percent);
4360
4694
  const rightPercent = Math.round((100 - paneSplitPercent) * 10) / 10;
4695
+ const dividerPercent = isResponseFirstStudioPaneLayout() ? rightPercent : paneSplitPercent;
4361
4696
  document.documentElement.style.setProperty("--studio-left-pane-fr", paneSplitPercent + "fr");
4362
4697
  document.documentElement.style.setProperty("--studio-right-pane-fr", rightPercent + "fr");
4363
4698
  if (paneResizeHandleEl) {
4699
+ const stacked = isStackedStudioPaneLayout();
4700
+ paneResizeHandleEl.setAttribute("aria-orientation", stacked ? "horizontal" : "vertical");
4364
4701
  paneResizeHandleEl.setAttribute("aria-valuemin", String(PANE_SPLIT_MIN_PERCENT));
4365
4702
  paneResizeHandleEl.setAttribute("aria-valuemax", String(PANE_SPLIT_MAX_PERCENT));
4366
- paneResizeHandleEl.setAttribute("aria-valuenow", String(Math.round(paneSplitPercent)));
4703
+ paneResizeHandleEl.setAttribute("aria-valuenow", String(Math.round(dividerPercent)));
4367
4704
  paneResizeHandleEl.setAttribute("aria-valuetext", "Editor " + Math.round(paneSplitPercent) + " percent, response " + Math.round(rightPercent) + " percent");
4705
+ paneResizeHandleEl.title = stacked
4706
+ ? "Drag to resize the stacked panes. Double-click to reset."
4707
+ : "Drag to resize the panes. Double-click to reset.";
4368
4708
  }
4369
4709
  if (!options || options.persist !== false) {
4370
4710
  try {
@@ -4396,17 +4736,23 @@
4396
4736
  : null;
4397
4737
  if (!mainEl || typeof mainEl.getBoundingClientRect !== "function") return paneSplitPercent;
4398
4738
  const rect = mainEl.getBoundingClientRect();
4399
- if (!rect.width) return paneSplitPercent;
4400
- const x = typeof event.clientX === "number" ? event.clientX : (rect.left + rect.width / 2);
4401
- return ((x - rect.left) / rect.width) * 100;
4739
+ const stacked = isStackedStudioPaneLayout();
4740
+ const extent = stacked ? rect.height : rect.width;
4741
+ if (!extent) return paneSplitPercent;
4742
+ const coordinate = stacked
4743
+ ? ((typeof event.clientY === "number" ? event.clientY : (rect.top + rect.height / 2)) - rect.top)
4744
+ : ((typeof event.clientX === "number" ? event.clientX : (rect.left + rect.width / 2)) - rect.left);
4745
+ const dividerPercent = (coordinate / extent) * 100;
4746
+ return isResponseFirstStudioPaneLayout() ? 100 - dividerPercent : dividerPercent;
4402
4747
  }
4403
4748
 
4404
4749
  function setupPaneResizeHandle() {
4405
4750
  if (!paneResizeHandleEl) return;
4751
+ setStudioPaneLayout(studioPaneLayout, { persist: false, silent: true });
4406
4752
  loadPaneSplitPercent();
4407
4753
  let dragging = false;
4408
4754
  let movedDuringDrag = false;
4409
- let pointerStartX = 0;
4755
+ let pointerStartPosition = 0;
4410
4756
  let activePaneResizePointerId = null;
4411
4757
  const finishDrag = () => {
4412
4758
  if (!dragging) return;
@@ -4431,7 +4777,9 @@
4431
4777
  event.stopPropagation();
4432
4778
  dragging = true;
4433
4779
  movedDuringDrag = false;
4434
- pointerStartX = typeof event.clientX === "number" ? event.clientX : 0;
4780
+ pointerStartPosition = isStackedStudioPaneLayout()
4781
+ ? (typeof event.clientY === "number" ? event.clientY : 0)
4782
+ : (typeof event.clientX === "number" ? event.clientX : 0);
4435
4783
  activePaneResizePointerId = event.pointerId;
4436
4784
  if (document.body && document.body.classList) document.body.classList.add("pane-resizing");
4437
4785
  try {
@@ -4447,7 +4795,10 @@
4447
4795
  });
4448
4796
  paneResizeHandleEl.addEventListener("pointermove", (event) => {
4449
4797
  if (!dragging) return;
4450
- const movement = typeof event.clientX === "number" ? Math.abs(event.clientX - pointerStartX) : 0;
4798
+ const currentPosition = isStackedStudioPaneLayout()
4799
+ ? (typeof event.clientY === "number" ? event.clientY : pointerStartPosition)
4800
+ : (typeof event.clientX === "number" ? event.clientX : pointerStartPosition);
4801
+ const movement = Math.abs(currentPosition - pointerStartPosition);
4451
4802
  if (!movedDuringDrag && movement < 3) return;
4452
4803
  movedDuringDrag = true;
4453
4804
  event.preventDefault();
@@ -4461,25 +4812,26 @@
4461
4812
  resetPaneSplitPercent();
4462
4813
  });
4463
4814
  paneResizeHandleEl.addEventListener("keydown", (event) => {
4815
+ const stacked = isStackedStudioPaneLayout();
4816
+ const decreaseKey = stacked ? "ArrowUp" : "ArrowLeft";
4817
+ const increaseKey = stacked ? "ArrowDown" : "ArrowRight";
4818
+ let dividerPercent = isResponseFirstStudioPaneLayout() ? 100 - paneSplitPercent : paneSplitPercent;
4464
4819
  if (event.key === "Home") {
4465
- event.preventDefault();
4466
- applyPaneSplitPercent(PANE_SPLIT_MIN_PERCENT);
4467
- return;
4468
- }
4469
- if (event.key === "End") {
4470
- event.preventDefault();
4471
- applyPaneSplitPercent(PANE_SPLIT_MAX_PERCENT);
4472
- return;
4473
- }
4474
- if (event.key === "Enter" || event.key === " " || event.key === "Spacebar") {
4820
+ dividerPercent = PANE_SPLIT_MIN_PERCENT;
4821
+ } else if (event.key === "End") {
4822
+ dividerPercent = PANE_SPLIT_MAX_PERCENT;
4823
+ } else if (event.key === "Enter" || event.key === " " || event.key === "Spacebar") {
4475
4824
  event.preventDefault();
4476
4825
  resetPaneSplitPercent();
4477
4826
  return;
4827
+ } else if (event.key === decreaseKey || event.key === increaseKey) {
4828
+ const step = event.shiftKey ? 10 : 5;
4829
+ dividerPercent += event.key === decreaseKey ? -step : step;
4830
+ } else {
4831
+ return;
4478
4832
  }
4479
- if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return;
4480
4833
  event.preventDefault();
4481
- const step = event.shiftKey ? 10 : 5;
4482
- applyPaneSplitPercent(paneSplitPercent + (event.key === "ArrowLeft" ? -step : step));
4834
+ applyPaneSplitPercent(isResponseFirstStudioPaneLayout() ? 100 - dividerPercent : dividerPercent);
4483
4835
  });
4484
4836
  }
4485
4837
 
@@ -4556,7 +4908,7 @@
4556
4908
  function shouldPreserveScrollForPaneActivationEvent(event) {
4557
4909
  const target = event && event.target;
4558
4910
  if (!(target instanceof Element)) return true;
4559
- if (target.closest("button, select, input, a, [role='button'], .studio-copy-block-btn, .preview-comment-add, .preview-comment-jump")) {
4911
+ if (target.closest("button, select, input, textarea, a, [role='button'], .studio-copy-block-btn, .preview-comment-add, .preview-comment-jump")) {
4560
4912
  return false;
4561
4913
  }
4562
4914
  return true;
@@ -4660,8 +5012,10 @@
4660
5012
  setStatus(label + " is unavailable in editor-only Studio views.", "warning");
4661
5013
  return false;
4662
5014
  }
4663
- const snapshot = snapshotStudioScrollablePositions();
4664
- setRightView(requestedView);
5015
+ const snapshot = snapshotStudioScrollablePositions().filter((entry) => (
5016
+ !(options && options.focusReplComposer && entry.el === critiqueViewEl)
5017
+ ));
5018
+ setRightView(requestedView, options);
4665
5019
  scheduleStudioScrollablePositionRestore(snapshot);
4666
5020
  if (!options || options.announce !== false) {
4667
5021
  setStatus("Right pane view: " + label + ".");
@@ -5047,6 +5401,16 @@
5047
5401
  return;
5048
5402
  }
5049
5403
 
5404
+ const isReplShortcut = (key.toLowerCase() === "r" || code === "KeyR")
5405
+ && (event.metaKey || event.ctrlKey)
5406
+ && event.altKey
5407
+ && !event.shiftKey;
5408
+ if (isReplShortcut) {
5409
+ event.preventDefault();
5410
+ switchRightPaneToView("repl", { focusReplComposer: true });
5411
+ return;
5412
+ }
5413
+
5050
5414
  const isSideQuestionsShortcut = (key.toLowerCase() === "q" || code === "KeyQ")
5051
5415
  && (event.metaKey || event.ctrlKey)
5052
5416
  && event.altKey
@@ -7543,7 +7907,7 @@
7543
7907
  refreshBtn.type = "button";
7544
7908
  refreshBtn.className = "studio-pdf-focus-btn studio-pdf-focus-refresh";
7545
7909
  refreshBtn.textContent = "Refresh";
7546
- refreshBtn.title = "Reload this PDF preview from disk. Shortcut: Cmd/Ctrl+Alt+R.";
7910
+ refreshBtn.title = "Reload this PDF preview from disk. Shortcut: Cmd/Ctrl+Alt+Shift+R.";
7547
7911
  refreshBtn.setAttribute("aria-label", "Refresh PDF preview from disk");
7548
7912
  refreshBtn.addEventListener("click", () => refreshStudioPdfFocusViewer());
7549
7913
  actions.appendChild(refreshBtn);
@@ -8106,7 +8470,7 @@
8106
8470
  const matches = (key === "r" || code === "KeyR")
8107
8471
  && (event.metaKey || event.ctrlKey)
8108
8472
  && event.altKey
8109
- && !event.shiftKey;
8473
+ && event.shiftKey;
8110
8474
  if (!matches) return false;
8111
8475
  if (!refreshVisibleStudioPdfPreviews()) return false;
8112
8476
  event.preventDefault();
@@ -8862,7 +9226,7 @@
8862
9226
  refreshBtn.type = "button";
8863
9227
  refreshBtn.className = "studio-pdf-card-action studio-pdf-card-refresh";
8864
9228
  refreshBtn.textContent = "Refresh";
8865
- refreshBtn.title = "Reload this PDF preview from disk. Shortcut: Cmd/Ctrl+Alt+R.";
9229
+ refreshBtn.title = "Reload this PDF preview from disk. Shortcut: Cmd/Ctrl+Alt+Shift+R.";
8866
9230
  refreshBtn.addEventListener("click", (event) => {
8867
9231
  event.preventDefault();
8868
9232
  event.stopPropagation();
@@ -9828,10 +10192,26 @@
9828
10192
  if (!actionBtn) return;
9829
10193
  event.preventDefault();
9830
10194
  const action = actionBtn.getAttribute("data-repl-action") || "";
10195
+ if (action === "quick-send") {
10196
+ submitReplQuickDraft();
10197
+ return;
10198
+ }
10199
+ if (action === "jump-controls") {
10200
+ focusReplSessionControls();
10201
+ return;
10202
+ }
10203
+ if (action === "quick-clear") {
10204
+ const session = getActiveReplSessionForCurrentRuntime();
10205
+ if (session) setReplQuickDraft(session, "");
10206
+ renderReplViewIfActive({ force: true });
10207
+ focusReplQuickComposer();
10208
+ return;
10209
+ }
9831
10210
  if (action === "start" || action === "new-session") {
9832
10211
  const requestId = makeRequestId();
9833
10212
  const command = getCurrentReplStartCommandFromDom();
9834
10213
  setReplCommandOverride(replRuntime, command);
10214
+ replPendingRequestId = requestId;
9835
10215
  replBusy = true;
9836
10216
  replError = "";
9837
10217
  replMessage = (action === "new-session" ? "Starting new " : "Starting ") + getReplRuntimeLabel(replRuntime) + " session" + (command ? " with custom command" : "") + "…";
@@ -9840,8 +10220,10 @@
9840
10220
  const message = { type: "repl_start_request", requestId, runtime: replRuntime, newSession: action === "new-session" };
9841
10221
  if (command) message.command = command;
9842
10222
  if (!sendMessage(message)) {
9843
- replBusy = false;
10223
+ if (replPendingRequestId === requestId) replPendingRequestId = "";
10224
+ replBusy = Boolean(replPendingRequestId);
9844
10225
  syncActionButtons();
10226
+ renderReplViewIfActive({ force: true });
9845
10227
  }
9846
10228
  return;
9847
10229
  }
@@ -9852,14 +10234,17 @@
9852
10234
  return;
9853
10235
  }
9854
10236
  const requestId = makeRequestId();
10237
+ replPendingRequestId = requestId;
9855
10238
  replBusy = true;
9856
10239
  replError = "";
9857
10240
  replMessage = "Stopping " + session.sessionName + "…";
9858
10241
  syncActionButtons();
9859
10242
  renderReplViewIfActive({ force: true });
9860
10243
  if (!sendMessage({ type: "repl_stop_request", requestId, sessionName: session.sessionName })) {
9861
- replBusy = false;
10244
+ if (replPendingRequestId === requestId) replPendingRequestId = "";
10245
+ replBusy = Boolean(replPendingRequestId);
9862
10246
  syncActionButtons();
10247
+ renderReplViewIfActive({ force: true });
9863
10248
  }
9864
10249
  return;
9865
10250
  }
@@ -9870,14 +10255,17 @@
9870
10255
  return;
9871
10256
  }
9872
10257
  const requestId = makeRequestId();
10258
+ replPendingRequestId = requestId;
9873
10259
  replBusy = true;
9874
10260
  replError = "";
9875
10261
  replMessage = "Interrupting REPL…";
9876
10262
  syncActionButtons();
9877
10263
  renderReplViewIfActive({ force: true });
9878
10264
  if (!sendMessage({ type: "repl_interrupt_request", requestId, sessionName: session.sessionName })) {
9879
- replBusy = false;
10265
+ if (replPendingRequestId === requestId) replPendingRequestId = "";
10266
+ replBusy = Boolean(replPendingRequestId);
9880
10267
  syncActionButtons();
10268
+ renderReplViewIfActive({ force: true });
9881
10269
  }
9882
10270
  return;
9883
10271
  }
@@ -9937,6 +10325,39 @@
9937
10325
  }
9938
10326
  }
9939
10327
 
10328
+ function handleReplPaneFocusIn(event) {
10329
+ if (!replQuickFocusRequested || rightView !== "repl") return;
10330
+ const target = event && event.target;
10331
+ if (target instanceof Element && !target.hasAttribute("data-repl-quick-draft")) {
10332
+ replQuickFocusRequested = false;
10333
+ }
10334
+ }
10335
+
10336
+ function handleReplPaneInput(event) {
10337
+ if (rightView !== "repl") return;
10338
+ const target = event && event.target;
10339
+ if (!(target instanceof HTMLTextAreaElement) || !target.hasAttribute("data-repl-quick-draft")) return;
10340
+ const session = getActiveReplSessionForCurrentRuntime();
10341
+ if (!session) return;
10342
+ const draft = setReplQuickDraft(session, target.value);
10343
+ if (target.value !== draft) target.value = draft;
10344
+ updateReplQuickComposerActions();
10345
+ }
10346
+
10347
+ function handleReplPaneKeydown(event) {
10348
+ if (rightView !== "repl" || !event || event.isComposing) return;
10349
+ const target = event.target;
10350
+ if (!(target instanceof HTMLTextAreaElement) || !target.hasAttribute("data-repl-quick-draft")) return;
10351
+ const submitShortcut = event.key === "Enter"
10352
+ && (event.metaKey || event.ctrlKey)
10353
+ && !event.altKey
10354
+ && !event.shiftKey;
10355
+ if (!submitShortcut) return;
10356
+ event.preventDefault();
10357
+ event.stopPropagation();
10358
+ submitReplQuickDraft();
10359
+ }
10360
+
9940
10361
  function handleReplPaneChange(event) {
9941
10362
  if (rightView !== "repl") return;
9942
10363
  const target = event.target;
@@ -9984,8 +10405,11 @@
9984
10405
  critiqueViewEl.addEventListener("click", handleGitChangesPaneClick);
9985
10406
  critiqueViewEl.addEventListener("click", handleStudioQuartoPreviewClick);
9986
10407
  critiqueViewEl.addEventListener("click", (event) => { void handleSideQuestionClick(event); });
10408
+ critiqueViewEl.addEventListener("focusin", handleReplPaneFocusIn);
9987
10409
  critiqueViewEl.addEventListener("input", handleSideQuestionInput);
10410
+ critiqueViewEl.addEventListener("input", handleReplPaneInput);
9988
10411
  critiqueViewEl.addEventListener("keydown", handleSideQuestionKeydown);
10412
+ critiqueViewEl.addEventListener("keydown", handleReplPaneKeydown);
9989
10413
  critiqueViewEl.addEventListener("change", handleSideQuestionChange);
9990
10414
  critiqueViewEl.addEventListener("change", handleReplPaneChange);
9991
10415
  critiqueViewEl.addEventListener("change", (event) => {
@@ -11721,6 +12145,28 @@
11721
12145
  return "tmux attach -t " + String(session.sessionName || "");
11722
12146
  }
11723
12147
 
12148
+ function buildReplQuickComposerHtml(activeSession, canSendToActiveSession) {
12149
+ const key = getReplQuickDraftKey(activeSession);
12150
+ const draft = getReplQuickDraft(activeSession);
12151
+ const pending = Boolean(replQuickPending && replQuickPending.key === key);
12152
+ const runtimeLabel = activeSession ? getReplRuntimeLabel(activeSession.runtime) : getReplRuntimeLabel(replRuntime);
12153
+ const tmuxTargetLabel = activeSession && activeSession.target ? ("tmux " + activeSession.target) : "";
12154
+ const targetDetails = activeSession
12155
+ ? ([runtimeLabel, activeSession.sessionName, tmuxTargetLabel].filter(Boolean).join(" · "))
12156
+ : (runtimeLabel + " · no session selected");
12157
+ const visibleTargetLabel = activeSession ? activeSession.sessionName : "No session selected";
12158
+ const inputDisabled = !activeSession || !key || pending || replBusy;
12159
+ const sendDisabled = !key || !canSendToActiveSession || wsState === "Disconnected" || pending || !draft.trim();
12160
+ return "<section class='repl-quick-composer' aria-labelledby='replQuickComposerTitle'>"
12161
+ + "<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>"
12162
+ + "<span class='repl-quick-hint'>Enter adds a line · Cmd/Ctrl+Enter sends</span></div>"
12163
+ + "<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>"
12164
+ + "<div class='repl-quick-actions'><span>Raw send through Studio’s safe submission and Shared REPL Record path.</span><div>"
12165
+ + "<button type='button' data-repl-action='quick-clear'" + (pending || !draft ? " disabled" : "") + ">Clear</button>"
12166
+ + "<button class='repl-quick-send-btn' type='button' data-repl-action='quick-send'" + (sendDisabled ? " disabled" : "") + ">" + (pending ? "Sending…" : "Send") + "</button>"
12167
+ + "</div></div></section>";
12168
+ }
12169
+
11724
12170
  function buildReplPanelHtml() {
11725
12171
  const runtimeOptions = [
11726
12172
  ["shell", "Shell"],
@@ -11772,6 +12218,7 @@
11772
12218
  + (replError ? "<div class='repl-notice repl-notice-error'>" + escapeHtml(replError) + "</div>" : "")
11773
12219
  + buildReplJournalHtml(transcript)
11774
12220
  + buildReplStudioActionsHtml()
12221
+ + buildReplQuickComposerHtml(activeSession, canSendToActiveSession)
11775
12222
  + buildReplMirrorHtml(body, transcript)
11776
12223
  + "</div>";
11777
12224
  }
@@ -15020,9 +15467,14 @@
15020
15467
  scheduleWorkspacePersistence();
15021
15468
  }
15022
15469
 
15023
- function setRightView(nextView) {
15470
+ function setRightView(nextView, options) {
15471
+ const automatedActivityChange = Boolean(options && options.activityTracking);
15472
+ if (activityTrackingOwnsWorkingView && !automatedActivityChange) {
15473
+ activityTrackingOwnsWorkingView = false;
15474
+ }
15024
15475
  const previousView = rightView;
15025
15476
  rightView = normalizeRightViewValue(nextView);
15477
+ if (rightView !== "repl") replQuickFocusRequested = false;
15026
15478
  syncRightViewModeOptions();
15027
15479
  rightViewSelect.value = rightView;
15028
15480
  if (rightView === "trace" && previousView !== "trace") {
@@ -15061,6 +15513,9 @@
15061
15513
  if (composer instanceof HTMLTextAreaElement) composer.focus({ preventScroll: true });
15062
15514
  }, 0);
15063
15515
  }
15516
+ if (rightView === "repl" && (previousView !== "repl" || (options && options.focusReplComposer))) {
15517
+ focusReplQuickComposer();
15518
+ }
15064
15519
  scheduleWorkspacePersistence();
15065
15520
  }
15066
15521
 
@@ -22342,6 +22797,7 @@
22342
22797
  ? "Literate send: Send to REPL uses the selection, current fenced code chunk, or all matching chunks if the cursor is outside a chunk."
22343
22798
  : "Raw send: Send to REPL uses the selection, or full editor if no selection.";
22344
22799
  }
22800
+ syncReplEchoModeSelect(showReplSend);
22345
22801
  if (critiqueBtn) {
22346
22802
  critiqueBtn.textContent = "Critique text";
22347
22803
  critiqueBtn.classList.remove("request-stop-active");
@@ -22404,6 +22860,7 @@
22404
22860
  ? "Literate send: Send to REPL uses the selection, current fenced code chunk, or all matching chunks if the cursor is outside a chunk."
22405
22861
  : "Raw send: Send to REPL uses the selection, or full editor if no selection.";
22406
22862
  }
22863
+ syncReplEchoModeSelect(rightView === "repl");
22407
22864
 
22408
22865
  if (critiqueBtn) {
22409
22866
  critiqueBtn.textContent = critiqueIsStop ? "Stop" : "Critique text";
@@ -22934,6 +23391,11 @@
22934
23391
  }
22935
23392
  if (rightView === "side-questions") renderSideQuestionView();
22936
23393
  syncAskAsideButton();
23394
+ if (agentBusyFromServer) {
23395
+ beginTrackedStudioActivity(pendingRequestId || "active");
23396
+ } else {
23397
+ finishTrackedStudioActivity();
23398
+ }
22937
23399
 
22938
23400
  if (pendingRequestId) {
22939
23401
  if (busy) {
@@ -23010,18 +23472,25 @@
23010
23472
  const previousError = replError;
23011
23473
  const previousMessage = replMessage;
23012
23474
  const wasBusy = replBusy;
23475
+ const responseRequestId = typeof message.requestId === "string" ? message.requestId : "";
23476
+ const settlesPendingRequest = Boolean(responseRequestId && responseRequestId === replPendingRequestId);
23477
+ const previousActiveLifetimeKey = getReplQuickDraftKey(getActiveReplSession());
23013
23478
  replTmuxAvailable = typeof message.tmuxAvailable === "boolean" ? message.tmuxAvailable : replTmuxAvailable;
23014
23479
  const sessionsChanged = setReplSessions(message.sessions);
23015
23480
  if (typeof message.activeSessionName === "string" && message.activeSessionName.trim()) {
23016
23481
  setActiveReplSessionForCurrentRuntime(message.activeSessionName);
23017
23482
  }
23018
23483
  const journalChanged = mergeReplJournalEntries(message.journalEntries);
23484
+ reconcileReplQuickPending();
23019
23485
  maybeImportLegacyReplJournalEntries(replActiveSessionName);
23020
23486
  if (typeof message.transcript === "string") replTranscript = trimReplTranscript(message.transcript);
23021
23487
  if (typeof message.capturedAt === "number") replCapturedAt = message.capturedAt;
23022
23488
  replError = typeof message.replError === "string" ? message.replError : (typeof message.captureError === "string" ? message.captureError : "");
23023
23489
  replMessage = typeof message.replMessage === "string" ? message.replMessage : "";
23024
- replBusy = false;
23490
+ if (settlesPendingRequest && replPendingRequestId === responseRequestId) replPendingRequestId = "";
23491
+ replBusy = Boolean(replPendingRequestId);
23492
+ const activeSessionIdentityChanged = Boolean(previousActiveLifetimeKey)
23493
+ && previousActiveLifetimeKey !== getReplQuickDraftKey(getActiveReplSession());
23025
23494
  const controlsChanged = wasBusy
23026
23495
  || sessionsChanged
23027
23496
  || previousTmuxAvailable !== replTmuxAvailable
@@ -23033,7 +23502,7 @@
23033
23502
  || previousMessage !== replMessage
23034
23503
  || journalChanged
23035
23504
  || (!previousCapturedAt && replCapturedAt);
23036
- if (viewChanged) renderReplViewIfActive();
23505
+ if (viewChanged) renderReplViewIfActive({ force: activeSessionIdentityChanged });
23037
23506
  updateReferenceBadge();
23038
23507
  return;
23039
23508
  }
@@ -23058,18 +23527,28 @@
23058
23527
  const previousError = replError;
23059
23528
  const previousMessage = replMessage;
23060
23529
  const wasBusy = replBusy;
23530
+ const responseRequestId = typeof message.requestId === "string" ? message.requestId : "";
23531
+ const settlesPendingRequest = Boolean(responseRequestId && responseRequestId === replPendingRequestId);
23532
+ const previousActiveLifetimeKey = getReplQuickDraftKey(getActiveReplSession());
23061
23533
  let sessionsChanged = false;
23062
23534
  if (message.session) {
23063
23535
  const session = normalizeReplSession(message.session);
23064
- if (session && !replSessions.some((candidate) => candidate.sessionName === session.sessionName)) {
23065
- replSessions = [...replSessions, session];
23066
- sessionsChanged = true;
23536
+ if (session) {
23537
+ const existingIndex = replSessions.findIndex((candidate) => candidate.sessionName === session.sessionName);
23538
+ if (existingIndex < 0) {
23539
+ replSessions = [...replSessions, session];
23540
+ sessionsChanged = true;
23541
+ } else if (serializeReplSessionsForCompare([replSessions[existingIndex]]) !== serializeReplSessionsForCompare([session])) {
23542
+ replSessions = replSessions.map((candidate, index) => index === existingIndex ? session : candidate);
23543
+ sessionsChanged = true;
23544
+ }
23067
23545
  }
23068
23546
  }
23069
23547
  if (typeof message.activeSessionName === "string" && message.activeSessionName.trim()) {
23070
23548
  setActiveReplSessionForCurrentRuntime(message.activeSessionName);
23071
23549
  }
23072
23550
  let journalChanged = mergeReplJournalEntries(message.journalEntries);
23551
+ reconcileReplQuickPending();
23073
23552
  maybeImportLegacyReplJournalEntries(replActiveSessionName);
23074
23553
  if (typeof message.transcript === "string") {
23075
23554
  replTranscript = trimReplTranscript(message.transcript);
@@ -23081,7 +23560,10 @@
23081
23560
  if (typeof message.capturedAt === "number") replCapturedAt = message.capturedAt;
23082
23561
  replError = typeof message.replError === "string" ? message.replError : "";
23083
23562
  if (typeof message.replMessage === "string") replMessage = message.replMessage;
23084
- replBusy = false;
23563
+ if (settlesPendingRequest && replPendingRequestId === responseRequestId) replPendingRequestId = "";
23564
+ replBusy = Boolean(replPendingRequestId);
23565
+ const activeSessionIdentityChanged = Boolean(previousActiveLifetimeKey)
23566
+ && previousActiveLifetimeKey !== getReplQuickDraftKey(getActiveReplSession());
23085
23567
  const controlsChanged = wasBusy || sessionsChanged || previousActiveSessionName !== replActiveSessionName;
23086
23568
  if (controlsChanged) syncActionButtons();
23087
23569
  const viewChanged = controlsChanged
@@ -23090,7 +23572,7 @@
23090
23572
  || previousMessage !== replMessage
23091
23573
  || journalChanged
23092
23574
  || (!previousCapturedAt && replCapturedAt);
23093
- if (viewChanged) renderReplViewIfActive();
23575
+ if (viewChanged) renderReplViewIfActive({ force: activeSessionIdentityChanged });
23094
23576
  updateReferenceBadge();
23095
23577
  return;
23096
23578
  }
@@ -23109,9 +23591,12 @@
23109
23591
  }
23110
23592
 
23111
23593
  if (message.type === "repl_send_ack") {
23112
- replBusy = false;
23594
+ const responseRequestId = typeof message.requestId === "string" ? message.requestId : "";
23595
+ if (responseRequestId && replPendingRequestId === responseRequestId) replPendingRequestId = "";
23596
+ replBusy = Boolean(replPendingRequestId);
23113
23597
  replMessage = "";
23114
23598
  replError = "";
23599
+ if (responseRequestId) settleReplQuickPending(responseRequestId, true);
23115
23600
  mergeReplJournalEntries(message.journalEntries);
23116
23601
  if (typeof message.requestId === "string") {
23117
23602
  replJournalEntries = replJournalEntries.map((entry) => entry.requestId === message.requestId ? { ...entry, status: "sent", updatedAt: Date.now() } : entry);
@@ -23127,6 +23612,8 @@
23127
23612
  pendingRequestId = typeof message.requestId === "string" ? message.requestId : pendingRequestId;
23128
23613
  pendingKind = typeof message.kind === "string" ? message.kind : "unknown";
23129
23614
  stickyStudioKind = pendingKind;
23615
+ agentBusyFromServer = true;
23616
+ beginTrackedStudioActivity(pendingRequestId || "active");
23130
23617
  if (pendingKind === "direct") {
23131
23618
  studioRunChainActive = true;
23132
23619
  }
@@ -23157,6 +23644,7 @@
23157
23644
  const busy = Boolean(message.busy);
23158
23645
  setBusy(busy);
23159
23646
  setWsState(busy ? "Submitting" : "Ready");
23647
+ finishTrackedStudioActivity(typeof message.requestId === "string" ? message.requestId : "");
23160
23648
  setStatus(typeof message.message === "string" ? message.message : "Compaction completed.", "success");
23161
23649
  return;
23162
23650
  }
@@ -23171,6 +23659,7 @@
23171
23659
  const busy = Boolean(message.busy);
23172
23660
  setBusy(busy);
23173
23661
  setWsState(busy ? "Submitting" : "Ready");
23662
+ finishTrackedStudioActivity(typeof message.requestId === "string" ? message.requestId : "");
23174
23663
  setStatus(typeof message.message === "string" ? message.message : "Compaction failed.", "error");
23175
23664
  return;
23176
23665
  }
@@ -23190,6 +23679,7 @@
23190
23679
  pendingRequestId = null;
23191
23680
  pendingKind = null;
23192
23681
  queuedLatestResponse = null;
23682
+ agentBusyFromServer = false;
23193
23683
  setBusy(false);
23194
23684
  setWsState("Ready");
23195
23685
 
@@ -23206,6 +23696,7 @@
23206
23696
  if (!appliedFromHistory && typeof message.markdown === "string") {
23207
23697
  handleIncomingResponse(message.markdown, responseKind, message.timestamp, message.thinking);
23208
23698
  }
23699
+ finishTrackedStudioActivity(completedRequestId || "");
23209
23700
 
23210
23701
  if (responseKind === "critique") {
23211
23702
  setStatus("Critique ready.", "success");
@@ -23508,6 +23999,11 @@
23508
23999
 
23509
24000
  setBusy(busy);
23510
24001
  setWsState(busy ? "Submitting" : "Ready");
24002
+ if (agentBusyFromServer) {
24003
+ beginTrackedStudioActivity(pendingRequestId || "active");
24004
+ } else {
24005
+ finishTrackedStudioActivity();
24006
+ }
23511
24007
 
23512
24008
  if (pendingRequestId) {
23513
24009
  if (busy) {
@@ -23550,6 +24046,7 @@
23550
24046
  clearArmedTitleAttention(message.requestId);
23551
24047
  }
23552
24048
  stickyStudioKind = null;
24049
+ finishTrackedStudioActivity(typeof message.requestId === "string" ? message.requestId : "");
23553
24050
  setBusy(false);
23554
24051
  setWsState("Ready");
23555
24052
  setStatus(typeof message.message === "string" ? message.message : "Studio is busy.", "warning");
@@ -23571,9 +24068,14 @@
23571
24068
  if (typeof message.requestId === "string") {
23572
24069
  clearArmedTitleAttention(message.requestId);
23573
24070
  }
23574
- if (replBusy) {
24071
+ const failedReplRequestId = typeof message.requestId === "string" && message.requestId === replPendingRequestId
24072
+ ? message.requestId
24073
+ : "";
24074
+ if (failedReplRequestId) {
24075
+ replPendingRequestId = "";
23575
24076
  replBusy = false;
23576
24077
  replError = typeof message.message === "string" ? message.message : "REPL request failed.";
24078
+ settleReplQuickPending(failedReplRequestId, false);
23577
24079
  if (typeof message.requestId === "string") {
23578
24080
  replJournalEntries = replJournalEntries.map((entry) => entry.requestId === message.requestId ? { ...entry, status: "error", output: replError, updatedAt: Date.now() } : entry);
23579
24081
  persistReplJournalEntries();
@@ -23581,6 +24083,7 @@
23581
24083
  renderReplViewIfActive({ force: true });
23582
24084
  }
23583
24085
  stickyStudioKind = null;
24086
+ finishTrackedStudioActivity(typeof message.requestId === "string" ? message.requestId : "");
23584
24087
  setBusy(false);
23585
24088
  setWsState("Ready");
23586
24089
  setStatus(typeof message.message === "string" ? message.message : "Request failed.", "error");
@@ -24116,6 +24619,18 @@
24116
24619
  });
24117
24620
  }
24118
24621
 
24622
+ if (studioPaneLayoutSelect) {
24623
+ studioPaneLayoutSelect.addEventListener("change", () => {
24624
+ setStudioPaneLayout(studioPaneLayoutSelect.value);
24625
+ });
24626
+ }
24627
+
24628
+ if (activityTrackingSelect) {
24629
+ activityTrackingSelect.addEventListener("change", () => {
24630
+ setActivityTrackingEnabled(activityTrackingSelect.value === "on");
24631
+ });
24632
+ }
24633
+
24119
24634
  if (annotationModeSelect) {
24120
24635
  annotationModeSelect.addEventListener("change", () => {
24121
24636
  setAnnotationsEnabled(annotationModeSelect.value !== "off");
@@ -24960,6 +25475,19 @@
24960
25475
  });
24961
25476
  }
24962
25477
 
25478
+ if (replEchoModeSelect) {
25479
+ replEchoModeSelect.addEventListener("change", () => {
25480
+ setReplEchoMode(replEchoModeSelect.value);
25481
+ syncActionButtons();
25482
+ setStatus(replEchoMode === "full"
25483
+ ? "Full pane echo enabled. Bounded submitted source code will remain in raw terminal history."
25484
+ : (replEchoMode === "off"
25485
+ ? "Pane echo disabled; new sends will not add alignment anchors to raw terminal history."
25486
+ : "Summary pane echo enabled; short submissions will be shown with compact anchors and a plain output divider."),
25487
+ replEchoMode === "full" ? "warning" : "success");
25488
+ });
25489
+ }
25490
+
24963
25491
  copyDraftBtn.addEventListener("click", async () => {
24964
25492
  const content = sourceTextEl.value;
24965
25493
  if (!content.trim()) {
@@ -25571,6 +26099,7 @@
25571
26099
  const storedLineNumbersEnabled = readStoredEditorLineNumbersEnabled();
25572
26100
  const initialLineNumbersEnabled = storedLineNumbersEnabled ?? Boolean(lineNumbersSelect && lineNumbersSelect.value === "on");
25573
26101
  setLineNumbersEnabled(initialLineNumbersEnabled);
26102
+ setActivityTrackingEnabled(activityTrackingEnabled, { persist: false, silent: true });
25574
26103
 
25575
26104
  const storedResponseHighlightEnabled = readStoredResponseHighlightEnabled();
25576
26105
  const initialResponseHighlightEnabled = storedResponseHighlightEnabled ?? Boolean(responseHighlightSelect && responseHighlightSelect.value === "on");
@@ -25580,6 +26109,7 @@
25580
26109
  const initialAnnotationsEnabled = storedAnnotationsEnabled ?? Boolean(annotationModeSelect ? annotationModeSelect.value !== "off" : true);
25581
26110
  setAnnotationsEnabled(initialAnnotationsEnabled, { silent: true });
25582
26111
  setReplSendMode(replSendMode);
26112
+ setReplEchoMode(replEchoMode);
25583
26113
 
25584
26114
  const sessionWorkspaceState = readPersistedWorkspaceState();
25585
26115
  const serverWorkspaceRecovery = await readServerWorkspaceRecoveryState();