pi-studio 0.9.55 → 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 CHANGED
@@ -4,6 +4,19 @@ All notable changes to `pi-studio` are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.9.56] — 2026-09-02
8
+
9
+ ### Added
10
+ - Add a versioned, session-owned **Shared REPL Record** that synchronizes compatible-client code, notes, lifecycle state, and captured output bidirectionally with `pi-repl` while keeping both extensions independently usable.
11
+ - Discover records through first-writer-wins tmux metadata bound to the exact session ID and creation time, with private bounded snapshots, atomic locked updates, stale-lock recovery, and a cross-client send lease held through output capture; timed-out or aborted submissions keep that lease until the runtime completion marker or exact-session shutdown.
12
+ - Import legacy browser-local Studio REPL entries once by stable ID, propagate clear operations, expose shared record details through the browser and `studio_repl_status`, and document direct pane typing as raw-mirror-only activity.
13
+
14
+ ### Changed
15
+ - Rename the compact Studio journal and export surfaces from **Studio REPL Record** to **Shared REPL Record**, identify entry origins, and make browser Markdown output byte-for-byte consistent with the canonical protocol renderer.
16
+
17
+ ### Fixed
18
+ - Prevent compatible Studio and `pi-repl` sends from concurrently attributing the same pane output to different entries.
19
+
7
20
  ## [0.9.55] — 2026-09-02
8
21
 
9
22
  ### Changed
package/README.md CHANGED
@@ -39,7 +39,7 @@ _The video shows an earlier version of the Studio interface. The basic workflow
39
39
  - Includes a right-pane **Changes** view for browsing the current git diff by file, previewing per-file diffs, opening changed files, loading the full diff into the editor, and copying the diff
40
40
  - Includes a right-pane **Files** view rooted in folders allowed for the current Studio session, with an **Allow folder…** action and location selector; exact-file grants can still be opened independently without exposing their parent folders. Files supports sorting by name/modified time/size, opening folders or the current root in Finder/the system file manager, loading text/code/CSV/TSV documents into the editor, opening a read-only **Preview (follow)** tab whose disk content is authoritative, previewing PDFs/images, opening previews in new Studio tabs, converting DOCX/ODT documents to editable Markdown when Pandoc is available after confirmation, copying paths, setting the current folder as the Studio working directory, and revealing files in the file manager
41
41
  - Imports detached text-file copies either through the browser's file picker or from a path on the computer running Pi, keeping the path option available in embedded and remote browser views; imported copies remain detached until saved or opened as file-backed documents
42
- - Includes an optional tmux-backed **REPL** view for Shell, Python, IPython, Julia, R, GHCi, and Clojure sessions, with Raw/Literate send modes, `Cmd/Ctrl+Shift+Enter` **Send to REPL**, session start/stop/interrupt controls, a compact refresh-persistent **Studio REPL Record** of user and Pi-sent code, a secondary raw tmux mirror, agent-facing `studio_repl_status` / `studio_repl_send` tools, and Markdown/PDF/HTML export
42
+ - Includes an optional tmux-backed **REPL** view for Shell, Python, IPython, Julia, R, GHCi, and Clojure sessions, with Raw/Literate send modes, `Cmd/Ctrl+Shift+Enter` **Send to REPL**, session start/stop/interrupt controls, a compact refresh-persistent **Shared REPL Record** of compatible-client submissions, a secondary raw tmux mirror, agent-facing `studio_repl_status` / `studio_repl_send` tools, and Markdown/PDF/HTML export
43
43
  - Includes a local persistent scratchpad for quick notes you want to keep out of the main editor until you're ready to copy or insert them, with a **Recent…** picker for recovering scratchpads saved under earlier file/draft identities
44
44
  - Includes a docked **Outline** rail for navigating document structure in the current editor text, with clickable entries that jump in the raw editor and reveal matching preview locations when available
45
45
  - Restores each browser tab’s editor workspace after refresh or cmux hidden-surface reconstruction, and provides an explicit **Reset editor** action when you want to discard the restored draft and return the tab to a fresh blank draft without changing responses or saved files
@@ -91,6 +91,8 @@ _The video shows an earlier version of the Studio interface. The basic workflow
91
91
  |---|---|
