pi-studio 0.9.57 → 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.
@@ -148,6 +148,8 @@
148
148
  const stripAnnotationsBtn = document.getElementById("stripAnnotationsBtn");
149
149
  const highlightSelect = document.getElementById("highlightSelect");
150
150
  const lineNumbersSelect = document.getElementById("lineNumbersSelect");
151
+ const studioPaneLayoutSelect = document.getElementById("studioPaneLayoutSelect");
152
+ const activityTrackingSelect = document.getElementById("activityTrackingSelect");
151
153
  const editorFontSizeSelect = document.getElementById("editorFontSizeSelect");
152
154
  const annotationModeSelect = document.getElementById("annotationModeSelect");
153
155
  const compactBtn = document.getElementById("compactBtn");
@@ -427,8 +429,8 @@
427
429
  rightViewSelect.title = isWatchedFilePreview
428
430
  ? "Read-only watched preview follows this file on disk."
429
431
  : (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.");
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.");
432
434
  }
433
435
 
434
436
  function getInitialRightView(source) {
@@ -481,6 +483,8 @@
481
483
  const REPL_TRANSCRIPT_MAX_CHARS = 200_000;
482
484
  const REPL_JOURNAL_OUTPUT_MAX_CHARS = 80_000;
483
485
  const REPL_JOURNAL_MAX_ENTRIES = 80;
486
+ const REPL_QUICK_DRAFT_MAX_CHARS = 20_000;
487
+ const REPL_QUICK_DRAFT_MAX_SESSIONS = 12;
484
488
  const PDF_EXPORT_FETCH_TIMEOUT_MS = 180_000;
485
489
  const HTML_EXPORT_FETCH_TIMEOUT_MS = 180_000;
486
490
  const HTML_ARTIFACT_MATH_RENDER_FETCH_TIMEOUT_MS = 30_000;
@@ -588,6 +592,10 @@
588
592
  let replFollow = true;
589
593
  let replPollTimer = null;
590
594
  let replBusy = false;
595
+ let replPendingRequestId = "";
596
+ const replQuickDrafts = new Map();
597
+ let replQuickPending = null;
598
+ let replQuickFocusRequested = false;
591
599
  let replSendMode = (() => {
592
600
  try {
593
601
  const stored = window.localStorage && window.localStorage.getItem("piStudio.replSendMode");
@@ -622,7 +630,7 @@
622
630
  mode: typeof entry.mode === "string" ? entry.mode : "raw",
623
631
  prose: typeof entry.prose === "string" ? entry.prose : "",
624
632
  code: typeof entry.code === "string" ? entry.code : "",
625
- output: typeof entry.output === "string" ? entry.output : "",
633
+ output: typeof entry.output === "string" ? stripStudioReplSubmissionEcho(entry.output) : "",
626
634
  beforeTranscript: "",
627
635
  status: typeof entry.status === "string" ? entry.status : "sent",
628
636
  skippedChunks: Math.max(0, Math.floor(Number(entry.skippedChunks) || 0)),
@@ -1302,6 +1310,10 @@
1302
1310
  return {
1303
1311
  sessionName,
1304
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,
1305
1317
  runtime: typeof session.runtime === "string" ? session.runtime : "unknown",
1306
1318
  label: typeof session.label === "string" && session.label.trim() ? session.label.trim() : sessionName,
1307
1319
  source: typeof session.source === "string" ? session.source : "tmux",
@@ -1430,6 +1442,63 @@
1430
1442
  : "Display short submissions in full, truncating after 6 lines or 600 characters, with compact anchors and a plain output divider.");
1431
1443
  }
1432
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
+
1433
1502
  function setReplJournalCollapsed(collapsed) {
1434
1503
  replJournalCollapsed = Boolean(collapsed);
1435
1504
  try {
@@ -1460,6 +1529,8 @@
1460
1529
  runtime: session.runtime,
1461
1530
  source: session.source,
1462
1531
  target: session.target,
1532
+ tmuxSessionId: session.tmuxSessionId,
1533
+ tmuxSessionCreatedAt: session.tmuxSessionCreatedAt,
1463
1534
  })));
1464
1535
  }
1465
1536
 
@@ -1579,7 +1650,53 @@
1579
1650
 
1580
1651
  function isReplControlFocused() {
1581
1652
  const activeEl = document.activeElement;
1582
- 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.");
1583
1700
  }
1584
1701
 
1585
1702
  function renderReplViewIfActive(options) {
@@ -1589,6 +1706,7 @@
1589
1706
  traceRenderRaf = window.requestAnimationFrame(() => {
1590
1707
  traceRenderRaf = null;
1591
1708
  refreshResponseUi();
1709
+ if (replQuickFocusRequested) focusReplQuickComposer();
1592
1710
  });
1593
1711
  }
