pi-studio 0.10.0 → 0.10.2
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 +30 -0
- package/README.md +101 -219
- package/assets/screenshots/dark-workspace.png +0 -0
- package/assets/screenshots/light-workspace.png +0 -0
- package/assets/screenshots/theme-zen-code.png +0 -0
- package/client/studio-client.js +244 -67
- package/client/studio.css +13 -1
- package/index.ts +484 -270
- 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/assets/screenshots/dark-code-mode.png +0 -0
- package/assets/screenshots/dark-critique.png +0 -0
- package/assets/screenshots/dark-fenced-code.png +0 -0
- package/assets/screenshots/dark-focus-mode.png +0 -0
- package/assets/screenshots/dark-julia-fenced.png +0 -0
- package/assets/screenshots/dark-math.png +0 -0
- package/assets/screenshots/dark-mermaid.png +0 -0
- package/assets/screenshots/dark-zen.png +0 -0
- package/assets/screenshots/light-code-mode.png +0 -0
- package/assets/screenshots/light-fenced-code.png +0 -0
- package/assets/screenshots/light-math.png +0 -0
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";
|
|
@@ -721,6 +733,9 @@
|
|
|
721
733
|
if (!entry || typeof entry !== "object") return null;
|
|
722
734
|
const hasCompatibleOrigin = entry.origin === "pi-repl" || entry.origin === "pi-studio";
|
|
723
735
|
const sharedSynced = entry.sharedSynced === true || (entry.sharedSynced !== false && hasCompatibleOrigin);
|
|
736
|
+
// Whether the output was cleaned where it was captured is separate from whether
|
|
737
|
+
// it reached the shared record: a failed record write leaves clean output.
|
|
738
|
+
const outputClean = sharedSynced || entry.outputClean === true;
|
|
724
739
|
const normalized = {
|
|
725
740
|
id: typeof entry.id === "string" && entry.id ? entry.id : ("repl-journal-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 8)),
|
|
726
741
|
requestId: typeof entry.requestId === "string" ? entry.requestId : "",
|
|
@@ -731,11 +746,14 @@
|
|
|
731
746
|
runtime: typeof entry.runtime === "string" ? entry.runtime : "python",
|
|
732
747
|
origin: hasCompatibleOrigin ? entry.origin : "",
|
|
733
748
|
legacyLocal: !sharedSynced,
|
|
749
|
+
outputClean,
|
|
734
750
|
label: typeof entry.label === "string" ? entry.label : "REPL send",
|
|
735
751
|
mode: typeof entry.mode === "string" ? entry.mode : "raw",
|
|
736
752
|
prose: typeof entry.prose === "string" ? entry.prose : "",
|
|
737
753
|
code: typeof entry.code === "string" ? entry.code : "",
|
|
738
|
-
|
|
754
|
+
// Studio's server and pi-repl record clean output; only this browser's
|
|
755
|
+
// older local entries can still hold a loader echo.
|
|
756
|
+
output: typeof entry.output === "string" ? (outputClean ? entry.output : stripStudioReplSubmissionEcho(entry.output)) : "",
|
|
739
757
|
beforeTranscript: "",
|
|
740
758
|
status: typeof entry.status === "string" ? entry.status : "sent",
|
|
741
759
|
skippedChunks: Math.max(0, Math.floor(Number(entry.skippedChunks) || 0)),
|
|
@@ -767,6 +785,7 @@
|
|
|
767
785
|
runtime: entry.runtime,
|
|
768
786
|
origin: entry.origin,
|
|
769
787
|
sharedSynced: !entry.legacyLocal,
|
|
788
|
+
outputClean: entry.outputClean === true,
|
|
770
789
|
label: entry.label,
|
|
771
790
|
mode: entry.mode,
|
|
772
791
|
prose: entry.prose,
|
|
@@ -786,7 +805,8 @@
|
|
|
786
805
|
let changed = false;
|
|
787
806
|
const next = [...replJournalEntries];
|
|
788
807
|
for (const rawEntry of entries) {
|
|
789
|
-
|
|
808
|
+
// These come from Studio's server, which has already cleaned their output.
|
|
809
|
+
const entry = normalizeReplJournalEntry(rawEntry && typeof rawEntry === "object" ? { ...rawEntry, outputClean: true } : rawEntry);
|
|
790
810
|
if (!entry) continue;
|
|
791
811
|
const existingIndex = next.findIndex((candidate) => (
|
|
792
812
|
(entry.requestId && candidate.requestId === entry.requestId)
|
|
@@ -920,6 +940,9 @@
|
|
|
920
940
|
const seconds = Math.max(0, Math.floor((Date.now() - run.startedAt) / 1000));
|
|
921
941
|
return "Running " + Math.floor(seconds / 60) + ":" + String(seconds % 60).padStart(2, "0");
|
|
922
942
|
}
|
|
943
|
+
// Oliver, 14 Oct: a Run waiting for Pi to start says so briefly here, so Queue steering fits
|
|
944
|
+
// beside it and the toolbar doesn't wrap and jump back. The footer keeps the detail.
|
|
945
|
+
if (uiBusy && pendingKind === "direct") return "Starting…";
|
|
923
946
|
return uiBusy ? getStudioBusyStatus(pendingKind) : "";
|
|
924
947
|
}
|
|
925
948
|
|
|
@@ -936,6 +959,7 @@
|
|
|
936
959
|
ui.headerStop.hidden = isEditorOnlyMode || !rightFocused || (!activeKind && !stopping && !ownedRun);
|
|
937
960
|
ui.headerStop.textContent = stopping ? "Stopping…" : "Stop";
|
|
938
961
|
ui.headerStop.disabled = Boolean(stopping) || !activeKind;
|
|
962
|
+
if (run && sendRunBtn) sendRunBtn.classList.toggle("request-pending", false);
|
|
939
963
|
if (run && !isEditorOnlyMode && sendRunBtn) {
|
|
940
964
|
const owned = run.origin === "studio" && Boolean(run.requestId && run.kind);
|
|
941
965
|
if (owned) {
|
|
@@ -1484,7 +1508,7 @@
|
|
|
1484
1508
|
if (!sessionName) return null;
|
|
1485
1509
|
return {
|
|
1486
1510
|
sessionName,
|
|
1487
|
-
target: typeof session.target === "string" ? session.target :
|
|
1511
|
+
target: typeof session.target === "string" ? session.target : "",
|
|
1488
1512
|
tmuxSessionId: typeof session.tmuxSessionId === "string" ? session.tmuxSessionId : "",
|
|
1489
1513
|
tmuxSessionCreatedAt: typeof session.tmuxSessionCreatedAt === "number" && Number.isFinite(session.tmuxSessionCreatedAt)
|
|
1490
1514
|
? session.tmuxSessionCreatedAt
|
|
@@ -1725,14 +1749,20 @@
|
|
|
1725
1749
|
function setReplSessions(sessions) {
|
|
1726
1750
|
const previous = serializeReplSessionsForCompare(replSessions);
|
|
1727
1751
|
const previousActive = replActiveSessionName;
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1752
|
+
// While a choice is unanswered, the chosen session keeps the lifetime it was chosen
|
|
1753
|
+
// with: no list, even one after a list that left it out, can swap in a replacement
|
|
1754
|
+
// with the same name (Astra, Sol).
|
|
1755
|
+
const chosen = isReplChoicePending() ? replChoicePin : null;
|
|
1756
|
+
replSessions = (Array.isArray(sessions) ? sessions.map(normalizeReplSession).filter(Boolean) : [])
|
|
1757
|
+
.map((session) => (chosen && session.sessionName === chosen.sessionName ? chosen : session));
|
|
1758
|
+
// The list may change while a choice is unanswered, but the selection waits for its reply.
|
|
1759
|
+
if (!isReplChoicePending()) {
|
|
1760
|
+
if (replActiveSessionName && !replSessions.some((session) => session.sessionName === replActiveSessionName)) {
|
|
1761
|
+
setActiveReplSession("");
|
|
1762
|
+
}
|
|
1763
|
+
if (!getActiveReplSessionForCurrentRuntime()) {
|
|
1764
|
+
selectReplSessionForRuntime(replRuntime, replActiveSessionName);
|
|
1765
|
+
}
|
|
1736
1766
|
}
|
|
1737
1767
|
return previous !== serializeReplSessionsForCompare(replSessions) || previousActive !== replActiveSessionName;
|
|
1738
1768
|
}
|
|
@@ -1785,7 +1815,7 @@
|
|
|
1785
1815
|
"The right pane is mirroring an active tmux-backed REPL session.",
|
|
1786
1816
|
"If the user refers to the active REPL, send code to this session rather than inventing a separate one.",
|
|
1787
1817
|
"Session name: " + session.sessionName,
|
|
1788
|
-
"tmux target: " + (session.target ||
|
|
1818
|
+
"tmux target: " + (session.target || session.sessionName),
|
|
1789
1819
|
"runtime: " + runtime,
|
|
1790
1820
|
"Use the studio_repl_send tool for code execution in this REPL. Pass sessionName when targeting this exact session.",
|
|
1791
1821
|
"Do not improvise raw tmux paste commands for multiline code; Studio handles runtime-specific safe submission.",
|
|
@@ -1800,21 +1830,94 @@
|
|
|
1800
1830
|
}
|
|
1801
1831
|
|
|
1802
1832
|
function setActiveReplSession(sessionName) {
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1833
|
+
replActiveSessionName = String(sessionName || "").trim();
|
|
1834
|
+
retireReplViewIfTargetChanged();
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
// The view belongs to one session lifetime, or to none. When the displayed target
|
|
1838
|
+
// changes, including to or from none or to a replacement with the same name, its
|
|
1839
|
+
// transcript, timestamp and live entry go with it (Sol).
|
|
1840
|
+
function retireReplViewIfTargetChanged() {
|
|
1841
|
+
const owner = getReplSessionLifetimeKey(getActiveReplSession());
|
|
1842
|
+
if (owner === replTranscriptOwner) return false;
|
|
1843
|
+
replTranscript = "";
|
|
1844
|
+
replCapturedAt = 0;
|
|
1845
|
+
activeReplJournalEntryId = "";
|
|
1846
|
+
replTranscriptOwner = owner;
|
|
1847
|
+
return true;
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
function getReplSessionLifetimeKey(session) {
|
|
1851
|
+
return session ? [session.sessionName, session.tmuxSessionId || "", Number(session.tmuxSessionCreatedAt) || 0].join("\u001f") : "";
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
function upsertReplSession(value) {
|
|
1855
|
+
const session = normalizeReplSession(value);
|
|
1856
|
+
if (!session) return false;
|
|
1857
|
+
const index = replSessions.findIndex((candidate) => candidate.sessionName === session.sessionName);
|
|
1858
|
+
if (index < 0) {
|
|
1859
|
+
replSessions = [...replSessions, session];
|
|
1860
|
+
return true;
|
|
1811
1861
|
}
|
|
1812
|
-
if (
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1862
|
+
if (serializeReplSessionsForCompare([replSessions[index]]) === serializeReplSessionsForCompare([session])) return false;
|
|
1863
|
+
replSessions = replSessions.map((candidate, candidateIndex) => candidateIndex === index ? session : candidate);
|
|
1864
|
+
return true;
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
function isReplChoicePending() {
|
|
1868
|
+
return replChoiceAnswered < replChoiceRevision;
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
// Only the reply to this page's latest choice ends the wait.
|
|
1872
|
+
function noteReplChoiceAnswer(message) {
|
|
1873
|
+
if (typeof message.choiceRevision === "number" && message.choiceRevision > replChoiceAnswered) {
|
|
1874
|
+
replChoiceAnswered = Math.min(message.choiceRevision, replChoiceRevision);
|
|
1816
1875
|
}
|
|
1817
|
-
|
|
1876
|
+
if (!isReplChoicePending()) replChoicePin = null;
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
// Adopts Studio's selection from a message that states it. A session brings its
|
|
1880
|
+
// runtime; none comes for a runtime; with no preference yet, the page keeps its
|
|
1881
|
+
// runtime and picks within it (Sol, Astra).
|
|
1882
|
+
function adoptReplSelection(message) {
|
|
1883
|
+
const sessionName = typeof message.activeSessionName === "string" ? message.activeSessionName.trim() : "";
|
|
1884
|
+
const runtime = getKnownReplRuntime(message.activeRuntime);
|
|
1885
|
+
replServerSelection = { sessionName, runtime };
|
|
1886
|
+
if (typeof message.shown === "number") replShownBasis = message.shown;
|
|
1887
|
+
if (runtime && runtime !== replRuntime) setReplRuntime(runtime);
|
|
1888
|
+
if (sessionName) {
|
|
1889
|
+
setActiveReplSessionForCurrentRuntime(sessionName);
|
|
1890
|
+
} else {
|
|
1891
|
+
selectReplSessionForRuntime(replRuntime, runtime ? "" : replActiveSessionName);
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
// Reports a selection the page made itself, against the selection it last adopted.
|
|
1896
|
+
// Studio applies it only if nothing has changed since; it isn't a choice (Sol, Astra).
|
|
1897
|
+
function reconcileReplSelection() {
|
|
1898
|
+
if (replShownBasis === null || isReplChoicePending() || wsState === "Disconnected") return false;
|
|
1899
|
+
const session = getActiveReplSessionForCurrentRuntime();
|
|
1900
|
+
const matches = session
|
|
1901
|
+
? session.sessionName === replServerSelection.sessionName
|
|
1902
|
+
: !replServerSelection.sessionName && replServerSelection.runtime === replRuntime;
|
|
1903
|
+
if (matches) return false;
|
|
1904
|
+
const target = session
|
|
1905
|
+
? { sessionName: session.sessionName, tmuxSessionId: session.tmuxSessionId, tmuxSessionCreatedAt: session.tmuxSessionCreatedAt }
|
|
1906
|
+
: { runtime: replRuntime };
|
|
1907
|
+
const key = replShownBasis + "\u001f" + JSON.stringify(target);
|
|
1908
|
+
if (key === replReconcileSent) return false;
|
|
1909
|
+
replReconcileSent = key;
|
|
1910
|
+
return sendMessage({ type: "repl_reconcile_request", basis: replShownBasis, ...target });
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
// A new connection has no choice outstanding and no basis until it adopts one.
|
|
1914
|
+
function resetReplSelectionBasis() {
|
|
1915
|
+
replChoiceAnswered = replChoiceRevision;
|
|
1916
|
+
replChoicePin = null;
|
|
1917
|
+
replShownBasis = null;
|
|
1918
|
+
replReconcileSent = "";
|
|
1919
|
+
replServerSelection = { sessionName: "", runtime: "" };
|
|
1920
|
+
if (rightView === "repl") requestReplList();
|
|
1818
1921
|
}
|
|
1819
1922
|
|
|
1820
1923
|
function trimReplTranscript(text) {
|
|
@@ -1828,6 +1931,7 @@
|
|
|
1828
1931
|
return sendMessage({ type: "repl_list_request" });
|
|
1829
1932
|
}
|
|
1830
1933
|
|
|
1934
|
+
// A poll: the displayed session as it is now, or the list when there is none.
|
|
1831
1935
|
function requestReplCapture() {
|
|
1832
1936
|
if (wsState === "Disconnected") return false;
|
|
1833
1937
|
if (replActiveSessionName) {
|
|
@@ -1836,6 +1940,25 @@
|
|
|
1836
1940
|
return sendMessage({ type: "repl_list_request" });
|
|
1837
1941
|
}
|
|
1838
1942
|
|
|
1943
|
+
// A choice on the Session or Runtime selector, which Studio counts. Until its reply
|
|
1944
|
+
// arrives, other replies don't change what the page shows.
|
|
1945
|
+
function requestReplChoice() {
|
|
1946
|
+
if (wsState === "Disconnected") return false;
|
|
1947
|
+
const choiceRevision = replChoiceRevision + 1;
|
|
1948
|
+
const sent = sendMessage({
|
|
1949
|
+
type: "repl_capture_request",
|
|
1950
|
+
choice: true,
|
|
1951
|
+
choiceRevision,
|
|
1952
|
+
runtime: replRuntime,
|
|
1953
|
+
...(replActiveSessionName ? { sessionName: replActiveSessionName } : {}),
|
|
1954
|
+
});
|
|
1955
|
+
if (sent) {
|
|
1956
|
+
replChoiceRevision = choiceRevision;
|
|
1957
|
+
replChoicePin = getActiveReplSession();
|
|
1958
|
+
}
|
|
1959
|
+
return sent;
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1839
1962
|
function isReplControlFocused() {
|
|
1840
1963
|
const activeEl = document.activeElement;
|
|
1841
1964
|
return activeEl instanceof Element && Boolean(activeEl.closest(".repl-controls, .repl-quick-composer"));
|
|
@@ -1901,6 +2024,8 @@
|
|
|
1901
2024
|
function startReplPolling() {
|
|
1902
2025
|
if (rightView !== "repl") return;
|
|
1903
2026
|
if (replPollTimer !== null) return;
|
|
2027
|
+
// Polls only observe, so the view starts from Studio's current selection.
|
|
2028
|
+
if (replActiveSessionName) requestReplList();
|
|
1904
2029
|
requestReplCapture();
|
|
1905
2030
|
replPollTimer = window.setInterval(() => {
|
|
1906
2031
|
if (rightView !== "repl") {
|
|
@@ -2178,6 +2303,7 @@
|
|
|
2178
2303
|
runtime: details.runtime || getActiveReplRuntime(),
|
|
2179
2304
|
origin: details.origin === "pi-repl" ? "pi-repl" : "pi-studio",
|
|
2180
2305
|
legacyLocal: !sharedSynced,
|
|
2306
|
+
outputClean: sharedSynced || details.outputClean === true,
|
|
2181
2307
|
label: details.label || "REPL send",
|
|
2182
2308
|
mode: details.mode || replSendMode,
|
|
2183
2309
|
prose: String(details.prose || ""),
|
|
@@ -2255,7 +2381,7 @@
|
|
|
2255
2381
|
return sent;
|
|
2256
2382
|
}
|
|
2257
2383
|
|
|
2258
|
-
function recordReplToolSend(message) {
|
|
2384
|
+
function recordReplToolSend(message, selects) {
|
|
2259
2385
|
const requestId = typeof message.toolCallId === "string" && message.toolCallId.trim()
|
|
2260
2386
|
? "tool:" + message.toolCallId.trim()
|
|
2261
2387
|
: (typeof message.requestId === "string" && message.requestId.trim() ? message.requestId.trim() : "");
|
|
@@ -2263,20 +2389,23 @@
|
|
|
2263
2389
|
if (!code.trim()) return false;
|
|
2264
2390
|
const runtime = normalizeReplRuntime(message.runtime || getActiveReplRuntime());
|
|
2265
2391
|
const sessionName = typeof message.sessionName === "string" ? message.sessionName : replActiveSessionName;
|
|
2266
|
-
|
|
2392
|
+
// The server has already cleaned this send's output; cleaning it again could drop real lines.
|
|
2393
|
+
const output = trimReplJournalOutput(String(message.output || "").trimEnd());
|
|
2267
2394
|
const details = {
|
|
2268
2395
|
requestId,
|
|
2269
2396
|
sessionName,
|
|
2270
2397
|
runtime,
|
|
2271
2398
|
origin: message.origin === "pi-repl" ? "pi-repl" : "pi-studio",
|
|
2272
2399
|
sharedSynced: message.sharedSynced === true,
|
|
2400
|
+
outputClean: true,
|
|
2273
2401
|
label: typeof message.label === "string" && message.label.trim() ? message.label.trim() : "Pi",
|
|
2274
2402
|
mode: "agent",
|
|
2275
2403
|
code,
|
|
2276
2404
|
output,
|
|
2277
2405
|
status: output.trim() ? "captured" : (message.timedOut ? "timeout" : "sent"),
|
|
2278
2406
|
};
|
|
2279
|
-
|
|
2407
|
+
// A result that doesn't select leaves this page's own entry tracking alone (Astra).
|
|
2408
|
+
if (selects !== false) activeReplJournalEntryId = "";
|
|
2280
2409
|
if (requestId) {
|
|
2281
2410
|
const existingIndex = replJournalEntries.findIndex((entry) => entry.requestId === requestId);
|
|
2282
2411
|
if (existingIndex >= 0) {
|
|
@@ -2371,7 +2500,8 @@
|
|
|
2371
2500
|
if (entryIndex < 0) return false;
|
|
2372
2501
|
const entry = replJournalEntries[entryIndex];
|
|
2373
2502
|
if (entry.sessionName && sessionName && entry.sessionName !== sessionName) return false;
|
|
2374
|
-
|
|
2503
|
+
// Finished output from Studio's server stands, whether or not the shared record took it.
|
|
2504
|
+
if ((entry.outputClean || !entry.legacyLocal) && (entry.status === "captured" || entry.status === "timeout" || entry.status === "error")) {
|
|
2375
2505
|
activeReplJournalEntryId = "";
|
|
2376
2506
|
return false;
|
|
2377
2507
|
}
|
|
@@ -11695,13 +11825,13 @@
|
|
|
11695
11825
|
selectReplSessionForRuntime(replRuntime, previousActive);
|
|
11696
11826
|
replError = "";
|
|
11697
11827
|
replMessage = "";
|
|
11698
|
-
if (replActiveSessionName) {
|
|
11699
|
-
requestReplCapture();
|
|
11700
|
-
} else {
|
|
11828
|
+
if (!replActiveSessionName) {
|
|
11701
11829
|
replTranscript = "";
|
|
11702
11830
|
replCapturedAt = 0;
|
|
11703
11831
|
syncActionButtons();
|
|
11704
11832
|
}
|
|
11833
|
+
// Counted even with no session, so nothing that finishes later takes it back.
|
|
11834
|
+
requestReplChoice();
|
|
11705
11835
|
renderReplViewIfActive({ force: true });
|
|
11706
11836
|
return;
|
|
11707
11837
|
}
|
|
@@ -11723,7 +11853,7 @@
|
|
|
11723
11853
|
replError = "";
|
|
11724
11854
|
replMessage = "";
|
|
11725
11855
|
replFollow = true;
|
|
11726
|
-
|
|
11856
|
+
requestReplChoice();
|
|
11727
11857
|
renderReplViewIfActive({ force: true });
|
|
11728
11858
|
}
|
|
11729
11859
|
}
|
|
@@ -17222,6 +17352,13 @@
|
|
|
17222
17352
|
},
|
|
17223
17353
|
replace: bufferId => retireStudioBufferSourceView(bufferId),
|
|
17224
17354
|
bind: () => bindSelectedStudioBuffer({ focusEditor: false }),
|
|
17355
|
+
opened: bufferId => {
|
|
17356
|
+
const buffer = bufferRecoveryClient?.snapshot()?.buffers.find(b => b.id === bufferId);
|
|
17357
|
+
const path = String(buffer?.sourceState.path || "");
|
|
17358
|
+
const name = path.split(/^(?:[A-Za-z]:\\|\\\\)/.test(path) ? /[\\/]/ : "/").pop();
|
|
17359
|
+
const where = !bufferSwitchingEnabled ? "" : buffer?.role === "document" ? " in the Document" : " in the Prompt";
|
|
17360
|
+
return name ? "Opened " + name + where + "." : "";
|
|
17361
|
+
},
|
|
17225
17362
|
reuse: async request => {
|
|
17226
17363
|
const result = await reuseHostedDocumentOwner(request.path, { sourcePath: request.sourcePath, resourceDir: request.resourceDir });
|
|
17227
17364
|
if (!result.handled) setStatus("The file owner is no longer available. Current work was kept; retry opening explicitly.", "warning");
|
|
@@ -27685,6 +27822,9 @@
|
|
|
27685
27822
|
// The new-layout Document has the Prompt tab just above, so no Return to Prompt here (Oliver, 10 Oct).
|
|
27686
27823
|
sendRunBtn.hidden = workspaceLayout && documentView && !directIsStop;
|
|
27687
27824
|
sendRunBtn.classList.toggle("request-stop-active", directIsStop);
|
|
27825
|
+
// Between the click and Stop the button keeps its look, dimmed, instead of flashing empty.
|
|
27826
|
+
sendRunBtn.classList.toggle("request-pending", !directIsStop && !documentView && uiBusy && pendingKind === "direct" && !studioRuntimeActivity?.run
|
|
27827
|
+
&& wsState !== "Disconnected" && wsState !== "Connecting");
|
|
27688
27828
|
sendRunBtn.classList.toggle("repl-secondary-action", rightView === "repl" && !directIsStop && !workspaceLayout);
|
|
27689
27829
|
sendRunBtn.disabled = documentView && !directIsStop ? !studioBuffersCanSwitch(true)
|
|
27690
27830
|
: wsState === "Disconnected" || (!directIsStop && (uiBusy || critiqueIsStop || !promptReady));
|
|
@@ -28454,25 +28594,30 @@
|
|
|
28454
28594
|
const responseRequestId = typeof message.requestId === "string" ? message.requestId : "";
|
|
28455
28595
|
const settlesPendingRequest = Boolean(responseRequestId && responseRequestId === replPendingRequestId);
|
|
28456
28596
|
const previousActiveLifetimeKey = getReplQuickDraftKey(getActiveReplSession());
|
|
28597
|
+
const previousRuntime = replRuntime;
|
|
28598
|
+
// Every repl_state states Studio's selection; while this page's own choice is
|
|
28599
|
+
// unanswered, only that choice's reply changes what the page shows.
|
|
28600
|
+
noteReplChoiceAnswer(message);
|
|
28601
|
+
const present = !isReplChoicePending();
|
|
28457
28602
|
replTmuxAvailable = typeof message.tmuxAvailable === "boolean" ? message.tmuxAvailable : replTmuxAvailable;
|
|
28458
28603
|
const sessionsChanged = setReplSessions(message.sessions);
|
|
28459
|
-
if (
|
|
28460
|
-
setActiveReplSessionForCurrentRuntime(message.activeSessionName);
|
|
28461
|
-
}
|
|
28604
|
+
if (present) adoptReplSelection(message);
|
|
28462
28605
|
const journalChanged = mergeReplJournalEntries(message.journalEntries);
|
|
28463
28606
|
reconcileReplQuickPending();
|
|
28464
28607
|
maybeImportLegacyReplJournalEntries(replActiveSessionName);
|
|
28465
|
-
if (typeof message.transcript === "string") replTranscript = trimReplTranscript(message.transcript);
|
|
28466
|
-
if (typeof message.capturedAt === "number") replCapturedAt = message.capturedAt;
|
|
28608
|
+
if (present && typeof message.transcript === "string") replTranscript = trimReplTranscript(message.transcript);
|
|
28609
|
+
if (present && typeof message.capturedAt === "number") replCapturedAt = message.capturedAt;
|
|
28467
28610
|
replError = typeof message.replError === "string" ? message.replError : (typeof message.captureError === "string" ? message.captureError : "");
|
|
28468
28611
|
replMessage = typeof message.replMessage === "string" ? message.replMessage : "";
|
|
28469
28612
|
if (settlesPendingRequest && replPendingRequestId === responseRequestId) replPendingRequestId = "";
|
|
28470
28613
|
replBusy = Boolean(replPendingRequestId);
|
|
28614
|
+
if (present) reconcileReplSelection();
|
|
28471
28615
|
const activeSessionIdentityChanged = Boolean(previousActiveLifetimeKey)
|
|
28472
28616
|
&& previousActiveLifetimeKey !== getReplQuickDraftKey(getActiveReplSession());
|
|
28473
28617
|
const controlsChanged = wasBusy
|
|
28474
28618
|
|| sessionsChanged
|
|
28475
28619
|
|| previousTmuxAvailable !== replTmuxAvailable
|
|
28620
|
+
|| previousRuntime !== replRuntime
|
|
28476
28621
|
|| previousActiveSessionName !== replActiveSessionName;
|
|
28477
28622
|
if (controlsChanged) syncActionButtons();
|
|
28478
28623
|
const viewChanged = controlsChanged
|
|
@@ -28487,14 +28632,28 @@
|
|
|
28487
28632
|
}
|
|
28488
28633
|
|
|
28489
28634
|
if (message.type === "repl_tool_send") {
|
|
28490
|
-
if
|
|
28491
|
-
|
|
28635
|
+
// The agent's send selects only if nothing was chosen since Studio admitted it,
|
|
28636
|
+
// and not while this page's own choice is unanswered (Sol, Astra).
|
|
28637
|
+
const selects = message.select !== false && !isReplChoicePending();
|
|
28638
|
+
const previousRuntime = replRuntime;
|
|
28639
|
+
const previousActiveSessionName = replActiveSessionName;
|
|
28640
|
+
if (selects) {
|
|
28641
|
+
if (message.session) upsertReplSession(message.session);
|
|
28642
|
+
adoptReplSelection({
|
|
28643
|
+
activeSessionName: typeof message.activeSessionName === "string" ? message.activeSessionName : message.sessionName,
|
|
28644
|
+
activeRuntime: typeof message.activeRuntime === "string" ? message.activeRuntime : message.runtime,
|
|
28645
|
+
shown: message.shown,
|
|
28646
|
+
});
|
|
28492
28647
|
}
|
|
28493
|
-
const changed = recordReplToolSend(message);
|
|
28648
|
+
const changed = recordReplToolSend(message, selects);
|
|
28494
28649
|
const journalChanged = mergeReplJournalEntries(message.journalEntries);
|
|
28495
|
-
if (
|
|
28496
|
-
|
|
28497
|
-
|
|
28650
|
+
if (selects && message.sessionName === replActiveSessionName) {
|
|
28651
|
+
if (typeof message.transcript === "string") replTranscript = trimReplTranscript(message.transcript);
|
|
28652
|
+
if (typeof message.capturedAt === "number") replCapturedAt = message.capturedAt;
|
|
28653
|
+
}
|
|
28654
|
+
if (selects) reconcileReplSelection();
|
|
28655
|
+
if (previousRuntime !== replRuntime || previousActiveSessionName !== replActiveSessionName) syncActionButtons();
|
|
28656
|
+
if (changed || journalChanged || selects) renderReplViewIfActive({ force: true });
|
|
28498
28657
|
updateReferenceBadge();
|
|
28499
28658
|
return;
|
|
28500
28659
|
}
|
|
@@ -28509,41 +28668,55 @@
|
|
|
28509
28668
|
const responseRequestId = typeof message.requestId === "string" ? message.requestId : "";
|
|
28510
28669
|
const settlesPendingRequest = Boolean(responseRequestId && responseRequestId === replPendingRequestId);
|
|
28511
28670
|
const previousActiveLifetimeKey = getReplQuickDraftKey(getActiveReplSession());
|
|
28671
|
+
const previousRuntime = replRuntime;
|
|
28512
28672
|
let sessionsChanged = false;
|
|
28513
|
-
|
|
28514
|
-
|
|
28515
|
-
|
|
28516
|
-
|
|
28517
|
-
|
|
28518
|
-
|
|
28519
|
-
|
|
28520
|
-
|
|
28521
|
-
|
|
28522
|
-
|
|
28673
|
+
let applies = false;
|
|
28674
|
+
if (message.observation === true) {
|
|
28675
|
+
// A poll's reply shows one session as it is now. It applies only to the exact
|
|
28676
|
+
// session this page displays, checked before anything changes (Sol, Astra).
|
|
28677
|
+
const observed = normalizeReplSession(message.session);
|
|
28678
|
+
const displayed = getActiveReplSession();
|
|
28679
|
+
const observedName = observed ? observed.sessionName : (typeof message.sessionName === "string" ? message.sessionName : "");
|
|
28680
|
+
if (!isReplChoicePending() && displayed && observedName === displayed.sessionName) {
|
|
28681
|
+
if (!observed) {
|
|
28682
|
+
// Its capture failed; the list says what Studio selects now.
|
|
28683
|
+
applies = true;
|
|
28684
|
+
requestReplList();
|
|
28685
|
+
} else if (getReplSessionLifetimeKey(observed) === getReplSessionLifetimeKey(displayed)) {
|
|
28686
|
+
applies = true;
|
|
28687
|
+
sessionsChanged = upsertReplSession(observed);
|
|
28688
|
+
} else {
|
|
28689
|
+
requestReplList();
|
|
28523
28690
|
}
|
|
28524
28691
|
}
|
|
28525
|
-
}
|
|
28526
|
-
|
|
28527
|
-
|
|
28692
|
+
} else {
|
|
28693
|
+
// A choice's reply, or a result that selected: it states Studio's selection.
|
|
28694
|
+
noteReplChoiceAnswer(message);
|
|
28695
|
+
applies = !isReplChoicePending();
|
|
28696
|
+
if (applies && message.session) sessionsChanged = upsertReplSession(message.session);
|
|
28697
|
+
if (applies) adoptReplSelection(message);
|
|
28698
|
+
applies = applies && Boolean(message.session) && message.session.sessionName === replActiveSessionName;
|
|
28528
28699
|
}
|
|
28529
28700
|
let journalChanged = mergeReplJournalEntries(message.journalEntries);
|
|
28530
28701
|
reconcileReplQuickPending();
|
|
28531
28702
|
maybeImportLegacyReplJournalEntries(replActiveSessionName);
|
|
28532
|
-
if (typeof message.transcript === "string") {
|
|
28703
|
+
if (applies && typeof message.transcript === "string") {
|
|
28533
28704
|
replTranscript = trimReplTranscript(message.transcript);
|
|
28534
|
-
journalChanged = updateActiveReplJournalEntryFromTranscript(
|
|
28535
|
-
|
|
28536
|
-
|
|
28537
|
-
|
|
28705
|
+
journalChanged = updateActiveReplJournalEntryFromTranscript(replActiveSessionName, replTranscript) || journalChanged;
|
|
28706
|
+
} else if (applies && typeof message.captureError === "string") {
|
|
28707
|
+
replTranscript = "";
|
|
28708
|
+
}
|
|
28709
|
+
if (applies && typeof message.capturedAt === "number") replCapturedAt = message.capturedAt;
|
|
28710
|
+
if (applies || message.observation !== true) {
|
|
28711
|
+
replError = typeof message.replError === "string" ? message.replError : (typeof message.captureError === "string" ? message.captureError : "");
|
|
28538
28712
|
}
|
|
28539
|
-
if (typeof message.capturedAt === "number") replCapturedAt = message.capturedAt;
|
|
28540
|
-
replError = typeof message.replError === "string" ? message.replError : "";
|
|
28541
28713
|
if (typeof message.replMessage === "string") replMessage = message.replMessage;
|
|
28542
28714
|
if (settlesPendingRequest && replPendingRequestId === responseRequestId) replPendingRequestId = "";
|
|
28543
28715
|
replBusy = Boolean(replPendingRequestId);
|
|
28716
|
+
if (message.observation !== true && !isReplChoicePending()) reconcileReplSelection();
|
|
28544
28717
|
const activeSessionIdentityChanged = Boolean(previousActiveLifetimeKey)
|
|
28545
28718
|
&& previousActiveLifetimeKey !== getReplQuickDraftKey(getActiveReplSession());
|
|
28546
|
-
const controlsChanged = wasBusy || sessionsChanged || previousActiveSessionName !== replActiveSessionName;
|
|
28719
|
+
const controlsChanged = wasBusy || sessionsChanged || previousRuntime !== replRuntime || previousActiveSessionName !== replActiveSessionName;
|
|
28547
28720
|
if (controlsChanged) syncActionButtons();
|
|
28548
28721
|
const viewChanged = controlsChanged
|
|
28549
28722
|
|| previousTranscript !== replTranscript
|
|
@@ -29368,6 +29541,7 @@
|
|
|
29368
29541
|
setWsState("Ready");
|
|
29369
29542
|
setStatus(wasReconnect ? "Reconnected. Syncing…" : "Connected. Syncing…");
|
|
29370
29543
|
sendMessage({ type: "hello" });
|
|
29544
|
+
resetReplSelectionBasis();
|
|
29371
29545
|
if (isWatchedFilePreview && watchedFilePreviewState.path) {
|
|
29372
29546
|
watchedFilePreviewState.generation = 0;
|
|
29373
29547
|
sendMessage({
|
|
@@ -32152,10 +32326,13 @@
|
|
|
32152
32326
|
if (restored.restored) {
|
|
32153
32327
|
const snapshot = client.snapshot();
|
|
32154
32328
|
const selected = snapshot.buffers.find((entry) => entry.id === snapshot.selectedBufferId);
|
|
32155
|
-
applyPersistedWorkspaceState(restored.editor, {
|
|
32329
|
+
const applied = applyPersistedWorkspaceState(restored.editor, {
|
|
32156
32330
|
verifiedBuffer: documentHostingEnabled,
|
|
32157
32331
|
metadataAssociations: selected && selected.metadata,
|
|
32158
32332
|
});
|
|
32333
|
+
// A fresh dual-mode launch is registered on the server first; only an
|
|
32334
|
+
// applied copy kept by this tab (a reload) is reported as restored.
|
|
32335
|
+
workspaceRestoredFromBrowser = applied && restored.fromBrowser === true;
|
|
32159
32336
|
if (sourceState.path) fileBackedBaselineText = restored.baselineText;
|
|
32160
32337
|
if (selected) {
|
|
32161
32338
|
sourceTextEl.setSelectionRange(selected.view.selectionStart, selected.view.selectionEnd, selected.view.selectionDirection);
|
|
@@ -32209,7 +32386,7 @@
|
|
|
32209
32386
|
if (documentHostingEnabled) syncActionButtons();
|
|
32210
32387
|
persistWorkspaceStateNow({ skipServer: serverWorkspaceRecovery.status === "unavailable" });
|
|
32211
32388
|
if (workspaceRestoredFromBrowser) {
|
|
32212
|
-
setStatus(bufferSwitchingEnabled ? "Restored Prompt and Document
|
|
32389
|
+
setStatus(bufferSwitchingEnabled ? "Restored the Prompt and Document." : "Restored editor workspace from this browser tab. Use Reset editor to discard it.", "success");
|
|
32213
32390
|
}
|
|
32214
32391
|
connect();
|
|
32215
32392
|
if (documentHostingEnabled && bufferRecoveryClient) { await bufferRecoveryClient.settled(); await setupDocumentHosting(); }
|
package/client/studio.css
CHANGED
|
@@ -294,6 +294,17 @@
|
|
|
294
294
|
filter: brightness(0.95);
|
|
295
295
|
}
|
|
296
296
|
|
|
297
|
+
/* Oliver, 14 Oct: from the click until Stop, Run keeps its shape and colour, dimmed, and
|
|
298
|
+
can't be clicked, so it doesn't flash empty while Pi starts. */
|
|
299
|
+
#sendRunBtn.request-pending:disabled:not(.repl-secondary-action) {
|
|
300
|
+
background: var(--accent);
|
|
301
|
+
border-color: var(--accent);
|
|
302
|
+
color: var(--accent-contrast);
|
|
303
|
+
font-weight: 600;
|
|
304
|
+
opacity: 0.7;
|
|
305
|
+
cursor: progress;
|
|
306
|
+
}
|
|
307
|
+
|
|
297
308
|
#sendRunBtn.request-stop-active,
|
|
298
309
|
#critiqueBtn.request-stop-active,
|
|
299
310
|
#showMeBtn.request-stop-active {
|
|
@@ -7528,7 +7539,8 @@
|
|
|
7528
7539
|
/* Honor existing handler eligibility after controls are regrouped. */
|
|
7529
7540
|
body.studio-ui-refresh [hidden] { display: none !important; }
|
|
7530
7541
|
/* Dogfood workspace: two logical rows, with normal wrapping in small panes. */
|
|
7531
|
-
|
|
7542
|
+
/* Room for "Running 00:00" from the start, so nothing moves when "Starting…" becomes the clock (Oliver, 14 Oct). */
|
|
7543
|
+
.studio-run-status { color: var(--muted); font-size: 12px; min-width: 13ch; font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
|
|
7532
7544
|
body.studio-ui-refresh #rightSectionHeader .studio-follow-button {
|
|
7533
7545
|
color: var(--muted); border: 0; border-radius: 0; background: transparent;
|
|
7534
7546
|
}
|