92
92
  | `studio_export_pdf` | Export direct Markdown/LaTeX, a local file, or the last model response to PDF. Defaults to writing a file without opening a viewer. |
93
93
  | `studio_export_html` | Export direct Markdown/LaTeX, a local file, or the last model response to standalone HTML. Defaults to writing a file without opening a viewer. |
94
+ | `studio_repl_status` | Inspect tmux-backed REPL sessions, including shared clean-record identity, compatible-client entries, and raw-mirror details. |
95
+ | `studio_repl_send` | Send code to a visible REPL under the cross-client send lease and add its captured output to the shared clean record. |
94
96
 
95
97
  ## Install
96
98
 
@@ -108,6 +110,16 @@ Run once without installing:
108
110
  pi -e https://github.com/omaclaren/pi-studio
109
111
  ```
110
112
 
113
+ ## Shared REPL record
114
+
115
+ Studio remains a standalone REPL client: it can create and use its own tmux sessions without `pi-repl`. When a compatible `pi-repl` installation uses the same tmux session, both clients automatically discover one session-owned clean record and see each other's code and captured output. No package imports or depends on the other.
116
+
117
+ Compatible clients publish a versioned opaque record ID in tmux and keep the bounded JSON record in a private per-user temporary directory. The record is tied to the exact tmux session ID and creation time, uses atomic locked updates, and serializes compatible sends from pre-capture through output capture so concurrent Studio and `pi-repl` submissions do not claim each other's output. Existing sessions attach lazily; Studio's legacy browser-local entries are imported once with stable IDs.
118
+
119
+ The clean record covers only submissions and notes whose boundaries a compatible client knows. Text typed directly into an attached tmux pane remains in the secondary raw pane/history mirror and is not presented as reliably parsed code/output. **Export record** produces a canonical Markdown representation with origin, mode, status, runtime, timestamp, and this limitation; PDF and HTML exports are derived from that Markdown.
120
+
121
+ See [`shared/REPL_SESSION_RECORD_PROTOCOL.md`](./shared/REPL_SESSION_RECORD_PROTOCOL.md) for protocol, safety, retention, and compatibility details.
122
+
111
123
  ## Side questions and research context
112
124
 
113
125
  Open **Review → Side question**, select **Side questions** in the right pane, or press **Cmd/Ctrl+Alt+Q**. A new side thread captures its starting text while keeping related-file access separate:
@@ -187,7 +199,7 @@ Studio only passes icon-pack arguments when a diagram actually references `lucid
187
199
  - Studio is designed as a complement to terminal pi, not a replacement.
188
200
  - Installing pi-studio makes the optional `pi-studio-dark` and `pi-studio-light` themes available in pi's theme selector; it does not change your active theme.
189
201
  - Editor/code font uses a best-effort terminal-monospace match when the current terminal config exposes it; set `PI_STUDIO_FONT_MONO` to force a specific CSS `font-family` stack. Use `PI_STUDIO_FONT_UI` or `PI_STUDIO_FONT_PROSE` to override the Studio UI or rendered-preview font stacks.
190
- - The optional REPL view requires `tmux`. Studio can start and stop Studio-owned `pi-studio-repl-*` sessions and can mirror detected `pi-repl-*` sessions, but it will not stop external `pi-repl-*` sessions.
202
+ - The optional REPL view requires `tmux`. Studio can start and stop Studio-owned `pi-studio-repl-*` sessions and can mirror detected `pi-repl-*` sessions, but it will not stop external `pi-repl-*` sessions. Compatible clients synchronize their clean record automatically; direct pane interaction remains available only in the raw mirror. A send timeout or tool abort does not stop submitted runtime code, so Studio keeps that session's cross-client lease until the completion marker appears or the exact tmux session ends.
191
203
  - Side-question web search requires `BRAVE_API_KEY`; without it, local and conversation context still work, and the web option is shown as unavailable. Side sub-sessions are deliberately read-only and can run independently while the main Pi agent is busy.
192
204
  - Full preview/PDF quality depends on `pandoc` (and `xelatex` for PDF):
193
205
  - `brew install pandoc`
package/ROADMAP.md CHANGED
@@ -69,6 +69,20 @@ A browser-neutral UX pass before buffer-first work:
69
69
  - normalize Studio buttons and dropdown controls across Chromium and WebKit without changing native select interaction;
