pi-studio 0.10.1 → 0.10.3
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 +28 -0
- package/README.md +13 -18
- package/client/studio-client.js +284 -67
- package/index.ts +486 -273
- package/package.json +1 -1
- package/shared/repl-gnuplot-processes.js +180 -0
- package/shared/repl-session-stop.js +285 -0
- package/shared/studio-browser-launcher.js +43 -4
- package/shared/studio-buffer-client.js +3 -1
- package/shared/studio-document-open-client.js +3 -1
- package/shared/studio-repl-selection.js +96 -0
- package/shared/studio-repl-tmux.js +216 -1
package/client/studio-client.js
CHANGED
|
@@ -674,6 +674,18 @@
|
|
|
674
674
|
let replTmuxAvailable = null;
|
|
675
675
|
let replSessions = [];
|
|
676
676
|
let replActiveSessionName = "";
|
|
677
|
+
// Studio's selection as this page last adopted it, with its `shown`, and this page's
|
|
678
|
+
// own choices: while one is unanswered, other replies don't change what the page
|
|
679
|
+
// shows (Sol, Astra).
|
|
680
|
+
let replServerSelection = { sessionName: "", runtime: "" };
|
|
681
|
+
let replShownBasis = null;
|
|
682
|
+
let replChoiceRevision = 0;
|
|
683
|
+
let replChoiceAnswered = 0;
|
|
684
|
+
// The session as chosen, kept with the unanswered choice rather than read from the list.
|
|
685
|
+
let replChoicePin = null;
|
|
686
|
+
let replReconcileSent = "";
|
|
687
|
+
// The session lifetime the transcript belongs to.
|
|
688
|
+
let replTranscriptOwner = "";
|
|
677
689
|
let replRuntime = (() => {
|
|
678
690
|
try {
|
|
679
691
|
return (window.localStorage && window.localStorage.getItem("piStudio.replRuntime")) || "python";
|
|
@@ -701,6 +713,9 @@
|
|
|
701
713
|
const replQuickDrafts = new Map();
|
|
702
714
|
let replQuickPending = null;
|
|
703
715
|
let replQuickFocusRequested = false;
|
|
716
|
+
// After a Quick send from the box, the cursor goes back to it (Oliver, 19 Oct):
|
|
717
|
+
// { requestId, settled }, cleared when restored or when anything else is chosen.
|
|
718
|
+
let replQuickRefocus = null;
|
|
704
719
|
let replSendMode = (() => {
|
|
705
720
|
try {
|
|
706
721
|
const stored = window.localStorage && window.localStorage.getItem("piStudio.replSendMode");
|
|
@@ -721,6 +736,9 @@
|
|
|
721
736
|
if (!entry || typeof entry !== "object") return null;
|
|
722
737
|
const hasCompatibleOrigin = entry.origin === "pi-repl" || entry.origin === "pi-studio";
|
|
723
738
|
const sharedSynced = entry.sharedSynced === true || (entry.sharedSynced !== false && hasCompatibleOrigin);
|
|
739
|
+
// Whether the output was cleaned where it was captured is separate from whether
|
|
740
|
+
// it reached the shared record: a failed record write leaves clean output.
|
|
741
|
+
const outputClean = sharedSynced || entry.outputClean === true;
|
|
724
742
|
const normalized = {
|
|
725
743
|
id: typeof entry.id === "string" && entry.id ? entry.id : ("repl-journal-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 8)),
|
|
726
744
|
requestId: typeof entry.requestId === "string" ? entry.requestId : "",
|
|
@@ -731,11 +749,14 @@
|
|
|
731
749
|
runtime: typeof entry.runtime === "string" ? entry.runtime : "python",
|
|
732
750
|
origin: hasCompatibleOrigin ? entry.origin : "",
|
|
733
751
|
legacyLocal: !sharedSynced,
|
|
752
|
+
outputClean,
|
|
734
753
|
label: typeof entry.label === "string" ? entry.label : "REPL send",
|
|
735
754
|
mode: typeof entry.mode === "string" ? entry.mode : "raw",
|
|
736
755
|
prose: typeof entry.prose === "string" ? entry.prose : "",
|
|
737
756
|
code: typeof entry.code === "string" ? entry.code : "",
|
|
738
|
-
|
|
757
|
+
// Studio's server and pi-repl record clean output; only this browser's
|
|
758
|
+
// older local entries can still hold a loader echo.
|
|
759
|
+
output: typeof entry.output === "string" ? (outputClean ? entry.output : stripStudioReplSubmissionEcho(entry.output)) : "",
|
|
739
760
|
beforeTranscript: "",
|
|
740
761
|
status: typeof entry.status === "string" ? entry.status : "sent",
|
|
741
762
|
skippedChunks: Math.max(0, Math.floor(Number(entry.skippedChunks) || 0)),
|
|
@@ -767,6 +788,7 @@
|
|
|
767
788
|
runtime: entry.runtime,
|
|
768
789
|
origin: entry.origin,
|
|
769
790
|
sharedSynced: !entry.legacyLocal,
|
|
791
|
+
outputClean: entry.outputClean === true,
|
|
770
792
|
label: entry.label,
|
|
771
793
|
mode: entry.mode,
|
|
772
794
|
prose: entry.prose,
|
|
@@ -786,7 +808,8 @@
|
|
|
786
808
|
let changed = false;
|
|
787
809
|
const next = [...replJournalEntries];
|
|
788
810
|
for (const rawEntry of entries) {
|
|
789
|
-
|
|
811
|
+
// These come from Studio's server, which has already cleaned their output.
|
|
812
|
+
const entry = normalizeReplJournalEntry(rawEntry && typeof rawEntry === "object" ? { ...rawEntry, outputClean: true } : rawEntry);
|
|
790
813
|
if (!entry) continue;
|
|
791
814
|
const existingIndex = next.findIndex((candidate) => (
|
|
792
815
|
(entry.requestId && candidate.requestId === entry.requestId)
|
|
@@ -1488,7 +1511,7 @@
|
|
|
1488
1511
|
if (!sessionName) return null;
|
|
1489
1512
|
return {
|
|
1490
1513
|
sessionName,
|
|
1491
|
-
target: typeof session.target === "string" ? session.target :
|
|
1514
|
+
target: typeof session.target === "string" ? session.target : "",
|
|
1492
1515
|
tmuxSessionId: typeof session.tmuxSessionId === "string" ? session.tmuxSessionId : "",
|
|
1493
1516
|
tmuxSessionCreatedAt: typeof session.tmuxSessionCreatedAt === "number" && Number.isFinite(session.tmuxSessionCreatedAt)
|
|
1494
1517
|
? session.tmuxSessionCreatedAt
|
|
@@ -1670,9 +1693,48 @@
|
|
|
1670
1693
|
if (submitted && replQuickDrafts.get(pending.key) === pending.text) {
|
|
1671
1694
|
replQuickDrafts.delete(pending.key);
|
|
1672
1695
|
}
|
|
1696
|
+
// The cursor goes back after the redraw, to the box then on screen (Sol, Astra).
|
|
1697
|
+
if (replQuickRefocus && replQuickRefocus.requestId === requestId) {
|
|
1698
|
+
replQuickRefocus.settled = true;
|
|
1699
|
+
renderReplViewIfActive({ force: true });
|
|
1700
|
+
}
|
|
1673
1701
|
return true;
|
|
1674
1702
|
}
|
|
1675
1703
|
|
|
1704
|
+
// Runs straight after a redraw. Nothing chosen elsewhere since the send (that clears
|
|
1705
|
+
// replQuickRefocus), and focus still on the page or in the box: the box gets it.
|
|
1706
|
+
function applyReplQuickRefocus() {
|
|
1707
|
+
const pending = replQuickRefocus;
|
|
1708
|
+
if (!pending || !pending.settled) return;
|
|
1709
|
+
if (rightView !== "repl" || !critiqueViewEl) {
|
|
1710
|
+
replQuickRefocus = null;
|
|
1711
|
+
return;
|
|
1712
|
+
}
|
|
1713
|
+
const activeEl = document.activeElement;
|
|
1714
|
+
if (activeEl instanceof Element && activeEl !== document.body && !activeEl.closest(".repl-quick-composer")) {
|
|
1715
|
+
replQuickRefocus = null;
|
|
1716
|
+
return;
|
|
1717
|
+
}
|
|
1718
|
+
const composer = critiqueViewEl.querySelector("[data-repl-quick-draft]");
|
|
1719
|
+
// Another send can keep the box disabled; the next redraw tries again.
|
|
1720
|
+
if (!(composer instanceof HTMLTextAreaElement) || composer.disabled) return;
|
|
1721
|
+
replQuickRefocus = null;
|
|
1722
|
+
try {
|
|
1723
|
+
composer.focus({ preventScroll: true });
|
|
1724
|
+
} catch {
|
|
1725
|
+
try { composer.focus(); } catch {}
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
// A click or focus outside the box, while the send runs or before the cursor is back,
|
|
1730
|
+
// means something else was chosen, so the box doesn't take focus back.
|
|
1731
|
+
function cancelReplQuickRefocusOutside(event) {
|
|
1732
|
+
if (!replQuickRefocus) return;
|
|
1733
|
+
const target = event && event.target;
|
|
1734
|
+
if (target instanceof Element && target.closest(".repl-quick-composer")) return;
|
|
1735
|
+
replQuickRefocus = null;
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1676
1738
|
function reconcileReplQuickPending() {
|
|
1677
1739
|
if (!replQuickPending) return false;
|
|
1678
1740
|
const entry = replJournalEntries.find((candidate) => candidate.requestId === replQuickPending.requestId);
|
|
@@ -1729,14 +1791,20 @@
|
|
|
1729
1791
|
function setReplSessions(sessions) {
|
|
1730
1792
|
const previous = serializeReplSessionsForCompare(replSessions);
|
|
1731
1793
|
const previousActive = replActiveSessionName;
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1794
|
+
// While a choice is unanswered, the chosen session keeps the lifetime it was chosen
|
|
1795
|
+
// with: no list, even one after a list that left it out, can swap in a replacement
|
|
1796
|
+
// with the same name (Astra, Sol).
|
|
1797
|
+
const chosen = isReplChoicePending() ? replChoicePin : null;
|
|
1798
|
+
replSessions = (Array.isArray(sessions) ? sessions.map(normalizeReplSession).filter(Boolean) : [])
|
|
1799
|
+
.map((session) => (chosen && session.sessionName === chosen.sessionName ? chosen : session));
|
|
1800
|
+
// The list may change while a choice is unanswered, but the selection waits for its reply.
|
|
1801
|
+
if (!isReplChoicePending()) {
|
|
1802
|
+
if (replActiveSessionName && !replSessions.some((session) => session.sessionName === replActiveSessionName)) {
|
|
1803
|
+
setActiveReplSession("");
|
|
1804
|
+
}
|
|
1805
|
+
if (!getActiveReplSessionForCurrentRuntime()) {
|
|
1806
|
+
selectReplSessionForRuntime(replRuntime, replActiveSessionName);
|
|
1807
|
+
}
|
|
1740
1808
|
}
|
|
1741
1809
|
return previous !== serializeReplSessionsForCompare(replSessions) || previousActive !== replActiveSessionName;
|
|
1742
1810
|
}
|
|
@@ -1789,7 +1857,7 @@
|
|
|
1789
1857
|
"The right pane is mirroring an active tmux-backed REPL session.",
|
|
1790
1858
|
"If the user refers to the active REPL, send code to this session rather than inventing a separate one.",
|
|
1791
1859
|
"Session name: " + session.sessionName,
|
|
1792
|
-
"tmux target: " + (session.target ||
|
|
1860
|
+
"tmux target: " + (session.target || session.sessionName),
|
|
1793
1861
|
"runtime: " + runtime,
|
|
1794
1862
|
"Use the studio_repl_send tool for code execution in this REPL. Pass sessionName when targeting this exact session.",
|
|
1795
1863
|
"Do not improvise raw tmux paste commands for multiline code; Studio handles runtime-specific safe submission.",
|
|
@@ -1804,21 +1872,94 @@
|
|
|
1804
1872
|
}
|
|
1805
1873
|
|
|
1806
1874
|
function setActiveReplSession(sessionName) {
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1875
|
+
replActiveSessionName = String(sessionName || "").trim();
|
|
1876
|
+
retireReplViewIfTargetChanged();
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
// The view belongs to one session lifetime, or to none. When the displayed target
|
|
1880
|
+
// changes, including to or from none or to a replacement with the same name, its
|
|
1881
|
+
// transcript, timestamp and live entry go with it (Sol).
|
|
1882
|
+
function retireReplViewIfTargetChanged() {
|
|
1883
|
+
const owner = getReplSessionLifetimeKey(getActiveReplSession());
|
|
1884
|
+
if (owner === replTranscriptOwner) return false;
|
|
1885
|
+
replTranscript = "";
|
|
1886
|
+
replCapturedAt = 0;
|
|
1887
|
+
activeReplJournalEntryId = "";
|
|
1888
|
+
replTranscriptOwner = owner;
|
|
1889
|
+
return true;
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
function getReplSessionLifetimeKey(session) {
|
|
1893
|
+
return session ? [session.sessionName, session.tmuxSessionId || "", Number(session.tmuxSessionCreatedAt) || 0].join("\u001f") : "";
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
function upsertReplSession(value) {
|
|
1897
|
+
const session = normalizeReplSession(value);
|
|
1898
|
+
if (!session) return false;
|
|
1899
|
+
const index = replSessions.findIndex((candidate) => candidate.sessionName === session.sessionName);
|
|
1900
|
+
if (index < 0) {
|
|
1901
|
+
replSessions = [...replSessions, session];
|
|
1902
|
+
return true;
|
|
1815
1903
|
}
|
|
1816
|
-
if (
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1904
|
+
if (serializeReplSessionsForCompare([replSessions[index]]) === serializeReplSessionsForCompare([session])) return false;
|
|
1905
|
+
replSessions = replSessions.map((candidate, candidateIndex) => candidateIndex === index ? session : candidate);
|
|
1906
|
+
return true;
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
function isReplChoicePending() {
|
|
1910
|
+
return replChoiceAnswered < replChoiceRevision;
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
// Only the reply to this page's latest choice ends the wait.
|
|
1914
|
+
function noteReplChoiceAnswer(message) {
|
|
1915
|
+
if (typeof message.choiceRevision === "number" && message.choiceRevision > replChoiceAnswered) {
|
|
1916
|
+
replChoiceAnswered = Math.min(message.choiceRevision, replChoiceRevision);
|
|
1917
|
+
}
|
|
1918
|
+
if (!isReplChoicePending()) replChoicePin = null;
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
// Adopts Studio's selection from a message that states it. A session brings its
|
|
1922
|
+
// runtime; none comes for a runtime; with no preference yet, the page keeps its
|
|
1923
|
+
// runtime and picks within it (Sol, Astra).
|
|
1924
|
+
function adoptReplSelection(message) {
|
|
1925
|
+
const sessionName = typeof message.activeSessionName === "string" ? message.activeSessionName.trim() : "";
|
|
1926
|
+
const runtime = getKnownReplRuntime(message.activeRuntime);
|
|
1927
|
+
replServerSelection = { sessionName, runtime };
|
|
1928
|
+
if (typeof message.shown === "number") replShownBasis = message.shown;
|
|
1929
|
+
if (runtime && runtime !== replRuntime) setReplRuntime(runtime);
|
|
1930
|
+
if (sessionName) {
|
|
1931
|
+
setActiveReplSessionForCurrentRuntime(sessionName);
|
|
1932
|
+
} else {
|
|
1933
|
+
selectReplSessionForRuntime(replRuntime, runtime ? "" : replActiveSessionName);
|
|
1820
1934
|
}
|
|
1821
|
-
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
// Reports a selection the page made itself, against the selection it last adopted.
|
|
1938
|
+
// Studio applies it only if nothing has changed since; it isn't a choice (Sol, Astra).
|
|
1939
|
+
function reconcileReplSelection() {
|
|
1940
|
+
if (replShownBasis === null || isReplChoicePending() || wsState === "Disconnected") return false;
|
|
1941
|
+
const session = getActiveReplSessionForCurrentRuntime();
|
|
1942
|
+
const matches = session
|
|
1943
|
+
? session.sessionName === replServerSelection.sessionName
|
|
1944
|
+
: !replServerSelection.sessionName && replServerSelection.runtime === replRuntime;
|
|
1945
|
+
if (matches) return false;
|
|
1946
|
+
const target = session
|
|
1947
|
+
? { sessionName: session.sessionName, tmuxSessionId: session.tmuxSessionId, tmuxSessionCreatedAt: session.tmuxSessionCreatedAt }
|
|
1948
|
+
: { runtime: replRuntime };
|
|
1949
|
+
const key = replShownBasis + "\u001f" + JSON.stringify(target);
|
|
1950
|
+
if (key === replReconcileSent) return false;
|
|
1951
|
+
replReconcileSent = key;
|
|
1952
|
+
return sendMessage({ type: "repl_reconcile_request", basis: replShownBasis, ...target });
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
// A new connection has no choice outstanding and no basis until it adopts one.
|
|
1956
|
+
function resetReplSelectionBasis() {
|
|
1957
|
+
replChoiceAnswered = replChoiceRevision;
|
|
1958
|
+
replChoicePin = null;
|
|
1959
|
+
replShownBasis = null;
|
|
1960
|
+
replReconcileSent = "";
|
|
1961
|
+
replServerSelection = { sessionName: "", runtime: "" };
|
|
1962
|
+
if (rightView === "repl") requestReplList();
|
|
1822
1963
|
}
|
|
1823
1964
|
|
|
1824
1965
|
function trimReplTranscript(text) {
|
|
@@ -1832,6 +1973,7 @@
|
|
|
1832
1973
|
return sendMessage({ type: "repl_list_request" });
|
|
1833
1974
|
}
|
|
1834
1975
|
|
|
1976
|
+
// A poll: the displayed session as it is now, or the list when there is none.
|
|
1835
1977
|
function requestReplCapture() {
|
|
1836
1978
|
if (wsState === "Disconnected") return false;
|
|
1837
1979
|
if (replActiveSessionName) {
|
|
@@ -1840,6 +1982,25 @@
|
|
|
1840
1982
|
return sendMessage({ type: "repl_list_request" });
|
|
1841
1983
|
}
|
|
1842
1984
|
|
|
1985
|
+
// A choice on the Session or Runtime selector, which Studio counts. Until its reply
|
|
1986
|
+
// arrives, other replies don't change what the page shows.
|
|
1987
|
+
function requestReplChoice() {
|
|
1988
|
+
if (wsState === "Disconnected") return false;
|
|
1989
|
+
const choiceRevision = replChoiceRevision + 1;
|
|
1990
|
+
const sent = sendMessage({
|
|
1991
|
+
type: "repl_capture_request",
|
|
1992
|
+
choice: true,
|
|
1993
|
+
choiceRevision,
|
|
1994
|
+
runtime: replRuntime,
|
|
1995
|
+
...(replActiveSessionName ? { sessionName: replActiveSessionName } : {}),
|
|
1996
|
+
});
|
|
1997
|
+
if (sent) {
|
|
1998
|
+
replChoiceRevision = choiceRevision;
|
|
1999
|
+
replChoicePin = getActiveReplSession();
|
|
2000
|
+
}
|
|
2001
|
+
return sent;
|
|
2002
|
+
}
|
|
2003
|
+
|
|
1843
2004
|
function isReplControlFocused() {
|
|
1844
2005
|
const activeEl = document.activeElement;
|
|
1845
2006
|
return activeEl instanceof Element && Boolean(activeEl.closest(".repl-controls, .repl-quick-composer"));
|
|
@@ -1898,6 +2059,7 @@
|
|
|
1898
2059
|
traceRenderRaf = window.requestAnimationFrame(() => {
|
|
1899
2060
|
traceRenderRaf = null;
|
|
1900
2061
|
refreshResponseUi();
|
|
2062
|
+
applyReplQuickRefocus();
|
|
1901
2063
|
if (replQuickFocusRequested) focusReplQuickComposer();
|
|
1902
2064
|
});
|
|
1903
2065
|
}
|
|
@@ -1905,6 +2067,8 @@
|
|
|
1905
2067
|
function startReplPolling() {
|
|
1906
2068
|
if (rightView !== "repl") return;
|
|
1907
2069
|
if (replPollTimer !== null) return;
|
|
2070
|
+
// Polls only observe, so the view starts from Studio's current selection.
|
|
2071
|
+
if (replActiveSessionName) requestReplList();
|
|
1908
2072
|
requestReplCapture();
|
|
1909
2073
|
replPollTimer = window.setInterval(() => {
|
|
1910
2074
|
if (rightView !== "repl") {
|
|
@@ -2182,6 +2346,7 @@
|
|
|
2182
2346
|
runtime: details.runtime || getActiveReplRuntime(),
|
|
2183
2347
|
origin: details.origin === "pi-repl" ? "pi-repl" : "pi-studio",
|
|
2184
2348
|
legacyLocal: !sharedSynced,
|
|
2349
|
+
outputClean: sharedSynced || details.outputClean === true,
|
|
2185
2350
|
label: details.label || "REPL send",
|
|
2186
2351
|
mode: details.mode || replSendMode,
|
|
2187
2352
|
prose: String(details.prose || ""),
|
|
@@ -2259,7 +2424,7 @@
|
|
|
2259
2424
|
return sent;
|
|
2260
2425
|
}
|
|
2261
2426
|
|
|
2262
|
-
function recordReplToolSend(message) {
|
|
2427
|
+
function recordReplToolSend(message, selects) {
|
|
2263
2428
|
const requestId = typeof message.toolCallId === "string" && message.toolCallId.trim()
|
|
2264
2429
|
? "tool:" + message.toolCallId.trim()
|
|
2265
2430
|
: (typeof message.requestId === "string" && message.requestId.trim() ? message.requestId.trim() : "");
|
|
@@ -2267,20 +2432,23 @@
|
|
|
2267
2432
|
if (!code.trim()) return false;
|
|
2268
2433
|
const runtime = normalizeReplRuntime(message.runtime || getActiveReplRuntime());
|
|
2269
2434
|
const sessionName = typeof message.sessionName === "string" ? message.sessionName : replActiveSessionName;
|
|
2270
|
-
|
|
2435
|
+
// The server has already cleaned this send's output; cleaning it again could drop real lines.
|
|
2436
|
+
const output = trimReplJournalOutput(String(message.output || "").trimEnd());
|
|
2271
2437
|
const details = {
|
|
2272
2438
|
requestId,
|
|
2273
2439
|
sessionName,
|
|
2274
2440
|
runtime,
|
|
2275
2441
|
origin: message.origin === "pi-repl" ? "pi-repl" : "pi-studio",
|
|
2276
2442
|
sharedSynced: message.sharedSynced === true,
|
|
2443
|
+
outputClean: true,
|
|
2277
2444
|
label: typeof message.label === "string" && message.label.trim() ? message.label.trim() : "Pi",
|
|
2278
2445
|
mode: "agent",
|
|
2279
2446
|
code,
|
|
2280
2447
|
output,
|
|
2281
2448
|
status: output.trim() ? "captured" : (message.timedOut ? "timeout" : "sent"),
|
|
2282
2449
|
};
|
|
2283
|
-
|
|
2450
|
+
// A result that doesn't select leaves this page's own entry tracking alone (Astra).
|
|
2451
|
+
if (selects !== false) activeReplJournalEntryId = "";
|
|
2284
2452
|
if (requestId) {
|
|
2285
2453
|
const existingIndex = replJournalEntries.findIndex((entry) => entry.requestId === requestId);
|
|
2286
2454
|
if (existingIndex >= 0) {
|
|
@@ -2375,7 +2543,8 @@
|
|
|
2375
2543
|
if (entryIndex < 0) return false;
|
|
2376
2544
|
const entry = replJournalEntries[entryIndex];
|
|
2377
2545
|
if (entry.sessionName && sessionName && entry.sessionName !== sessionName) return false;
|
|
2378
|
-
|
|
2546
|
+
// Finished output from Studio's server stands, whether or not the shared record took it.
|
|
2547
|
+
if ((entry.outputClean || !entry.legacyLocal) && (entry.status === "captured" || entry.status === "timeout" || entry.status === "error")) {
|
|
2379
2548
|
activeReplJournalEntryId = "";
|
|
2380
2549
|
return false;
|
|
2381
2550
|
}
|
|
@@ -2650,6 +2819,8 @@
|
|
|
2650
2819
|
});
|
|
2651
2820
|
if (!requestId) return false;
|
|
2652
2821
|
replQuickPending = { requestId, key, text };
|
|
2822
|
+
const activeEl = document.activeElement;
|
|
2823
|
+
replQuickRefocus = activeEl instanceof Element && activeEl.closest(".repl-quick-composer") ? { requestId, settled: false } : null;
|
|
2653
2824
|
renderReplViewIfActive({ force: true });
|
|
2654
2825
|
return true;
|
|
2655
2826
|
}
|
|
@@ -11699,13 +11870,13 @@
|
|
|
11699
11870
|
selectReplSessionForRuntime(replRuntime, previousActive);
|
|
11700
11871
|
replError = "";
|
|
11701
11872
|
replMessage = "";
|
|
11702
|
-
if (replActiveSessionName) {
|
|
11703
|
-
requestReplCapture();
|
|
11704
|
-
} else {
|
|
11873
|
+
if (!replActiveSessionName) {
|
|
11705
11874
|
replTranscript = "";
|
|
11706
11875
|
replCapturedAt = 0;
|
|
11707
11876
|
syncActionButtons();
|
|
11708
11877
|
}
|
|
11878
|
+
// Counted even with no session, so nothing that finishes later takes it back.
|
|
11879
|
+
requestReplChoice();
|
|
11709
11880
|
renderReplViewIfActive({ force: true });
|
|
11710
11881
|
return;
|
|
11711
11882
|
}
|
|
@@ -11727,7 +11898,7 @@
|
|
|
11727
11898
|
replError = "";
|
|
11728
11899
|
replMessage = "";
|
|
11729
11900
|
replFollow = true;
|
|
11730
|
-
|
|
11901
|
+
requestReplChoice();
|
|
11731
11902
|
renderReplViewIfActive({ force: true });
|
|
11732
11903
|
}
|
|
11733
11904
|
}
|
|
@@ -11743,6 +11914,8 @@
|
|
|
11743
11914
|
critiqueViewEl.addEventListener("click", handleStudioQuartoPreviewClick);
|
|
11744
11915
|
critiqueViewEl.addEventListener("click", (event) => { void handleSideQuestionClick(event); });
|
|
11745
11916
|
critiqueViewEl.addEventListener("focusin", handleReplPaneFocusIn);
|
|
11917
|
+
document.addEventListener("focusin", cancelReplQuickRefocusOutside, true);
|
|
11918
|
+
document.addEventListener("pointerdown", cancelReplQuickRefocusOutside, true);
|
|
11746
11919
|
critiqueViewEl.addEventListener("input", handleSideQuestionInput);
|
|
11747
11920
|
critiqueViewEl.addEventListener("input", handleReplPaneInput);
|
|
11748
11921
|
critiqueViewEl.addEventListener("keydown", handleTracePaneKeydown);
|
|
@@ -17226,6 +17399,13 @@
|
|
|
17226
17399
|
},
|
|
17227
17400
|
replace: bufferId => retireStudioBufferSourceView(bufferId),
|
|
17228
17401
|
bind: () => bindSelectedStudioBuffer({ focusEditor: false }),
|
|
17402
|
+
opened: bufferId => {
|
|
17403
|
+
const buffer = bufferRecoveryClient?.snapshot()?.buffers.find(b => b.id === bufferId);
|
|
17404
|
+
const path = String(buffer?.sourceState.path || "");
|
|
17405
|
+
const name = path.split(/^(?:[A-Za-z]:\\|\\\\)/.test(path) ? /[\\/]/ : "/").pop();
|
|
17406
|
+
const where = !bufferSwitchingEnabled ? "" : buffer?.role === "document" ? " in the Document" : " in the Prompt";
|
|
17407
|
+
return name ? "Opened " + name + where + "." : "";
|
|
17408
|
+
},
|
|
17229
17409
|
reuse: async request => {
|
|
17230
17410
|
const result = await reuseHostedDocumentOwner(request.path, { sourcePath: request.sourcePath, resourceDir: request.resourceDir });
|
|
17231
17411
|
if (!result.handled) setStatus("The file owner is no longer available. Current work was kept; retry opening explicitly.", "warning");
|
|
@@ -28461,25 +28641,30 @@
|
|
|
28461
28641
|
const responseRequestId = typeof message.requestId === "string" ? message.requestId : "";
|
|
28462
28642
|
const settlesPendingRequest = Boolean(responseRequestId && responseRequestId === replPendingRequestId);
|
|
28463
28643
|
const previousActiveLifetimeKey = getReplQuickDraftKey(getActiveReplSession());
|
|
28644
|
+
const previousRuntime = replRuntime;
|
|
28645
|
+
// Every repl_state states Studio's selection; while this page's own choice is
|
|
28646
|
+
// unanswered, only that choice's reply changes what the page shows.
|
|
28647
|
+
noteReplChoiceAnswer(message);
|
|
28648
|
+
const present = !isReplChoicePending();
|
|
28464
28649
|
replTmuxAvailable = typeof message.tmuxAvailable === "boolean" ? message.tmuxAvailable : replTmuxAvailable;
|
|
28465
28650
|
const sessionsChanged = setReplSessions(message.sessions);
|
|
28466
|
-
if (
|
|
28467
|
-
setActiveReplSessionForCurrentRuntime(message.activeSessionName);
|
|
28468
|
-
}
|
|
28651
|
+
if (present) adoptReplSelection(message);
|
|
28469
28652
|
const journalChanged = mergeReplJournalEntries(message.journalEntries);
|
|
28470
28653
|
reconcileReplQuickPending();
|
|
28471
28654
|
maybeImportLegacyReplJournalEntries(replActiveSessionName);
|
|
28472
|
-
if (typeof message.transcript === "string") replTranscript = trimReplTranscript(message.transcript);
|
|
28473
|
-
if (typeof message.capturedAt === "number") replCapturedAt = message.capturedAt;
|
|
28655
|
+
if (present && typeof message.transcript === "string") replTranscript = trimReplTranscript(message.transcript);
|
|
28656
|
+
if (present && typeof message.capturedAt === "number") replCapturedAt = message.capturedAt;
|
|
28474
28657
|
replError = typeof message.replError === "string" ? message.replError : (typeof message.captureError === "string" ? message.captureError : "");
|
|
28475
28658
|
replMessage = typeof message.replMessage === "string" ? message.replMessage : "";
|
|
28476
28659
|
if (settlesPendingRequest && replPendingRequestId === responseRequestId) replPendingRequestId = "";
|
|
28477
28660
|
replBusy = Boolean(replPendingRequestId);
|
|
28661
|
+
if (present) reconcileReplSelection();
|
|
28478
28662
|
const activeSessionIdentityChanged = Boolean(previousActiveLifetimeKey)
|
|
28479
28663
|
&& previousActiveLifetimeKey !== getReplQuickDraftKey(getActiveReplSession());
|
|
28480
28664
|
const controlsChanged = wasBusy
|
|
28481
28665
|
|| sessionsChanged
|
|
28482
28666
|
|| previousTmuxAvailable !== replTmuxAvailable
|
|
28667
|
+
|| previousRuntime !== replRuntime
|
|
28483
28668
|
|| previousActiveSessionName !== replActiveSessionName;
|
|
28484
28669
|
if (controlsChanged) syncActionButtons();
|
|
28485
28670
|
const viewChanged = controlsChanged
|
|
@@ -28494,14 +28679,28 @@
|
|
|
28494
28679
|
}
|
|
28495
28680
|
|
|
28496
28681
|
if (message.type === "repl_tool_send") {
|
|
28497
|
-
if
|
|
28498
|
-
|
|
28682
|
+
// The agent's send selects only if nothing was chosen since Studio admitted it,
|
|
28683
|
+
// and not while this page's own choice is unanswered (Sol, Astra).
|
|
28684
|
+
const selects = message.select !== false && !isReplChoicePending();
|
|
28685
|
+
const previousRuntime = replRuntime;
|
|
28686
|
+
const previousActiveSessionName = replActiveSessionName;
|
|
28687
|
+
if (selects) {
|
|
28688
|
+
if (message.session) upsertReplSession(message.session);
|
|
28689
|
+
adoptReplSelection({
|
|
28690
|
+
activeSessionName: typeof message.activeSessionName === "string" ? message.activeSessionName : message.sessionName,
|
|
28691
|
+
activeRuntime: typeof message.activeRuntime === "string" ? message.activeRuntime : message.runtime,
|
|
28692
|
+
shown: message.shown,
|
|
28693
|
+
});
|
|
28499
28694
|
}
|
|
28500
|
-
const changed = recordReplToolSend(message);
|
|
28695
|
+
const changed = recordReplToolSend(message, selects);
|
|
28501
28696
|
const journalChanged = mergeReplJournalEntries(message.journalEntries);
|
|
28502
|
-
if (
|
|
28503
|
-
|
|
28504
|
-
|
|
28697
|
+
if (selects && message.sessionName === replActiveSessionName) {
|
|
28698
|
+
if (typeof message.transcript === "string") replTranscript = trimReplTranscript(message.transcript);
|
|
28699
|
+
if (typeof message.capturedAt === "number") replCapturedAt = message.capturedAt;
|
|
28700
|
+
}
|
|
28701
|
+
if (selects) reconcileReplSelection();
|
|
28702
|
+
if (previousRuntime !== replRuntime || previousActiveSessionName !== replActiveSessionName) syncActionButtons();
|
|
28703
|
+
if (changed || journalChanged || selects) renderReplViewIfActive({ force: true });
|
|
28505
28704
|
updateReferenceBadge();
|
|
28506
28705
|
return;
|
|
28507
28706
|
}
|
|
@@ -28516,41 +28715,55 @@
|
|
|
28516
28715
|
const responseRequestId = typeof message.requestId === "string" ? message.requestId : "";
|
|
28517
28716
|
const settlesPendingRequest = Boolean(responseRequestId && responseRequestId === replPendingRequestId);
|
|
28518
28717
|
const previousActiveLifetimeKey = getReplQuickDraftKey(getActiveReplSession());
|
|
28718
|
+
const previousRuntime = replRuntime;
|
|
28519
28719
|
let sessionsChanged = false;
|
|
28520
|
-
|
|
28521
|
-
|
|
28522
|
-
|
|
28523
|
-
|
|
28524
|
-
|
|
28525
|
-
|
|
28526
|
-
|
|
28527
|
-
|
|
28528
|
-
|
|
28529
|
-
|
|
28720
|
+
let applies = false;
|
|
28721
|
+
if (message.observation === true) {
|
|
28722
|
+
// A poll's reply shows one session as it is now. It applies only to the exact
|
|
28723
|
+
// session this page displays, checked before anything changes (Sol, Astra).
|
|
28724
|
+
const observed = normalizeReplSession(message.session);
|
|
28725
|
+
const displayed = getActiveReplSession();
|
|
28726
|
+
const observedName = observed ? observed.sessionName : (typeof message.sessionName === "string" ? message.sessionName : "");
|
|
28727
|
+
if (!isReplChoicePending() && displayed && observedName === displayed.sessionName) {
|
|
28728
|
+
if (!observed) {
|
|
28729
|
+
// Its capture failed; the list says what Studio selects now.
|
|
28730
|
+
applies = true;
|
|
28731
|
+
requestReplList();
|
|
28732
|
+
} else if (getReplSessionLifetimeKey(observed) === getReplSessionLifetimeKey(displayed)) {
|
|
28733
|
+
applies = true;
|
|
28734
|
+
sessionsChanged = upsertReplSession(observed);
|
|
28735
|
+
} else {
|
|
28736
|
+
requestReplList();
|
|
28530
28737
|
}
|
|
28531
28738
|
}
|
|
28532
|
-
}
|
|
28533
|
-
|
|
28534
|
-
|
|
28739
|
+
} else {
|
|
28740
|
+
// A choice's reply, or a result that selected: it states Studio's selection.
|
|
28741
|
+
noteReplChoiceAnswer(message);
|
|
28742
|
+
applies = !isReplChoicePending();
|
|
28743
|
+
if (applies && message.session) sessionsChanged = upsertReplSession(message.session);
|
|
28744
|
+
if (applies) adoptReplSelection(message);
|
|
28745
|
+
applies = applies && Boolean(message.session) && message.session.sessionName === replActiveSessionName;
|
|
28535
28746
|
}
|
|
28536
28747
|
let journalChanged = mergeReplJournalEntries(message.journalEntries);
|
|
28537
28748
|
reconcileReplQuickPending();
|
|
28538
28749
|
maybeImportLegacyReplJournalEntries(replActiveSessionName);
|
|
28539
|
-
if (typeof message.transcript === "string") {
|
|
28750
|
+
if (applies && typeof message.transcript === "string") {
|
|
28540
28751
|
replTranscript = trimReplTranscript(message.transcript);
|
|
28541
|
-
journalChanged = updateActiveReplJournalEntryFromTranscript(
|
|
28542
|
-
|
|
28543
|
-
|
|
28544
|
-
|
|
28752
|
+
journalChanged = updateActiveReplJournalEntryFromTranscript(replActiveSessionName, replTranscript) || journalChanged;
|
|
28753
|
+
} else if (applies && typeof message.captureError === "string") {
|
|
28754
|
+
replTranscript = "";
|
|
28755
|
+
}
|
|
28756
|
+
if (applies && typeof message.capturedAt === "number") replCapturedAt = message.capturedAt;
|
|
28757
|
+
if (applies || message.observation !== true) {
|
|
28758
|
+
replError = typeof message.replError === "string" ? message.replError : (typeof message.captureError === "string" ? message.captureError : "");
|
|
28545
28759
|
}
|
|
28546
|
-
if (typeof message.capturedAt === "number") replCapturedAt = message.capturedAt;
|
|
28547
|
-
replError = typeof message.replError === "string" ? message.replError : "";
|
|
28548
28760
|
if (typeof message.replMessage === "string") replMessage = message.replMessage;
|
|
28549
28761
|
if (settlesPendingRequest && replPendingRequestId === responseRequestId) replPendingRequestId = "";
|
|
28550
28762
|
replBusy = Boolean(replPendingRequestId);
|
|
28763
|
+
if (message.observation !== true && !isReplChoicePending()) reconcileReplSelection();
|
|
28551
28764
|
const activeSessionIdentityChanged = Boolean(previousActiveLifetimeKey)
|
|
28552
28765
|
&& previousActiveLifetimeKey !== getReplQuickDraftKey(getActiveReplSession());
|
|
28553
|
-
const controlsChanged = wasBusy || sessionsChanged || previousActiveSessionName !== replActiveSessionName;
|
|
28766
|
+
const controlsChanged = wasBusy || sessionsChanged || previousRuntime !== replRuntime || previousActiveSessionName !== replActiveSessionName;
|
|
28554
28767
|
if (controlsChanged) syncActionButtons();
|
|
28555
28768
|
const viewChanged = controlsChanged
|
|
28556
28769
|
|| previousTranscript !== replTranscript
|
|
@@ -29375,6 +29588,7 @@
|
|
|
29375
29588
|
setWsState("Ready");
|
|
29376
29589
|
setStatus(wasReconnect ? "Reconnected. Syncing…" : "Connected. Syncing…");
|
|
29377
29590
|
sendMessage({ type: "hello" });
|
|
29591
|
+
resetReplSelectionBasis();
|
|
29378
29592
|
if (isWatchedFilePreview && watchedFilePreviewState.path) {
|
|
29379
29593
|
watchedFilePreviewState.generation = 0;
|
|
29380
29594
|
sendMessage({
|
|
@@ -32159,10 +32373,13 @@
|
|
|
32159
32373
|
if (restored.restored) {
|
|
32160
32374
|
const snapshot = client.snapshot();
|
|
32161
32375
|
const selected = snapshot.buffers.find((entry) => entry.id === snapshot.selectedBufferId);
|
|
32162
|
-
applyPersistedWorkspaceState(restored.editor, {
|
|
32376
|
+
const applied = applyPersistedWorkspaceState(restored.editor, {
|
|
32163
32377
|
verifiedBuffer: documentHostingEnabled,
|
|
32164
32378
|
metadataAssociations: selected && selected.metadata,
|
|
32165
32379
|
});
|
|
32380
|
+
// A fresh dual-mode launch is registered on the server first; only an
|
|
32381
|
+
// applied copy kept by this tab (a reload) is reported as restored.
|
|
32382
|
+
workspaceRestoredFromBrowser = applied && restored.fromBrowser === true;
|
|
32166
32383
|
if (sourceState.path) fileBackedBaselineText = restored.baselineText;
|
|
32167
32384
|
if (selected) {
|
|
32168
32385
|
sourceTextEl.setSelectionRange(selected.view.selectionStart, selected.view.selectionEnd, selected.view.selectionDirection);
|
|
@@ -32216,7 +32433,7 @@
|
|
|
32216
32433
|
if (documentHostingEnabled) syncActionButtons();
|
|
32217
32434
|
persistWorkspaceStateNow({ skipServer: serverWorkspaceRecovery.status === "unavailable" });
|
|
32218
32435
|
if (workspaceRestoredFromBrowser) {
|
|
32219
|
-
setStatus(bufferSwitchingEnabled ? "Restored Prompt and Document
|
|
32436
|
+
setStatus(bufferSwitchingEnabled ? "Restored the Prompt and Document." : "Restored editor workspace from this browser tab. Use Reset editor to discard it.", "success");
|
|
32220
32437
|
}
|
|
32221
32438
|
connect();
|
|
32222
32439
|
if (documentHostingEnabled && bufferRecoveryClient) { await bufferRecoveryClient.settled(); await setupDocumentHosting(); }
|