pi-studio 0.9.4 → 0.9.6

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.
@@ -237,6 +237,7 @@
237
237
  const HTML_EXPORT_FETCH_TIMEOUT_MS = 180_000;
238
238
  const EDITOR_TAB_TEXT = " ";
239
239
  const QUIZ_DEFAULT_COUNT = 5;
240
+ const QUIZ_SCOPES = ["editor", "selection", "file", "folder", "repo"];
240
241
  const QUIZ_ANGLES = ["general", "scientist", "mathematician", "statistician", "developer", "reviewer"];
241
242
  const QUIZ_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high"];
242
243
  let quizOverlayEl = null;
@@ -249,6 +250,11 @@
249
250
  pending: false,
250
251
  sourceText: "",
251
252
  sourceLabel: "Studio editor",
253
+ sourcePath: "",
254
+ contextPath: "",
255
+ resourceDir: "",
256
+ focusPrompt: "",
257
+ includeEditorContext: false,
252
258
  scope: "editor",
253
259
  angle: "general",
254
260
  thinking: "minimal",
@@ -286,27 +292,33 @@
286
292
  return "raw";
287
293
  }
288
294
  })();
295
+ function normalizeReplJournalEntry(entry) {
296
+ if (!entry || typeof entry !== "object") return null;
297
+ const normalized = {
298
+ id: typeof entry.id === "string" && entry.id ? entry.id : ("repl-journal-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 8)),
299
+ requestId: typeof entry.requestId === "string" ? entry.requestId : "",
300
+ createdAt: typeof entry.createdAt === "number" && Number.isFinite(entry.createdAt) ? entry.createdAt : Date.now(),
301
+ updatedAt: typeof entry.updatedAt === "number" && Number.isFinite(entry.updatedAt) ? entry.updatedAt : Date.now(),
302
+ sessionName: typeof entry.sessionName === "string" ? entry.sessionName : "",
303
+ runtime: typeof entry.runtime === "string" ? entry.runtime : "python",
304
+ label: typeof entry.label === "string" ? entry.label : "REPL send",
305
+ mode: typeof entry.mode === "string" ? entry.mode : "raw",
306
+ prose: typeof entry.prose === "string" ? entry.prose : "",
307
+ code: typeof entry.code === "string" ? entry.code : "",
308
+ output: typeof entry.output === "string" ? entry.output : "",
309
+ beforeTranscript: "",
310
+ status: typeof entry.status === "string" ? entry.status : "sent",
311
+ skippedChunks: Math.max(0, Math.floor(Number(entry.skippedChunks) || 0)),
312
+ };
313
+ return (normalized.code.trim() || normalized.prose.trim() || normalized.output.trim()) ? normalized : null;
314
+ }
315
+
289
316
  function loadPersistedReplJournalEntries() {
290
317
  try {
291
318
  const raw = window.localStorage ? window.localStorage.getItem("piStudio.replStudioEntries.v1") : null;
292
319
  const parsed = raw ? JSON.parse(raw) : [];
293
320
  if (!Array.isArray(parsed)) return [];
294
- return parsed.map((entry) => ({
295
- id: typeof entry.id === "string" && entry.id ? entry.id : ("repl-journal-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 8)),
296
- requestId: typeof entry.requestId === "string" ? entry.requestId : "",
297
- createdAt: typeof entry.createdAt === "number" && Number.isFinite(entry.createdAt) ? entry.createdAt : Date.now(),
298
- updatedAt: typeof entry.updatedAt === "number" && Number.isFinite(entry.updatedAt) ? entry.updatedAt : Date.now(),
299
- sessionName: typeof entry.sessionName === "string" ? entry.sessionName : "",
300
- runtime: typeof entry.runtime === "string" ? entry.runtime : "python",
301
- label: typeof entry.label === "string" ? entry.label : "REPL send",
302
- mode: typeof entry.mode === "string" ? entry.mode : "raw",
303
- prose: typeof entry.prose === "string" ? entry.prose : "",
304
- code: typeof entry.code === "string" ? entry.code : "",
305
- output: typeof entry.output === "string" ? entry.output : "",
306
- beforeTranscript: "",
307
- status: typeof entry.status === "string" ? entry.status : "sent",
308
- skippedChunks: Math.max(0, Math.floor(Number(entry.skippedChunks) || 0)),
309
- })).filter((entry) => entry.code.trim() || entry.prose.trim() || entry.output.trim()).slice(-REPL_JOURNAL_MAX_ENTRIES);
321
+ return parsed.map(normalizeReplJournalEntry).filter(Boolean).slice(-REPL_JOURNAL_MAX_ENTRIES);
310
322
  } catch {
311
323
  return [];
312
324
  }
@@ -336,6 +348,47 @@
336
348
  }
337
349
  }
338
350
 
351
+ function mergeReplJournalEntries(entries) {
352
+ if (!Array.isArray(entries) || !entries.length) return false;
353
+ let changed = false;
354
+ const next = [...replJournalEntries];
355
+ for (const rawEntry of entries) {
356
+ const entry = normalizeReplJournalEntry(rawEntry);
357
+ if (!entry) continue;
358
+ const existingIndex = next.findIndex((candidate) => (
359
+ (entry.requestId && candidate.requestId === entry.requestId)
360
+ || candidate.id === entry.id
361
+ ));
362
+ if (existingIndex >= 0) {
363
+ const existing = next[existingIndex];
364
+ const merged = {
365
+ ...existing,
366
+ ...entry,
367
+ label: existing.label || entry.label,
368
+ mode: existing.mode || entry.mode,
369
+ prose: existing.prose || entry.prose,
370
+ beforeTranscript: existing.beforeTranscript || "",
371
+ createdAt: existing.createdAt || entry.createdAt,
372
+ updatedAt: Math.max(existing.updatedAt || 0, entry.updatedAt || 0),
373
+ skippedChunks: existing.skippedChunks || entry.skippedChunks,
374
+ };
375
+ if (JSON.stringify(existing) !== JSON.stringify(merged)) {
376
+ next[existingIndex] = merged;
377
+ changed = true;
378
+ }
379
+ } else {
380
+ next.push(entry);
381
+ changed = true;
382
+ }
383
+ }
384
+ if (!changed) return false;
385
+ replJournalEntries = next
386
+ .sort((a, b) => (a.createdAt || 0) - (b.createdAt || 0))
387
+ .slice(-REPL_JOURNAL_MAX_ENTRIES);
388
+ persistReplJournalEntries();
389
+ return true;
390
+ }
391
+
339
392
  let replJournalEntries = loadPersistedReplJournalEntries();
340
393
  let activeReplJournalEntryId = "";
341
394
  let replJournalCollapsed = (() => {
@@ -919,6 +972,27 @@
919
972
  return String(value || "").trim().toLowerCase() === "literate" ? "literate" : "raw";
920
973
  }
921
974
 
975
+ function isMacShortcutPlatform() {
976
+ try {
977
+ const platform = String((navigator && navigator.platform) || "");
978
+ return /Mac|iPhone|iPad|iPod/i.test(platform);
979
+ } catch {
980
+ return true;
981
+ }
982
+ }
983
+
984
+ function getStudioShortcutLabel(kind) {
985
+ const mac = isMacShortcutPlatform();
986
+ if (kind === "repl-send") return mac ? "⇧⌘↵" : "Ctrl+Shift+Enter";
987
+ if (kind === "run") return mac ? "⌘↵" : "Ctrl+Enter";
988
+ return "";
989
+ }
990
+
991
+ function withStudioShortcutLabel(label, kind) {
992
+ const shortcut = getStudioShortcutLabel(kind);
993
+ return shortcut ? (label + " " + shortcut) : label;
994
+ }
995
+
922
996
  function setReplSendMode(mode) {
923
997
  replSendMode = normalizeReplSendMode(mode);
924
998
  if (replSendModeSelect) replSendModeSelect.value = replSendMode;
@@ -1005,9 +1079,18 @@
1005
1079
  function setActiveReplSession(sessionName) {
1006
1080
  const name = String(sessionName || "").trim();
1007
1081
  if (!name) {
1082
+ if (replActiveSessionName) {
1083
+ replTranscript = "";
1084
+ replCapturedAt = 0;
1085
+ }
1008
1086
  replActiveSessionName = "";
1009
1087
  return;
1010
1088
  }
1089
+ if (replActiveSessionName && replActiveSessionName !== name) {
1090
+ replTranscript = "";
1091
+ replCapturedAt = 0;
1092
+ activeReplJournalEntryId = "";
1093
+ }
1011
1094
  replActiveSessionName = name;
1012
1095
  }
1013
1096
 
@@ -1266,7 +1349,7 @@
1266
1349
 
1267
1350
  const allBlocks = parseMarkdownCodeFences(range.raw);
1268
1351
  if (allBlocks.length) {
1269
- return { error: "Place the cursor inside a code chunk, select text, or use Run all chunks. Switch send mode to Raw send to send the full editor." };
1352
+ return buildAllChunksReplSendPayload();
1270
1353
  }
1271
1354
 
1272
1355
  return {
@@ -1404,6 +1487,24 @@
1404
1487
  return isEcho ? lines.slice(1).join("\n").replace(/^\s+/, "") : value;
1405
1488
  }
1406
1489
 
1490
+ function stripStudioReplSubmissionEcho(delta) {
1491
+ let value = String(delta || "").replace(/^\s+/, "");
1492
+ // The raw mirror below remains raw; REPL Studio cards hide only the
1493
+ // temp-file wrapper used to submit multiline snippets safely. The
1494
+ // pi-studio-re fragment catches IPython's wrapped pi-studio-repl paths.
1495
+ const submissionEchoPatterns = [
1496
+ /^.*exec\(open\([\s\S]*?pi-studio-re[\s\S]*?globals\(\)\)\s*$/gm,
1497
+ /^.*include\([\s\S]*?pi-studio-re[\s\S]*?\.jl"\)\s*$/gm,
1498
+ /^.*source\([\s\S]*?pi-studio-re[\s\S]*?local\s*=\s*\.GlobalEnv\)\s*$/gm,
1499
+ /^.*:script\s+[\s\S]*?pi-studio-re[\s\S]*?\.ghci"?\s*$/gm,
1500
+ /^.*\(do\s+\(load-file\s+[\s\S]*?pi-studio-re[\s\S]*?:pi-studio\/silent\)\s*$/gm,
1501
+ ];
1502
+ for (const pattern of submissionEchoPatterns) {
1503
+ value = value.replace(pattern, "");
1504
+ }
1505
+ return value.replace(/^(?:\s*\n)+/, "");
1506
+ }
1507
+
1407
1508
  function stripTrailingReplPromptsFromOutput(output) {
1408
1509
  const lines = String(output || "").replace(/\r\n/g, "\n").split("\n");
1409
1510
  while (lines.length > 0 && /^\s*(?:>>>|\.\.\.|In \[\d+\]:|julia>|>|\+|ghci>|Prelude>|\*?[A-Za-z0-9_.:]+>|[^\s>]+=>)\s*$/.test(lines[lines.length - 1] || "")) {
@@ -1420,7 +1521,10 @@
1420
1521
  }
1421
1522
 
1422
1523
  function cleanReplCapturedOutput(delta, entry) {
1423
- return trimReplJournalOutput(stripTrailingReplPromptsFromOutput(stripSubsequentReplInputsFromOutput(stripSubmittedCodeEchoFromReplDelta(delta, entry))));
1524
+ const withoutSubmissionEcho = stripStudioReplSubmissionEcho(delta);
1525
+ const withoutCodeEcho = stripSubmittedCodeEchoFromReplDelta(withoutSubmissionEcho, entry);
1526
+ const withoutLaterInputs = stripSubsequentReplInputsFromOutput(withoutCodeEcho);
1527
+ return trimReplJournalOutput(stripTrailingReplPromptsFromOutput(withoutLaterInputs));
1424
1528
  }
1425
1529
 
1426
1530
  function updateActiveReplJournalEntryFromTranscript(sessionName, transcript) {
@@ -1446,13 +1550,17 @@
1446
1550
  return fence + (language ? language : "") + "\n" + value.replace(/\s+$/, "") + "\n" + fence;
1447
1551
  }
1448
1552
 
1449
- function buildReplJournalMarkdown() {
1450
- const lines = ["# REPL Studio", "", "Generated: " + new Date().toLocaleString(), ""];
1451
- if (!replJournalEntries.length) {
1452
- lines.push("_No REPL Studio entries yet._");
1553
+ function buildReplJournalMarkdown(entries) {
1554
+ const visibleEntries = Array.isArray(entries) ? entries : getVisibleReplJournalEntries();
1555
+ const sessionName = getActiveReplJournalSessionName();
1556
+ const lines = ["# REPL Studio", "", "Generated: " + new Date().toLocaleString()];
1557
+ if (sessionName) lines.push("Session: `" + sessionName + "`");
1558
+ lines.push("");
1559
+ if (!visibleEntries.length) {
1560
+ lines.push(sessionName ? ("_No REPL Studio entries for `" + sessionName + "` yet._") : "_No REPL Studio entries yet._");
1453
1561
  return lines.join("\n");
1454
1562
  }
1455
- replJournalEntries.forEach((entry, index) => {
1563
+ visibleEntries.forEach((entry, index) => {
1456
1564
  lines.push("## " + (index + 1) + ". " + (entry.label || "REPL entry"));
1457
1565
  lines.push("");
1458
1566
  lines.push("- Time: " + new Date(entry.createdAt || Date.now()).toLocaleString());
@@ -1479,53 +1587,62 @@
1479
1587
  }
1480
1588
 
1481
1589
  async function copyReplJournalToClipboard() {
1482
- if (!replJournalEntries.length) {
1483
- setStatus("No REPL Studio entries to copy yet.", "warning");
1590
+ const entries = getVisibleReplJournalEntries();
1591
+ if (!entries.length) {
1592
+ setStatus("No REPL Studio entries to copy for this session yet.", "warning");
1484
1593
  return;
1485
1594
  }
1486
- if (await writeTextToClipboard(buildReplJournalMarkdown())) {
1487
- setStatus("Copied REPL Studio as Markdown.", "success");
1595
+ if (await writeTextToClipboard(buildReplJournalMarkdown(entries))) {
1596
+ setStatus("Copied REPL Studio session as Markdown.", "success");
1488
1597
  } else {
1489
1598
  setStatus("Clipboard write failed.", "warning");
1490
1599
  }
1491
1600
  }
1492
1601
 
1493
1602
  function exportReplJournalMarkdown() {
1494
- if (!replJournalEntries.length) {
1495
- setStatus("No REPL Studio entries to export yet.", "warning");
1603
+ const entries = getVisibleReplJournalEntries();
1604
+ if (!entries.length) {
1605
+ setStatus("No REPL Studio entries to export for this session yet.", "warning");
1496
1606
  return;
1497
1607
  }
1498
- const blob = new Blob([buildReplJournalMarkdown()], { type: "text/markdown;charset=utf-8" });
1608
+ const blob = new Blob([buildReplJournalMarkdown(entries)], { type: "text/markdown;charset=utf-8" });
1499
1609
  const blobUrl = URL.createObjectURL(blob);
1500
1610
  const link = document.createElement("a");
1501
1611
  const stamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
1612
+ const sessionSlug = getActiveReplJournalSessionName().replace(/[^-_.A-Za-z0-9]+/g, "-");
1502
1613
  link.href = blobUrl;
1503
- link.download = "repl-studio-" + stamp + ".md";
1614
+ link.download = "repl-studio" + (sessionSlug ? "-" + sessionSlug : "") + "-" + stamp + ".md";
1504
1615
  document.body.appendChild(link);
1505
1616
  link.click();
1506
1617
  link.remove();
1507
1618
  window.setTimeout(() => URL.revokeObjectURL(blobUrl), 1000);
1508
- setStatus("Exported REPL Studio Markdown.", "success");
1619
+ setStatus("Exported REPL Studio session Markdown.", "success");
1509
1620
  }
1510
1621
 
1511
1622
  function clearReplJournal() {
1512
- replJournalEntries = [];
1623
+ const sessionName = getActiveReplJournalSessionName();
1624
+ if (sessionName) {
1625
+ replJournalEntries = replJournalEntries.filter((entry) => entry.sessionName !== sessionName);
1626
+ } else {
1627
+ replJournalEntries = [];
1628
+ }
1513
1629
  activeReplJournalEntryId = "";
1514
1630
  persistReplJournalEntries();
1515
- setStatus("Cleared REPL Studio.", "success");
1631
+ setStatus(sessionName ? "Cleared REPL Studio for this session." : "Cleared REPL Studio.", "success");
1516
1632
  renderReplViewIfActive({ force: true });
1517
1633
  }
1518
1634
 
1519
1635
  function loadReplJournalIntoEditor() {
1520
- if (!replJournalEntries.length) {
1521
- setStatus("No REPL Studio entries to load yet.", "warning");
1636
+ const entries = getVisibleReplJournalEntries();
1637
+ if (!entries.length) {
1638
+ setStatus("No REPL Studio entries to load for this session yet.", "warning");
1522
1639
  return;
1523
1640
  }
1524
- const markdown = buildReplJournalMarkdown();
1641
+ const markdown = buildReplJournalMarkdown(entries);
1525
1642
  setEditorText(markdown, { preserveScroll: false, preserveSelection: false });
1526
1643
  setSourceState({ source: "blank", label: "REPL Studio", path: null });
1527
1644
  setEditorLanguage("markdown");
1528
- setStatus("Loaded REPL Studio into editor.", "success");
1645
+ setStatus("Loaded REPL Studio session into editor.", "success");
1529
1646
  }
1530
1647
 
1531
1648
  function addSelectedReplJournalNote() {
@@ -2095,13 +2212,16 @@
2095
2212
  const actionLineOneEl = makeStudioUiRefreshElement("div", "studio-refresh-action-line");
2096
2213
  if (!isEditorOnlyMode && sendRunBtn) actionLineOneEl.appendChild(sendRunBtn);
2097
2214
  if (!isEditorOnlyMode && queueSteerBtn) actionLineOneEl.appendChild(queueSteerBtn);
2098
- if (!isEditorOnlyMode && sendReplBtn) actionLineOneEl.appendChild(sendReplBtn);
2099
- if (!isEditorOnlyMode && replSendModeSelect) actionLineOneEl.appendChild(replSendModeSelect);
2215
+ const replActionLineEl = makeStudioUiRefreshElement("div", "studio-refresh-action-line repl-action-line");
2216
+ replActionLineEl.hidden = true;
2217
+ if (!isEditorOnlyMode && sendReplBtn) replActionLineEl.appendChild(sendReplBtn);
2218
+ if (!isEditorOnlyMode && replSendModeSelect) replActionLineEl.appendChild(replSendModeSelect);
2100
2219
  const actionLineTwoEl = makeStudioUiRefreshElement("div", "studio-refresh-action-line");
2101
2220
  actionLineTwoEl.appendChild(copyDraftBtn);
2102
2221
  if (openCompanionBtn) actionLineTwoEl.appendChild(openCompanionBtn);
2103
2222
  if (!isEditorOnlyMode && sendEditorBtn) actionLineTwoEl.appendChild(sendEditorBtn);
2104
2223
  if (actionLineOneEl.childNodes.length > 0) actionsEl.appendChild(actionLineOneEl);
2224
+ if (replActionLineEl.childNodes.length > 0) actionsEl.appendChild(replActionLineEl);
2105
2225
  actionsEl.appendChild(actionLineTwoEl);
2106
2226
 
2107
2227
  const stateEl = makeStudioUiRefreshElement("div", "studio-refresh-toolbar-state");
@@ -3119,6 +3239,7 @@
3119
3239
  && event.shiftKey
3120
3240
  && activePane === "left"
3121
3241
  && !isEditorOnlyMode
3242
+ && rightView === "repl"
3122
3243
  ) {
3123
3244
  event.preventDefault();
3124
3245
  if (sendReplBtn && !sendReplBtn.hidden && !sendReplBtn.disabled) {
@@ -5006,6 +5127,18 @@
5006
5127
  }
5007
5128
  return;
5008
5129
  }
5130
+ if (action === "copy-attach-command") {
5131
+ const session = getActiveReplSession();
5132
+ const text = getReplAttachCommand(session);
5133
+ if (!text.trim()) {
5134
+ setStatus("Start or select a REPL session first.", "warning");
5135
+ return;
5136
+ }
5137
+ void writeTextToClipboard(text).then((ok) => {
5138
+ setStatus(ok ? "Copied tmux attach command." : "Clipboard write failed.", ok ? "success" : "warning");
5139
+ });
5140
+ return;
5141
+ }
5009
5142
  if (action === "run-all-chunks") {
5010
5143
  sendEditorTextToRepl({ action: "all-chunks" });
5011
5144
  return;
@@ -5327,8 +5460,9 @@
5327
5460
  setStatus("Switch right pane to Response (Preview), Editor (Preview), or REPL Studio to export PDF.", "warning");
5328
5461
  return;
5329
5462
  }
5330
- if (exportingReplJournal && !replJournalEntries.length) {
5331
- setStatus("No REPL Studio entries to export yet.", "warning");
5463
+ const replJournalExportEntries = exportingReplJournal ? getVisibleReplJournalEntries() : [];
5464
+ if (exportingReplJournal && !replJournalExportEntries.length) {
5465
+ setStatus("No REPL Studio entries to export for this session yet.", "warning");
5332
5466
  return;
5333
5467
  }
5334
5468
 
@@ -5339,7 +5473,7 @@
5339
5473
  }
5340
5474
 
5341
5475
  const markdown = exportingReplJournal
5342
- ? buildReplJournalMarkdown()
5476
+ ? buildReplJournalMarkdown(replJournalExportEntries)
5343
5477
  : (rightView === "editor-preview"
5344
5478
  ? prepareEditorTextForPdfExport(sourceTextEl.value)
5345
5479
  : prepareEditorTextForPreview(latestResponseMarkdown));
@@ -5499,13 +5633,14 @@
5499
5633
  setStatus("Switch right pane to Response (Preview), Editor (Preview), or REPL Studio to export HTML.", "warning");
5500
5634
  return;
5501
5635
  }
5502
- if (exportingReplJournal && !replJournalEntries.length) {
5503
- setStatus("No REPL Studio entries to export yet.", "warning");
5636
+ const replJournalExportEntries = exportingReplJournal ? getVisibleReplJournalEntries() : [];
5637
+ if (exportingReplJournal && !replJournalExportEntries.length) {
5638
+ setStatus("No REPL Studio entries to export for this session yet.", "warning");
5504
5639
  return;
5505
5640
  }
5506
5641
 
5507
5642
  const htmlArtifactSource = exportingReplJournal ? "" : getRightPaneHtmlArtifactSource();
5508
- const markdown = exportingReplJournal ? buildReplJournalMarkdown() : (htmlArtifactSource || (rightView === "editor-preview"
5643
+ const markdown = exportingReplJournal ? buildReplJournalMarkdown(replJournalExportEntries) : (htmlArtifactSource || (rightView === "editor-preview"
5509
5644
  ? prepareEditorTextForHtmlExport(sourceTextEl.value)
5510
5645
  : prepareEditorTextForPreview(latestResponseMarkdown)));
5511
5646
  if (!markdown || !markdown.trim()) {
@@ -6003,8 +6138,24 @@
6003
6138
  return remaining < 56;
6004
6139
  }
6005
6140
 
6141
+ function getActiveReplJournalSessionName() {
6142
+ return String(replActiveSessionName || "").trim();
6143
+ }
6144
+
6145
+ function getVisibleReplJournalEntries() {
6146
+ const sessionName = getActiveReplJournalSessionName();
6147
+ if (!sessionName) return replJournalEntries;
6148
+ return replJournalEntries.filter((entry) => entry.sessionName === sessionName);
6149
+ }
6150
+
6151
+ function getHiddenReplJournalEntryCount() {
6152
+ const sessionName = getActiveReplJournalSessionName();
6153
+ if (!sessionName) return 0;
6154
+ return replJournalEntries.filter((entry) => entry.sessionName && entry.sessionName !== sessionName).length;
6155
+ }
6156
+
6006
6157
  function isReplJournalExpanded() {
6007
- return rightView === "repl" && !replJournalCollapsed && replJournalEntries.length > 0;
6158
+ return rightView === "repl" && !replJournalCollapsed && getVisibleReplJournalEntries().length > 0;
6008
6159
  }
6009
6160
 
6010
6161
  function shouldAutoStickReplView() {
@@ -6190,23 +6341,26 @@
6190
6341
 
6191
6342
  function buildReplStudioActionsHtml() {
6192
6343
  if (replJournalCollapsed) return "";
6193
- const hasEntries = replJournalEntries.length > 0;
6344
+ const hasEntries = getVisibleReplJournalEntries().length > 0;
6194
6345
  const buttons = "<button type='button' data-repl-action='load-journal'" + (hasEntries ? "" : " disabled") + ">Load in editor</button>"
6195
6346
  + "<button type='button' data-repl-action='copy-journal'" + (hasEntries ? "" : " disabled") + ">Copy Markdown</button>"
6196
6347
  + "<button type='button' data-repl-action='export-journal'" + (hasEntries ? "" : " disabled") + ">Export .md</button>"
6197
- + "<button type='button' data-repl-action='clear-journal'" + (hasEntries ? "" : " disabled") + ">Clear</button>";
6348
+ + "<button type='button' data-repl-action='clear-journal'" + (hasEntries ? "" : " disabled") + ">Clear session</button>";
6198
6349
  return "<div class='repl-studio-below-actions'><div class='repl-journal-actions'>" + buttons + "</div></div>";
6199
6350
  }
6200
6351
 
6201
6352
  function buildReplJournalHtml(transcript) {
6202
- const hasEntries = replJournalEntries.length > 0;
6203
- const entryCount = replJournalEntries.length;
6353
+ const visibleEntries = getVisibleReplJournalEntries();
6354
+ const hasEntries = visibleEntries.length > 0;
6355
+ const entryCount = visibleEntries.length;
6356
+ const hiddenEntryCount = getHiddenReplJournalEntryCount();
6357
+ const sessionName = getActiveReplJournalSessionName();
6204
6358
  const collapsedClass = replJournalCollapsed ? " is-collapsed" : "";
6205
6359
  const toggleButton = "<button type='button' data-repl-action='journal-toggle' aria-expanded='" + (replJournalCollapsed ? "false" : "true") + "'>" + (replJournalCollapsed ? "Show REPL Studio" : "Hide REPL Studio") + "</button>";
6206
6360
  const toggleActions = "<div class='repl-journal-actions'>" + toggleButton + "</div>";
6207
6361
  const summaryText = hasEntries
6208
- ? (entryCount + " Studio entr" + (entryCount === 1 ? "y" : "ies") + ". Export is Markdown.")
6209
- : "Studio-sent code and notes will appear here.";
6362
+ ? (entryCount + " Studio entr" + (entryCount === 1 ? "y" : "ies") + (sessionName ? " for " + sessionName : "") + ". Export is Markdown.")
6363
+ : (sessionName ? "No Studio entries for " + sessionName + "." : "Studio-sent code and notes will appear here.");
6210
6364
  if (replJournalCollapsed) {
6211
6365
  return "<section class='repl-journal repl-journal-compact" + collapsedClass + "'>"
6212
6366
  + "<div class='repl-journal-compact-row'>"
@@ -6215,12 +6369,12 @@
6215
6369
  + "</div>"
6216
6370
  + "</section>";
6217
6371
  }
6218
- const omitted = Math.max(0, replJournalEntries.length - 12);
6372
+ const omitted = Math.max(0, visibleEntries.length - 12);
6219
6373
  const bannerText = extractReplStudioBanner(transcript, getActiveReplRuntime());
6220
6374
  const banner = bannerText
6221
6375
  ? "<pre class='repl-studio-banner'>" + escapeHtml(bannerText) + "</pre>"
6222
6376
  : "";
6223
- const cards = replJournalEntries.slice(-12).map((entry) => {
6377
+ const cards = visibleEntries.slice(-12).map((entry) => {
6224
6378
  const meta = buildReplStudioMeta(entry);
6225
6379
  const prompt = getReplStudioPrompt(entry.runtime);
6226
6380
  const codeText = String(entry.code || "").trimEnd();
@@ -6246,11 +6400,17 @@
6246
6400
  + pending
6247
6401
  + "</article>";
6248
6402
  }).join("");
6403
+ const emptyText = sessionName
6404
+ ? (String(transcript || "").trim()
6405
+ ? "No REPL Studio entries for this tmux session yet. The raw tmux mirror below still has this session's history; send code from Studio to build a clean record."
6406
+ : "No REPL Studio entries for this tmux session yet. Send code from the editor, or use More → Add note (Literate send) to record prose.")
6407
+ : "No REPL Studio entries yet. Send code from the editor, or use More → Add note (Literate send) to record prose.";
6249
6408
  const terminalContent = banner
6250
- + (hasEntries ? cards : "<div class='repl-studio-empty'>No REPL Studio entries yet. Send code from the editor, or use More → Add note (Literate send) to record prose.</div>");
6409
+ + (hasEntries ? cards : "<div class='repl-studio-empty'>" + escapeHtml(emptyText) + "</div>");
6251
6410
  return "<section class='repl-journal'>"
6252
- + "<div class='repl-journal-header'><div><h3>REPL Studio</h3><p>Clean collaborative Studio REPL record. The raw tmux mirror is available below.</p></div>" + toggleActions + "</div>"
6253
- + (omitted ? "<div class='repl-journal-omitted'>Showing latest 12 entries; " + escapeHtml(String(omitted)) + " older entries remain in export.</div>" : "")
6411
+ + "<div class='repl-journal-header'><div><h3>REPL Studio</h3><p>Clean collaborative Studio REPL record for the selected tmux session. The raw tmux mirror is available below.</p></div>" + toggleActions + "</div>"
6412
+ + (hiddenEntryCount ? "<div class='repl-journal-omitted'>" + escapeHtml(String(hiddenEntryCount)) + " entr" + (hiddenEntryCount === 1 ? "y" : "ies") + " from other REPL sessions hidden.</div>" : "")
6413
+ + (omitted ? "<div class='repl-journal-omitted'>Showing latest 12 entries for this session; " + escapeHtml(String(omitted)) + " older entries remain in export.</div>" : "")
6254
6414
  + "<div class='repl-journal-list'>" + terminalContent + "</div>"
6255
6415
  + "</section>";
6256
6416
  }
@@ -6278,6 +6438,11 @@
6278
6438
  + "</section>";
6279
6439
  }
6280
6440
 
6441
+ function getReplAttachCommand(session) {
6442
+ if (!session || !session.sessionName) return "";
6443
+ return "tmux attach -t " + String(session.sessionName || "");
6444
+ }
6445
+
6281
6446
  function buildReplPanelHtml() {
6282
6447
  const runtimeOptions = [
6283
6448
  ["shell", "Shell"],
@@ -6313,6 +6478,7 @@
6313
6478
  + "<button type='button' data-repl-action='new-session'" + (replBusy || replTmuxAvailable === false ? " disabled" : "") + " title='Start a new additional session for this runtime.'>New session</button>"
6314
6479
  + "<button type='button' data-repl-action='stop-session'" + (canStopActiveSession ? "" : " disabled") + " title='Stop the selected Studio-owned REPL session.'>Stop session</button>"
6315
6480
  + "<button type='button' data-repl-action='interrupt'" + (activeSession && !replBusy ? "" : " disabled") + " title='Send Ctrl+C to the active REPL session.'>Interrupt</button>"
6481
+ + "<button type='button' data-repl-action='copy-attach-command'" + (activeSession ? "" : " disabled") + " title='Copy command for attaching to this tmux session in a terminal.'>Copy attach command</button>"
6316
6482
  + "<button type='button' data-repl-action='run-all-chunks'" + (canSendToActiveSession ? "" : " disabled") + " title='Literate send: send all fenced code chunks matching the active REPL runtime.'>Run all chunks</button>"
6317
6483
  + "<button type='button' data-repl-action='journal-note' title='Add the selected prose/current paragraph to REPL Studio (Literate send) without sending it to the runtime.'>Add note</button>"
6318
6484
  + "<button type='button' data-repl-action='refresh'>Refresh</button>"
@@ -6590,8 +6756,9 @@
6590
6756
 
6591
6757
  const rightPaneShowsPreview = rightView === "preview" || rightView === "editor-preview";
6592
6758
  const exportingReplJournal = rightView === "repl";
6759
+ const replJournalExportEntries = exportingReplJournal ? getVisibleReplJournalEntries() : [];
6593
6760
  const exportText = exportingReplJournal
6594
- ? (replJournalEntries.length ? buildReplJournalMarkdown() : "")
6761
+ ? (replJournalExportEntries.length ? buildReplJournalMarkdown(replJournalExportEntries) : "")
6595
6762
  : (rightView === "editor-preview" ? prepareEditorTextForPreview(sourceTextEl.value) : latestResponseMarkdown);
6596
6763
  const canExportPreview = (rightPaneShowsPreview || exportingReplJournal) && Boolean(String(exportText || "").trim());
6597
6764
  const htmlArtifactExportSource = canExportPreview && !exportingReplJournal ? getRightPaneHtmlArtifactSource() : "";
@@ -6603,8 +6770,8 @@
6603
6770
  : (exportingReplJournal ? "Export REPL Studio" : "Export right preview");
6604
6771
  if (rightView === "trace") {
6605
6772
  exportPdfBtn.title = "Working view does not support preview export.";
6606
- } else if (exportingReplJournal && !replJournalEntries.length) {
6607
- exportPdfBtn.title = "No REPL Studio entries to export yet.";
6773
+ } else if (exportingReplJournal && !replJournalExportEntries.length) {
6774
+ exportPdfBtn.title = "No REPL Studio entries to export for this session yet.";
6608
6775
  } else if (rightView === "markdown") {
6609
6776
  exportPdfBtn.title = "Switch right pane to Response (Preview), Editor (Preview), or REPL Studio to export.";
6610
6777
  } else if (!canExportPreview) {
@@ -6634,7 +6801,7 @@
6634
6801
  ? (exportingReplJournal
6635
6802
  ? "Choose a format and export REPL Studio."
6636
6803
  : (isHtmlArtifactPreview ? "Export this HTML preview." : "Choose a format and export the current right-pane preview."))
6637
- : (exportingReplJournal ? "No REPL Studio entries to export yet." : "Switch right pane to a non-empty preview before exporting.");
6804
+ : (exportingReplJournal ? "No REPL Studio entries to export for this session yet." : "Switch right pane to a non-empty preview before exporting.");
6638
6805
  }
6639
6806
  if (!canExportPreview || previewExportInProgress) {
6640
6807
  closeExportPreviewMenu();
@@ -7310,6 +7477,21 @@
7310
7477
  return "draft_" + makeRequestId();
7311
7478
  }
7312
7479
 
7480
+ function normalizeQuizScope(scope) {
7481
+ const value = String(scope || "").trim().toLowerCase();
7482
+ return QUIZ_SCOPES.includes(value) ? value : "editor";
7483
+ }
7484
+
7485
+ function getQuizScopeLabel(scope) {
7486
+ switch (normalizeQuizScope(scope)) {
7487
+ case "selection": return "Selection";
7488
+ case "file": return "Current file";
7489
+ case "folder": return "Folder";
7490
+ case "repo": return "Repo";
7491
+ default: return "Editor";
7492
+ }
7493
+ }
7494
+
7313
7495
  function normalizeQuizAngle(angle) {
7314
7496
  const value = String(angle || "").trim().toLowerCase();
7315
7497
  return QUIZ_ANGLES.includes(value) ? value : "general";
@@ -7487,7 +7669,52 @@
7487
7669
 
7488
7670
  function getQuizSourceLabel(scope) {
7489
7671
  const base = sourceState && sourceState.label ? sourceState.label : "Studio editor";
7490
- return scope === "selection" ? base + " selection" : base;
7672
+ const normalizedScope = normalizeQuizScope(scope);
7673
+ if (normalizedScope === "selection") return base + " selection";
7674
+ if (normalizedScope === "file") return base === "blank" ? "current file" : base;
7675
+ if (normalizedScope === "folder") return "folder context";
7676
+ if (normalizedScope === "repo") return "repo context";
7677
+ return base;
7678
+ }
7679
+
7680
+ function dirnameForDisplayPath(path) {
7681
+ const value = String(path || "").replace(/\\/g, "/");
7682
+ const index = value.lastIndexOf("/");
7683
+ return index > 0 ? value.slice(0, index) : "";
7684
+ }
7685
+
7686
+ function getCurrentResourceDirValue() {
7687
+ return resourceDirInput ? String(resourceDirInput.value || "").trim() : "";
7688
+ }
7689
+
7690
+ function getDefaultQuizContextPath(scope) {
7691
+ const normalizedScope = normalizeQuizScope(scope);
7692
+ const sourcePath = sourceState && sourceState.path ? String(sourceState.path) : "";
7693
+ const resourceDir = getCurrentResourceDirValue();
7694
+ if (normalizedScope === "file") return sourcePath || "";
7695
+ if (normalizedScope === "folder") return resourceDir || dirnameForDisplayPath(sourcePath) || "";
7696
+ if (normalizedScope === "repo") return sourcePath || resourceDir || "";
7697
+ return "";
7698
+ }
7699
+
7700
+ function isQuizContextScope(scope) {
7701
+ const normalizedScope = normalizeQuizScope(scope);
7702
+ return normalizedScope === "file" || normalizedScope === "folder" || normalizedScope === "repo";
7703
+ }
7704
+
7705
+ function getQuizScopeFocusHint(scope) {
7706
+ const normalizedScope = normalizeQuizScope(scope);
7707
+ const focus = String(quizState.focusPrompt || "").toLowerCase();
7708
+ const asksForCode = /\b(code|implementation|technical|source|actual code)\b/.test(focus);
7709
+ const editorLang = normalizeFenceLanguage(editorLanguage || "");
7710
+ const editorLooksLikeDoc = !editorLang || editorLang === "markdown" || editorLang === "latex";
7711
+ if (asksForCode && (normalizedScope === "editor" || normalizedScope === "selection") && editorLooksLikeDoc) {
7712
+ return "Focus guidance only applies to the selected scope. Choose Folder or Repo to include code files.";
7713
+ }
7714
+ if ((normalizedScope === "folder" || normalizedScope === "repo") && asksForCode) {
7715
+ return "Code-focused guidance will prioritize source/test files over README and docs.";
7716
+ }
7717
+ return "";
7491
7718
  }
7492
7719
 
7493
7720
  function ensureQuizOverlay() {
@@ -7500,7 +7727,7 @@
7500
7727
  document.body.appendChild(quizOverlayEl);
7501
7728
  quizDialogEl = quizOverlayEl.querySelector(".studio-quiz-dialog");
7502
7729
  quizOverlayEl.addEventListener("click", (event) => {
7503
- if (event.target === quizOverlayEl) closeQuizOverlay();
7730
+ if (event.target === quizOverlayEl) minimizeQuizOverlay();
7504
7731
  });
7505
7732
  quizDialogEl.addEventListener("input", (event) => {
7506
7733
  const target = event.target;
@@ -7510,6 +7737,15 @@
7510
7737
  if (card) card.answer = target.value;
7511
7738
  quizState.answer = target.value;
7512
7739
  }
7740
+ if (target.matches("[data-quiz-field='contextPath']")) {
7741
+ quizState.contextPath = target.value;
7742
+ }
7743
+ if (target.matches("[data-quiz-field='focusPrompt']")) {
7744
+ quizState.focusPrompt = target.value;
7745
+ }
7746
+ if (target.matches("[data-quiz-field='includeEditorContext']")) {
7747
+ quizState.includeEditorContext = Boolean(target.checked);
7748
+ }
7513
7749
  });
7514
7750
  quizDialogEl.addEventListener("click", (event) => {
7515
7751
  const target = event.target instanceof Element ? event.target.closest("[data-quiz-action]") : null;
@@ -7517,6 +7753,13 @@
7517
7753
  event.preventDefault();
7518
7754
  handleQuizAction(target.getAttribute("data-quiz-action") || "");
7519
7755
  });
7756
+ quizDialogEl.addEventListener("change", (event) => {
7757
+ const target = event.target;
7758
+ if (!(target instanceof HTMLElement) || !target.matches("[data-quiz-field]")) return;
7759
+ if (target.matches("[data-quiz-field='contextPath']")) return;
7760
+ readQuizSetupFields();
7761
+ renderQuizOverlay({ preserveScroll: true });
7762
+ });
7520
7763
  quizDialogEl.addEventListener("keydown", handleQuizKeydown);
7521
7764
  return quizOverlayEl;
7522
7765
  }
@@ -7524,16 +7767,25 @@
7524
7767
  function resetQuizStateFromEditor() {
7525
7768
  const previousAngle = normalizeQuizAngle(quizState.angle);
7526
7769
  const previousThinking = normalizeQuizThinking(quizState.thinking);
7770
+ const previousFocusPrompt = String(quizState.focusPrompt || "");
7771
+ const previousIncludeEditorContext = Boolean(quizState.includeEditorContext);
7527
7772
  const previousCount = quizState.questionCount || QUIZ_DEFAULT_COUNT;
7528
7773
  const selection = getEditorSelectionRange();
7529
7774
  const hasSelection = Boolean(selection.selected && selection.selected.trim());
7530
7775
  const scope = hasSelection ? "selection" : "editor";
7776
+ const sourcePath = sourceState && sourceState.path ? String(sourceState.path) : "";
7777
+ const resourceDir = getCurrentResourceDirValue();
7531
7778
  quizState = {
7532
7779
  open: true,
7533
7780
  requestId: null,
7534
7781
  pending: false,
7535
7782
  sourceText: hasSelection ? selection.selected : selection.raw,
7536
7783
  sourceLabel: getQuizSourceLabel(scope),
7784
+ sourcePath,
7785
+ contextPath: getDefaultQuizContextPath(scope),
7786
+ resourceDir,
7787
+ focusPrompt: previousFocusPrompt,
7788
+ includeEditorContext: previousIncludeEditorContext,
7537
7789
  scope,
7538
7790
  angle: previousAngle,
7539
7791
  thinking: previousThinking,
@@ -7581,6 +7833,15 @@
7581
7833
  setStatus("Quiz minimized — use Review → Quiz me to resume.", "success");
7582
7834
  }
7583
7835
 
7836
+ function endQuizOverlay() {
7837
+ const hadResumableQuiz = hasResumableQuiz();
7838
+ closeQuizOverlay();
7839
+ resetQuizStateFromEditor();
7840
+ quizState.open = false;
7841
+ syncActionButtons();
7842
+ if (hadResumableQuiz) setStatus("Quiz closed.", "success");
7843
+ }
7844
+
7584
7845
  function handleQuizKeydown(event) {
7585
7846
  if (!event) return;
7586
7847
  const key = typeof event.key === "string" ? event.key : "";
@@ -7624,18 +7885,25 @@
7624
7885
  }
7625
7886
 
7626
7887
  function renderQuizSetupHtml() {
7627
- const scope = quizState.scope === "selection" ? "selection" : "editor";
7888
+ const scope = normalizeQuizScope(quizState.scope);
7628
7889
  const angle = normalizeQuizAngle(quizState.angle);
7629
7890
  const thinking = normalizeQuizThinking(quizState.thinking);
7630
7891
  const count = Math.max(1, Math.min(8, Math.floor(Number(quizState.questionCount) || QUIZ_DEFAULT_COUNT)));
7631
7892
  const selection = getEditorSelectionRange();
7632
7893
  const hasSelection = Boolean(selection.selected && selection.selected.trim());
7894
+ const contextPath = String(quizState.contextPath || getDefaultQuizContextPath(scope) || "");
7895
+ const includeEditorContext = Boolean(quizState.includeEditorContext);
7896
+ const contextScope = isQuizContextScope(scope);
7897
+ const contextScopeUsesEditor = scope === "file" || includeEditorContext;
7898
+ const focusHint = getQuizScopeFocusHint(scope);
7899
+ const scopeText = scope === "selection"
7900
+ ? selection.selected
7901
+ : ((scope === "editor" || contextScopeUsesEditor) ? selection.raw : "");
7633
7902
  return "<div class='studio-quiz-setup'>"
7634
7903
  + "<p class='studio-quiz-copy'>A short active-recall loop: answer one question, check it, ask about the card if useful, then move on.</p>"
7635
7904
  + "<div class='studio-quiz-fields'>"
7636
7905
  + "<label>Scope<select data-quiz-field='scope'>"
7637
- + renderQuizOption("editor", scope, "Editor")
7638
- + (hasSelection ? renderQuizOption("selection", scope, "Selection") : "")
7906
+ + QUIZ_SCOPES.map((candidate) => candidate === "selection" && !hasSelection ? "" : renderQuizOption(candidate, scope, getQuizScopeLabel(candidate))).join("")
7639
7907
  + "</select></label>"
7640
7908
  + "<label>Angle<select data-quiz-field='angle'>"
7641
7909
  + QUIZ_ANGLES.map((candidate) => renderQuizOption(candidate, angle, getQuizAngleLabel(candidate))).join("")
@@ -7645,7 +7913,11 @@
7645
7913
  + "</select></label>"
7646
7914
  + "<label>Questions<input data-quiz-field='count' type='number' min='1' max='8' value='" + String(count) + "'></label>"
7647
7915
  + "</div>"
7648
- + "<div class='studio-quiz-source-note'>Source: " + escapeHtml(getQuizSourceLabel(scope)) + " · " + escapeHtml(String((scope === "selection" ? selection.selected : selection.raw).trim().length)) + " chars · Studio model: " + escapeHtml(getQuizModelLabel()) + "</div>"
7916
+ + (contextScope ? "<label class='studio-quiz-context-path-label'>Context path<input data-quiz-field='contextPath' type='text' value='" + escapeHtml(contextPath) + "' placeholder='Folder, file, or repo path; blank uses Studio working directory'></label>" : "")
7917
+ + ((scope === "folder" || scope === "repo") ? "<label class='studio-quiz-include-editor-label'><input data-quiz-field='includeEditorContext' type='checkbox'" + (includeEditorContext ? " checked" : "") + "> Include current editor text as an anchor</label>" : "")
7918
+ + "<label class='studio-quiz-focus-label'>Focus guidance<textarea data-quiz-field='focusPrompt' rows='2' placeholder='Optional: e.g. focus on implementation details in code files; avoid README overview questions'>" + escapeHtml(quizState.focusPrompt || "") + "</textarea></label>"
7919
+ + "<div class='studio-quiz-source-note'>Scope: " + escapeHtml(getQuizScopeLabel(scope)) + (scopeText.trim() ? " · " + escapeHtml(String(scopeText.trim().length)) + " active chars" : (scope === "folder" || scope === "repo" ? " · editor text excluded" : "")) + (contextScope && contextPath ? " · Context: " + escapeHtml(contextPath) : "") + " · Studio model: " + escapeHtml(getQuizModelLabel()) + "</div>"
7920
+ + (focusHint ? "<div class='studio-quiz-hint'>" + escapeHtml(focusHint) + "</div>" : "")
7649
7921
  + (quizState.error ? "<div class='studio-quiz-error'>" + escapeHtml(quizState.error) + "</div>" : "")
7650
7922
  + (quizState.status ? "<div class='studio-quiz-status'>" + escapeHtml(quizState.status) + "</div>" : "")
7651
7923
  + "<div class='studio-quiz-actions'><button data-quiz-action='start' type='button'" + (quizState.pending ? " disabled" : "") + ">" + (quizState.pending ? "Generating…" : "Start quiz") + "</button></div>"
@@ -7705,7 +7977,7 @@
7705
7977
  + "<div><div class='studio-quiz-eyebrow'>Review</div><h2>Quiz me</h2></div>"
7706
7978
  + "<div class='studio-quiz-header-actions'>"
7707
7979
  + "<button class='studio-quiz-minimize' data-quiz-action='minimize' type='button'>Minimize</button>"
7708
- + "<button class='studio-quiz-close' data-quiz-action='close' type='button' aria-label='Close quiz'>Close</button>"
7980
+ + "<button class='studio-quiz-close' data-quiz-action='close' type='button' aria-label='Close and discard quiz' title='Close and discard this quiz'>Close</button>"
7709
7981
  + "</div>"
7710
7982
  + "</div>"
7711
7983
  + bodyHtml;
@@ -7735,20 +8007,34 @@
7735
8007
  const angleEl = quizDialogEl.querySelector("[data-quiz-field='angle']");
7736
8008
  const thinkingEl = quizDialogEl.querySelector("[data-quiz-field='thinking']");
7737
8009
  const countEl = quizDialogEl.querySelector("[data-quiz-field='count']");
8010
+ const contextPathEl = quizDialogEl.querySelector("[data-quiz-field='contextPath']");
8011
+ const focusPromptEl = quizDialogEl.querySelector("[data-quiz-field='focusPrompt']");
8012
+ const includeEditorContextEl = quizDialogEl.querySelector("[data-quiz-field='includeEditorContext']");
7738
8013
  const selection = getEditorSelectionRange();
7739
- const scope = scopeEl && scopeEl.value === "selection" && selection.selected.trim() ? "selection" : "editor";
8014
+ let scope = normalizeQuizScope(scopeEl ? scopeEl.value : quizState.scope);
8015
+ if (scope === "selection" && !selection.selected.trim()) scope = "editor";
8016
+ const sourcePath = sourceState && sourceState.path ? String(sourceState.path) : "";
8017
+ const resourceDir = getCurrentResourceDirValue();
7740
8018
  quizState.scope = scope;
7741
8019
  quizState.angle = normalizeQuizAngle(angleEl ? angleEl.value : quizState.angle);
7742
8020
  quizState.thinking = normalizeQuizThinking(thinkingEl ? thinkingEl.value : quizState.thinking);
7743
8021
  quizState.questionCount = Math.max(1, Math.min(8, Math.floor(Number(countEl ? countEl.value : quizState.questionCount) || QUIZ_DEFAULT_COUNT)));
7744
- quizState.sourceText = scope === "selection" ? selection.selected : selection.raw;
7745
- quizState.sourceLabel = getQuizSourceLabel(scope);
8022
+ quizState.includeEditorContext = Boolean(includeEditorContextEl && includeEditorContextEl.checked);
8023
+ const shouldSendEditorText = scope === "selection" || scope === "editor" || scope === "file" || quizState.includeEditorContext;
8024
+ quizState.sourceText = scope === "selection" ? selection.selected : (shouldSendEditorText ? selection.raw : "");
8025
+ quizState.sourceLabel = shouldSendEditorText ? (sourceState && sourceState.label ? sourceState.label : getQuizSourceLabel(scope)) : getQuizSourceLabel(scope);
8026
+ quizState.sourcePath = sourcePath;
8027
+ quizState.resourceDir = resourceDir;
8028
+ quizState.contextPath = isQuizContextScope(scope)
8029
+ ? String(contextPathEl ? contextPathEl.value : (quizState.contextPath || getDefaultQuizContextPath(scope)) || "").trim()
8030
+ : "";
8031
+ quizState.focusPrompt = String(focusPromptEl ? focusPromptEl.value : quizState.focusPrompt || "").trim();
7746
8032
  }
7747
8033
 
7748
8034
  function startQuizRequest() {
7749
8035
  readQuizSetupFields();
7750
8036
  const sourceText = String(quizState.sourceText || "").trim();
7751
- if (!sourceText) {
8037
+ if (!sourceText && !isQuizContextScope(quizState.scope)) {
7752
8038
  quizState.error = "Quiz source is empty.";
7753
8039
  renderQuizOverlay({ preserveScroll: true });
7754
8040
  return;
@@ -7764,6 +8050,10 @@
7764
8050
  requestId,
7765
8051
  sourceText,
7766
8052
  sourceLabel: quizState.sourceLabel,
8053
+ sourcePath: quizState.sourcePath || "",
8054
+ contextPath: quizState.contextPath || "",
8055
+ resourceDir: quizState.resourceDir || "",
8056
+ focusPrompt: quizState.focusPrompt || "",
7767
8057
  scope: quizState.scope,
7768
8058
  angle: quizState.angle,
7769
8059
  thinking: quizState.thinking,
@@ -7847,7 +8137,7 @@
7847
8137
 
7848
8138
  function handleQuizAction(action) {
7849
8139
  if (action === "close") {
7850
- closeQuizOverlay();
8140
+ endQuizOverlay();
7851
8141
  return;
7852
8142
  }
7853
8143
  if (action === "minimize") {
@@ -13056,7 +13346,7 @@
13056
13346
  if (isEditorOnlyMode) {
13057
13347
  if (sendRunBtn) {
13058
13348
  sendRunBtn.textContent = "Run editor text";
13059
- sendRunBtn.classList.remove("request-stop-active");
13349
+ sendRunBtn.classList.remove("request-stop-active", "repl-secondary-action");
13060
13350
  sendRunBtn.disabled = true;
13061
13351
  sendRunBtn.title = "Run is unavailable in editor-only mode.";
13062
13352
  }
@@ -13069,6 +13359,9 @@
13069
13359
  if (sendReplBtn) {
13070
13360
  sendReplBtn.hidden = true;
13071
13361
  sendReplBtn.disabled = true;
13362
+ sendReplBtn.classList.remove("repl-primary-action");
13363
+ const replActionLine = sendReplBtn.closest(".repl-action-line");
13364
+ if (replActionLine instanceof HTMLElement) replActionLine.hidden = true;
13072
13365
  }
13073
13366
  if (replSendModeSelect) {
13074
13367
  replSendModeSelect.hidden = true;
@@ -13089,11 +13382,12 @@
13089
13382
  }
13090
13383
 
13091
13384
  if (sendRunBtn) {
13092
- sendRunBtn.textContent = directIsStop ? "Stop" : "Run editor text";
13385
+ sendRunBtn.textContent = directIsStop ? "Stop" : (rightView === "repl" ? withStudioShortcutLabel("Run editor text", "run") : "Run editor text");
13093
13386
  sendRunBtn.classList.toggle("request-stop-active", directIsStop);
13387
+ sendRunBtn.classList.toggle("repl-secondary-action", rightView === "repl" && !directIsStop);
13094
13388
  sendRunBtn.disabled = wsState === "Disconnected" || (!directIsStop && (uiBusy || critiqueIsStop));
13095
13389
  const replHint = rightView === "repl" && getActiveReplSession()
13096
- ? " Includes active REPL identity for prompts that refer to it."
13390
+ ? " Sends text to Pi, not the REPL; use Send chunk/selection or Send to REPL to execute code in the active REPL."
13097
13391
  : "";
13098
13392
  sendRunBtn.title = directIsStop
13099
13393
  ? "Stop the active run. Shortcut: Esc."
@@ -13113,22 +13407,27 @@
13113
13407
  : "Queue steering is available while Run editor text is active.";
13114
13408
  }
13115
13409
 
13410
+ const hasReplSession = Boolean(getActiveReplSession());
13116
13411
  if (sendReplBtn) {
13117
- const hasSession = Boolean(getActiveReplSession());
13118
- sendReplBtn.hidden = rightView !== "repl";
13119
- sendReplBtn.disabled = wsState === "Disconnected" || uiBusy || replBusy || !hasSession;
13120
- sendReplBtn.title = hasSession
13412
+ const showReplSend = rightView === "repl";
13413
+ sendReplBtn.hidden = !showReplSend;
13414
+ sendReplBtn.disabled = !showReplSend || wsState === "Disconnected" || uiBusy || replBusy || !hasReplSession;
13415
+ sendReplBtn.classList.toggle("repl-primary-action", showReplSend);
13416
+ sendReplBtn.textContent = showReplSend ? withStudioShortcutLabel(replSendMode === "literate" ? "Send selection/chunks" : "Send to REPL", "repl-send") : "Send to REPL";
13417
+ sendReplBtn.title = hasReplSession
13121
13418
  ? (replSendMode === "literate"
13122
- ? "Literate send: selected code/prose, or the current fenced code chunk. Shortcut: Cmd/Ctrl+Shift+Enter."
13419
+ ? "Literate send: selection, current fenced code chunk, or all matching chunks if the cursor is outside a chunk. Shortcut: Cmd/Ctrl+Shift+Enter."
13123
13420
  : "Raw send: selection, or full editor if no selection. Shortcut: Cmd/Ctrl+Shift+Enter.")
13124
13421
  : "Start or select a REPL session in the right pane first.";
13422
+ const replActionLine = sendReplBtn.closest(".repl-action-line");
13423
+ if (replActionLine instanceof HTMLElement) replActionLine.hidden = !showReplSend;
13125
13424
  }
13126
13425
  if (replSendModeSelect) {
13127
13426
  replSendModeSelect.hidden = rightView !== "repl";
13128
13427
  replSendModeSelect.disabled = wsState === "Disconnected" || uiBusy || replBusy;
13129
13428
  replSendModeSelect.value = replSendMode;
13130
13429
  replSendModeSelect.title = replSendMode === "literate"
13131
- ? "Literate send: Send to REPL uses the selection/current fenced code chunk."
13430
+ ? "Literate send: Send to REPL uses the selection, current fenced code chunk, or all matching chunks if the cursor is outside a chunk."
13132
13431
  : "Raw send: Send to REPL uses the selection, or full editor if no selection.";
13133
13432
  }
13134
13433
 
@@ -13478,6 +13777,7 @@
13478
13777
  if (typeof message.activeSessionName === "string" && message.activeSessionName.trim()) {
13479
13778
  setActiveReplSession(message.activeSessionName);
13480
13779
  }
13780
+ const journalChanged = mergeReplJournalEntries(message.journalEntries);
13481
13781
  if (typeof message.transcript === "string") replTranscript = trimReplTranscript(message.transcript);
13482
13782
  if (typeof message.capturedAt === "number") replCapturedAt = message.capturedAt;
13483
13783
  replError = typeof message.replError === "string" ? message.replError : (typeof message.captureError === "string" ? message.captureError : "");
@@ -13492,6 +13792,7 @@
13492
13792
  || previousTranscript !== replTranscript
13493
13793
  || previousError !== replError
13494
13794
  || previousMessage !== replMessage
13795
+ || journalChanged
13495
13796
  || (!previousCapturedAt && replCapturedAt);
13496
13797
  if (viewChanged) renderReplViewIfActive();
13497
13798
  updateReferenceBadge();
@@ -13503,9 +13804,10 @@
13503
13804
  setActiveReplSession(message.sessionName);
13504
13805
  }
13505
13806
  const changed = recordReplToolSend(message);
13807
+ const journalChanged = mergeReplJournalEntries(message.journalEntries);
13506
13808
  if (typeof message.transcript === "string") replTranscript = trimReplTranscript(message.transcript);
13507
13809
  if (typeof message.capturedAt === "number") replCapturedAt = message.capturedAt;
13508
- if (changed) renderReplViewIfActive({ force: true });
13810
+ if (changed || journalChanged) renderReplViewIfActive({ force: true });
13509
13811
  updateReferenceBadge();
13510
13812
  return;
13511
13813
  }
@@ -13528,13 +13830,13 @@
13528
13830
  if (typeof message.activeSessionName === "string" && message.activeSessionName.trim()) {
13529
13831
  setActiveReplSession(message.activeSessionName);
13530
13832
  }
13531
- let journalChanged = false;
13833
+ let journalChanged = mergeReplJournalEntries(message.journalEntries);
13532
13834
  if (typeof message.transcript === "string") {
13533
13835
  replTranscript = trimReplTranscript(message.transcript);
13534
13836
  journalChanged = updateActiveReplJournalEntryFromTranscript(
13535
13837
  typeof message.activeSessionName === "string" && message.activeSessionName.trim() ? message.activeSessionName : replActiveSessionName,
13536
13838
  replTranscript
13537
- );
13839
+ ) || journalChanged;
13538
13840
  }
13539
13841
  if (typeof message.capturedAt === "number") replCapturedAt = message.capturedAt;
13540
13842
  replError = typeof message.replError === "string" ? message.replError : "";
@@ -13557,6 +13859,7 @@
13557
13859
  replBusy = false;
13558
13860
  replMessage = "";
13559
13861
  replError = "";
13862
+ mergeReplJournalEntries(message.journalEntries);
13560
13863
  if (typeof message.requestId === "string") {
13561
13864
  replJournalEntries = replJournalEntries.map((entry) => entry.requestId === message.requestId ? { ...entry, status: "sent", updatedAt: Date.now() } : entry);
13562
13865
  persistReplJournalEntries();
@@ -14635,10 +14938,6 @@
14635
14938
 
14636
14939
  if (quizBtn) {
14637
14940
  quizBtn.addEventListener("click", () => {
14638
- if (!hasResumableQuiz() && !String(sourceTextEl.value || "").trim()) {
14639
- setStatus("Add editor text before starting a quiz.", "warning");
14640
- return;
14641
- }
14642
14941
  openQuizOverlay();
14643
14942
  });
14644
14943
  }