1594
1712
 
@@ -2020,9 +2138,11 @@
2020
2138
  let value = String(delta || "").replace(/^\s+/, "");
2021
2139
  // The raw mirror below remains raw; Studio record cards hide only the
2022
2140
  // temp-file wrapper used to submit multiline snippets safely. Match
2023
- // both legacy long Studio roots and compact private control roots.
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.
2024
2144
  const submissionEchoPatterns = [
2025
- /^.*exec\(open\([\s\S]*?(?:pi-studio-re|pi-rc-)[\s\S]*?globals\(\)\)\s*$/gm,
2145
+ /^.*exec\(open\([\s\S]*?(?:pi-studio-re|pi-rc-)[\s\S]*?\)\)\s*$/gm,
2026
2146
  /^.*include\([\s\S]*?(?:pi-studio-re|pi-rc-)[\s\S]*?\.jl"\)\s*$/gm,
2027
2147
  /^.*source\([\s\S]*?(?:pi-studio-re|pi-rc-)[\s\S]*?local\s*=\s*\.GlobalEnv\)\s*$/gm,
2028
2148
  /^.*:script\s+[\s\S]*?(?:pi-studio-re|pi-rc-)[\s\S]*?\.ghci"?\s*$/gm,
@@ -2218,11 +2338,15 @@
2218
2338
  const session = getActiveReplSessionForCurrentRuntime();
2219
2339
  if (!session) {
2220
2340
  setStatus("Start or select a " + getReplRuntimeLabel(replRuntime) + " REPL session first.", "warning");
2221
- return;
2341
+ return null;
2342
+ }
2343
+ if (replBusy) {
2344
+ setStatus("A REPL request is already in progress.", "warning");
2345
+ return null;
2222
2346
  }
2223
2347
  if (!payload || payload.error) {
2224
2348
  setStatus(payload && payload.error ? payload.error : "Nothing to send to REPL.", "warning");
2225
- return;
2349
+ return null;
2226
2350
  }
2227
2351
  if (payload.noteOnly) {
2228
2352
  if (String(payload.prose || "").trim()) {
@@ -2241,12 +2365,12 @@
2241
2365
  } else {
2242
2366
  setStatus("No code or prose found to send.", "warning");
2243
2367
  }
2244
- return;
2368
+ return null;
2245
2369
  }
2246
2370
  const text = String(payload.text || "");
2247
2371
  if (!text.trim()) {
2248
- setStatus("Editor text is empty.", "warning");
2249
- return;
2372
+ setStatus("REPL submission is empty.", "warning");
2373
+ return null;
2250
2374
  }
2251
2375
  const requestId = makeRequestId();
2252
2376
  const journalEntry = addReplJournalEntry({
@@ -2262,6 +2386,7 @@
2262
2386
  skippedChunks: payload.skippedChunks,
2263
2387
  });
2264
2388
  activeReplJournalEntryId = journalEntry.id;
2389
+ replPendingRequestId = requestId;
2265
2390
  replBusy = true;
2266
2391
  syncActionButtons();
2267
2392
  renderReplViewIfActive({ force: true });
@@ -2280,11 +2405,15 @@
2280
2405
  prose: journalEntry.prose,
2281
2406
  skippedChunks: journalEntry.skippedChunks,
2282
2407
  })) {
2283
- replBusy = false;
2408
+ if (replPendingRequestId === requestId) replPendingRequestId = "";
2409
+ replBusy = Boolean(replPendingRequestId);
2284
2410
  replJournalEntries = replJournalEntries.map((entry) => entry.id === journalEntry.id ? { ...entry, status: "error" } : entry);
2285
2411
  persistReplJournalEntries();
2286
2412
  syncActionButtons();
2413
+ renderReplViewIfActive({ force: true });
2414
+ return null;
2287
2415
  }
2416
+ return requestId;
2288
2417
  }
2289
2418
 
2290
2419
  function sendEditorTextToRepl(options) {
@@ -2300,6 +2429,36 @@
2300
2429
  sendReplPayload(replSendMode === "literate" ? buildLiterateReplSendPayload() : buildRawReplSendPayload());
2301
2430
  }