70
70
  - expose X-high and Max side-question thinking only when supported by the active model, while retaining Low as the default.
71
71
 
72
+ ## 0.9.56 — Shared REPL record (shipped 2026-09-02)
73
+
74
+ A coordinated interoperability release with `pi-repl` 0.4.0:
75
+
76
+ - make the clean structured record belong to the exact tmux session lifetime rather than one browser or extension;
77
+ - discover an opaque versioned record ID through first-writer-wins tmux metadata while keeping bounded content in private user-scoped sidecar storage;
78
+ - synchronize compatible submissions, literate notes, lifecycle status, captured output, clear operations, and bounded legacy Studio migration in both directions;
79
+ - hold one cross-client send lease from pre-send capture through completion capture—including after caller timeout or abort—so compatible clients cannot claim each other's output;
80
+ - preserve raw pane/history output as the honest source for direct attached-pane activity rather than inferring unreliable semantic boundaries;
81
+ - keep Studio and `pi-repl` independently installable and usable, with graceful fallback for unavailable, malformed, stale, or unsupported shared records;
82
+ - produce one canonical Markdown representation across Studio and `pi-repl`.
83
+
84
+ This release does not change Studio's single-buffer architecture or move buffer-first work forward from `0.10.0`.
85
+
72
86
  ## 0.10.0 — Buffer-first editing
73
87
 
74
88
  The first architectural `0.10` release should add:
@@ -92,4 +106,5 @@ Cross-environment checks should cover, where relevant:
92
106
  - local, SSH, and `--no-browser` Pi sessions;
93
107
  - paths with spaces, Unicode, traversal attempts, and symlinks;
94
108
  - supported and unavailable browser APIs;
95
- - browser reconstruction, refresh recovery, and clean shutdown.
109
+ - browser reconstruction, refresh recovery, and clean shutdown;
110
+ - for shared REPL work: bidirectional visibility, compatible-client send contention, output attribution, restart persistence, clear/import behavior, stale or malformed records, and direct attached-pane activity.
@@ -597,13 +597,18 @@
597
597
  })();
