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.
- package/CHANGELOG.md +25 -0
- package/README.md +1 -1
- package/client/studio-client.js +389 -90
- package/client/studio.css +92 -14
- package/index.ts +536 -75
- package/package.json +1 -1
package/client/studio-client.js
CHANGED
|
@@ -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((
|
|
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
|
|
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
|
-
|
|
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
|
|
1451
|
-
|
|
1452
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1483
|
-
|
|
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
|
-
|
|
1495
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1521
|
-
|
|
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
|
-
|
|
2099
|
-
|
|
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
|
-
|
|
5331
|
-
|
|
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
|
-
|
|
5503
|
-
|
|
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 &&
|
|
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 =
|
|
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
|
|
6203
|
-
const
|
|
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,
|
|
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 =
|
|
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'>
|
|
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
|
-
+ (
|
|
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
|
-
? (
|
|
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 && !
|
|
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
|
-
|
|
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)
|
|
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
|
|
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
|
-
+
|
|
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
|
-
+ "<
|
|
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
|
-
|
|
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.
|
|
7745
|
-
|
|
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
|
-
|
|
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
|
-
? "
|
|
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
|
|
13118
|
-
sendReplBtn.hidden =
|
|
13119
|
-
sendReplBtn.disabled = wsState === "Disconnected" || uiBusy || replBusy || !
|
|
13120
|
-
sendReplBtn.
|
|
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:
|
|
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
|
|
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 =
|
|
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
|
}
|