2302
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
+
2303
2462
  function renderTraceViewIfActive() {
2304
2463
  if (rightView !== "trace") return;
2305
2464
  if (traceRenderRaf !== null) return;
@@ -2341,9 +2500,15 @@
2341
2500
  let paneFocusTarget = initialPaneFocusTarget;
2342
2501
  let paneSplitPercent = 50;
2343
2502
  const PANE_SPLIT_STORAGE_KEY = "piStudio.paneSplitPercent";
2503
+ const PANE_LAYOUT_STORAGE_KEY = "piStudio.paneLayout";
2504
+ const ACTIVITY_TRACKING_STORAGE_KEY = "piStudio.trackActivity";
2344
2505
  const PANE_SPLIT_MIN_PERCENT = 20;
2345
2506
  const PANE_SPLIT_MAX_PERCENT = 80;
2346
2507
  const PANE_SPLIT_SNAP_TO_CENTER_PERCENT = 1;
2508
+ let studioPaneLayout = readStudioPaneLayout();
2509
+ let activityTrackingEnabled = readActivityTrackingEnabled();
2510
+ let activityTrackingOwnsWorkingView = false;
2511
+ let activityTrackingRequestId = "";
2347
2512
  const STUDIO_WORKSPACE_MAX_TEXT_CHARS = 900_000;
2348
2513
  const STUDIO_WORKSPACE_PERSIST_INTERVAL_MS = 300;
2349
2514
  const STUDIO_WORKSPACE_RECOVERY_FETCH_TIMEOUT_MS = 1_500;
@@ -2879,7 +3044,11 @@
2879
3044
  : "Off";
2880
3045
  const lineLabel = lineNumbersEnabled ? "Lines on" : "Lines off";
2881
3046
  const editorSizeLabel = formatStudioFontSizeLabel(editorFontSize);
2882
- 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(" · ") : ""));
2883
3052
  }
2884
3053
  syncStudioUiRefreshReviewTrigger();
2885
3054
  }
@@ -3150,6 +3319,8 @@
3150
3319
  const viewButton = makeStudioUiRefreshElement("button", "", "View");
3151
3320
  const viewMenu = makeStudioUiRefreshMenu(viewButton, "view", "studio-refresh-view-anchor");
3152
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]);
3153
3324
  stateEl.appendChild(annotationsMenu.anchor);
3154
3325
  stateEl.appendChild(viewMenu.anchor);
3155
3326
 
@@ -4390,6 +4561,127 @@
4390
4561
  setStatus(descriptor.fileBacked ? "Detached editor from file origin into a new draft." : "Reset editor origin to a new draft.", "success");
4391
4562
  }
4392
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
+
4393
4685
  function clampPaneSplitPercent(value) {
4394
4686
  const numeric = Number(value);
4395
4687
  if (!Number.isFinite(numeric)) return 50;
@@ -4400,13 +4692,19 @@
4400
4692
  function applyPaneSplitPercent(percent, options) {
4401
4693
  paneSplitPercent = clampPaneSplitPercent(percent);
4402
4694
  const rightPercent = Math.round((100 - paneSplitPercent) * 10) / 10;
4695
+ const dividerPercent = isResponseFirstStudioPaneLayout() ? rightPercent : paneSplitPercent;
4403
4696
  document.documentElement.style.setProperty("--studio-left-pane-fr", paneSplitPercent + "fr");
4404
4697
  document.documentElement.style.setProperty("--studio-right-pane-fr", rightPercent + "fr");
4405
4698
  if (paneResizeHandleEl) {
4699
+ const stacked = isStackedStudioPaneLayout();
4700
+ paneResizeHandleEl.setAttribute("aria-orientation", stacked ? "horizontal" : "vertical");
4406
4701
  paneResizeHandleEl.setAttribute("aria-valuemin", String(PANE_SPLIT_MIN_PERCENT));
4407
4702
  paneResizeHandleEl.setAttribute("aria-valuemax", String(PANE_SPLIT_MAX_PERCENT));
4408
- paneResizeHandleEl.setAttribute("aria-valuenow", String(Math.round(paneSplitPercent)));
4703
+ paneResizeHandleEl.setAttribute("aria-valuenow", String(Math.round(dividerPercent)));
4409
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.";
4410
4708
  }
4411
4709
  if (!options || options.persist !== false) {
4412
4710
  try {
@@ -4438,17 +4736,23 @@
4438
4736
  : null;
4439
4737
  if (!mainEl || typeof mainEl.getBoundingClientRect !== "function") return paneSplitPercent;
4440
4738
  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;
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;
4444
4747
  }
4445
4748
 
4446
4749
  function setupPaneResizeHandle() {
4447
4750
  if (!paneResizeHandleEl) return;
4751
+ setStudioPaneLayout(studioPaneLayout, { persist: false, silent: true });
4448
4752
  loadPaneSplitPercent();
4449
4753
  let dragging = false;
4450
4754
  let movedDuringDrag = false;
4451
- let pointerStartX = 0;
4755
+ let pointerStartPosition = 0;
4452
4756
  let activePaneResizePointerId = null;
4453
4757
  const finishDrag = () => {
4454
4758
  if (!dragging) return;
@@ -4473,7 +4777,9 @@
4473
4777
  event.stopPropagation();
4474
4778
  dragging = true;
4475
4779
  movedDuringDrag = false;
4476
- 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);
4477
4783
  activePaneResizePointerId = event.pointerId;
4478
4784
  if (document.body && document.body.classList) document.body.classList.add("pane-resizing");
4479
4785
  try {
@@ -4489,7 +4795,10 @@
4489
4795
  });
4490
4796
  paneResizeHandleEl.addEventListener("pointermove", (event) => {
4491
4797
  if (!dragging) return;
4492
- 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);
4493
4802
  if (!movedDuringDrag && movement < 3) return;
4494
4803
  movedDuringDrag = true;
4495
4804
  event.preventDefault();
@@ -4503,25 +4812,26 @@
4503
4812
  resetPaneSplitPercent();
4504
4813
  });
4505
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;
4506
4819
  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") {
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") {
4517
4824
  event.preventDefault();
4518
4825
  resetPaneSplitPercent();
4519
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;
4520
4832
  }
4521
- if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return;
4522
4833
  event.preventDefault();
4523
- const step = event.shiftKey ? 10 : 5;
4524
- applyPaneSplitPercent(paneSplitPercent + (event.key === "ArrowLeft" ? -step : step));
4834
+ applyPaneSplitPercent(isResponseFirstStudioPaneLayout() ? 100 - dividerPercent : dividerPercent);
4525
4835
  });