598
598
  function normalizeReplJournalEntry(entry) {
599
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);
600
602
  const normalized = {
601
603
  id: typeof entry.id === "string" && entry.id ? entry.id : ("repl-journal-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 8)),
602
604
  requestId: typeof entry.requestId === "string" ? entry.requestId : "",
603
605
  createdAt: typeof entry.createdAt === "number" && Number.isFinite(entry.createdAt) ? entry.createdAt : Date.now(),
604
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,
605
608
  sessionName: typeof entry.sessionName === "string" ? entry.sessionName : "",
606
609
  runtime: typeof entry.runtime === "string" ? entry.runtime : "python",
610
+ origin: hasCompatibleOrigin ? entry.origin : "",
611
+ legacyLocal: !sharedSynced,
607
612
  label: typeof entry.label === "string" ? entry.label : "REPL send",
608
613
  mode: typeof entry.mode === "string" ? entry.mode : "raw",
609
614
  prose: typeof entry.prose === "string" ? entry.prose : "",
@@ -635,8 +640,11 @@
635
640
  requestId: entry.requestId,
636
641
  createdAt: entry.createdAt,
637
642
  updatedAt: entry.updatedAt,
643
+ completedAt: entry.completedAt,
638
644
  sessionName: entry.sessionName,
639
645
  runtime: entry.runtime,
646
+ origin: entry.origin,
647
+ sharedSynced: !entry.legacyLocal,
640
648
  label: entry.label,
641
649
  mode: entry.mode,
642
650
  prose: entry.prose,
@@ -671,6 +679,7 @@
671
679
  mode: existing.mode || entry.mode,
672
680
  prose: existing.prose || entry.prose,
673
681
  beforeTranscript: existing.beforeTranscript || "",
682
+ legacyLocal: entry.legacyLocal,
674
683
  createdAt: existing.createdAt || entry.createdAt,
675
684
  updatedAt: Math.max(existing.updatedAt || 0, entry.updatedAt || 0),
676
685
  skippedChunks: existing.skippedChunks || entry.skippedChunks,
@@ -694,6 +703,7 @@
694
703
 
695
704
  let replJournalEntries = loadPersistedReplJournalEntries();
696
705
  let activeReplJournalEntryId = "";
706
+ const replJournalImportPendingSessions = new Set();
697
707
  let replJournalCollapsed = (() => {
698
708
  try {
699
709
  const stored = window.localStorage ? window.localStorage.getItem("piStudio.replStudioCollapsed") : null;
@@ -1815,13 +1825,17 @@
1815
1825
  }
1816
1826
 
1817
1827
  function createReplJournalEntry(details) {
1828
+ const sharedSynced = details.sharedSynced === true;
1818
1829
  return {
1819
1830
  id: "repl-journal-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 8),
1820
1831
  requestId: details.requestId || "",
1821
1832
  createdAt: Date.now(),
1822
1833
  updatedAt: Date.now(),
1834
+ completedAt: typeof details.completedAt === "number" ? details.completedAt : null,
1823
1835
  sessionName: details.sessionName || "",
1824
1836
  runtime: details.runtime || getActiveReplRuntime(),
1837
+ origin: details.origin === "pi-repl" ? "pi-repl" : "pi-studio",
1838
+ legacyLocal: !sharedSynced,
1825
1839
  label: details.label || "REPL send",
1826
1840
  mode: details.mode || replSendMode,
1827
1841
  prose: String(details.prose || ""),
@@ -1840,6 +1854,65 @@
1840
1854
  return entry;
1841
1855
  }
1842
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
+
1843
1916
  function recordReplToolSend(message) {
1844
1917
  const requestId = typeof message.toolCallId === "string" && message.toolCallId.trim()
1845
1918
  ? "tool:" + message.toolCallId.trim()
@@ -1853,6 +1926,8 @@
1853
1926
  requestId,
1854
1927
  sessionName,
1855
1928
  runtime,
1929
+ origin: message.origin === "pi-repl" ? "pi-repl" : "pi-studio",
1930
+ sharedSynced: message.sharedSynced === true,
1856
1931
  label: typeof message.label === "string" && message.label.trim() ? message.label.trim() : "Pi",
1857
1932
  mode: "agent",
1858
1933
  code,
@@ -1971,19 +2046,28 @@
1971
2046
  function buildReplJournalMarkdown(entries) {
1972
2047
  const visibleEntries = Array.isArray(entries) ? entries : getVisibleReplJournalEntries();
1973
2048
  const sessionName = getActiveReplJournalSessionName();
1974
- const lines = ["# Studio REPL Record", "", "Generated: " + new Date().toLocaleString()];
1975
- if (sessionName) lines.push("Session: `" + sessionName + "`");
1976
- lines.push("");
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
+ ];
1977
2058
  if (!visibleEntries.length) {
1978
- lines.push(sessionName ? ("_No Studio REPL record entries for `" + sessionName + "` yet._") : "_No Studio REPL record entries yet._");
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._", "");
1979
2061
  return lines.join("\n");
1980
2062
  }
1981
2063
  visibleEntries.forEach((entry, index) => {
1982
2064
  lines.push("## " + (index + 1) + ". " + (entry.label || "REPL entry"));
1983
2065
  lines.push("");
1984
- lines.push("- Time: " + new Date(entry.createdAt || Date.now()).toLocaleString());
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"));
1985
2070
  if (entry.runtime) lines.push("- Runtime: " + entry.runtime);
1986
- if (entry.sessionName) lines.push("- Session: `" + entry.sessionName + "`");
1987
2071
  if (entry.skippedChunks) lines.push("- Skipped chunks: " + entry.skippedChunks);
1988
2072
  lines.push("");
1989
2073
  if (String(entry.prose || "").trim()) {
@@ -1991,7 +2075,10 @@
1991
2075
  lines.push("");
1992
2076
  }
1993
2077
  if (String(entry.code || "").trim()) {
1994
- lines.push(getMarkdownFenceForText(entry.code, entry.runtime === "ipython" ? "python" : entry.runtime));
2078
+ const markdownRuntime = entry.runtime === "ipython"
2079
+ ? "python"
2080
+ : (entry.runtime === "unknown" || entry.runtime === "shell" ? "" : entry.runtime);
2081
+ lines.push(getMarkdownFenceForText(entry.code, markdownRuntime));
1995
2082
  lines.push("");
1996
2083
  }
1997
2084
  if (String(entry.output || "").trim()) {
@@ -2001,17 +2088,18 @@
2001
2088
  lines.push("");
2002
2089
  }
2003
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._", "");
2004
2092
  return lines.join("\n").replace(/\n{4,}/g, "\n\n\n").trimEnd() + "\n";
2005
2093
  }
2006
2094
 
2007
2095
  async function copyReplJournalToClipboard() {
2008
2096
  const entries = getVisibleReplJournalEntries();
2009
2097
  if (!entries.length) {
2010
- setStatus("No Studio REPL record entries to copy for this session yet.", "warning");
2098
+ setStatus("No shared REPL record entries to copy for this session yet.", "warning");
2011
2099
  return;
2012
2100
  }
2013
2101
  if (await writeTextToClipboard(buildReplJournalMarkdown(entries))) {
2014
- setStatus("Copied Studio REPL record as Markdown.", "success");
2102
+ setStatus("Copied shared REPL record as Markdown.", "success");
2015
2103
  } else {
2016
2104
  setStatus("Clipboard write failed.", "warning");
2017
2105
  }
@@ -2020,7 +2108,7 @@
2020
2108
  function exportReplJournalMarkdown() {
2021
2109
  const entries = getVisibleReplJournalEntries();
2022
2110
  if (!entries.length) {
2023
- setStatus("No Studio REPL record entries to export for this session yet.", "warning");
2111
+ setStatus("No shared REPL record entries to export for this session yet.", "warning");
2024
2112
  return;
2025
2113
  }
2026
2114
  const blob = new Blob([buildReplJournalMarkdown(entries)], { type: "text/markdown;charset=utf-8" });
@@ -2029,12 +2117,12 @@
2029
2117
  const stamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
2030
2118
  const sessionSlug = getActiveReplJournalSessionName().replace(/[^-_.A-Za-z0-9]+/g, "-");
2031
2119
  link.href = blobUrl;
2032
- link.download = "repl-studio" + (sessionSlug ? "-" + sessionSlug : "") + "-" + stamp + ".md";
2120
+ link.download = "repl-record" + (sessionSlug ? "-" + sessionSlug : "") + "-" + stamp + ".md";
2033
2121
  document.body.appendChild(link);
2034
2122
  link.click();
2035
2123
  link.remove();
2036
2124
  window.setTimeout(() => URL.revokeObjectURL(blobUrl), 1000);
2037
- setStatus("Exported Studio REPL record Markdown.", "success");
2125
+ setStatus("Exported shared REPL record Markdown.", "success");
2038
2126
  }
2039
2127
 
2040
2128
  function clearReplJournal() {
@@ -2046,30 +2134,33 @@
2046
2134
  }
2047
2135
  activeReplJournalEntryId = "";
2048
2136
  persistReplJournalEntries();
2049
- setStatus(sessionName ? "Cleared Studio REPL record for this session." : "Cleared Studio REPL record.", "success");
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");
2050
2141
  renderReplViewIfActive({ force: true });
2051
2142
  }
2052
2143
 
2053
2144
  function loadReplJournalIntoEditor() {
2054
2145
  const entries = getVisibleReplJournalEntries();
2055
2146
  if (!entries.length) {
2056
- setStatus("No Studio REPL record entries to load for this session yet.", "warning");
2147
+ setStatus("No shared REPL record entries to load for this session yet.", "warning");
2057
2148
  return;
2058
2149
  }
2059
2150
  const markdown = buildReplJournalMarkdown(entries);
2060
2151
  setEditorText(markdown, { preserveScroll: false, preserveSelection: false });
2061
- setSourceState({ source: "blank", label: "Studio REPL Record", path: null });
2152
+ setSourceState({ source: "blank", label: "Shared REPL Record", path: null });
2062
2153
  setEditorLanguage("markdown");
2063
- setStatus("Loaded Studio REPL record into editor.", "success");
2154
+ setStatus("Loaded shared REPL record into editor.", "success");
2064
2155
  }
2065
2156
 
2066
2157
  function addSelectedReplJournalNote() {
2067
2158
  const note = getSelectedOrCurrentParagraphForReplNote();
2068
2159
  if (!note.trim()) {
2069
- setStatus("Select prose or place the cursor in a paragraph to add a Studio REPL record note.", "warning");
2160
+ setStatus("Select prose or place the cursor in a paragraph to add a shared REPL record note.", "warning");
2070
2161
  return;
2071
2162
  }
2072
- addReplJournalEntry({
2163
+ const entry = addReplJournalEntry({
2073
2164
  label: "note",
2074
2165
  prose: note,
2075
2166
  status: "note",
@@ -2077,7 +2168,8 @@
2077
2168
  sessionName: replActiveSessionName,
2078
2169
  runtime: getActiveReplRuntime(),
2079
2170
  });
2080
- setStatus("Added note to Studio REPL record.", "success");
2171
+ syncReplJournalEntry(entry);
2172
+ setStatus("Added note to shared REPL record.", "success");
2081
2173
  renderReplViewIfActive({ force: true });
2082
2174
  }
2083
2175
 
@@ -2093,7 +2185,7 @@
2093
2185
  }
2094
2186
  if (payload.noteOnly) {
2095
2187
  if (String(payload.prose || "").trim()) {
2096
- addReplJournalEntry({
2188
+ const entry = addReplJournalEntry({
2097
2189
  label: payload.label || "note",
2098
2190
  prose: payload.prose,
2099
2191
  status: "note",
@@ -2102,7 +2194,8 @@
2102
2194
  runtime: getActiveReplRuntime(),
2103
2195
  skippedChunks: payload.skippedChunks,
2104
2196
  });
2105
- setStatus("Added prose to Studio REPL record.", "success");
2197
+ syncReplJournalEntry(entry);
2198
+ setStatus("Added prose to shared REPL record.", "success");
2106
2199
  renderReplViewIfActive({ force: true });
2107
2200
  } else {
2108
2201
  setStatus("No code or prose found to send.", "warning");
@@ -2133,7 +2226,18 @@
2133
2226
  renderReplViewIfActive({ force: true });
2134
2227
  const skippedSuffix = payload.skippedChunks ? " (skipped " + payload.skippedChunks + " incompatible chunk" + (payload.skippedChunks === 1 ? "" : "s") + ")" : "";
2135
2228
  setStatus("Sending " + (payload.label || "editor text") + " to REPL…" + skippedSuffix, "info");
2136
- if (!sendMessage({ type: "repl_send_request", requestId, sessionName: session.sessionName, text })) {
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
+ })) {
2137
2241
  replBusy = false;
2138
2242
  replJournalEntries = replJournalEntries.map((entry) => entry.id === journalEntry.id ? { ...entry, status: "error" } : entry);
2139
2243
  persistReplJournalEntries();
@@ -10223,7 +10327,7 @@
10223
10327
  }
10224
10328
  const replJournalExportEntries = exportingReplJournal ? getVisibleReplJournalEntries() : [];
10225
10329
  if (exportingReplJournal && !replJournalExportEntries.length) {
10226
- setStatus("No Studio REPL record entries to export for this session yet.", "warning");
10330
+ setStatus("No shared REPL record entries to export for this session yet.", "warning");
10227
10331
  return;
10228
10332
  }
10229
10333
 
@@ -10468,7 +10572,7 @@
10468
10572
  }
10469
10573
  const replJournalExportEntries = exportingReplJournal ? getVisibleReplJournalEntries() : [];
10470
10574
  if (exportingReplJournal && !replJournalExportEntries.length) {
10471
- setStatus("No Studio REPL record entries to export for this session yet.", "warning");
10575
+ setStatus("No shared REPL record entries to export for this session yet.", "warning");
10472
10576
  return;
10473
10577
  }
10474
10578
 
@@ -10501,7 +10605,7 @@
10501
10605
  let filenameHint = exportingSideThread
10502
10606
  ? getSideQuestionTranscriptFilename(sideThreadExportedAt).replace(/\.md$/i, ".html")
10503
10607
  : (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"));
10608
+ let titleHint = exportingSideThread ? "Pi Studio side questions" : (exportingReplJournal ? "Shared REPL Record" : (isEditorPreview ? "Studio editor preview" : "Studio response preview"));
10505
10609
  if (sourcePath) {
10506
10610
  const baseName = sourcePath.split(/[\\/]/).pop() || "studio";
10507
10611
  const stem = baseName.replace(/\.[^.]+$/, "") || "studio";
@@ -11479,6 +11583,7 @@
11479
11583
  const parts = [];
11480
11584
  const kind = getReplStudioEntryKind(entry);
11481
11585
  if (kind !== "Raw") parts.push(kind);
11586
+ if (entry.origin) parts.push(entry.origin);
11482
11587
  const time = formatReferenceTime(entry.createdAt);
11483
11588
  if (time) parts.push(time);
11484
11589
  if (entry.skippedChunks) parts.push("skipped " + String(entry.skippedChunks));
@@ -11532,12 +11637,12 @@
11532
11637
  const toggleButton = "<button type='button' data-repl-action='journal-toggle' aria-expanded='" + (replJournalCollapsed ? "false" : "true") + "'>" + (replJournalCollapsed ? "Show record" : "Hide record") + "</button>";
11533
11638
  const toggleActions = "<div class='repl-journal-actions'>" + toggleButton + "</div>";
11534
11639
  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.");
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.");
11537
11642
  if (replJournalCollapsed) {
11538
11643
  return "<section class='repl-journal repl-journal-compact" + collapsedClass + "'>"
11539
11644
  + "<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>"
11645
+ + "<div class='repl-journal-compact-title'><span class='repl-journal-chip'>Shared REPL Record</span><span>" + escapeHtml(summaryText) + "</span></div>"
11541
11646
  + "<div class='repl-journal-actions'>" + toggleButton + "</div>"
11542
11647
  + "</div>"
11543
11648
  + "</section>";
@@ -11575,14 +11680,14 @@
11575
11680
  }).join("");
11576
11681
  const emptyText = sessionName
11577
11682
  ? (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.";
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.";
11581
11686
  const terminalContent = banner
11582
11687
  + (hasEntries ? cards : "<div class='repl-studio-empty'>" + escapeHtml(emptyText) + "</div>");
11583
11688
  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>"
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>"
11586
11691
  + (omitted ? "<div class='repl-journal-omitted'>Showing latest 12 entries for this session; " + escapeHtml(String(omitted)) + " older entries remain in export.</div>" : "")
11587
11692
  + "<div class='repl-journal-list'>" + terminalContent + "</div>"
11588
11693
  + "</section>";
@@ -11606,7 +11711,7 @@
11606
11711
  + "</section>";
11607
11712
  }
11608
11713
  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>"
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>"
11610
11715
  + body
11611
11716
  + "</section>";
11612
11717
  }
@@ -11656,7 +11761,7 @@
11656
11761
  + "<button type='button' data-repl-action='interrupt'" + (activeSession && !replBusy ? "" : " disabled") + " title='Send Ctrl+C to the active REPL session.'>Interrupt</button>"
11657
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>"
11658
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>"
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>"
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>"
11660
11765
  + "<button type='button' data-repl-action='refresh'>Refresh</button>"
11661
11766
  + "<button type='button' data-repl-action='follow'>Follow: " + (replFollow ? "On" : "Off") + "</button>"
11662
11767
  + "</div>"
@@ -13677,7 +13782,7 @@
13677
13782
  } else if (exportingSideThread) {
13678
13783
  exportPdfBtn.title = "Save or copy Markdown, open it in an editor, or export the visible side discussion as PDF or HTML.";
13679
13784
  } else if (exportingReplJournal && !replJournalExportEntries.length) {
13680
- exportPdfBtn.title = "No Studio REPL record entries to export for this session yet.";
13785
+ exportPdfBtn.title = "No shared REPL record entries to export for this session yet.";
13681
13786
  } else if (rightView === "markdown") {
13682
13787
  exportPdfBtn.title = "Switch right pane to Response (Preview), Editor (Preview), REPL, or Side questions to export.";
13683
13788
  } else if (!canExportPreview) {
@@ -13685,7 +13790,7 @@
13685
13790
  } else if (isHtmlArtifactPreview) {
13686
13791
  exportPdfBtn.title = "This is an interactive HTML preview. Export as HTML; PDF export is not available yet.";
13687
13792
  } else if (exportingReplJournal) {
13688
- exportPdfBtn.title = "Choose PDF export or an HTML export destination for the Studio REPL record.";
13793
+ exportPdfBtn.title = "Choose PDF export or an HTML export destination for the shared REPL record.";
13689
13794
  } else {
13690
13795
  exportPdfBtn.title = "Choose PDF export or an HTML export destination for the current right-pane preview.";
13691
13796
  }
@@ -13697,7 +13802,7 @@
13697
13802
  ? "This transcript is too large for rendered export; save or copy the Markdown instead."
13698
13803
  : (isHtmlArtifactPreview
13699
13804
  ? "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.")));
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.")));
13701
13806
  }
13702
13807
  if (exportPreviewPdfBtn) {
13703
13808
  exportPreviewPdfBtn.disabled = exportBusy || !canExportPreview || isHtmlArtifactPreview || sideThreadRenderTooLarge;
@@ -13705,7 +13810,7 @@
13705
13810
  ? "This transcript is too large for rendered export; save or copy the Markdown instead."
13706
13811
  : (isHtmlArtifactPreview
13707
13812
  ? "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.")));
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.")));
13709
13814
  }
13710
13815
  if (exportPreviewHtmlStudioBtn) {
13711
13816
  exportPreviewHtmlStudioBtn.disabled = exportBusy || !canExportPreview || sideThreadRenderTooLarge;
@@ -13713,7 +13818,7 @@
13713
13818
  ? "This transcript is too large for rendered export; save or copy the Markdown instead."
13714
13819
  : (isHtmlArtifactPreview
13715
13820
  ? "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.")));
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.")));
13717
13822
  }
13718
13823
  if (exportPreviewHtmlBtn) {
13719
13824
  exportPreviewHtmlBtn.disabled = exportBusy || !canExportPreview || sideThreadRenderTooLarge;
@@ -13721,7 +13826,7 @@
13721
13826
  ? "This transcript is too large for rendered export; save or copy the Markdown instead."
13722
13827
  : (isHtmlArtifactPreview
13723
13828
  ? "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.")));
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.")));
13725
13830
  }
