pi-studio 0.9.54 → 0.9.56
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 +22 -0
- package/README.md +16 -3
- package/ROADMAP.md +24 -1
- package/client/studio-client.js +259 -65
- package/client/studio.css +141 -5
- package/index.ts +806 -213
- package/package.json +1 -1
- package/shared/REPL_SESSION_RECORD_PROTOCOL.md +70 -0
- package/shared/repl-session-record.js +623 -0
- package/shared/studio-side-question.js +32 -3
package/client/studio-client.js
CHANGED
|
@@ -74,7 +74,9 @@
|
|
|
74
74
|
: null;
|
|
75
75
|
const referenceBadgeEl = document.getElementById("referenceBadge");
|
|
76
76
|
const editorViewSelect = document.getElementById("editorViewSelect");
|
|
77
|
+
const editorViewSelectWrap = document.getElementById("editorViewSelectWrap");
|
|
77
78
|
const rightViewSelect = document.getElementById("rightViewSelect");
|
|
79
|
+
const rightViewSelectWrap = document.getElementById("rightViewSelectWrap");
|
|
78
80
|
const followSelect = document.getElementById("followSelect");
|
|
79
81
|
const responseHighlightSelect = document.getElementById("responseHighlightSelect");
|
|
80
82
|
const responseFontSizeSelect = document.getElementById("responseFontSizeSelect");
|
|
@@ -486,6 +488,7 @@
|
|
|
486
488
|
const EDITOR_TAB_TEXT = " ";
|
|
487
489
|
const QUIZ_DEFAULT_COUNT = 5;
|
|
488
490
|
const SIDE_QUESTION_THINKING_STORAGE_KEY = "piStudio.sideQuestionThinking";
|
|
491
|
+
const SIDE_QUESTION_THINKING_LEVELS = Object.freeze(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
|
|
489
492
|
const SIDE_QUESTION_GATHER_STORAGE_KEY = "piStudio.sideQuestionGatherScope";
|
|
490
493
|
const SIDE_QUESTION_TOOLS_STORAGE_KEY = "piStudio.sideQuestionTools";
|
|
491
494
|
const SIDE_QUESTION_TRANSCRIPT_RENDER_MAX_CHARS = 400_000;
|
|
@@ -496,6 +499,7 @@
|
|
|
496
499
|
const QUIZ_ANGLES = ["general", "scientist", "mathematician", "statistician", "developer", "reviewer"];
|
|
497
500
|
const QUIZ_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high"];
|
|
498
501
|
let sideQuestionState = null;
|
|
502
|
+
let sideQuestionThinkingLevels = [...SIDE_QUESTION_THINKING_LEVELS];
|
|
499
503
|
let sideQuestionWebSearchAvailable = false;
|
|
500
504
|
let sideQuestionAvailablePiTools = [];
|
|
501
505
|
let sideQuestionPreviewRenderNonce = 0;
|
|
@@ -524,11 +528,12 @@
|
|
|
524
528
|
thinking: (() => {
|
|
525
529
|
try {
|
|
526
530
|
const value = window.localStorage && window.localStorage.getItem(SIDE_QUESTION_THINKING_STORAGE_KEY);
|
|
527
|
-
return
|
|
531
|
+
return SIDE_QUESTION_THINKING_LEVELS.includes(value) ? value : "low";
|
|
528
532
|
} catch { return "low"; }
|
|
529
533
|
})(),
|
|
530
534
|
draft: "",
|
|
531
535
|
};
|
|
536
|
+
let sideQuestionPreferredThinking = sideQuestionUi.thinking;
|
|
532
537
|
let quizOverlayEl = null;
|
|
533
538
|
let quizDialogEl = null;
|
|
534
539
|
let quizPreviewRenderNonce = 0;
|
|
@@ -592,13 +597,18 @@
|
|
|
592
597
|
})();
|
|
593
598
|
function normalizeReplJournalEntry(entry) {
|
|
594
599
|
if (!entry || typeof entry !== "object") return null;
|
|
600
|
+
const hasCompatibleOrigin = entry.origin === "pi-repl" || entry.origin === "pi-studio";
|
|
601
|
+
const sharedSynced = entry.sharedSynced === true || (entry.sharedSynced !== false && hasCompatibleOrigin);
|
|
595
602
|
const normalized = {
|
|
596
603
|
id: typeof entry.id === "string" && entry.id ? entry.id : ("repl-journal-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 8)),
|
|
597
604
|
requestId: typeof entry.requestId === "string" ? entry.requestId : "",
|
|
598
605
|
createdAt: typeof entry.createdAt === "number" && Number.isFinite(entry.createdAt) ? entry.createdAt : Date.now(),
|
|
599
606
|
updatedAt: typeof entry.updatedAt === "number" && Number.isFinite(entry.updatedAt) ? entry.updatedAt : Date.now(),
|
|
607
|
+
completedAt: typeof entry.completedAt === "number" && Number.isFinite(entry.completedAt) ? entry.completedAt : null,
|
|
600
608
|
sessionName: typeof entry.sessionName === "string" ? entry.sessionName : "",
|
|
601
609
|
runtime: typeof entry.runtime === "string" ? entry.runtime : "python",
|
|
610
|
+
origin: hasCompatibleOrigin ? entry.origin : "",
|
|
611
|
+
legacyLocal: !sharedSynced,
|
|
602
612
|
label: typeof entry.label === "string" ? entry.label : "REPL send",
|
|
603
613
|
mode: typeof entry.mode === "string" ? entry.mode : "raw",
|
|
604
614
|
prose: typeof entry.prose === "string" ? entry.prose : "",
|
|
@@ -630,8 +640,11 @@
|
|
|
630
640
|
requestId: entry.requestId,
|
|
631
641
|
createdAt: entry.createdAt,
|
|
632
642
|
updatedAt: entry.updatedAt,
|
|
643
|
+
completedAt: entry.completedAt,
|
|
633
644
|
sessionName: entry.sessionName,
|
|
634
645
|
runtime: entry.runtime,
|
|
646
|
+
origin: entry.origin,
|
|
647
|
+
sharedSynced: !entry.legacyLocal,
|
|
635
648
|
label: entry.label,
|
|
636
649
|
mode: entry.mode,
|
|
637
650
|
prose: entry.prose,
|
|
@@ -666,6 +679,7 @@
|
|
|
666
679
|
mode: existing.mode || entry.mode,
|
|
667
680
|
prose: existing.prose || entry.prose,
|
|
668
681
|
beforeTranscript: existing.beforeTranscript || "",
|
|
682
|
+
legacyLocal: entry.legacyLocal,
|
|
669
683
|
createdAt: existing.createdAt || entry.createdAt,
|
|
670
684
|
updatedAt: Math.max(existing.updatedAt || 0, entry.updatedAt || 0),
|
|
671
685
|
skippedChunks: existing.skippedChunks || entry.skippedChunks,
|
|
@@ -689,6 +703,7 @@
|
|
|
689
703
|
|
|
690
704
|
let replJournalEntries = loadPersistedReplJournalEntries();
|
|
691
705
|
let activeReplJournalEntryId = "";
|
|
706
|
+
const replJournalImportPendingSessions = new Set();
|
|
692
707
|
let replJournalCollapsed = (() => {
|
|
693
708
|
try {
|
|
694
709
|
const stored = window.localStorage ? window.localStorage.getItem("piStudio.replStudioCollapsed") : null;
|
|
@@ -1810,13 +1825,17 @@
|
|
|
1810
1825
|
}
|
|
1811
1826
|
|
|
1812
1827
|
function createReplJournalEntry(details) {
|
|
1828
|
+
const sharedSynced = details.sharedSynced === true;
|
|
1813
1829
|
return {
|
|
1814
1830
|
id: "repl-journal-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 8),
|
|
1815
1831
|
requestId: details.requestId || "",
|
|
1816
1832
|
createdAt: Date.now(),
|
|
1817
1833
|
updatedAt: Date.now(),
|
|
1834
|
+
completedAt: typeof details.completedAt === "number" ? details.completedAt : null,
|
|
1818
1835
|
sessionName: details.sessionName || "",
|
|
1819
1836
|
runtime: details.runtime || getActiveReplRuntime(),
|
|
1837
|
+
origin: details.origin === "pi-repl" ? "pi-repl" : "pi-studio",
|
|
1838
|
+
legacyLocal: !sharedSynced,
|
|
1820
1839
|
label: details.label || "REPL send",
|
|
1821
1840
|
mode: details.mode || replSendMode,
|
|
1822
1841
|
prose: String(details.prose || ""),
|
|
@@ -1835,6 +1854,65 @@
|
|
|
1835
1854
|
return entry;
|
|
1836
1855
|
}
|
|
1837
1856
|
|
|
1857
|
+
function syncReplJournalEntry(entry) {
|
|
1858
|
+
if (!entry || !entry.sessionName || wsState === "Disconnected") return false;
|
|
1859
|
+
return sendMessage({
|
|
1860
|
+
type: "repl_journal_upsert_request",
|
|
1861
|
+
requestId: makeRequestId(),
|
|
1862
|
+
sessionName: entry.sessionName,
|
|
1863
|
+
entry: {
|
|
1864
|
+
id: entry.id,
|
|
1865
|
+
requestId: entry.requestId,
|
|
1866
|
+
createdAt: entry.createdAt,
|
|
1867
|
+
updatedAt: entry.updatedAt,
|
|
1868
|
+
completedAt: entry.completedAt,
|
|
1869
|
+
runtime: entry.runtime,
|
|
1870
|
+
origin: "pi-studio",
|
|
1871
|
+
label: entry.label,
|
|
1872
|
+
mode: entry.mode,
|
|
1873
|
+
prose: entry.prose,
|
|
1874
|
+
code: entry.code,
|
|
1875
|
+
output: entry.output,
|
|
1876
|
+
status: entry.status,
|
|
1877
|
+
skippedChunks: entry.skippedChunks,
|
|
1878
|
+
},
|
|
1879
|
+
});
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
function maybeImportLegacyReplJournalEntries(sessionName) {
|
|
1883
|
+
const name = String(sessionName || "").trim();
|
|
1884
|
+
if (!name || replJournalImportPendingSessions.has(name) || wsState === "Disconnected") return false;
|
|
1885
|
+
const entries = replJournalEntries.filter((entry) => entry.sessionName === name && entry.legacyLocal).slice(-80);
|
|
1886
|
+
if (!entries.length) return false;
|
|
1887
|
+
replJournalImportPendingSessions.add(name);
|
|
1888
|
+
const sent = sendMessage({
|
|
1889
|
+
type: "repl_journal_import_request",
|
|
1890
|
+
requestId: makeRequestId(),
|
|
1891
|
+
sessionName: name,
|
|
1892
|
+
entries: entries.map((entry) => ({
|
|
1893
|
+
id: entry.id,
|
|
1894
|
+
requestId: entry.requestId,
|
|
1895
|
+
createdAt: entry.createdAt,
|
|
1896
|
+
updatedAt: entry.updatedAt,
|
|
1897
|
+
runtime: entry.runtime,
|
|
1898
|
+
origin: "pi-studio",
|
|
1899
|
+
label: entry.label,
|
|
1900
|
+
mode: entry.mode,
|
|
1901
|
+
prose: entry.prose,
|
|
1902
|
+
code: entry.code,
|
|
1903
|
+
output: entry.output,
|
|
1904
|
+
status: entry.status,
|
|
1905
|
+
skippedChunks: entry.skippedChunks,
|
|
1906
|
+
})),
|
|
1907
|
+
});
|
|
1908
|
+
if (!sent) {
|
|
1909
|
+
replJournalImportPendingSessions.delete(name);
|
|
1910
|
+
} else {
|
|
1911
|
+
window.setTimeout(() => replJournalImportPendingSessions.delete(name), 5000);
|
|
1912
|
+
}
|
|
1913
|
+
return sent;
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1838
1916
|
function recordReplToolSend(message) {
|
|
1839
1917
|
const requestId = typeof message.toolCallId === "string" && message.toolCallId.trim()
|
|
1840
1918
|
? "tool:" + message.toolCallId.trim()
|
|
@@ -1848,6 +1926,8 @@
|
|
|
1848
1926
|
requestId,
|
|
1849
1927
|
sessionName,
|
|
1850
1928
|
runtime,
|
|
1929
|
+
origin: message.origin === "pi-repl" ? "pi-repl" : "pi-studio",
|
|
1930
|
+
sharedSynced: message.sharedSynced === true,
|
|
1851
1931
|
label: typeof message.label === "string" && message.label.trim() ? message.label.trim() : "Pi",
|
|
1852
1932
|
mode: "agent",
|
|
1853
1933
|
code,
|
|
@@ -1966,19 +2046,28 @@
|
|
|
1966
2046
|
function buildReplJournalMarkdown(entries) {
|
|
1967
2047
|
const visibleEntries = Array.isArray(entries) ? entries : getVisibleReplJournalEntries();
|
|
1968
2048
|
const sessionName = getActiveReplJournalSessionName();
|
|
1969
|
-
const
|
|
1970
|
-
|
|
1971
|
-
|
|
2049
|
+
const updatedAt = visibleEntries.reduce((latest, entry) => Math.max(latest, Number(entry.updatedAt) || 0), 0) || Date.now();
|
|
2050
|
+
const lines = [
|
|
2051
|
+
"# Shared REPL Record",
|
|
2052
|
+
"",
|
|
2053
|
+
"Session: `" + (sessionName || "unknown") + "`",
|
|
2054
|
+
"Record protocol: pi-repl-session-record v1",
|
|
2055
|
+
"Updated: " + new Date(updatedAt).toISOString(),
|
|
2056
|
+
"",
|
|
2057
|
+
];
|
|
1972
2058
|
if (!visibleEntries.length) {
|
|
1973
|
-
lines.push(
|
|
2059
|
+
lines.push("_No compatible-client entries have been recorded for this tmux session._", "");
|
|
2060
|
+
lines.push("_This clean record contains submissions made through compatible clients. Commands typed directly into an attached tmux pane remain available only in the raw pane/history mirror._", "");
|
|
1974
2061
|
return lines.join("\n");
|
|
1975
2062
|
}
|
|
1976
2063
|
visibleEntries.forEach((entry, index) => {
|
|
1977
2064
|
lines.push("## " + (index + 1) + ". " + (entry.label || "REPL entry"));
|
|
1978
2065
|
lines.push("");
|
|
1979
|
-
lines.push("- Time: " + new Date(entry.createdAt || Date.now()).
|
|
2066
|
+
lines.push("- Time: " + new Date(entry.createdAt || Date.now()).toISOString());
|
|
2067
|
+
lines.push("- Origin: " + (entry.origin || "unknown"));
|
|
2068
|
+
lines.push("- Mode: " + (entry.mode || "raw"));
|
|
2069
|
+
lines.push("- Status: " + (entry.status || "sent"));
|
|
1980
2070
|
if (entry.runtime) lines.push("- Runtime: " + entry.runtime);
|
|
1981
|
-
if (entry.sessionName) lines.push("- Session: `" + entry.sessionName + "`");
|
|
1982
2071
|
if (entry.skippedChunks) lines.push("- Skipped chunks: " + entry.skippedChunks);
|
|
1983
2072
|
lines.push("");
|
|
1984
2073
|
if (String(entry.prose || "").trim()) {
|
|
@@ -1986,7 +2075,10 @@
|
|
|
1986
2075
|
lines.push("");
|
|
1987
2076
|
}
|
|
1988
2077
|
if (String(entry.code || "").trim()) {
|
|
1989
|
-
|
|
2078
|
+
const markdownRuntime = entry.runtime === "ipython"
|
|
2079
|
+
? "python"
|
|
2080
|
+
: (entry.runtime === "unknown" || entry.runtime === "shell" ? "" : entry.runtime);
|
|
2081
|
+
lines.push(getMarkdownFenceForText(entry.code, markdownRuntime));
|
|
1990
2082
|
lines.push("");
|
|
1991
2083
|
}
|
|
1992
2084
|
if (String(entry.output || "").trim()) {
|
|
@@ -1996,17 +2088,18 @@
|
|
|
1996
2088
|
lines.push("");
|
|
1997
2089
|
}
|
|
1998
2090
|
});
|
|
2091
|
+
lines.push("_This clean record contains submissions made through compatible clients. Commands typed directly into an attached tmux pane remain available only in the raw pane/history mirror._", "");
|
|
1999
2092
|
return lines.join("\n").replace(/\n{4,}/g, "\n\n\n").trimEnd() + "\n";
|
|
2000
2093
|
}
|
|
2001
2094
|
|
|
2002
2095
|
async function copyReplJournalToClipboard() {
|
|
2003
2096
|
const entries = getVisibleReplJournalEntries();
|
|
2004
2097
|
if (!entries.length) {
|
|
2005
|
-
setStatus("No
|
|
2098
|
+
setStatus("No shared REPL record entries to copy for this session yet.", "warning");
|
|
2006
2099
|
return;
|
|
2007
2100
|
}
|
|
2008
2101
|
if (await writeTextToClipboard(buildReplJournalMarkdown(entries))) {
|
|
2009
|
-
setStatus("Copied
|
|
2102
|
+
setStatus("Copied shared REPL record as Markdown.", "success");
|
|
2010
2103
|
} else {
|
|
2011
2104
|
setStatus("Clipboard write failed.", "warning");
|
|
2012
2105
|
}
|
|
@@ -2015,7 +2108,7 @@
|
|
|
2015
2108
|
function exportReplJournalMarkdown() {
|
|
2016
2109
|
const entries = getVisibleReplJournalEntries();
|
|
2017
2110
|
if (!entries.length) {
|
|
2018
|
-
setStatus("No
|
|
2111
|
+
setStatus("No shared REPL record entries to export for this session yet.", "warning");
|
|
2019
2112
|
return;
|
|
2020
2113
|
}
|
|
2021
2114
|
const blob = new Blob([buildReplJournalMarkdown(entries)], { type: "text/markdown;charset=utf-8" });
|
|
@@ -2024,12 +2117,12 @@
|
|
|
2024
2117
|
const stamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
2025
2118
|
const sessionSlug = getActiveReplJournalSessionName().replace(/[^-_.A-Za-z0-9]+/g, "-");
|
|
2026
2119
|
link.href = blobUrl;
|
|
2027
|
-
link.download = "repl-
|
|
2120
|
+
link.download = "repl-record" + (sessionSlug ? "-" + sessionSlug : "") + "-" + stamp + ".md";
|
|
2028
2121
|
document.body.appendChild(link);
|
|
2029
2122
|
link.click();
|
|
2030
2123
|
link.remove();
|
|
2031
2124
|
window.setTimeout(() => URL.revokeObjectURL(blobUrl), 1000);
|
|
2032
|
-
setStatus("Exported
|
|
2125
|
+
setStatus("Exported shared REPL record Markdown.", "success");
|
|
2033
2126
|
}
|
|
2034
2127
|
|
|
2035
2128
|
function clearReplJournal() {
|
|
@@ -2041,30 +2134,33 @@
|
|
|
2041
2134
|
}
|
|
2042
2135
|
activeReplJournalEntryId = "";
|
|
2043
2136
|
persistReplJournalEntries();
|
|
2044
|
-
|
|
2137
|
+
if (sessionName) {
|
|
2138
|
+
sendMessage({ type: "repl_journal_clear_request", requestId: makeRequestId(), sessionName });
|
|
2139
|
+
}
|
|
2140
|
+
setStatus(sessionName ? "Cleared shared REPL record for this session." : "Cleared local REPL record cache.", "success");
|
|
2045
2141
|
renderReplViewIfActive({ force: true });
|
|
2046
2142
|
}
|
|
2047
2143
|
|
|
2048
2144
|
function loadReplJournalIntoEditor() {
|
|
2049
2145
|
const entries = getVisibleReplJournalEntries();
|
|
2050
2146
|
if (!entries.length) {
|
|
2051
|
-
setStatus("No
|
|
2147
|
+
setStatus("No shared REPL record entries to load for this session yet.", "warning");
|
|
2052
2148
|
return;
|
|
2053
2149
|
}
|
|
2054
2150
|
const markdown = buildReplJournalMarkdown(entries);
|
|
2055
2151
|
setEditorText(markdown, { preserveScroll: false, preserveSelection: false });
|
|
2056
|
-
setSourceState({ source: "blank", label: "
|
|
2152
|
+
setSourceState({ source: "blank", label: "Shared REPL Record", path: null });
|
|
2057
2153
|
setEditorLanguage("markdown");
|
|
2058
|
-
setStatus("Loaded
|
|
2154
|
+
setStatus("Loaded shared REPL record into editor.", "success");
|
|
2059
2155
|
}
|
|
2060
2156
|
|
|
2061
2157
|
function addSelectedReplJournalNote() {
|
|
2062
2158
|
const note = getSelectedOrCurrentParagraphForReplNote();
|
|
2063
2159
|
if (!note.trim()) {
|
|
2064
|
-
setStatus("Select prose or place the cursor in a paragraph to add a
|
|
2160
|
+
setStatus("Select prose or place the cursor in a paragraph to add a shared REPL record note.", "warning");
|
|
2065
2161
|
return;
|
|
2066
2162
|
}
|
|
2067
|
-
addReplJournalEntry({
|
|
2163
|
+
const entry = addReplJournalEntry({
|
|
2068
2164
|
label: "note",
|
|
2069
2165
|
prose: note,
|
|
2070
2166
|
status: "note",
|
|
@@ -2072,7 +2168,8 @@
|
|
|
2072
2168
|
sessionName: replActiveSessionName,
|
|
2073
2169
|
runtime: getActiveReplRuntime(),
|
|
2074
2170
|
});
|
|
2075
|
-
|
|
2171
|
+
syncReplJournalEntry(entry);
|
|
2172
|
+
setStatus("Added note to shared REPL record.", "success");
|
|
2076
2173
|
renderReplViewIfActive({ force: true });
|
|
2077
2174
|
}
|
|
2078
2175
|
|
|
@@ -2088,7 +2185,7 @@
|
|
|
2088
2185
|
}
|
|
2089
2186
|
if (payload.noteOnly) {
|
|
2090
2187
|
if (String(payload.prose || "").trim()) {
|
|
2091
|
-
addReplJournalEntry({
|
|
2188
|
+
const entry = addReplJournalEntry({
|
|
2092
2189
|
label: payload.label || "note",
|
|
2093
2190
|
prose: payload.prose,
|
|
2094
2191
|
status: "note",
|
|
@@ -2097,7 +2194,8 @@
|
|
|
2097
2194
|
runtime: getActiveReplRuntime(),
|
|
2098
2195
|
skippedChunks: payload.skippedChunks,
|
|
2099
2196
|
});
|
|
2100
|
-
|
|
2197
|
+
syncReplJournalEntry(entry);
|
|
2198
|
+
setStatus("Added prose to shared REPL record.", "success");
|
|
2101
2199
|
renderReplViewIfActive({ force: true });
|
|
2102
2200
|
} else {
|
|
2103
2201
|
setStatus("No code or prose found to send.", "warning");
|
|
@@ -2128,7 +2226,18 @@
|
|
|
2128
2226
|
renderReplViewIfActive({ force: true });
|
|
2129
2227
|
const skippedSuffix = payload.skippedChunks ? " (skipped " + payload.skippedChunks + " incompatible chunk" + (payload.skippedChunks === 1 ? "" : "s") + ")" : "";
|
|
2130
2228
|
setStatus("Sending " + (payload.label || "editor text") + " to REPL…" + skippedSuffix, "info");
|
|
2131
|
-
if (!sendMessage({
|
|
2229
|
+
if (!sendMessage({
|
|
2230
|
+
type: "repl_send_request",
|
|
2231
|
+
requestId,
|
|
2232
|
+
sessionName: session.sessionName,
|
|
2233
|
+
text,
|
|
2234
|
+
journalEntryId: journalEntry.id,
|
|
2235
|
+
createdAt: journalEntry.createdAt,
|
|
2236
|
+
label: journalEntry.label,
|
|
2237
|
+
mode: journalEntry.mode,
|
|
2238
|
+
prose: journalEntry.prose,
|
|
2239
|
+
skippedChunks: journalEntry.skippedChunks,
|
|
2240
|
+
})) {
|
|
2132
2241
|
replBusy = false;
|
|
2133
2242
|
replJournalEntries = replJournalEntries.map((entry) => entry.id === journalEntry.id ? { ...entry, status: "error" } : entry);
|
|
2134
2243
|
persistReplJournalEntries();
|
|
@@ -2914,9 +3023,28 @@
|
|
|
2914
3023
|
}
|
|
2915
3024
|
titleGroupEl.appendChild(makeStudioUiRefreshSeparator());
|
|
2916
3025
|
if (isEditorOnlyMode) {
|
|
2917
|
-
|
|
3026
|
+
const staticTitleEl = makeStudioUiRefreshElement(
|
|
3027
|
+
"span",
|
|
3028
|
+
"studio-refresh-static-title",
|
|
3029
|
+
isWatchedFilePreview ? "Source" : "Editor (Raw)",
|
|
3030
|
+
);
|
|
3031
|
+
if (isWatchedFilePreview) {
|
|
3032
|
+
staticTitleEl.classList.add("studio-watched-source-title");
|
|
3033
|
+
staticTitleEl.setAttribute("aria-label", "Source, read-only. Follows the file on disk.");
|
|
3034
|
+
staticTitleEl.title = "This source is read-only and follows the file on disk.";
|
|
3035
|
+
}
|
|
3036
|
+
titleGroupEl.appendChild(staticTitleEl);
|
|
3037
|
+
if (isWatchedFilePreview) {
|
|
3038
|
+
const readOnlyBadgeEl = makeStudioUiRefreshElement(
|
|
3039
|
+
"span",
|
|
3040
|
+
"studio-watched-source-read-only-badge",
|
|
3041
|
+
"Read-only · follows disk",
|
|
3042
|
+
);
|
|
3043
|
+
readOnlyBadgeEl.setAttribute("aria-hidden", "true");
|
|
3044
|
+
titleGroupEl.appendChild(readOnlyBadgeEl);
|
|
3045
|
+
}
|
|
2918
3046
|
} else if (editorViewSelect) {
|
|
2919
|
-
titleGroupEl.appendChild(editorViewSelect);
|
|
3047
|
+
titleGroupEl.appendChild(editorViewSelectWrap || editorViewSelect);
|
|
2920
3048
|
}
|
|
2921
3049
|
if (contextMenu) {
|
|
2922
3050
|
titleGroupEl.appendChild(makeStudioUiRefreshSeparator());
|
|
@@ -2942,7 +3070,7 @@
|
|
|
2942
3070
|
rightTitleGroupEl.appendChild(rightFocusBtn);
|
|
2943
3071
|
rightTitleGroupEl.appendChild(makeStudioUiRefreshSeparator());
|
|
2944
3072
|
}
|
|
2945
|
-
rightTitleGroupEl.appendChild(rightViewSelect);
|
|
3073
|
+
rightTitleGroupEl.appendChild(rightViewSelectWrap || rightViewSelect);
|
|
2946
3074
|
rightIdentityEl.appendChild(rightTitleGroupEl);
|
|
2947
3075
|
const rightToolsEl = makeStudioUiRefreshElement("div", "studio-refresh-pane-tools");
|
|
2948
3076
|
if (watchedOpenEditableBtn && isWatchedFilePreview) rightToolsEl.appendChild(watchedOpenEditableBtn);
|
|
@@ -3615,6 +3743,9 @@
|
|
|
3615
3743
|
if (typeof message.thinkingLevel === "string") {
|
|
3616
3744
|
piThinkingLevel = message.thinkingLevel.trim();
|
|
3617
3745
|
}
|
|
3746
|
+
if (Array.isArray(message.sideQuestionThinkingLevels)) {
|
|
3747
|
+
applySideQuestionThinkingLevels(message.sideQuestionThinkingLevels);
|
|
3748
|
+
}
|
|
3618
3749
|
renderFooterModelMenu();
|
|
3619
3750
|
}
|
|
3620
3751
|
|
|
@@ -3649,8 +3780,8 @@
|
|
|
3649
3780
|
});
|
|
3650
3781
|
footerModelMenuEl.innerHTML = ""
|
|
3651
3782
|
+ "<div class='footer-model-menu-heading'>Pi model & thinking</div>"
|
|
3652
|
-
+ "<label class='footer-model-menu-field'><span>Pi model</span><select id='footerPiModelSelect'>" + modelOptionsHtml.join("") + "</select></label>"
|
|
3653
|
-
+ "<label class='footer-model-menu-field'><span>Thinking</span><select id='footerPiThinkingSelect'>" + thinkingOptionsHtml.join("") + "</select></label>"
|
|
3783
|
+
+ "<label class='footer-model-menu-field'><span>Pi model</span><span class='studio-menu-select-wrap'><select id='footerPiModelSelect'>" + modelOptionsHtml.join("") + "</select></span></label>"
|
|
3784
|
+
+ "<label class='footer-model-menu-field'><span>Thinking</span><span class='studio-menu-select-wrap'><select id='footerPiThinkingSelect'>" + thinkingOptionsHtml.join("") + "</select></span></label>"
|
|
3654
3785
|
+ "<div class='footer-model-menu-note'>Affects future Pi turns. Studio Suggest has its own model setting.</div>";
|
|
3655
3786
|
}
|
|
3656
3787
|
|
|
@@ -3730,7 +3861,7 @@
|
|
|
3730
3861
|
footerThemeMenuEl.innerHTML = ""
|
|
3731
3862
|
+ "<div class='footer-model-menu-heading'>Pi theme</div>"
|
|
3732
3863
|
+ (optionsHtml.length
|
|
3733
|
-
? "<label class='footer-model-menu-field'><span>Active theme</span><select id='footerPiThemeSelect'>" + optionsHtml.join("") + "</select></label>"
|
|
3864
|
+
? "<label class='footer-model-menu-field'><span>Active theme</span><span class='studio-menu-select-wrap'><select id='footerPiThemeSelect'>" + optionsHtml.join("") + "</select></span></label>"
|
|
3734
3865
|
: "<div class='footer-model-menu-note'>No Pi themes are available yet.</div>")
|
|
3735
3866
|
+ "<div class='footer-model-menu-note'>Switches the active Pi theme and persists it to Pi settings.</div>";
|
|
3736
3867
|
}
|
|
@@ -4359,7 +4490,9 @@
|
|
|
4359
4490
|
].forEach(([btn, pane]) => {
|
|
4360
4491
|
if (!btn) return;
|
|
4361
4492
|
const isFocusedPane = paneFocusTarget === pane;
|
|
4362
|
-
const paneName = pane === "right"
|
|
4493
|
+
const paneName = pane === "right"
|
|
4494
|
+
? (isWatchedFilePreview ? "watched preview" : "response")
|
|
4495
|
+
: (isWatchedFilePreview ? "read-only source" : "editor");
|
|
4363
4496
|
btn.classList.toggle("is-active", isFocusedPane);
|
|
4364
4497
|
btn.setAttribute("aria-pressed", isFocusedPane ? "true" : "false");
|
|
4365
4498
|
btn.textContent = isFocusedPane ? "Exit focus" : "Focus pane";
|
|
@@ -10194,7 +10327,7 @@
|
|
|
10194
10327
|
}
|
|
10195
10328
|
const replJournalExportEntries = exportingReplJournal ? getVisibleReplJournalEntries() : [];
|
|
10196
10329
|
if (exportingReplJournal && !replJournalExportEntries.length) {
|
|
10197
|
-
setStatus("No
|
|
10330
|
+
setStatus("No shared REPL record entries to export for this session yet.", "warning");
|
|
10198
10331
|
return;
|
|
10199
10332
|
}
|
|
10200
10333
|
|
|
@@ -10439,7 +10572,7 @@
|
|
|
10439
10572
|
}
|
|
10440
10573
|
const replJournalExportEntries = exportingReplJournal ? getVisibleReplJournalEntries() : [];
|
|
10441
10574
|
if (exportingReplJournal && !replJournalExportEntries.length) {
|
|
10442
|
-
setStatus("No
|
|
10575
|
+
setStatus("No shared REPL record entries to export for this session yet.", "warning");
|
|
10443
10576
|
return;
|
|
10444
10577
|
}
|
|
10445
10578
|
|
|
@@ -10472,7 +10605,7 @@
|
|
|
10472
10605
|
let filenameHint = exportingSideThread
|
|
10473
10606
|
? getSideQuestionTranscriptFilename(sideThreadExportedAt).replace(/\.md$/i, ".html")
|
|
10474
10607
|
: (exportingReplJournal ? "repl-studio.html" : (isEditorPreview ? "studio-editor-preview.html" : ("studio-response-" + formatStudioExportTimestamp() + ".studio.html")));
|
|
10475
|
-
let titleHint = exportingSideThread ? "Pi Studio side questions" : (exportingReplJournal ? "
|
|
10608
|
+
let titleHint = exportingSideThread ? "Pi Studio side questions" : (exportingReplJournal ? "Shared REPL Record" : (isEditorPreview ? "Studio editor preview" : "Studio response preview"));
|
|
10476
10609
|
if (sourcePath) {
|
|
10477
10610
|
const baseName = sourcePath.split(/[\\/]/).pop() || "studio";
|
|
10478
10611
|
const stem = baseName.replace(/\.[^.]+$/, "") || "studio";
|
|
@@ -11450,6 +11583,7 @@
|
|
|
11450
11583
|
const parts = [];
|
|
11451
11584
|
const kind = getReplStudioEntryKind(entry);
|
|
11452
11585
|
if (kind !== "Raw") parts.push(kind);
|
|
11586
|
+
if (entry.origin) parts.push(entry.origin);
|
|
11453
11587
|
const time = formatReferenceTime(entry.createdAt);
|
|
11454
11588
|
if (time) parts.push(time);
|
|
11455
11589
|
if (entry.skippedChunks) parts.push("skipped " + String(entry.skippedChunks));
|
|
@@ -11503,12 +11637,12 @@
|
|
|
11503
11637
|
const toggleButton = "<button type='button' data-repl-action='journal-toggle' aria-expanded='" + (replJournalCollapsed ? "false" : "true") + "'>" + (replJournalCollapsed ? "Show record" : "Hide record") + "</button>";
|
|
11504
11638
|
const toggleActions = "<div class='repl-journal-actions'>" + toggleButton + "</div>";
|
|
11505
11639
|
const summaryText = hasEntries
|
|
11506
|
-
? (entryCount + "
|
|
11507
|
-
: (sessionName ? "No
|
|
11640
|
+
? (entryCount + " shared record entr" + (entryCount === 1 ? "y" : "ies") + (sessionName ? " for " + sessionName : "") + ". Export is Markdown.")
|
|
11641
|
+
: (sessionName ? "No compatible-client entries for " + sessionName + "." : "pi-repl and pi-studio submissions will appear here.");
|
|
11508
11642
|
if (replJournalCollapsed) {
|
|
11509
11643
|
return "<section class='repl-journal repl-journal-compact" + collapsedClass + "'>"
|
|
11510
11644
|
+ "<div class='repl-journal-compact-row'>"
|
|
11511
|
-
+ "<div class='repl-journal-compact-title'><span class='repl-journal-chip'>
|
|
11645
|
+
+ "<div class='repl-journal-compact-title'><span class='repl-journal-chip'>Shared REPL Record</span><span>" + escapeHtml(summaryText) + "</span></div>"
|
|
11512
11646
|
+ "<div class='repl-journal-actions'>" + toggleButton + "</div>"
|
|
11513
11647
|
+ "</div>"
|
|
11514
11648
|
+ "</section>";
|
|
@@ -11546,14 +11680,14 @@
|
|
|
11546
11680
|
}).join("");
|
|
11547
11681
|
const emptyText = sessionName
|
|
11548
11682
|
? (String(transcript || "").trim()
|
|
11549
|
-
? "No
|
|
11550
|
-
: "No
|
|
11551
|
-
: "No
|
|
11683
|
+
? "No compatible-client record entries yet. The raw tmux mirror below still has this session's history; send code through pi-repl or Studio to build a clean record."
|
|
11684
|
+
: "No compatible-client record entries yet. Send code from pi-repl or Studio, or use More → Add note (Literate send) to record prose.")
|
|
11685
|
+
: "No compatible-client record entries yet. Send code from pi-repl or Studio, or use More → Add note (Literate send) to record prose.";
|
|
11552
11686
|
const terminalContent = banner
|
|
11553
11687
|
+ (hasEntries ? cards : "<div class='repl-studio-empty'>" + escapeHtml(emptyText) + "</div>");
|
|
11554
11688
|
return "<section class='repl-journal'>"
|
|
11555
|
-
+ "<div class='repl-journal-header'><h3>
|
|
11556
|
-
+ "<p class='repl-journal-description'>Clean record for
|
|
11689
|
+
+ "<div class='repl-journal-header'><h3>Shared REPL Record</h3>" + toggleActions + "</div>"
|
|
11690
|
+
+ "<p class='repl-journal-description'>Clean record shared by compatible pi-repl and pi-studio clients for this exact tmux session. Direct pane typing remains in the raw mirror below.</p>"
|
|
11557
11691
|
+ (omitted ? "<div class='repl-journal-omitted'>Showing latest 12 entries for this session; " + escapeHtml(String(omitted)) + " older entries remain in export.</div>" : "")
|
|
11558
11692
|
+ "<div class='repl-journal-list'>" + terminalContent + "</div>"
|
|
11559
11693
|
+ "</section>";
|
|
@@ -11577,7 +11711,7 @@
|
|
|
11577
11711
|
+ "</section>";
|
|
11578
11712
|
}
|
|
11579
11713
|
return "<section class='repl-mirror'>"
|
|
11580
|
-
+ "<div class='repl-journal-header'><div><h3>Raw REPL Mirror</h3><p>Best-effort tmux pane mirror. Useful for directly typed commands and debugging; the
|
|
11714
|
+
+ "<div class='repl-journal-header'><div><h3>Raw REPL Mirror</h3><p>Best-effort tmux pane mirror. Useful for directly typed commands and debugging; the shared clean record above has reliable client-submission boundaries.</p></div>" + actions + "</div>"
|
|
11581
11715
|
+ body
|
|
11582
11716
|
+ "</section>";
|
|
11583
11717
|
}
|
|
@@ -11616,10 +11750,10 @@
|
|
|
11616
11750
|
return "<div class='repl-panel'>"
|
|
11617
11751
|
+ "<div class='repl-toolbar'>"
|
|
11618
11752
|
+ "<div class='repl-controls'>"
|
|
11619
|
-
+ "<label class='repl-control-label'>Runtime <select data-repl-runtime aria-label='REPL runtime'>" + runtimeOptions + "</select></label>"
|
|
11753
|
+
+ "<label class='repl-control-label'>Runtime <select class='studio-flat-select' data-repl-runtime aria-label='REPL runtime'>" + runtimeOptions + "</select></label>"
|
|
11620
11754
|
+ "<label class='repl-control-label repl-command-label'>Start cmd <input data-repl-command type='text' value='" + escapeHtml(replCommand) + "' placeholder='default' aria-label='REPL start command' title='Command used by Start for this runtime. Leave blank for the default; use this for envs, e.g. .venv/bin/python, uv run python, or conda run --no-capture-output -n env python.'></label>"
|
|
11621
11755
|
+ "<button class='repl-start-btn' type='button' data-repl-action='start'" + (replBusy || replTmuxAvailable === false ? " disabled" : "") + " title='Start or switch to a " + escapeHtml(runtimeLabel) + " session using the selected runtime and start command.'>Start</button>"
|
|
11622
|
-
+ "<label class='repl-control-label repl-session-label'>Session <select data-repl-session aria-label='REPL session'" + (visibleSessions.length ? "" : " disabled") + ">" + sessionOptions + "</select></label>"
|
|
11756
|
+
+ "<label class='repl-control-label repl-session-label'>Session <select class='studio-flat-select' data-repl-session aria-label='REPL session'" + (visibleSessions.length ? "" : " disabled") + ">" + sessionOptions + "</select></label>"
|
|
11623
11757
|
+ "<details class='repl-more-controls'>"
|
|
11624
11758
|
+ "<summary title='More REPL actions'>More</summary>"
|
|
11625
11759
|
+ "<div class='repl-more-menu'>"
|
|
@@ -11627,7 +11761,7 @@
|
|
|
11627
11761
|
+ "<button type='button' data-repl-action='interrupt'" + (activeSession && !replBusy ? "" : " disabled") + " title='Send Ctrl+C to the active REPL session.'>Interrupt</button>"
|
|
11628
11762
|
+ "<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>"
|
|
11629
11763
|
+ "<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>"
|
|
11630
|
-
+ "<button type='button' data-repl-action='journal-note' title='Add the selected prose/current paragraph to the
|
|
11764
|
+
+ "<button type='button' data-repl-action='journal-note' title='Add the selected prose/current paragraph to the shared REPL record (Literate send) without sending it to the runtime.'>Add note</button>"
|
|
11631
11765
|
+ "<button type='button' data-repl-action='refresh'>Refresh</button>"
|
|
11632
11766
|
+ "<button type='button' data-repl-action='follow'>Follow: " + (replFollow ? "On" : "Off") + "</button>"
|
|
11633
11767
|
+ "</div>"
|
|
@@ -11845,7 +11979,7 @@
|
|
|
11845
11979
|
const options = getFileBrowserSortOptions().map((option) => {
|
|
11846
11980
|
return "<option value='" + escapeHtml(option.value) + "'" + (option.value === currentSort ? " selected" : "") + ">" + escapeHtml(option.label) + "</option>";
|
|
11847
11981
|
}).join("");
|
|
11848
|
-
return "<select class='files-sort-select' data-files-sort aria-label='Files sort order' title='Sort file browser entries. Folders remain grouped first.'>" + options + "</select>";
|
|
11982
|
+
return "<select class='files-sort-select studio-flat-select' data-files-sort aria-label='Files sort order' title='Sort file browser entries. Folders remain grouped first.'>" + options + "</select>";
|
|
11849
11983
|
}
|
|
11850
11984
|
|
|
11851
11985
|
function buildFileBrowserEntryRowHtml(entry) {
|
|
@@ -11900,7 +12034,7 @@
|
|
|
11900
12034
|
const label = String((location && location.label) || basenameForStudioPath(path) || path);
|
|
11901
12035
|
return "<option value='" + escapeHtml(path) + "'" + (path === rootDir ? " selected" : "") + ">" + escapeHtml(label + " — " + path) + "</option>";
|
|
11902
12036
|
}).join("");
|
|
11903
|
-
return "<select class='files-location-select' data-files-location aria-label='Allowed Files location' title='Choose a folder allowed for this Studio session on the computer running Pi.'>" + options + "</select>";
|
|
12037
|
+
return "<select class='files-location-select studio-flat-select' data-files-location aria-label='Allowed Files location' title='Choose a folder allowed for this Studio session on the computer running Pi.'>" + options + "</select>";
|
|
11904
12038
|
}
|
|
11905
12039
|
|
|
11906
12040
|
function buildFileBrowserPanelHtml() {
|
|
@@ -12990,6 +13124,49 @@
|
|
|
12990
13124
|
return values.map(([value, label]) => "<option value='" + escapeHtml(value) + "'" + (value === current ? " selected" : "") + ">" + escapeHtml(label) + "</option>").join("");
|
|
12991
13125
|
}
|
|
12992
13126
|
|
|
13127
|
+
function clampSideQuestionThinkingLevel(value, levels) {
|
|
13128
|
+
const available = Array.isArray(levels) && levels.length ? levels : [...SIDE_QUESTION_THINKING_LEVELS];
|
|
13129
|
+
if (available.includes(value)) return value;
|
|
13130
|
+
const requestedIndex = SIDE_QUESTION_THINKING_LEVELS.indexOf(value);
|
|
13131
|
+
if (requestedIndex < 0) return available.includes("low") ? "low" : available[0];
|
|
13132
|
+
for (let index = requestedIndex; index < SIDE_QUESTION_THINKING_LEVELS.length; index += 1) {
|
|
13133
|
+
const candidate = SIDE_QUESTION_THINKING_LEVELS[index];
|
|
13134
|
+
if (available.includes(candidate)) return candidate;
|
|
13135
|
+
}
|
|
13136
|
+
for (let index = requestedIndex - 1; index >= 0; index -= 1) {
|
|
13137
|
+
const candidate = SIDE_QUESTION_THINKING_LEVELS[index];
|
|
13138
|
+
if (available.includes(candidate)) return candidate;
|
|
13139
|
+
}
|
|
13140
|
+
return "off";
|
|
13141
|
+
}
|
|
13142
|
+
|
|
13143
|
+
function applySideQuestionThinkingLevels(value) {
|
|
13144
|
+
const seen = new Set();
|
|
13145
|
+
const normalized = (Array.isArray(value) ? value : []).filter((level) => {
|
|
13146
|
+
if (typeof level !== "string" || !SIDE_QUESTION_THINKING_LEVELS.includes(level) || seen.has(level)) return false;
|
|
13147
|
+
seen.add(level);
|
|
13148
|
+
return true;
|
|
13149
|
+
});
|
|
13150
|
+
const next = normalized.length ? normalized : [...SIDE_QUESTION_THINKING_LEVELS];
|
|
13151
|
+
const levelsChanged = next.length !== sideQuestionThinkingLevels.length
|
|
13152
|
+
|| next.some((level, index) => level !== sideQuestionThinkingLevels[index]);
|
|
13153
|
+
sideQuestionThinkingLevels = next;
|
|
13154
|
+
const nextThinking = clampSideQuestionThinkingLevel(sideQuestionPreferredThinking, next);
|
|
13155
|
+
const thinkingChanged = nextThinking !== sideQuestionUi.thinking;
|
|
13156
|
+
if (thinkingChanged) sideQuestionUi.thinking = nextThinking;
|
|
13157
|
+
if ((levelsChanged || thinkingChanged) && rightView === "side-questions" && (!sideQuestionState || !sideQuestionState.threadId)) {
|
|
13158
|
+
renderSideQuestionView();
|
|
13159
|
+
}
|
|
13160
|
+
}
|
|
13161
|
+
|
|
13162
|
+
function getSideQuestionThinkingOptions() {
|
|
13163
|
+
return sideQuestionThinkingLevels.map((level) => {
|
|
13164
|
+
if (level === "xhigh") return [level, "X-high (slower)"];
|
|
13165
|
+
if (level === "max") return [level, "Max (slowest)"];
|
|
13166
|
+
return [level, level.charAt(0).toUpperCase() + level.slice(1)];
|
|
13167
|
+
});
|
|
13168
|
+
}
|
|
13169
|
+
|
|
12993
13170
|
function renderSideQuestionPiToolPicker() {
|
|
12994
13171
|
const selected = new Set(sideQuestionUi.toolIds);
|
|
12995
13172
|
const selectedCount = sideQuestionUi.toolIds.length;
|
|
@@ -13026,15 +13203,15 @@
|
|
|
13026
13203
|
return "<div class='side-question-empty'>"
|
|
13027
13204
|
+ "<div class='side-question-intro'><h2>Side question</h2><p>Ask something without adding it to the main Pi conversation. Choose the starting text and whether Studio may look through related files.</p></div>"
|
|
13028
13205
|
+ "<div class='side-question-context-grid'>"
|
|
13029
|
-
+ "<label>Starting text<select data-side-question-field='focusMode' aria-describedby='sideQuestionContextRule'>" + sideQuestionSelectOptions([
|
|
13206
|
+
+ "<label>Starting text<select class='studio-flat-select' data-side-question-field='focusMode' aria-describedby='sideQuestionContextRule'>" + sideQuestionSelectOptions([
|
|
13030
13207
|
["auto", "Automatic"], ["selection", "Editor selection only"], ["section", "Heading block or nearby text at cursor"], ["editor", "Whole editor document"], ["response", "Displayed response"], ["none", "No starting text"],
|
|
13031
13208
|
], sideQuestionUi.focusMode) + "</select></label>"
|
|
13032
|
-
+ "<label>Also use files from<select data-side-question-field='gatherScope'>" + sideQuestionSelectOptions([
|
|
13209
|
+
+ "<label>Also use files from<select class='studio-flat-select' data-side-question-field='gatherScope'>" + sideQuestionSelectOptions([
|
|
13033
13210
|
["none", "No other files"], ["folder", "Same folder as document"], ["repo", "Repository"], ["custom", "Choose a folder"],
|
|
13034
13211
|
], scope) + "</select></label>"
|
|
13035
|
-
+ "<label>Thinking<select data-side-question-field='thinking'>" + sideQuestionSelectOptions(
|
|
13036
|
-
|
|
13037
|
-
|
|
13212
|
+
+ "<label>Thinking<select class='studio-flat-select' data-side-question-field='thinking'>" + sideQuestionSelectOptions(
|
|
13213
|
+
getSideQuestionThinkingOptions(), sideQuestionUi.thinking
|
|
13214
|
+
) + "</select></label>"
|
|
13038
13215
|
+ "</div>"
|
|
13039
13216
|
+ "<details class='side-question-context-rule'><summary id='sideQuestionContextRule'>Automatic: selection → heading block at cursor → nearby text</summary><p>A heading block starts at the nearest Markdown/LaTeX heading above the cursor and ends before the next heading of the same or higher level. With no heading, Studio uses the surrounding text block or a nearby excerpt.</p></details>"
|
|
13040
13217
|
+ (scope === "none" ? "" : "<p class='side-question-context-access-note'>Related-file access is limited to folders allowed for this Studio session. Studio asks before starting if this folder is not already allowed.</p>")
|
|
@@ -13392,9 +13569,10 @@
|
|
|
13392
13569
|
if (sideQuestionUi.gatherScope !== "repo") sideQuestionUi.gitContext = false;
|
|
13393
13570
|
try { if (window.localStorage) window.localStorage.setItem(SIDE_QUESTION_GATHER_STORAGE_KEY, sideQuestionUi.gatherScope); } catch {}
|
|
13394
13571
|
}
|
|
13395
|
-
if (field === "thinking") {
|
|
13572
|
+
if (field === "thinking" && sideQuestionThinkingLevels.includes(target.value)) {
|
|
13573
|
+
sideQuestionPreferredThinking = target.value;
|
|
13396
13574
|
sideQuestionUi.thinking = target.value;
|
|
13397
|
-
try { if (window.localStorage) window.localStorage.setItem(SIDE_QUESTION_THINKING_STORAGE_KEY,
|
|
13575
|
+
try { if (window.localStorage) window.localStorage.setItem(SIDE_QUESTION_THINKING_STORAGE_KEY, sideQuestionPreferredThinking); } catch {}
|
|
13398
13576
|
}
|
|
13399
13577
|
if (field === "includeConversation") sideQuestionUi.includeConversation = target.checked;
|
|
13400
13578
|
if (field === "gitContext") sideQuestionUi.gitContext = target.checked && getSideQuestionGatherScope() === "repo";
|
|
@@ -13604,7 +13782,7 @@
|
|
|
13604
13782
|
} else if (exportingSideThread) {
|
|
13605
13783
|
exportPdfBtn.title = "Save or copy Markdown, open it in an editor, or export the visible side discussion as PDF or HTML.";
|
|
13606
13784
|
} else if (exportingReplJournal && !replJournalExportEntries.length) {
|
|
13607
|
-
exportPdfBtn.title = "No
|
|
13785
|
+
exportPdfBtn.title = "No shared REPL record entries to export for this session yet.";
|
|
13608
13786
|
} else if (rightView === "markdown") {
|
|
13609
13787
|
exportPdfBtn.title = "Switch right pane to Response (Preview), Editor (Preview), REPL, or Side questions to export.";
|
|
13610
13788
|
} else if (!canExportPreview) {
|
|
@@ -13612,7 +13790,7 @@
|
|
|
13612
13790
|
} else if (isHtmlArtifactPreview) {
|
|
13613
13791
|
exportPdfBtn.title = "This is an interactive HTML preview. Export as HTML; PDF export is not available yet.";
|
|
13614
13792
|
} else if (exportingReplJournal) {
|
|
13615
|
-
exportPdfBtn.title = "Choose PDF export or an HTML export destination for the
|
|
13793
|
+
exportPdfBtn.title = "Choose PDF export or an HTML export destination for the shared REPL record.";
|
|
13616
13794
|
} else {
|
|
13617
13795
|
exportPdfBtn.title = "Choose PDF export or an HTML export destination for the current right-pane preview.";
|
|
13618
13796
|
}
|
|
@@ -13624,7 +13802,7 @@
|
|
|
13624
13802
|
? "This transcript is too large for rendered export; save or copy the Markdown instead."
|
|
13625
13803
|
: (isHtmlArtifactPreview
|
|
13626
13804
|
? "Interactive HTML preview PDF export is not available yet."
|
|
13627
|
-
: (exportingSideThread ? "Export the side-thread transcript as PDF and open it in Studio." : (exportingReplJournal ? "Export the
|
|
13805
|
+
: (exportingSideThread ? "Export the side-thread transcript as PDF and open it in Studio." : (exportingReplJournal ? "Export the shared REPL record as PDF and open it in Studio." : "Export the current right-pane preview as PDF and open it in Studio.")));
|
|
13628
13806
|
}
|
|
13629
13807
|
if (exportPreviewPdfBtn) {
|
|
13630
13808
|
exportPreviewPdfBtn.disabled = exportBusy || !canExportPreview || isHtmlArtifactPreview || sideThreadRenderTooLarge;
|
|
@@ -13632,7 +13810,7 @@
|
|
|
13632
13810
|
? "This transcript is too large for rendered export; save or copy the Markdown instead."
|
|
13633
13811
|
: (isHtmlArtifactPreview
|
|
13634
13812
|
? "Interactive HTML preview PDF export is not available yet."
|
|
13635
|
-
: (exportingSideThread ? "Export the side-thread transcript as PDF and open it in the default PDF viewer." : (exportingReplJournal ? "Export the
|
|
13813
|
+
: (exportingSideThread ? "Export the side-thread transcript as PDF and open it in the default PDF viewer." : (exportingReplJournal ? "Export the shared REPL record as PDF and open it in the default PDF viewer." : "Export the current right-pane preview as PDF and open it in the default PDF viewer.")));
|
|
13636
13814
|
}
|
|
13637
13815
|
if (exportPreviewHtmlStudioBtn) {
|
|
13638
13816
|
exportPreviewHtmlStudioBtn.disabled = exportBusy || !canExportPreview || sideThreadRenderTooLarge;
|
|
@@ -13640,7 +13818,7 @@
|
|
|
13640
13818
|
? "This transcript is too large for rendered export; save or copy the Markdown instead."
|
|
13641
13819
|
: (isHtmlArtifactPreview
|
|
13642
13820
|
? "Export the authored HTML preview and open it in a new Studio editor tab."
|
|
13643
|
-
: (exportingSideThread ? "Export the side-thread transcript as standalone HTML and open it in a new Studio editor tab." : (exportingReplJournal ? "Export the
|
|
13821
|
+
: (exportingSideThread ? "Export the side-thread transcript as standalone HTML and open it in a new Studio editor tab." : (exportingReplJournal ? "Export the shared REPL record as standalone HTML and open it in a new Studio editor tab." : "Export the current right-pane preview as standalone HTML and open it in a new Studio editor tab.")));
|
|
13644
13822
|
}
|
|
13645
13823
|
if (exportPreviewHtmlBtn) {
|
|
13646
13824
|
exportPreviewHtmlBtn.disabled = exportBusy || !canExportPreview || sideThreadRenderTooLarge;
|
|
@@ -13648,7 +13826,7 @@
|
|
|
13648
13826
|
? "This transcript is too large for rendered export; save or copy the Markdown instead."
|
|
13649
13827
|
: (isHtmlArtifactPreview
|
|
13650
13828
|
? "Export the authored HTML preview and open it in the default browser."
|
|
13651
|
-
: (exportingSideThread ? "Export the side-thread transcript as standalone HTML and open it in the default browser." : (exportingReplJournal ? "Export the
|
|
13829
|
+
: (exportingSideThread ? "Export the side-thread transcript as standalone HTML and open it in the default browser." : (exportingReplJournal ? "Export the shared REPL record as standalone HTML and open it in the default browser." : "Export the current right-pane preview as standalone HTML and open it in the default browser.")));
|
|
13652
13830
|
}
|
|
13653
13831
|
if (exportPreviewControlsEl) {
|
|
13654
13832
|
exportPreviewControlsEl.hidden = rightView === "editor-quarto-preview";
|
|
@@ -13656,11 +13834,11 @@
|
|
|
13656
13834
|
? (exportingSideThread
|
|
13657
13835
|
? "Choose a durable export for the visible side discussion."
|
|
13658
13836
|
: (exportingReplJournal
|
|
13659
|
-
? "Choose a format and export destination for the
|
|
13837
|
+
? "Choose a format and export destination for the shared REPL record."
|
|
13660
13838
|
: (isHtmlArtifactPreview ? "Export this HTML preview to Studio or browser." : "Choose a format and export destination for the current right-pane preview.")))
|
|
13661
13839
|
: (exportingSideThread
|
|
13662
13840
|
? "No completed side discussion is available to export yet."
|
|
13663
|
-
: (exportingReplJournal ? "No
|
|
13841
|
+
: (exportingReplJournal ? "No shared REPL record entries to export for this session yet." : "Switch right pane to a non-empty preview before exporting."));
|
|
13664
13842
|
}
|
|
13665
13843
|
if (!canExportPreview || previewExportInProgress || sideQuestionMarkdownExportRequest) {
|
|
13666
13844
|
closeExportPreviewMenu();
|
|
@@ -16321,13 +16499,13 @@
|
|
|
16321
16499
|
return "<div class='studio-quiz-setup'>"
|
|
16322
16500
|
+ "<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>"
|
|
16323
16501
|
+ "<div class='studio-quiz-fields'>"
|
|
16324
|
-
+ "<label>Scope<select data-quiz-field='scope'>"
|
|
16502
|
+
+ "<label>Scope<select class='studio-flat-select' data-quiz-field='scope'>"
|
|
16325
16503
|
+ QUIZ_SCOPES.map((candidate) => candidate === "selection" && !hasSelection ? "" : renderQuizOption(candidate, scope, getQuizScopeLabel(candidate))).join("")
|
|
16326
16504
|
+ "</select></label>"
|
|
16327
|
-
+ "<label>Angle<select data-quiz-field='angle'>"
|
|
16505
|
+
+ "<label>Angle<select class='studio-flat-select' data-quiz-field='angle'>"
|
|
16328
16506
|
+ QUIZ_ANGLES.map((candidate) => renderQuizOption(candidate, angle, getQuizAngleLabel(candidate))).join("")
|
|
16329
16507
|
+ "</select></label>"
|
|
16330
|
-
+ "<label>Thinking<select data-quiz-field='thinking'>"
|
|
16508
|
+
+ "<label>Thinking<select class='studio-flat-select' data-quiz-field='thinking'>"
|
|
16331
16509
|
+ QUIZ_THINKING_LEVELS.map((candidate) => renderQuizOption(candidate, thinking, getQuizThinkingLabel(candidate))).join("")
|
|
16332
16510
|
+ "</select></label>"
|
|
16333
16511
|
+ "<label>Questions<input data-quiz-field='count' type='number' min='1' max='8' value='" + String(count) + "'></label>"
|
|
@@ -22490,6 +22668,7 @@
|
|
|
22490
22668
|
}
|
|
22491
22669
|
|
|
22492
22670
|
if (message.type === "side_question_state") {
|
|
22671
|
+
if (Array.isArray(message.sideQuestionThinkingLevels)) applySideQuestionThinkingLevels(message.sideQuestionThinkingLevels);
|
|
22493
22672
|
sideQuestionWebSearchAvailable = message.webSearchAvailable === true;
|
|
22494
22673
|
if (Array.isArray(message.availablePiTools)) applySideQuestionToolCatalog(message.availablePiTools);
|
|
22495
22674
|
const previousStatus = sideQuestionState && sideQuestionState.status;
|
|
@@ -22837,6 +23016,7 @@
|
|
|
22837
23016
|
setActiveReplSessionForCurrentRuntime(message.activeSessionName);
|
|
22838
23017
|
}
|
|
22839
23018
|
const journalChanged = mergeReplJournalEntries(message.journalEntries);
|
|
23019
|
+
maybeImportLegacyReplJournalEntries(replActiveSessionName);
|
|
22840
23020
|
if (typeof message.transcript === "string") replTranscript = trimReplTranscript(message.transcript);
|
|
22841
23021
|
if (typeof message.capturedAt === "number") replCapturedAt = message.capturedAt;
|
|
22842
23022
|
replError = typeof message.replError === "string" ? message.replError : (typeof message.captureError === "string" ? message.captureError : "");
|
|
@@ -22890,6 +23070,7 @@
|
|
|
22890
23070
|
setActiveReplSessionForCurrentRuntime(message.activeSessionName);
|
|
22891
23071
|
}
|
|
22892
23072
|
let journalChanged = mergeReplJournalEntries(message.journalEntries);
|
|
23073
|
+
maybeImportLegacyReplJournalEntries(replActiveSessionName);
|
|
22893
23074
|
if (typeof message.transcript === "string") {
|
|
22894
23075
|
replTranscript = trimReplTranscript(message.transcript);
|
|
22895
23076
|
journalChanged = updateActiveReplJournalEntryFromTranscript(
|
|
@@ -22914,6 +23095,19 @@
|
|
|
22914
23095
|
return;
|
|
22915
23096
|
}
|
|
22916
23097
|
|
|
23098
|
+
if (message.type === "repl_journal_ack") {
|
|
23099
|
+
const sessionName = typeof message.sessionName === "string" ? message.sessionName : "";
|
|
23100
|
+
if (sessionName) replJournalImportPendingSessions.delete(sessionName);
|
|
23101
|
+
if (message.cleared && sessionName) {
|
|
23102
|
+
replJournalEntries = replJournalEntries.filter((entry) => entry.sessionName !== sessionName);
|
|
23103
|
+
persistReplJournalEntries();
|
|
23104
|
+
} else {
|
|
23105
|
+
mergeReplJournalEntries(message.journalEntries);
|
|
23106
|
+
}
|
|
23107
|
+
renderReplViewIfActive({ force: true });
|
|
23108
|
+
return;
|
|
23109
|
+
}
|
|
23110
|
+
|
|
22917
23111
|
if (message.type === "repl_send_ack") {
|
|
22918
23112
|
replBusy = false;
|
|
22919
23113
|
replMessage = "";
|