4526
4836
  }
4527
4837
 
@@ -4598,7 +4908,7 @@
4598
4908
  function shouldPreserveScrollForPaneActivationEvent(event) {
4599
4909
  const target = event && event.target;
4600
4910
  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")) {
4911
+ if (target.closest("button, select, input, textarea, a, [role='button'], .studio-copy-block-btn, .preview-comment-add, .preview-comment-jump")) {
4602
4912
  return false;
4603
4913
  }
4604
4914
  return true;
@@ -4702,8 +5012,10 @@
4702
5012
  setStatus(label + " is unavailable in editor-only Studio views.", "warning");
4703
5013
  return false;
4704
5014
  }
4705
- const snapshot = snapshotStudioScrollablePositions();
4706
- setRightView(requestedView);
5015
+ const snapshot = snapshotStudioScrollablePositions().filter((entry) => (
5016
+ !(options && options.focusReplComposer && entry.el === critiqueViewEl)
5017
+ ));
5018
+ setRightView(requestedView, options);
4707
5019
  scheduleStudioScrollablePositionRestore(snapshot);
4708
5020
  if (!options || options.announce !== false) {
4709
5021
  setStatus("Right pane view: " + label + ".");
@@ -5089,6 +5401,16 @@
5089
5401
  return;
5090
5402
  }
5091
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
+
5092
5414
  const isSideQuestionsShortcut = (key.toLowerCase() === "q" || code === "KeyQ")
5093
5415
  && (event.metaKey || event.ctrlKey)
5094
5416
  && event.altKey
@@ -7585,7 +7907,7 @@
7585
7907
  refreshBtn.type = "button";
7586
7908
  refreshBtn.className = "studio-pdf-focus-btn studio-pdf-focus-refresh";
7587
7909
  refreshBtn.textContent = "Refresh";
7588
- 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.";
7589
7911
  refreshBtn.setAttribute("aria-label", "Refresh PDF preview from disk");
7590
7912
  refreshBtn.addEventListener("click", () => refreshStudioPdfFocusViewer());
7591
7913
  actions.appendChild(refreshBtn);
@@ -8148,7 +8470,7 @@
8148
8470
  const matches = (key === "r" || code === "KeyR")
8149
8471
  && (event.metaKey || event.ctrlKey)
8150
8472
  && event.altKey
8151
- && !event.shiftKey;
8473
+ && event.shiftKey;
8152
8474
  if (!matches) return false;
8153
8475
  if (!refreshVisibleStudioPdfPreviews()) return false;
8154
8476
  event.preventDefault();
@@ -8904,7 +9226,7 @@
8904
9226
  refreshBtn.type = "button";
8905
9227
  refreshBtn.className = "studio-pdf-card-action studio-pdf-card-refresh";
8906
9228
  refreshBtn.textContent = "Refresh";
