pi-studio 0.9.55 → 0.9.57

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -132,6 +132,7 @@
132
132
  const queueSteerBtn = document.getElementById("queueSteerBtn");
133
133
  const sendReplBtn = document.getElementById("sendReplBtn");
134
134
  const replSendModeSelect = document.getElementById("replSendModeSelect");
135
+ const replEchoModeSelect = document.getElementById("replEchoModeSelect");
135
136
  const copyDraftBtn = document.getElementById("copyDraftBtn");
136
137
  const suggestCompletionBtn = document.getElementById("suggestCompletionBtn");
137
138
  const suggestCompletionOptionsBtn = document.getElementById("suggestCompletionOptionsBtn");
@@ -595,15 +596,28 @@
595
596
  return "raw";
596
597
  }
597
598
  })();
599
+ let replEchoMode = (() => {
600
+ try {
601
+ const stored = String((window.localStorage && window.localStorage.getItem("piStudio.replEchoMode.v2")) || "").trim().toLowerCase();
602
+ return stored === "summary" || stored === "full" ? stored : "off";
603
+ } catch {
604
+ return "off";
605
+ }
606
+ })();
598
607
  function normalizeReplJournalEntry(entry) {
599
608
  if (!entry || typeof entry !== "object") return null;
609
+ const hasCompatibleOrigin = entry.origin === "pi-repl" || entry.origin === "pi-studio";
610
+ const sharedSynced = entry.sharedSynced === true || (entry.sharedSynced !== false && hasCompatibleOrigin);
600
611
  const normalized = {
601
612
  id: typeof entry.id === "string" && entry.id ? entry.id : ("repl-journal-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 8)),
602
613
  requestId: typeof entry.requestId === "string" ? entry.requestId : "",
603
614
  createdAt: typeof entry.createdAt === "number" && Number.isFinite(entry.createdAt) ? entry.createdAt : Date.now(),
604
615
  updatedAt: typeof entry.updatedAt === "number" && Number.isFinite(entry.updatedAt) ? entry.updatedAt : Date.now(),
616
+ completedAt: typeof entry.completedAt === "number" && Number.isFinite(entry.completedAt) ? entry.completedAt : null,
605
617
  sessionName: typeof entry.sessionName === "string" ? entry.sessionName : "",
606
618
  runtime: typeof entry.runtime === "string" ? entry.runtime : "python",
619
+ origin: hasCompatibleOrigin ? entry.origin : "",
620
+ legacyLocal: !sharedSynced,
607
621
  label: typeof entry.label === "string" ? entry.label : "REPL send",
608
622
  mode: typeof entry.mode === "string" ? entry.mode : "raw",
609
623
  prose: typeof entry.prose === "string" ? entry.prose : "",
@@ -635,8 +649,11 @@
635
649
  requestId: entry.requestId,
636
650
  createdAt: entry.createdAt,
637
651
  updatedAt: entry.updatedAt,
652
+ completedAt: entry.completedAt,
638
653
  sessionName: entry.sessionName,
639
654
  runtime: entry.runtime,
655
+ origin: entry.origin,
656
+ sharedSynced: !entry.legacyLocal,
640
657
  label: entry.label,
641
658
  mode: entry.mode,
642
659
  prose: entry.prose,
@@ -671,6 +688,7 @@
671
688
  mode: existing.mode || entry.mode,
672
689
  prose: existing.prose || entry.prose,
673
690
  beforeTranscript: existing.beforeTranscript || "",
691
+ legacyLocal: entry.legacyLocal,
674
692
  createdAt: existing.createdAt || entry.createdAt,
675
693
  updatedAt: Math.max(existing.updatedAt || 0, entry.updatedAt || 0),
676
694
  skippedChunks: existing.skippedChunks || entry.skippedChunks,
@@ -694,6 +712,7 @@
694
712
 
695
713
  let replJournalEntries = loadPersistedReplJournalEntries();
696
714
  let activeReplJournalEntryId = "";
715
+ const replJournalImportPendingSessions = new Set();
697
716
  let replJournalCollapsed = (() => {
698
717
  try {
699
718
  const stored = window.localStorage ? window.localStorage.getItem("piStudio.replStudioCollapsed") : null;
@@ -1343,6 +1362,11 @@
1343
1362
  return String(value || "").trim().toLowerCase() === "literate" ? "literate" : "raw";
1344
1363
  }
1345
1364
 
1365
+ function normalizeReplEchoMode(value) {
1366
+ const normalized = String(value || "").trim().toLowerCase();
1367
+ return normalized === "summary" || normalized === "full" ? normalized : "off";
1368
+ }
1369
+
1346
1370
  function isMacShortcutPlatform() {
1347
1371
  try {
1348
1372
  const platform = String((navigator && navigator.platform) || "");
@@ -1384,6 +1408,28 @@
1384
1408
  }
1385
1409
  }
1386
1410
 
1411
+ function setReplEchoMode(mode) {
1412
+ replEchoMode = normalizeReplEchoMode(mode);
1413
+ if (replEchoModeSelect) replEchoModeSelect.value = replEchoMode;
1414
+ try {
1415
+ if (window.localStorage) window.localStorage.setItem("piStudio.replEchoMode.v2", replEchoMode);
1416
+ } catch {
1417
+ // Ignore storage failures.
1418
+ }
1419
+ }
1420
+
1421
+ function syncReplEchoModeSelect(visible) {
1422
+ if (!replEchoModeSelect) return;
1423
+ replEchoModeSelect.hidden = !visible;
1424
+ replEchoModeSelect.disabled = !visible || wsState === "Disconnected" || uiBusy || replBusy;
1425
+ replEchoModeSelect.value = replEchoMode;
1426
+ replEchoModeSelect.title = replEchoMode === "off"
1427
+ ? "Do not add a submitted-code display or alignment anchors to the raw REPL pane."
1428
+ : (replEchoMode === "full"
1429
+ ? "Display up to 40 lines or 4,000 characters of submitted code; source remains in raw terminal history."
1430
+ : "Display short submissions in full, truncating after 6 lines or 600 characters, with compact anchors and a plain output divider.");
1431
+ }
1432
+
1387
1433
  function setReplJournalCollapsed(collapsed) {
1388
1434
  replJournalCollapsed = Boolean(collapsed);
1389
1435
  try {
@@ -1815,13 +1861,17 @@
1815
1861
  }
1816
1862
 
1817
1863
  function createReplJournalEntry(details) {
1864
+ const sharedSynced = details.sharedSynced === true;
1818
1865
  return {
1819
1866
  id: "repl-journal-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 8),
1820
1867
  requestId: details.requestId || "",
1821
1868
  createdAt: Date.now(),
1822
1869
  updatedAt: Date.now(),
1870
+ completedAt: typeof details.completedAt === "number" ? details.completedAt : null,
1823
1871
  sessionName: details.sessionName || "",
1824
1872
  runtime: details.runtime || getActiveReplRuntime(),
1873
+ origin: details.origin === "pi-repl" ? "pi-repl" : "pi-studio",
1874
+ legacyLocal: !sharedSynced,
1825
1875
  label: details.label || "REPL send",
1826
1876
  mode: details.mode || replSendMode,
1827
1877
  prose: String(details.prose || ""),
@@ -1840,6 +1890,65 @@
1840
1890
  return entry;
1841
1891
  }
1842
1892
 
1893
+ function syncReplJournalEntry(entry) {
1894
+ if (!entry || !entry.sessionName || wsState === "Disconnected") return false;
1895
+ return sendMessage({
1896
+ type: "repl_journal_upsert_request",
1897
+ requestId: makeRequestId(),
1898
+ sessionName: entry.sessionName,
1899
+ entry: {
1900
+ id: entry.id,
1901
+ requestId: entry.requestId,
1902
+ createdAt: entry.createdAt,
1903
+ updatedAt: entry.updatedAt,
1904
+ completedAt: entry.completedAt,
1905
+ runtime: entry.runtime,
1906
+ origin: "pi-studio",
1907
+ label: entry.label,
1908
+ mode: entry.mode,
1909
+ prose: entry.prose,
1910
+ code: entry.code,
1911
+ output: entry.output,
1912
+ status: entry.status,
1913
+ skippedChunks: entry.skippedChunks,
1914
+ },
1915
+ });
1916
+ }
1917
+
1918
+ function maybeImportLegacyReplJournalEntries(sessionName) {
1919
+ const name = String(sessionName || "").trim();
1920
+ if (!name || replJournalImportPendingSessions.has(name) || wsState === "Disconnected") return false;
1921
+ const entries = replJournalEntries.filter((entry) => entry.sessionName === name && entry.legacyLocal).slice(-80);
1922
+ if (!entries.length) return false;
1923
+ replJournalImportPendingSessions.add(name);
1924
+ const sent = sendMessage({
1925
+ type: "repl_journal_import_request",
1926
+ requestId: makeRequestId(),
1927
+ sessionName: name,
1928
+ entries: entries.map((entry) => ({
1929
+ id: entry.id,
1930
+ requestId: entry.requestId,
1931
+ createdAt: entry.createdAt,
1932
+ updatedAt: entry.updatedAt,
1933
+ runtime: entry.runtime,
1934
+ origin: "pi-studio",
1935
+ label: entry.label,
1936
+ mode: entry.mode,
1937
+ prose: entry.prose,
1938
+ code: entry.code,
1939
+ output: entry.output,
1940
+ status: entry.status,
1941
+ skippedChunks: entry.skippedChunks,
1942
+ })),
1943
+ });
1944
+ if (!sent) {
1945
+ replJournalImportPendingSessions.delete(name);
1946
+ } else {
1947
+ window.setTimeout(() => replJournalImportPendingSessions.delete(name), 5000);
1948
+ }
1949
+ return sent;
1950
+ }
1951
+
1843
1952
  function recordReplToolSend(message) {
1844
1953
  const requestId = typeof message.toolCallId === "string" && message.toolCallId.trim()
1845
1954
  ? "tool:" + message.toolCallId.trim()
@@ -1853,6 +1962,8 @@
1853
1962
  requestId,
1854
1963
  sessionName,
1855
1964
  runtime,
1965
+ origin: message.origin === "pi-repl" ? "pi-repl" : "pi-studio",
1966
+ sharedSynced: message.sharedSynced === true,
1856
1967
  label: typeof message.label === "string" && message.label.trim() ? message.label.trim() : "Pi",
1857
1968
  mode: "agent",
1858
1969
  code,
@@ -1908,14 +2019,15 @@
1908
2019
  function stripStudioReplSubmissionEcho(delta) {
1909
2020
  let value = String(delta || "").replace(/^\s+/, "");
1910
2021
  // The raw mirror below remains raw; Studio record cards hide only the
1911
- // temp-file wrapper used to submit multiline snippets safely. The
1912
- // pi-studio-re fragment catches IPython's wrapped pi-studio-repl paths.
2022
+ // temp-file wrapper used to submit multiline snippets safely. Match
2023
+ // both legacy long Studio roots and compact private control roots.
1913
2024
  const submissionEchoPatterns = [
1914
- /^.*exec\(open\([\s\S]*?pi-studio-re[\s\S]*?globals\(\)\)\s*$/gm,
1915
- /^.*include\([\s\S]*?pi-studio-re[\s\S]*?\.jl"\)\s*$/gm,
1916
- /^.*source\([\s\S]*?pi-studio-re[\s\S]*?local\s*=\s*\.GlobalEnv\)\s*$/gm,
1917
- /^.*:script\s+[\s\S]*?pi-studio-re[\s\S]*?\.ghci"?\s*$/gm,
1918
- /^.*\(do\s+\(load-file\s+[\s\S]*?pi-studio-re[\s\S]*?:pi-studio\/silent\)\s*$/gm,
2025
+ /^.*exec\(open\([\s\S]*?(?:pi-studio-re|pi-rc-)[\s\S]*?globals\(\)\)\s*$/gm,
2026
+ /^.*include\([\s\S]*?(?:pi-studio-re|pi-rc-)[\s\S]*?\.jl"\)\s*$/gm,
2027
+ /^.*source\([\s\S]*?(?:pi-studio-re|pi-rc-)[\s\S]*?local\s*=\s*\.GlobalEnv\)\s*$/gm,
2028
+ /^.*:script\s+[\s\S]*?(?:pi-studio-re|pi-rc-)[\s\S]*?\.ghci"?\s*$/gm,
2029
+ /^.*\(do\s+\(load-file\s+[\s\S]*?(?:pi-studio-re|pi-rc-)[\s\S]*?:pi-studio\/silent\)\s*$/gm,
2030
+ /^.*\.\s+[\s\S]*?(?:pi-studio-re|pi-rc-)[\s\S]*?\.sh[\s\S]*?(?:done\.flag|[a-f0-9]{16}\.done).*$/gm,
1919
2031
  ];
1920
2032
  for (const pattern of submissionEchoPatterns) {
1921
2033
  value = value.replace(pattern, "");
@@ -1951,6 +2063,10 @@
1951
2063
  if (entryIndex < 0) return false;
1952
2064
  const entry = replJournalEntries[entryIndex];
1953
2065
  if (entry.sessionName && sessionName && entry.sessionName !== sessionName) return false;
2066
+ if (!entry.legacyLocal && (entry.status === "captured" || entry.status === "timeout" || entry.status === "error")) {
2067
+ activeReplJournalEntryId = "";
2068
+ return false;
2069
+ }
1954
2070
  const delta = cleanReplCapturedOutput(extractReplTranscriptDelta(entry.beforeTranscript, transcript), entry);
1955
2071
  if (!delta.trim()) return false;
1956
2072
  if (entry.output === delta && entry.status === "captured") return false;
@@ -1971,19 +2087,28 @@
1971
2087
  function buildReplJournalMarkdown(entries) {
1972
2088
  const visibleEntries = Array.isArray(entries) ? entries : getVisibleReplJournalEntries();
1973
2089
  const sessionName = getActiveReplJournalSessionName();
1974
- const lines = ["# Studio REPL Record", "", "Generated: " + new Date().toLocaleString()];
1975
- if (sessionName) lines.push("Session: `" + sessionName + "`");
1976
- lines.push("");
2090
+ const updatedAt = visibleEntries.reduce((latest, entry) => Math.max(latest, Number(entry.updatedAt) || 0), 0) || Date.now();
2091
+ const lines = [
2092
+ "# Shared REPL Record",
2093
+ "",
2094
+ "Session: `" + (sessionName || "unknown") + "`",
2095
+ "Record protocol: pi-repl-session-record v1",
2096
+ "Updated: " + new Date(updatedAt).toISOString(),
2097
+ "",
2098
+ ];
1977
2099
  if (!visibleEntries.length) {
1978
- lines.push(sessionName ? ("_No Studio REPL record entries for `" + sessionName + "` yet._") : "_No Studio REPL record entries yet._");
2100
+ lines.push("_No compatible-client entries have been recorded for this tmux session._", "");
2101
+ 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._", "");
1979
2102
  return lines.join("\n");
1980
2103
  }
1981
2104
  visibleEntries.forEach((entry, index) => {
1982
2105
  lines.push("## " + (index + 1) + ". " + (entry.label || "REPL entry"));
1983
2106
  lines.push("");
1984
- lines.push("- Time: " + new Date(entry.createdAt || Date.now()).toLocaleString());
2107
+ lines.push("- Time: " + new Date(entry.createdAt || Date.now()).toISOString());
2108
+ lines.push("- Origin: " + (entry.origin || "unknown"));
2109
+ lines.push("- Mode: " + (entry.mode || "raw"));
2110
+ lines.push("- Status: " + (entry.status || "sent"));
1985
2111
  if (entry.runtime) lines.push("- Runtime: " + entry.runtime);
1986
- if (entry.sessionName) lines.push("- Session: `" + entry.sessionName + "`");
1987
2112
  if (entry.skippedChunks) lines.push("- Skipped chunks: " + entry.skippedChunks);
1988
2113
  lines.push("");
1989
2114
  if (String(entry.prose || "").trim()) {
@@ -1991,7 +2116,10 @@
1991
2116
  lines.push("");
1992
2117
  }
1993
2118
  if (String(entry.code || "").trim()) {
1994
- lines.push(getMarkdownFenceForText(entry.code, entry.runtime === "ipython" ? "python" : entry.runtime));
2119
+ const markdownRuntime = entry.runtime === "ipython"
2120
+ ? "python"
2121
+ : (entry.runtime === "unknown" || entry.runtime === "shell" ? "" : entry.runtime);
2122
+ lines.push(getMarkdownFenceForText(entry.code, markdownRuntime));
1995
2123
  lines.push("");
1996
2124
  }
1997
2125
  if (String(entry.output || "").trim()) {
@@ -2001,17 +2129,18 @@
2001
2129
  lines.push("");
2002
2130
  }
2003
2131
  });
2132
+ 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._", "");
2004
2133
  return lines.join("\n").replace(/\n{4,}/g, "\n\n\n").trimEnd() + "\n";
2005
2134
  }
2006
2135
 
2007
2136
  async function copyReplJournalToClipboard() {
2008
2137
  const entries = getVisibleReplJournalEntries();
2009
2138
  if (!entries.length) {
2010
- setStatus("No Studio REPL record entries to copy for this session yet.", "warning");
2139
+ setStatus("No shared REPL record entries to copy for this session yet.", "warning");
2011
2140
  return;
2012
2141
  }
2013
2142
  if (await writeTextToClipboard(buildReplJournalMarkdown(entries))) {
2014
- setStatus("Copied Studio REPL record as Markdown.", "success");
2143
+ setStatus("Copied shared REPL record as Markdown.", "success");
2015
2144
  } else {
2016
2145
  setStatus("Clipboard write failed.", "warning");
2017
2146
  }
@@ -2020,7 +2149,7 @@
2020
2149
  function exportReplJournalMarkdown() {
2021
2150
  const entries = getVisibleReplJournalEntries();
2022
2151
  if (!entries.length) {
2023
- setStatus("No Studio REPL record entries to export for this session yet.", "warning");
2152
+ setStatus("No shared REPL record entries to export for this session yet.", "warning");
2024
2153
  return;
2025
2154
  }
2026
2155
  const blob = new Blob([buildReplJournalMarkdown(entries)], { type: "text/markdown;charset=utf-8" });
@@ -2029,12 +2158,12 @@
2029
2158
  const stamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
2030
2159
  const sessionSlug = getActiveReplJournalSessionName().replace(/[^-_.A-Za-z0-9]+/g, "-");
2031
2160
  link.href = blobUrl;
2032
- link.download = "repl-studio" + (sessionSlug ? "-" + sessionSlug : "") + "-" + stamp + ".md";
2161
+ link.download = "repl-record" + (sessionSlug ? "-" + sessionSlug : "") + "-" + stamp + ".md";
2033
2162
  document.body.appendChild(link);
2034
2163
  link.click();
2035
2164
  link.remove();
2036
2165
  window.setTimeout(() => URL.revokeObjectURL(blobUrl), 1000);
2037
- setStatus("Exported Studio REPL record Markdown.", "success");
2166
+ setStatus("Exported shared REPL record Markdown.", "success");
2038
2167
  }
2039
2168
 
2040
2169
  function clearReplJournal() {
@@ -2046,30 +2175,33 @@
2046
2175
  }
2047
2176
  activeReplJournalEntryId = "";
2048
2177
  persistReplJournalEntries();
2049
- setStatus(sessionName ? "Cleared Studio REPL record for this session." : "Cleared Studio REPL record.", "success");
2178
+ if (sessionName) {
2179
+ sendMessage({ type: "repl_journal_clear_request", requestId: makeRequestId(), sessionName });
2180
+ }
2181
+ setStatus(sessionName ? "Cleared shared REPL record for this session." : "Cleared local REPL record cache.", "success");
2050
2182
  renderReplViewIfActive({ force: true });
2051
2183
  }
2052
2184
 
2053
2185
  function loadReplJournalIntoEditor() {
2054
2186
  const entries = getVisibleReplJournalEntries();
2055
2187
  if (!entries.length) {
2056
- setStatus("No Studio REPL record entries to load for this session yet.", "warning");
2188
+ setStatus("No shared REPL record entries to load for this session yet.", "warning");
2057
2189
  return;
2058
2190
  }
2059
2191
  const markdown = buildReplJournalMarkdown(entries);
2060
2192
  setEditorText(markdown, { preserveScroll: false, preserveSelection: false });
2061
- setSourceState({ source: "blank", label: "Studio REPL Record", path: null });
2193
+ setSourceState({ source: "blank", label: "Shared REPL Record", path: null });
2062
2194
  setEditorLanguage("markdown");
2063
- setStatus("Loaded Studio REPL record into editor.", "success");
2195
+ setStatus("Loaded shared REPL record into editor.", "success");
2064
2196
  }
2065
2197
 
2066
2198
  function addSelectedReplJournalNote() {
2067
2199
  const note = getSelectedOrCurrentParagraphForReplNote();
2068
2200
  if (!note.trim()) {
2069
- setStatus("Select prose or place the cursor in a paragraph to add a Studio REPL record note.", "warning");
2201
+ setStatus("Select prose or place the cursor in a paragraph to add a shared REPL record note.", "warning");
2070
2202
  return;
2071
2203
  }
2072
- addReplJournalEntry({
2204
+ const entry = addReplJournalEntry({
2073
2205
  label: "note",
2074
2206
  prose: note,
2075
2207
  status: "note",
@@ -2077,7 +2209,8 @@
2077
2209
  sessionName: replActiveSessionName,
2078
2210
  runtime: getActiveReplRuntime(),
2079
2211
  });
2080
- setStatus("Added note to Studio REPL record.", "success");
2212
+ syncReplJournalEntry(entry);
2213
+ setStatus("Added note to shared REPL record.", "success");
2081
2214
  renderReplViewIfActive({ force: true });
2082
2215
  }
2083
2216
 
@@ -2093,7 +2226,7 @@
2093
2226
  }
2094
2227
  if (payload.noteOnly) {
2095
2228
  if (String(payload.prose || "").trim()) {
2096
- addReplJournalEntry({
2229
+ const entry = addReplJournalEntry({
2097
2230
  label: payload.label || "note",
2098
2231
  prose: payload.prose,
2099
2232
  status: "note",
@@ -2102,7 +2235,8 @@
2102
2235
  runtime: getActiveReplRuntime(),
2103
2236
  skippedChunks: payload.skippedChunks,
2104
2237
  });
2105
- setStatus("Added prose to Studio REPL record.", "success");
2238
+ syncReplJournalEntry(entry);
2239
+ setStatus("Added prose to shared REPL record.", "success");
2106
2240
  renderReplViewIfActive({ force: true });
2107
2241
  } else {
2108
2242
  setStatus("No code or prose found to send.", "warning");
@@ -2133,7 +2267,19 @@
2133
2267
  renderReplViewIfActive({ force: true });
2134
2268
  const skippedSuffix = payload.skippedChunks ? " (skipped " + payload.skippedChunks + " incompatible chunk" + (payload.skippedChunks === 1 ? "" : "s") + ")" : "";
2135
2269
  setStatus("Sending " + (payload.label || "editor text") + " to REPL…" + skippedSuffix, "info");
2136
- if (!sendMessage({ type: "repl_send_request", requestId, sessionName: session.sessionName, text })) {
2270
+ if (!sendMessage({
2271
+ type: "repl_send_request",
2272
+ requestId,
2273
+ sessionName: session.sessionName,
2274
+ text,
2275
+ echoMode: replEchoMode,
2276
+ journalEntryId: journalEntry.id,
2277
+ createdAt: journalEntry.createdAt,
2278
+ label: journalEntry.label,
2279
+ mode: journalEntry.mode,
2280
+ prose: journalEntry.prose,
2281
+ skippedChunks: journalEntry.skippedChunks,
2282
+ })) {
2137
2283
  replBusy = false;
2138
2284
  replJournalEntries = replJournalEntries.map((entry) => entry.id === journalEntry.id ? { ...entry, status: "error" } : entry);
2139
2285
  persistReplJournalEntries();
@@ -10223,7 +10369,7 @@
10223
10369
  }
10224
10370
  const replJournalExportEntries = exportingReplJournal ? getVisibleReplJournalEntries() : [];
10225
10371
  if (exportingReplJournal && !replJournalExportEntries.length) {
10226
- setStatus("No Studio REPL record entries to export for this session yet.", "warning");
10372
+ setStatus("No shared REPL record entries to export for this session yet.", "warning");
10227
10373
  return;
10228
10374
  }
10229
10375
 
@@ -10468,7 +10614,7 @@
10468
10614
  }
10469
10615
  const replJournalExportEntries = exportingReplJournal ? getVisibleReplJournalEntries() : [];
10470
10616
  if (exportingReplJournal && !replJournalExportEntries.length) {
10471
- setStatus("No Studio REPL record entries to export for this session yet.", "warning");
10617
+ setStatus("No shared REPL record entries to export for this session yet.", "warning");
10472
10618
  return;
10473
10619
  }
10474
10620
 
@@ -10501,7 +10647,7 @@
10501
10647
  let filenameHint = exportingSideThread
10502
10648
  ? getSideQuestionTranscriptFilename(sideThreadExportedAt).replace(/\.md$/i, ".html")
10503
10649
  : (exportingReplJournal ? "repl-studio.html" : (isEditorPreview ? "studio-editor-preview.html" : ("studio-response-" + formatStudioExportTimestamp() + ".studio.html")));
10504
- let titleHint = exportingSideThread ? "Pi Studio side questions" : (exportingReplJournal ? "Studio REPL Record" : (isEditorPreview ? "Studio editor preview" : "Studio response preview"));
10650
+ let titleHint = exportingSideThread ? "Pi Studio side questions" : (exportingReplJournal ? "Shared REPL Record" : (isEditorPreview ? "Studio editor preview" : "Studio response preview"));
10505
10651
  if (sourcePath) {
10506
10652
  const baseName = sourcePath.split(/[\\/]/).pop() || "studio";
10507
10653
  const stem = baseName.replace(/\.[^.]+$/, "") || "studio";
@@ -11479,6 +11625,7 @@
11479
11625
  const parts = [];
11480
11626
  const kind = getReplStudioEntryKind(entry);
11481
11627
  if (kind !== "Raw") parts.push(kind);
11628
+ if (entry.origin) parts.push(entry.origin);
11482
11629
  const time = formatReferenceTime(entry.createdAt);
11483
11630
  if (time) parts.push(time);
11484
11631
  if (entry.skippedChunks) parts.push("skipped " + String(entry.skippedChunks));
@@ -11532,12 +11679,12 @@
11532
11679
  const toggleButton = "<button type='button' data-repl-action='journal-toggle' aria-expanded='" + (replJournalCollapsed ? "false" : "true") + "'>" + (replJournalCollapsed ? "Show record" : "Hide record") + "</button>";
11533
11680
  const toggleActions = "<div class='repl-journal-actions'>" + toggleButton + "</div>";
11534
11681
  const summaryText = hasEntries
11535
- ? (entryCount + " Studio entr" + (entryCount === 1 ? "y" : "ies") + (sessionName ? " for " + sessionName : "") + ". Export is Markdown.")
11536
- : (sessionName ? "No Studio entries for " + sessionName + "." : "Studio-sent code and notes will appear here.");
11682
+ ? (entryCount + " shared record entr" + (entryCount === 1 ? "y" : "ies") + (sessionName ? " for " + sessionName : "") + ". Export is Markdown.")
11683
+ : (sessionName ? "No compatible-client entries for " + sessionName + "." : "pi-repl and pi-studio submissions will appear here.");
11537
11684
  if (replJournalCollapsed) {
11538
11685
  return "<section class='repl-journal repl-journal-compact" + collapsedClass + "'>"
11539
11686
  + "<div class='repl-journal-compact-row'>"
11540
- + "<div class='repl-journal-compact-title'><span class='repl-journal-chip'>Studio REPL Record</span><span>" + escapeHtml(summaryText) + "</span></div>"
11687
+ + "<div class='repl-journal-compact-title'><span class='repl-journal-chip'>Shared REPL Record</span><span>" + escapeHtml(summaryText) + "</span></div>"
11541
11688
  + "<div class='repl-journal-actions'>" + toggleButton + "</div>"
11542
11689
  + "</div>"
11543
11690
  + "</section>";
@@ -11575,14 +11722,14 @@
11575
11722
  }).join("");
11576
11723
  const emptyText = sessionName
11577
11724
  ? (String(transcript || "").trim()
11578
- ? "No Studio REPL record entries yet. The raw tmux mirror below still has this session's history; send code from Studio to build a clean record."
11579
- : "No Studio REPL record entries yet. Send code from the editor, or use More → Add note (Literate send) to record prose.")
11580
- : "No Studio REPL record entries yet. Send code from the editor, or use More → Add note (Literate send) to record prose.";
11725
+ ? "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."
11726
+ : "No compatible-client record entries yet. Send code from pi-repl or Studio, or use More → Add note (Literate send) to record prose.")
11727
+ : "No compatible-client record entries yet. Send code from pi-repl or Studio, or use More → Add note (Literate send) to record prose.";
11581
11728
  const terminalContent = banner
11582
11729
  + (hasEntries ? cards : "<div class='repl-studio-empty'>" + escapeHtml(emptyText) + "</div>");
11583
11730
  return "<section class='repl-journal'>"
11584
- + "<div class='repl-journal-header'><h3>Studio REPL Record</h3>" + toggleActions + "</div>"
11585
- + "<p class='repl-journal-description'>Clean record for the selected tmux session. Raw tmux mirror below.</p>"
11731
+ + "<div class='repl-journal-header'><h3>Shared REPL Record</h3>" + toggleActions + "</div>"
11732
+ + "<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>"
11586
11733
  + (omitted ? "<div class='repl-journal-omitted'>Showing latest 12 entries for this session; " + escapeHtml(String(omitted)) + " older entries remain in export.</div>" : "")
11587
11734
  + "<div class='repl-journal-list'>" + terminalContent + "</div>"
11588
11735
  + "</section>";
@@ -11606,7 +11753,7 @@
11606
11753
  + "</section>";
11607
11754
  }
11608
11755
  return "<section class='repl-mirror'>"
11609
- + "<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 Studio record above is cleaner.</p></div>" + actions + "</div>"
11756
+ + "<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>"
11610
11757
  + body
11611
11758
  + "</section>";
11612
11759
  }
@@ -11656,7 +11803,7 @@
11656
11803
  + "<button type='button' data-repl-action='interrupt'" + (activeSession && !replBusy ? "" : " disabled") + " title='Send Ctrl+C to the active REPL session.'>Interrupt</button>"
11657
11804
  + "<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>"
11658
11805
  + "<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>"
11659
- + "<button type='button' data-repl-action='journal-note' title='Add the selected prose/current paragraph to the Studio REPL record (Literate send) without sending it to the runtime.'>Add note</button>"
11806
+ + "<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>"
11660
11807
  + "<button type='button' data-repl-action='refresh'>Refresh</button>"
11661
11808
  + "<button type='button' data-repl-action='follow'>Follow: " + (replFollow ? "On" : "Off") + "</button>"
11662
11809
  + "</div>"
@@ -13677,7 +13824,7 @@
13677
13824
  } else if (exportingSideThread) {
13678
13825
  exportPdfBtn.title = "Save or copy Markdown, open it in an editor, or export the visible side discussion as PDF or HTML.";
13679
13826
  } else if (exportingReplJournal && !replJournalExportEntries.length) {
13680
- exportPdfBtn.title = "No Studio REPL record entries to export for this session yet.";
13827
+ exportPdfBtn.title = "No shared REPL record entries to export for this session yet.";
13681
13828
  } else if (rightView === "markdown") {
13682
13829
  exportPdfBtn.title = "Switch right pane to Response (Preview), Editor (Preview), REPL, or Side questions to export.";
13683
13830
  } else if (!canExportPreview) {
@@ -13685,7 +13832,7 @@
13685
13832
  } else if (isHtmlArtifactPreview) {
13686
13833
  exportPdfBtn.title = "This is an interactive HTML preview. Export as HTML; PDF export is not available yet.";
13687
13834
  } else if (exportingReplJournal) {
13688
- exportPdfBtn.title = "Choose PDF export or an HTML export destination for the Studio REPL record.";
13835
+ exportPdfBtn.title = "Choose PDF export or an HTML export destination for the shared REPL record.";
13689
13836
  } else {
13690
13837
  exportPdfBtn.title = "Choose PDF export or an HTML export destination for the current right-pane preview.";
13691
13838
  }
@@ -13697,7 +13844,7 @@
13697
13844
  ? "This transcript is too large for rendered export; save or copy the Markdown instead."
13698
13845
  : (isHtmlArtifactPreview
13699
13846
  ? "Interactive HTML preview PDF export is not available yet."
13700
- : (exportingSideThread ? "Export the side-thread transcript as PDF and open it in Studio." : (exportingReplJournal ? "Export the Studio REPL record as PDF and open it in Studio." : "Export the current right-pane preview as PDF and open it in Studio.")));
13847
+ : (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.")));
13701
13848
  }
13702
13849
  if (exportPreviewPdfBtn) {
13703
13850
  exportPreviewPdfBtn.disabled = exportBusy || !canExportPreview || isHtmlArtifactPreview || sideThreadRenderTooLarge;
@@ -13705,7 +13852,7 @@
13705
13852
  ? "This transcript is too large for rendered export; save or copy the Markdown instead."
13706
13853
  : (isHtmlArtifactPreview
13707
13854
  ? "Interactive HTML preview PDF export is not available yet."
13708
- : (exportingSideThread ? "Export the side-thread transcript as PDF and open it in the default PDF viewer." : (exportingReplJournal ? "Export the Studio 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.")));
13855
+ : (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.")));
13709
13856
  }
13710
13857
  if (exportPreviewHtmlStudioBtn) {
13711
13858
  exportPreviewHtmlStudioBtn.disabled = exportBusy || !canExportPreview || sideThreadRenderTooLarge;
@@ -13713,7 +13860,7 @@
13713
13860
  ? "This transcript is too large for rendered export; save or copy the Markdown instead."
13714
13861
  : (isHtmlArtifactPreview
13715
13862
  ? "Export the authored HTML preview and open it in a new Studio editor tab."
13716
- : (exportingSideThread ? "Export the side-thread transcript as standalone HTML and open it in a new Studio editor tab." : (exportingReplJournal ? "Export the Studio 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.")));
13863
+ : (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.")));
13717
13864
  }
13718
13865
  if (exportPreviewHtmlBtn) {
13719
13866
  exportPreviewHtmlBtn.disabled = exportBusy || !canExportPreview || sideThreadRenderTooLarge;
@@ -13721,7 +13868,7 @@
13721
13868
  ? "This transcript is too large for rendered export; save or copy the Markdown instead."
13722
13869
  : (isHtmlArtifactPreview
13723
13870
  ? "Export the authored HTML preview and open it in the default browser."
13724
- : (exportingSideThread ? "Export the side-thread transcript as standalone HTML and open it in the default browser." : (exportingReplJournal ? "Export the Studio 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.")));
13871
+ : (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.")));
13725
13872
  }
13726
13873
  if (exportPreviewControlsEl) {
13727
13874
  exportPreviewControlsEl.hidden = rightView === "editor-quarto-preview";
@@ -13729,11 +13876,11 @@
13729
13876
  ? (exportingSideThread
13730
13877
  ? "Choose a durable export for the visible side discussion."
13731
13878
  : (exportingReplJournal
13732
- ? "Choose a format and export destination for the Studio REPL record."
13879
+ ? "Choose a format and export destination for the shared REPL record."
13733
13880
  : (isHtmlArtifactPreview ? "Export this HTML preview to Studio or browser." : "Choose a format and export destination for the current right-pane preview.")))
13734
13881
  : (exportingSideThread
13735
13882
  ? "No completed side discussion is available to export yet."
13736
- : (exportingReplJournal ? "No Studio REPL record entries to export for this session yet." : "Switch right pane to a non-empty preview before exporting."));
13883
+ : (exportingReplJournal ? "No shared REPL record entries to export for this session yet." : "Switch right pane to a non-empty preview before exporting."));
13737
13884
  }
13738
13885
  if (!canExportPreview || previewExportInProgress || sideQuestionMarkdownExportRequest) {
13739
13886
  closeExportPreviewMenu();
@@ -22237,6 +22384,7 @@
22237
22384
  ? "Literate send: Send to REPL uses the selection, current fenced code chunk, or all matching chunks if the cursor is outside a chunk."
22238
22385
  : "Raw send: Send to REPL uses the selection, or full editor if no selection.";
22239
22386
  }
22387
+ syncReplEchoModeSelect(showReplSend);
22240
22388
  if (critiqueBtn) {
22241
22389
  critiqueBtn.textContent = "Critique text";
22242
22390
  critiqueBtn.classList.remove("request-stop-active");
@@ -22299,6 +22447,7 @@
22299
22447
  ? "Literate send: Send to REPL uses the selection, current fenced code chunk, or all matching chunks if the cursor is outside a chunk."
22300
22448
  : "Raw send: Send to REPL uses the selection, or full editor if no selection.";
22301
22449
  }
22450
+ syncReplEchoModeSelect(rightView === "repl");
22302
22451
 
22303
22452
  if (critiqueBtn) {
22304
22453
  critiqueBtn.textContent = critiqueIsStop ? "Stop" : "Critique text";
@@ -22911,6 +23060,7 @@
22911
23060
  setActiveReplSessionForCurrentRuntime(message.activeSessionName);
22912
23061
  }
22913
23062
  const journalChanged = mergeReplJournalEntries(message.journalEntries);
23063
+ maybeImportLegacyReplJournalEntries(replActiveSessionName);
22914
23064
  if (typeof message.transcript === "string") replTranscript = trimReplTranscript(message.transcript);
22915
23065
  if (typeof message.capturedAt === "number") replCapturedAt = message.capturedAt;
22916
23066
  replError = typeof message.replError === "string" ? message.replError : (typeof message.captureError === "string" ? message.captureError : "");
@@ -22964,6 +23114,7 @@
22964
23114
  setActiveReplSessionForCurrentRuntime(message.activeSessionName);
22965
23115
  }
22966
23116
  let journalChanged = mergeReplJournalEntries(message.journalEntries);
23117
+ maybeImportLegacyReplJournalEntries(replActiveSessionName);
22967
23118
  if (typeof message.transcript === "string") {
22968
23119
  replTranscript = trimReplTranscript(message.transcript);
22969
23120
  journalChanged = updateActiveReplJournalEntryFromTranscript(
@@ -22988,6 +23139,19 @@
22988
23139
  return;
22989
23140
  }
22990
23141
 
23142
+ if (message.type === "repl_journal_ack") {
23143
+ const sessionName = typeof message.sessionName === "string" ? message.sessionName : "";
23144
+ if (sessionName) replJournalImportPendingSessions.delete(sessionName);
23145
+ if (message.cleared && sessionName) {
23146
+ replJournalEntries = replJournalEntries.filter((entry) => entry.sessionName !== sessionName);
23147
+ persistReplJournalEntries();
23148
+ } else {
23149
+ mergeReplJournalEntries(message.journalEntries);
23150
+ }
23151
+ renderReplViewIfActive({ force: true });
23152
+ return;
23153
+ }
23154
+
22991
23155
  if (message.type === "repl_send_ack") {
22992
23156
  replBusy = false;
22993
23157
  replMessage = "";
@@ -24840,6 +25004,19 @@
24840
25004
  });
24841
25005
  }
24842
25006
 
25007
+ if (replEchoModeSelect) {
25008
+ replEchoModeSelect.addEventListener("change", () => {
25009
+ setReplEchoMode(replEchoModeSelect.value);
25010
+ syncActionButtons();
25011
+ setStatus(replEchoMode === "full"
25012
+ ? "Full pane echo enabled. Bounded submitted source code will remain in raw terminal history."
25013
+ : (replEchoMode === "off"
25014
+ ? "Pane echo disabled; new sends will not add alignment anchors to raw terminal history."
25015
+ : "Summary pane echo enabled; short submissions will be shown with compact anchors and a plain output divider."),
25016
+ replEchoMode === "full" ? "warning" : "success");
25017
+ });
25018
+ }
25019
+
24843
25020
  copyDraftBtn.addEventListener("click", async () => {
24844
25021
  const content = sourceTextEl.value;
24845
25022
  if (!content.trim()) {
@@ -25460,6 +25637,7 @@
25460
25637
  const initialAnnotationsEnabled = storedAnnotationsEnabled ?? Boolean(annotationModeSelect ? annotationModeSelect.value !== "off" : true);
25461
25638
  setAnnotationsEnabled(initialAnnotationsEnabled, { silent: true });
25462
25639
  setReplSendMode(replSendMode);
25640
+ setReplEchoMode(replEchoMode);
25463
25641
 
25464
25642
  const sessionWorkspaceState = readPersistedWorkspaceState();
25465
25643
  const serverWorkspaceRecovery = await readServerWorkspaceRecoveryState();