pi-studio 0.10.2 → 0.10.4
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 +11 -0
- package/client/studio-client.js +47 -0
- package/index.ts +2 -3
- package/package.json +1 -1
- package/shared/repl-session-stop.js +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,17 @@ All notable changes to `pi-studio` are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.10.4] — 2026-10-19
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- Stopping a Python, Julia or other runtime session works again on a Mac that runs a process as `nobody`. Since 0.10.2, Stop refused there with "Could not parse process ownership data; refusing unsafe shutdown."
|
|
11
|
+
|
|
12
|
+
## [0.10.3] — 2026-10-19
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- IPython shows a value once, as `Out[5]: array([11])`. Studio used to print it again on the next line.
|
|
16
|
+
- After a Quick send, the cursor goes back to the box when the send finishes, so you can type the next line. It stays where you put it if you clicked elsewhere meanwhile.
|
|
17
|
+
|
|
7
18
|
## [0.10.2] — 2026-10-19
|
|
8
19
|
|
|
9
20
|
### Changed
|
package/client/studio-client.js
CHANGED
|
@@ -713,6 +713,9 @@
|
|
|
713
713
|
const replQuickDrafts = new Map();
|
|
714
714
|
let replQuickPending = null;
|
|
715
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;
|
|
716
719
|
let replSendMode = (() => {
|
|
717
720
|
try {
|
|
718
721
|
const stored = window.localStorage && window.localStorage.getItem("piStudio.replSendMode");
|
|
@@ -1690,9 +1693,48 @@
|
|
|
1690
1693
|
if (submitted && replQuickDrafts.get(pending.key) === pending.text) {
|
|
1691
1694
|
replQuickDrafts.delete(pending.key);
|
|
1692
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
|
+
}
|
|
1693
1701
|
return true;
|
|
1694
1702
|
}
|
|
1695
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
|
+
|
|
1696
1738
|
function reconcileReplQuickPending() {
|
|
1697
1739
|
if (!replQuickPending) return false;
|
|
1698
1740
|
const entry = replJournalEntries.find((candidate) => candidate.requestId === replQuickPending.requestId);
|
|
@@ -2017,6 +2059,7 @@
|
|
|
2017
2059
|
traceRenderRaf = window.requestAnimationFrame(() => {
|
|
2018
2060
|
traceRenderRaf = null;
|
|
2019
2061
|
refreshResponseUi();
|
|
2062
|
+
applyReplQuickRefocus();
|
|
2020
2063
|
if (replQuickFocusRequested) focusReplQuickComposer();
|
|
2021
2064
|
});
|
|
2022
2065
|
}
|
|
@@ -2776,6 +2819,8 @@
|
|
|
2776
2819
|
});
|
|
2777
2820
|
if (!requestId) return false;
|
|
2778
2821
|
replQuickPending = { requestId, key, text };
|
|
2822
|
+
const activeEl = document.activeElement;
|
|
2823
|
+
replQuickRefocus = activeEl instanceof Element && activeEl.closest(".repl-quick-composer") ? { requestId, settled: false } : null;
|
|
2779
2824
|
renderReplViewIfActive({ force: true });
|
|
2780
2825
|
return true;
|
|
2781
2826
|
}
|
|
@@ -11869,6 +11914,8 @@
|
|
|
11869
11914
|
critiqueViewEl.addEventListener("click", handleStudioQuartoPreviewClick);
|
|
11870
11915
|
critiqueViewEl.addEventListener("click", (event) => { void handleSideQuestionClick(event); });
|
|
11871
11916
|
critiqueViewEl.addEventListener("focusin", handleReplPaneFocusIn);
|
|
11917
|
+
document.addEventListener("focusin", cancelReplQuickRefocusOutside, true);
|
|
11918
|
+
document.addEventListener("pointerdown", cancelReplQuickRefocusOutside, true);
|
|
11872
11919
|
critiqueViewEl.addEventListener("input", handleSideQuestionInput);
|
|
11873
11920
|
critiqueViewEl.addEventListener("input", handleReplPaneInput);
|
|
11874
11921
|
critiqueViewEl.addEventListener("keydown", handleTracePaneKeydown);
|
package/index.ts
CHANGED
|
@@ -11640,9 +11640,8 @@ function buildStudioPythonControlSource(runtime: "python" | "ipython", code: str
|
|
|
11640
11640
|
" __pi_studio_ip = get_ipython()",
|
|
11641
11641
|
" if __pi_studio_ip is None:",
|
|
11642
11642
|
" raise RuntimeError('Expected IPython session, but get_ipython() returned None.')",
|
|
11643
|
-
|
|
11644
|
-
|
|
11645
|
-
" print(repr(__pi_studio_result.result))",
|
|
11643
|
+
// run_cell shows the value itself (Out[N]: ...), so nothing more is printed here.
|
|
11644
|
+
` __pi_studio_ip.run_cell(${JSON.stringify(code)}, store_history=False)`,
|
|
11646
11645
|
"except Exception:",
|
|
11647
11646
|
" __pi_studio_traceback.print_exc()",
|
|
11648
11647
|
"finally:",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-studio",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.4",
|
|
4
4
|
"description": "Two-pane browser workspace for pi with prompt/response editing, annotations, critiques, active quiz, prompt/response history, live previews, and tmux-backed REPL/literate REPL workflows",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -12,7 +12,8 @@ const paneFormat = "#{pid}\t#{session_id}\t#{session_name}\t#{session_created}\t
|
|
|
12
12
|
export function parseStopProcessTable(text) {
|
|
13
13
|
return text.split("\n").filter((line) => line.trim()).map((line) => {
|
|
14
14
|
const f = line.trim().split(/\s+/);
|
|
15
|
-
|
|
15
|
+
// macOS lists processes of `nobody` with uid -2; it never matches ours.
|
|
16
|
+
if (f.length !== 11 || !f.slice(0, 4).every((v, i) => (i === 3 ? /^-?\d+$/ : /^\d+$/).test(v) && Number.isSafeInteger(Number(v)))) throw new Error("Could not parse process ownership data; refusing unsafe shutdown.");
|
|
16
17
|
return { pid: Number(f[0]), ppid: Number(f[1]), pgid: Number(f[2]), uid: Number(f[3]), tty: f[4], startedAt: f.slice(5, 10).join(" "), state: f[10] };
|
|
17
18
|
});
|
|
18
19
|
}
|