8907
- 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.";
8908
9230
  refreshBtn.addEventListener("click", (event) => {
8909
9231
  event.preventDefault();
8910
9232
  event.stopPropagation();
@@ -9870,10 +10192,26 @@
9870
10192
  if (!actionBtn) return;
9871
10193
  event.preventDefault();
9872
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
+ }
9873
10210
  if (action === "start" || action === "new-session") {
9874
10211
  const requestId = makeRequestId();
9875
10212
  const command = getCurrentReplStartCommandFromDom();
9876
10213
  setReplCommandOverride(replRuntime, command);
10214
+ replPendingRequestId = requestId;
9877
10215
  replBusy = true;
9878
10216
  replError = "";
9879
10217
  replMessage = (action === "new-session" ? "Starting new " : "Starting ") + getReplRuntimeLabel(replRuntime) + " session" + (command ? " with custom command" : "") + "…";
@@ -9882,8 +10220,10 @@
9882
10220
  const message = { type: "repl_start_request", requestId, runtime: replRuntime, newSession: action === "new-session" };
9883
10221
  if (command) message.command = command;
9884
10222
  if (!sendMessage(message)) {
9885
- replBusy = false;
10223
+ if (replPendingRequestId === requestId) replPendingRequestId = "";
10224
+ replBusy = Boolean(replPendingRequestId);
9886
10225
  syncActionButtons();
10226
+ renderReplViewIfActive({ force: true });
9887
10227
  }
9888
10228
  return;
9889
10229
  }
@@ -9894,14 +10234,17 @@
9894
10234
  return;
9895
10235
  }
9896
10236
  const requestId = makeRequestId();
10237
+ replPendingRequestId = requestId;
9897
10238
  replBusy = true;
9898
10239
  replError = "";
9899
10240
  replMessage = "Stopping " + session.sessionName + "…";
9900
10241
  syncActionButtons();
9901
10242
  renderReplViewIfActive({ force: true });
9902
10243
  if (!sendMessage({ type: "repl_stop_request", requestId, sessionName: session.sessionName })) {
9903
- replBusy = false;
10244
+ if (replPendingRequestId === requestId) replPendingRequestId = "";
10245
+ replBusy = Boolean(replPendingRequestId);
9904
10246
  syncActionButtons();
10247
+ renderReplViewIfActive({ force: true });
9905
10248
  }
9906
10249
  return;
9907
10250
  }
@@ -9912,14 +10255,17 @@
9912
10255
  return;
9913
10256
  }
9914
10257
  const requestId = makeRequestId();
10258
+ replPendingRequestId = requestId;
9915
10259
  replBusy = true;
9916
10260
  replError = "";
9917
10261
  replMessage = "Interrupting REPL…";
9918
10262
  syncActionButtons();
9919
10263
  renderReplViewIfActive({ force: true });
9920
10264
  if (!sendMessage({ type: "repl_interrupt_request", requestId, sessionName: session.sessionName })) {
9921
- replBusy = false;
10265
+ if (replPendingRequestId === requestId) replPendingRequestId = "";
10266
+ replBusy = Boolean(replPendingRequestId);
9922
10267
  syncActionButtons();
10268
+ renderReplViewIfActive({ force: true });
9923
10269
  }
9924
10270
  return;
9925
10271
  }
@@ -9979,6 +10325,39 @@
9979
10325
  }
9980
10326
  }