13726
13831
  if (exportPreviewControlsEl) {
13727
13832
  exportPreviewControlsEl.hidden = rightView === "editor-quarto-preview";
@@ -13729,11 +13834,11 @@
13729
13834
  ? (exportingSideThread
13730
13835
  ? "Choose a durable export for the visible side discussion."
13731
13836
  : (exportingReplJournal
13732
- ? "Choose a format and export destination for the Studio REPL record."
13837
+ ? "Choose a format and export destination for the shared REPL record."
13733
13838
  : (isHtmlArtifactPreview ? "Export this HTML preview to Studio or browser." : "Choose a format and export destination for the current right-pane preview.")))
13734
13839
  : (exportingSideThread
13735
13840
  ? "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."));
13841
+ : (exportingReplJournal ? "No shared REPL record entries to export for this session yet." : "Switch right pane to a non-empty preview before exporting."));
13737
13842
  }
13738
13843
  if (!canExportPreview || previewExportInProgress || sideQuestionMarkdownExportRequest) {
13739
13844
  closeExportPreviewMenu();
@@ -22911,6 +23016,7 @@
22911
23016
  setActiveReplSessionForCurrentRuntime(message.activeSessionName);
22912
23017
  }
22913
23018
  const journalChanged = mergeReplJournalEntries(message.journalEntries);
23019
+ maybeImportLegacyReplJournalEntries(replActiveSessionName);
22914
23020
  if (typeof message.transcript === "string") replTranscript = trimReplTranscript(message.transcript);
22915
23021
  if (typeof message.capturedAt === "number") replCapturedAt = message.capturedAt;
22916
23022
  replError = typeof message.replError === "string" ? message.replError : (typeof message.captureError === "string" ? message.captureError : "");
@@ -22964,6 +23070,7 @@
22964
23070
  setActiveReplSessionForCurrentRuntime(message.activeSessionName);
22965
23071
  }
22966
23072
  let journalChanged = mergeReplJournalEntries(message.journalEntries);
23073
+ maybeImportLegacyReplJournalEntries(replActiveSessionName);
22967
23074
  if (typeof message.transcript === "string") {
22968
23075
  replTranscript = trimReplTranscript(message.transcript);
22969
23076
  journalChanged = updateActiveReplJournalEntryFromTranscript(
@@ -22988,6 +23095,19 @@
22988
23095
  return;
22989
23096
  }
22990
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
+
22991
23111
  if (message.type === "repl_send_ack") {
22992
23112
  replBusy = false;
22993
23113
  replMessage = "";