9981
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
+
9982
10361
  function handleReplPaneChange(event) {
9983
10362
  if (rightView !== "repl") return;
9984
10363
  const target = event.target;
@@ -10026,8 +10405,11 @@
10026
10405
  critiqueViewEl.addEventListener("click", handleGitChangesPaneClick);
10027
10406
  critiqueViewEl.addEventListener("click", handleStudioQuartoPreviewClick);
10028
10407
  critiqueViewEl.addEventListener("click", (event) => { void handleSideQuestionClick(event); });
10408
+ critiqueViewEl.addEventListener("focusin", handleReplPaneFocusIn);
10029
10409
  critiqueViewEl.addEventListener("input", handleSideQuestionInput);
10410
+ critiqueViewEl.addEventListener("input", handleReplPaneInput);
10030
10411
  critiqueViewEl.addEventListener("keydown", handleSideQuestionKeydown);
10412
+ critiqueViewEl.addEventListener("keydown", handleReplPaneKeydown);
10031
10413
  critiqueViewEl.addEventListener("change", handleSideQuestionChange);
10032
10414
  critiqueViewEl.addEventListener("change", handleReplPaneChange);
10033
10415
  critiqueViewEl.addEventListener("change", (event) => {
@@ -11763,6 +12145,28 @@
11763
12145
  return "tmux attach -t " + String(session.sessionName || "");
11764
12146
  }
11765
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
+
11766
12170
  function buildReplPanelHtml() {
11767
12171
  const runtimeOptions = [
11768
12172
  ["shell", "Shell"],
@@ -11814,6 +12218,7 @@
11814
12218
  + (replError ? "<div class='repl-notice repl-notice-error'>" + escapeHtml(replError) + "</div>" : "")
11815
12219
  + buildReplJournalHtml(transcript)
11816
12220
  + buildReplStudioActionsHtml()
12221
+ + buildReplQuickComposerHtml(activeSession, canSendToActiveSession)
11817
12222
  + buildReplMirrorHtml(body, transcript)
11818
12223
  + "</div>";
11819
12224
  }
@@ -15062,9 +15467,14 @@
15062
15467
  scheduleWorkspacePersistence();
15063
15468
  }
15064
15469
 
15065
- function setRightView(nextView) {
15470
+ function setRightView(nextView, options) {
15471
+ const automatedActivityChange = Boolean(options && options.activityTracking);
15472
+ if (activityTrackingOwnsWorkingView && !automatedActivityChange) {
15473
+ activityTrackingOwnsWorkingView = false;
15474
+ }
15066
15475
  const previousView = rightView;
15067
15476
  rightView = normalizeRightViewValue(nextView);
15477
+ if (rightView !== "repl") replQuickFocusRequested = false;
15068
15478
  syncRightViewModeOptions();
15069
15479
  rightViewSelect.value = rightView;
15070
15480
  if (rightView === "trace" && previousView !== "trace") {
@@ -15103,6 +15513,9 @@
15103
15513
  if (composer instanceof HTMLTextAreaElement) composer.focus({ preventScroll: true });
15104
15514
  }, 0);
15105
15515
  }
15516
+ if (rightView === "repl" && (previousView !== "repl" || (options && options.focusReplComposer))) {
15517
+ focusReplQuickComposer();
15518
+ }
15106
15519
  scheduleWorkspacePersistence();
15107
15520
  }
15108
15521
 
@@ -22978,6 +23391,11 @@
22978
23391
  }
22979
23392
  if (rightView === "side-questions") renderSideQuestionView();
22980
23393
  syncAskAsideButton();
23394
+ if (agentBusyFromServer) {
23395
+ beginTrackedStudioActivity(pendingRequestId || "active");
23396
+ } else {
23397
+ finishTrackedStudioActivity();
23398
+ }
22981
23399
 
22982
23400
  if (pendingRequestId) {
22983
23401
  if (busy) {
@@ -23054,18 +23472,25 @@
23054
23472
  const previousError = replError;
23055
23473
  const previousMessage = replMessage;
23056
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());
23057
23478
  replTmuxAvailable = typeof message.tmuxAvailable === "boolean" ? message.tmuxAvailable : replTmuxAvailable;
23058
23479
  const sessionsChanged = setReplSessions(message.sessions);
23059
23480
  if (typeof message.activeSessionName === "string" && message.activeSessionName.trim()) {
23060
23481
  setActiveReplSessionForCurrentRuntime(message.activeSessionName);
23061
23482
  }
23062
23483
  const journalChanged = mergeReplJournalEntries(message.journalEntries);
23484
+ reconcileReplQuickPending();
23063
23485
  maybeImportLegacyReplJournalEntries(replActiveSessionName);
23064
23486
  if (typeof message.transcript === "string") replTranscript = trimReplTranscript(message.transcript);
23065
23487
  if (typeof message.capturedAt === "number") replCapturedAt = message.capturedAt;
23066
23488
  replError = typeof message.replError === "string" ? message.replError : (typeof message.captureError === "string" ? message.captureError : "");
23067
23489
  replMessage = typeof message.replMessage === "string" ? message.replMessage : "";
23068
- replBusy = false;
23490
+ if (settlesPendingRequest && replPendingRequestId === responseRequestId) replPendingRequestId = "";
23491
+ replBusy = Boolean(replPendingRequestId);
23492
+ const activeSessionIdentityChanged = Boolean(previousActiveLifetimeKey)
23493
+ && previousActiveLifetimeKey !== getReplQuickDraftKey(getActiveReplSession());
23069
23494
  const controlsChanged = wasBusy
23070
23495
  || sessionsChanged
23071
23496
  || previousTmuxAvailable !== replTmuxAvailable
@@ -23077,7 +23502,7 @@
23077
23502
  || previousMessage !== replMessage
23078
23503
  || journalChanged
23079
23504
  || (!previousCapturedAt && replCapturedAt);
23080
- if (viewChanged) renderReplViewIfActive();
23505
+ if (viewChanged) renderReplViewIfActive({ force: activeSessionIdentityChanged });
23081
23506
  updateReferenceBadge();
23082
23507
  return;
23083
23508
  }
@@ -23102,18 +23527,28 @@
23102
23527
  const previousError = replError;
23103
23528
  const previousMessage = replMessage;
23104
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());
23105
23533
  let sessionsChanged = false;
23106
23534
  if (message.session) {
23107
23535
  const session = normalizeReplSession(message.session);
23108
- if (session && !replSessions.some((candidate) => candidate.sessionName === session.sessionName)) {
23109
- replSessions = [...replSessions, session];
23110
- 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
+ }
23111
23545
  }
23112
23546
  }
23113
23547
  if (typeof message.activeSessionName === "string" && message.activeSessionName.trim()) {
23114
23548
  setActiveReplSessionForCurrentRuntime(message.activeSessionName);
23115
23549
  }
23116
23550
  let journalChanged = mergeReplJournalEntries(message.journalEntries);
23551
+ reconcileReplQuickPending();
23117
23552
  maybeImportLegacyReplJournalEntries(replActiveSessionName);
23118
23553
  if (typeof message.transcript === "string") {
23119
23554
  replTranscript = trimReplTranscript(message.transcript);
@@ -23125,7 +23560,10 @@
23125
23560
  if (typeof message.capturedAt === "number") replCapturedAt = message.capturedAt;
23126
23561
  replError = typeof message.replError === "string" ? message.replError : "";
23127
23562
  if (typeof message.replMessage === "string") replMessage = message.replMessage;
23128
- replBusy = false;
23563
+ if (settlesPendingRequest && replPendingRequestId === responseRequestId) replPendingRequestId = "";
23564
+ replBusy = Boolean(replPendingRequestId);
23565
+ const activeSessionIdentityChanged = Boolean(previousActiveLifetimeKey)
23566
+ && previousActiveLifetimeKey !== getReplQuickDraftKey(getActiveReplSession());
23129
23567
  const controlsChanged = wasBusy || sessionsChanged || previousActiveSessionName !== replActiveSessionName;
23130
23568
  if (controlsChanged) syncActionButtons();
23131
23569
  const viewChanged = controlsChanged
@@ -23134,7 +23572,7 @@
23134
23572
  || previousMessage !== replMessage
23135
23573
  || journalChanged
23136
23574
  || (!previousCapturedAt && replCapturedAt);
23137
- if (viewChanged) renderReplViewIfActive();
23575
+ if (viewChanged) renderReplViewIfActive({ force: activeSessionIdentityChanged });
23138
23576
  updateReferenceBadge();
23139
23577
  return;
23140
23578
  }
@@ -23153,9 +23591,12 @@
23153
23591
  }
23154
23592
 
23155
23593
  if (message.type === "repl_send_ack") {
23156
- replBusy = false;
23594
+ const responseRequestId = typeof message.requestId === "string" ? message.requestId : "";
23595
+ if (responseRequestId && replPendingRequestId === responseRequestId) replPendingRequestId = "";
23596
+ replBusy = Boolean(replPendingRequestId);
23157
23597
  replMessage = "";
23158
23598
  replError = "";
23599
+ if (responseRequestId) settleReplQuickPending(responseRequestId, true);
23159
23600
  mergeReplJournalEntries(message.journalEntries);
23160
23601
  if (typeof message.requestId === "string") {
23161
23602
  replJournalEntries = replJournalEntries.map((entry) => entry.requestId === message.requestId ? { ...entry, status: "sent", updatedAt: Date.now() } : entry);
@@ -23171,6 +23612,8 @@
23171
23612
  pendingRequestId = typeof message.requestId === "string" ? message.requestId : pendingRequestId;
23172
23613
  pendingKind = typeof message.kind === "string" ? message.kind : "unknown";
23173
23614
  stickyStudioKind = pendingKind;
23615
+ agentBusyFromServer = true;
23616
+ beginTrackedStudioActivity(pendingRequestId || "active");
23174
23617
  if (pendingKind === "direct") {
23175
23618
  studioRunChainActive = true;
23176
23619
  }
@@ -23201,6 +23644,7 @@
23201
23644
  const busy = Boolean(message.busy);
23202
23645
  setBusy(busy);
23203
23646
  setWsState(busy ? "Submitting" : "Ready");
23647
+ finishTrackedStudioActivity(typeof message.requestId === "string" ? message.requestId : "");
23204
23648
  setStatus(typeof message.message === "string" ? message.message : "Compaction completed.", "success");
23205
23649
  return;
23206
23650
  }
@@ -23215,6 +23659,7 @@
23215
23659
  const busy = Boolean(message.busy);
23216
23660
  setBusy(busy);
23217
23661
  setWsState(busy ? "Submitting" : "Ready");
23662
+ finishTrackedStudioActivity(typeof message.requestId === "string" ? message.requestId : "");
23218
23663
  setStatus(typeof message.message === "string" ? message.message : "Compaction failed.", "error");
23219
23664
  return;
23220
23665
  }
@@ -23234,6 +23679,7 @@
23234
23679
  pendingRequestId = null;
23235
23680
  pendingKind = null;
23236
23681
  queuedLatestResponse = null;
23682
+ agentBusyFromServer = false;
23237
23683
  setBusy(false);
23238
23684
  setWsState("Ready");
23239
23685
 
@@ -23250,6 +23696,7 @@
23250
23696
  if (!appliedFromHistory && typeof message.markdown === "string") {
23251
23697
  handleIncomingResponse(message.markdown, responseKind, message.timestamp, message.thinking);
23252
23698
  }
23699
+ finishTrackedStudioActivity(completedRequestId || "");
23253
23700
 
23254
23701
  if (responseKind === "critique") {
23255
23702
  setStatus("Critique ready.", "success");
@@ -23552,6 +23999,11 @@
23552
23999
 
23553
24000
  setBusy(busy);
23554
24001
  setWsState(busy ? "Submitting" : "Ready");
24002
+ if (agentBusyFromServer) {
24003
+ beginTrackedStudioActivity(pendingRequestId || "active");
24004
+ } else {
24005
+ finishTrackedStudioActivity();
24006
+ }
23555
24007
 
23556
24008
  if (pendingRequestId) {
23557
24009
  if (busy) {
@@ -23594,6 +24046,7 @@
23594
24046
  clearArmedTitleAttention(message.requestId);
23595
24047
  }
23596
24048
  stickyStudioKind = null;
24049
+ finishTrackedStudioActivity(typeof message.requestId === "string" ? message.requestId : "");
23597
24050
  setBusy(false);
23598
24051
  setWsState("Ready");
23599
24052
  setStatus(typeof message.message === "string" ? message.message : "Studio is busy.", "warning");
@@ -23615,9 +24068,14 @@
23615
24068
  if (typeof message.requestId === "string") {
23616
24069
  clearArmedTitleAttention(message.requestId);
23617
24070
  }
23618
- if (replBusy) {
24071
+ const failedReplRequestId = typeof message.requestId === "string" && message.requestId === replPendingRequestId
24072
+ ? message.requestId
24073
+ : "";
24074
+ if (failedReplRequestId) {
24075
+ replPendingRequestId = "";
23619
24076
  replBusy = false;
23620
24077
  replError = typeof message.message === "string" ? message.message : "REPL request failed.";
24078
+ settleReplQuickPending(failedReplRequestId, false);
23621
24079
  if (typeof message.requestId === "string") {
23622
24080
  replJournalEntries = replJournalEntries.map((entry) => entry.requestId === message.requestId ? { ...entry, status: "error", output: replError, updatedAt: Date.now() } : entry);
23623
24081
  persistReplJournalEntries();
@@ -23625,6 +24083,7 @@
23625
24083
  renderReplViewIfActive({ force: true });
23626
24084
  }
23627
24085
  stickyStudioKind = null;
24086
+ finishTrackedStudioActivity(typeof message.requestId === "string" ? message.requestId : "");
23628
24087
  setBusy(false);
23629
24088
  setWsState("Ready");
23630
24089
  setStatus(typeof message.message === "string" ? message.message : "Request failed.", "error");
@@ -24160,6 +24619,18 @@
24160
24619
  });
24161
24620
  }
24162
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
+
24163
24634
  if (annotationModeSelect) {
24164
24635
  annotationModeSelect.addEventListener("change", () => {
24165
24636
  setAnnotationsEnabled(annotationModeSelect.value !== "off");
@@ -25628,6 +26099,7 @@
25628
26099
  const storedLineNumbersEnabled = readStoredEditorLineNumbersEnabled();
25629
26100
  const initialLineNumbersEnabled = storedLineNumbersEnabled ?? Boolean(lineNumbersSelect && lineNumbersSelect.value === "on");
25630
26101
  setLineNumbersEnabled(initialLineNumbersEnabled);
26102
+ setActivityTrackingEnabled(activityTrackingEnabled, { persist: false, silent: true });
25631
26103
 
25632
26104
  const storedResponseHighlightEnabled = readStoredResponseHighlightEnabled();
25633
26105
  const initialResponseHighlightEnabled = storedResponseHighlightEnabled ?? Boolean(responseHighlightSelect && responseHighlightSelect.value === "on");