beatrina 0.9.83 → 1.0.1
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/app/Beatrina.app/Contents/Info.plist +86 -0
- package/app/Beatrina.app/Contents/MacOS/beatrina +0 -0
- package/app/Beatrina.app/Contents/Resources/icon.icns +0 -0
- package/app/beatrina.png +0 -0
- package/app/linux-arm64/beatrina +0 -0
- package/app/win-x64/Beatrina-setup.exe +0 -0
- package/beatrina_V1.0.1.html +1906 -0
- package/beatrina_V1.0.1.html.inputs.json +1 -0
- package/bin/beatrina.mjs +200 -12
- package/bin/cli.mjs +8 -3
- package/bin/finder.mjs +1 -1
- package/bin/shortcut.mjs +15 -15
- package/build-info.json +1 -1
- package/host/engine-node-job.mjs +35 -0
- package/host/engine-pool.mjs +153 -26
- package/host/engine-r.mjs +8 -1
- package/host/jobs/document-child.mjs +271 -0
- package/host/latex-compile.mjs +673 -0
- package/host/main.mjs +58 -18
- package/host/planes/analyze.mjs +18 -2
- package/host/planes/jobs.mjs +38 -5
- package/host/planes/sessions.mjs +5 -1
- package/host/pwa/icon-192.png +0 -0
- package/host/pwa/icon-512.png +0 -0
- package/host/quarto.mjs +118 -0
- package/host/server.mjs +83 -2
- package/host/worker-plane.mjs +7 -1
- package/kernel/check/acceptance.mjs +43 -12
- package/kernel/check/knit-file.mjs +55 -19
- package/kernel/check/session.mjs +19 -4
- package/kernel/job-run.R +4 -1
- package/kernel/kernel-version +1 -1
- package/kernel/worker-boot.R +2 -1
- package/kernel/worker.R +2 -1
- package/lib/engine-labels.js +7 -1
- package/menu/Beatrina Menu.app/Contents/Info.plist +2 -2
- package/menu/Beatrina Menu.app/Contents/MacOS/Beatrina Menu +0 -0
- package/package.json +4 -3
- package/beatrina_V0.9.83.html +0 -1903
- package/beatrina_V0.9.83.html.inputs.json +0 -1
|
@@ -12,6 +12,10 @@
|
|
|
12
12
|
// kept · readline() · file ops and completion while R is busy · Force Stop ·
|
|
13
13
|
// the terminal · Python and JavaScript chunks when those engines exist.
|
|
14
14
|
//
|
|
15
|
+
// WITH NO R (plans/getting_rid_of_R.md Stage 2) the host is still judged —
|
|
16
|
+
// startup, the document (save, open, list), the terminal, the other engines —
|
|
17
|
+
// and every R area is reported SKIP with the reason, never counted as a pass.
|
|
18
|
+
//
|
|
15
19
|
// Usage
|
|
16
20
|
// node tools/beatrina/acceptance.mjs # this checkout's host
|
|
17
21
|
// node tools/beatrina/acceptance.mjs --installed # the installed beatrina package's host
|
|
@@ -61,17 +65,44 @@ if (!session.announced) {
|
|
|
61
65
|
record("startup", "the supervisor announces a URL", "fail", session.log().split("\n").slice(-6).join(" | "));
|
|
62
66
|
await shutdown(1);
|
|
63
67
|
}
|
|
64
|
-
const { frames, until, ready, page } = session;
|
|
65
|
-
if (!ready) { record("startup", "R announces ready", "fail", session.log().slice(-400)); await shutdown(1); }
|
|
66
|
-
|
|
68
|
+
const { frames, until, ready, page, hostReady, noR } = session;
|
|
69
|
+
if (!ready && !noR) { record("startup", "R announces ready", "fail", session.log().slice(-400)); await shutdown(1); }
|
|
70
|
+
if (noR) {
|
|
71
|
+
record("startup", "the host is ready with no R (host-ready)", hostReady ? "pass" : "fail", "", session.startupMs);
|
|
72
|
+
record("startup", "R", "skip", `R is not available: ${noR}. Every R area below is skipped, not passed.`);
|
|
73
|
+
} else {
|
|
74
|
+
record("startup", `R ready (${ready.r || "?"}, worker mode ${ready.mode || "?"})`, "pass", "", session.startupMs, { r: ready.r, mode: ready.mode });
|
|
75
|
+
}
|
|
76
|
+
/** An R area runs only when R does; otherwise it is ONE skip line with the reason. */
|
|
77
|
+
const wantR = (area) => {
|
|
78
|
+
if (!want(area)) return false;
|
|
79
|
+
if (noR) { record(area, `${area} (needs R)`, "skip", noR); return false; }
|
|
80
|
+
return true;
|
|
81
|
+
};
|
|
67
82
|
|
|
68
83
|
let seq = 0;
|
|
69
84
|
const text = (id) => session.text(id);
|
|
70
85
|
const run = (source, opts = {}) => session.run(source, opts);
|
|
71
|
-
const
|
|
86
|
+
const announced = ready || hostReady;
|
|
87
|
+
const engines = Array.isArray(announced.engines) ? announced.engines : ["r"];
|
|
88
|
+
|
|
89
|
+
// ── the document: what a person does with no engine at all ──────────────────
|
|
90
|
+
if (want("document")) {
|
|
91
|
+
const doc = path.join(scratch, "chapter.qmd");
|
|
92
|
+
const body = "---\ntitle: Acceptance\n---\n\n# Method\n\nWords, saved by the host [@key].\n";
|
|
93
|
+
const w = await session.ask("writefile", { path: doc, text: body });
|
|
94
|
+
record("document", "Save writes a .qmd", w.reply && !w.reply.error && fs.readFileSync(doc, "utf8") === body ? "pass" : "fail",
|
|
95
|
+
w.reply ? (w.reply.error || "") : "no reply", w.ms);
|
|
96
|
+
const rd = await session.ask("readfile", { path: doc });
|
|
97
|
+
record("document", "Open reads it back, byte for byte", rd.reply && rd.reply.text === body ? "pass" : "fail",
|
|
98
|
+
rd.reply ? (rd.reply.error || "") : "no reply", rd.ms);
|
|
99
|
+
const ls = await session.ask("files", { path: scratch });
|
|
100
|
+
record("document", "the file browser lists it", ls.reply && !ls.reply.error && JSON.stringify(ls.reply).includes("chapter.qmd") ? "pass" : "fail",
|
|
101
|
+
ls.reply ? (ls.reply.error || "") : "no reply", ls.ms);
|
|
102
|
+
}
|
|
72
103
|
|
|
73
104
|
// ── run ──────────────────────────────────────────────────────────────────────
|
|
74
|
-
if (
|
|
105
|
+
if (wantR("run")) {
|
|
75
106
|
const r = await run('keep <- 42; cat("keep=", keep, "\\n")');
|
|
76
107
|
record("run", "a chunk runs and prints", r.done?.status === "ok" && /keep= 42/.test(r.out()) ? "pass" : "fail", r.out().trim(), r.ms);
|
|
77
108
|
const warm = [];
|
|
@@ -109,7 +140,7 @@ async function stopCase(label, source, { engine, probe, expect, after = 900, kee
|
|
|
109
140
|
record("stop", `${label}: interrupted with the session kept, ${REPEAT - misses.length}/${REPEAT}`, misses.length ? "fail" : "pass",
|
|
110
141
|
summary, lat.length ? median(lat) : null, { series: lat, misses: misses.length, repeat: REPEAT });
|
|
111
142
|
}
|
|
112
|
-
if (
|
|
143
|
+
if (wantR("stop")) {
|
|
113
144
|
const probe = 'cat("keep=", exists("keep") && keep == 42, "\\n")';
|
|
114
145
|
await run("keep <- 42");
|
|
115
146
|
const keep = "keep <- 42";
|
|
@@ -119,7 +150,7 @@ if (want("stop")) {
|
|
|
119
150
|
}
|
|
120
151
|
|
|
121
152
|
// ── readline ─────────────────────────────────────────────────────────────────
|
|
122
|
-
if (
|
|
153
|
+
if (wantR("readline")) {
|
|
123
154
|
const id = `r${++seq}`;
|
|
124
155
|
const from = frames.length;
|
|
125
156
|
page.send({ type: "exec", id, source: 'ans <- readline("Your name: "); cat("hello", ans, "\\n")' });
|
|
@@ -135,7 +166,7 @@ if (want("readline")) {
|
|
|
135
166
|
|
|
136
167
|
// ── while busy ───────────────────────────────────────────────────────────────
|
|
137
168
|
const ask = (type, payload, ms) => session.ask(type, payload, ms);
|
|
138
|
-
if (
|
|
169
|
+
if (wantR("busy")) {
|
|
139
170
|
fs.writeFileSync(path.join(scratch, "note.txt"), "hello");
|
|
140
171
|
const id = `b${++seq}`;
|
|
141
172
|
const from = frames.length;
|
|
@@ -160,7 +191,7 @@ if (want("busy")) {
|
|
|
160
191
|
}
|
|
161
192
|
|
|
162
193
|
// ── force stop ───────────────────────────────────────────────────────────────
|
|
163
|
-
if (
|
|
194
|
+
if (wantR("force")) {
|
|
164
195
|
await run("keep <- 42");
|
|
165
196
|
const id = `f${++seq}`;
|
|
166
197
|
const from = frames.length;
|
|
@@ -179,7 +210,7 @@ if (want("force")) {
|
|
|
179
210
|
// R dies under a running chunk (kill -9, as a segfault or the OOM killer would):
|
|
180
211
|
// the chunk must END with a sentence, the page must be told, and a fresh R must
|
|
181
212
|
// answer — never a spinner that waits for a process that is gone.
|
|
182
|
-
if (
|
|
213
|
+
if (wantR("crash")) {
|
|
183
214
|
const probe = await run('cat("pid=", Sys.getpid(), "\\n")');
|
|
184
215
|
const pid = Number((probe.out().match(/pid= (\d+)/) || [])[1]);
|
|
185
216
|
const id = `c${++seq}`;
|
|
@@ -211,7 +242,7 @@ function rssKb(pid) {
|
|
|
211
242
|
const v = Number(String(r.stdout).trim());
|
|
212
243
|
return Number.isFinite(v) && v > 0 ? v : null;
|
|
213
244
|
}
|
|
214
|
-
if (
|
|
245
|
+
if (wantR("memory")) {
|
|
215
246
|
const rounds = Math.max(4, Number(argv.memoryRounds || 8));
|
|
216
247
|
const samples = [];
|
|
217
248
|
await run("df <- data.frame(id = seq_len(5000), x = rnorm(5000), g = sample(letters, 5000, TRUE))");
|
|
@@ -251,7 +282,7 @@ for (const [engine, setup, probe, sleeper] of [
|
|
|
251
282
|
["js", "globalThis.keepJs = 42; console.log('js keep', keepJs)", "console.log('js keep', keepJs)", "for (let i = 0; i < 1e13; i++) {}\nconsole.log('after-stop')"],
|
|
252
283
|
]) {
|
|
253
284
|
if (!want(engine)) continue;
|
|
254
|
-
const row = Array.isArray(
|
|
285
|
+
const row = Array.isArray(announced.engine_status) ? announced.engine_status.find((e) => e.engine === engine) : null;
|
|
255
286
|
if (!engines.includes(engine)) { record(engine, `${engine} chunks`, "skip", row ? row.detail || "engine not available" : "this host has no such engine"); continue; }
|
|
256
287
|
const first = await run(setup, { engine, ms: 120000 });
|
|
257
288
|
record(engine, `a ${engine} chunk runs (first run starts the engine)`, first.done?.status === "ok" && /keep 42/.test(first.out()) ? "pass" : "fail", first.out().trim() || first.done?.message || "", first.ms);
|
|
@@ -1,3 +1,29 @@
|
|
|
1
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
2
|
+
var __esm = (fn, res, err) => function __init() {
|
|
3
|
+
if (err) throw err[0];
|
|
4
|
+
try {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
} catch (e) {
|
|
7
|
+
throw err = [e], e;
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// lib/engine-labels.js
|
|
12
|
+
var KNOWN_ENGINES, BY_KEY, AUTHORING_ENGINES;
|
|
13
|
+
var init_engine_labels = __esm({
|
|
14
|
+
"lib/engine-labels.js"() {
|
|
15
|
+
KNOWN_ENGINES = Object.freeze([
|
|
16
|
+
Object.freeze({ key: "r", label: "R" }),
|
|
17
|
+
Object.freeze({ key: "python", label: "Python" }),
|
|
18
|
+
Object.freeze({ key: "js", label: "JavaScript" }),
|
|
19
|
+
Object.freeze({ key: "bash", label: "Bash" }),
|
|
20
|
+
Object.freeze({ key: "sql", label: "SQL" })
|
|
21
|
+
]);
|
|
22
|
+
BY_KEY = new Map(KNOWN_ENGINES.map((e) => [e.key, e.label]));
|
|
23
|
+
AUTHORING_ENGINES = Object.freeze(KNOWN_ENGINES.map((e) => e.key));
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
1
27
|
// tools/beatrina/knit-file.mjs
|
|
2
28
|
import fs from "node:fs";
|
|
3
29
|
import path from "node:path";
|
|
@@ -1719,8 +1745,12 @@ var BEATRINA_CSS = `
|
|
|
1719
1745
|
margin-top:10px;border:1px solid #cbd8e6;border-radius:var(--cn-radius-md);
|
|
1720
1746
|
background:#f7fafd;overflow:hidden;
|
|
1721
1747
|
}
|
|
1748
|
+
/* z-index 210: BELOW the header (220). The header is a stacking context, so every menu it opens
|
|
1749
|
+
lives at its level \u2014 above 820 a floating AI panel covered them all, and a document switcher
|
|
1750
|
+
opened beside it could not be clicked (journey windows, 2026-09-24). The panel is kept below the
|
|
1751
|
+
header's edge instead (ai-shell.js clampIntoView). */
|
|
1722
1752
|
.cn-root .beatrina-ai-panel.is-float{
|
|
1723
|
-
position:fixed;z-index:
|
|
1753
|
+
position:fixed;z-index:210;margin:0;box-shadow:var(--cn-shadow-lg);
|
|
1724
1754
|
/* A float panel had NO height cap, so it grew with its thread until the
|
|
1725
1755
|
composer was below the fold and its Send button could not be clicked at
|
|
1726
1756
|
all \u2014 the panel is position:fixed, so nothing scrolls it back. The docked
|
|
@@ -1784,7 +1814,7 @@ var BEATRINA_CSS = `
|
|
|
1784
1814
|
document MAKES ROOM rather than being covered. A floating panel is fine for
|
|
1785
1815
|
a glance and wrong for a conversation \u2014 it sits on the thing being read. */
|
|
1786
1816
|
.cn-root .beatrina-ai-panel.is-side{
|
|
1787
|
-
position:fixed;top:0;bottom:0;z-index:
|
|
1817
|
+
position:fixed;top:0;bottom:0;z-index:210;margin:0;border-radius:0;
|
|
1788
1818
|
width:var(--beatrina-ai-side-w,400px);max-height:none;height:auto;
|
|
1789
1819
|
box-shadow:var(--cn-shadow-lg);
|
|
1790
1820
|
}
|
|
@@ -4467,7 +4497,10 @@ var BEATRINA_CSS = `
|
|
|
4467
4497
|
.cn-root .beatrina-md-tool:focus-visible{outline:0;box-shadow:var(--cn-focus-ring);}
|
|
4468
4498
|
.cn-root .beatrina-md-tool.is-cite{color:var(--cn-accent);}
|
|
4469
4499
|
.cn-root .beatrina-md-tool-label{font:600 var(--cn-fs-sm) var(--cn-font);}
|
|
4470
|
-
|
|
4500
|
+
/* width as well as flex-basis: WebKit leaves a basis-only item out of the bar's max-content
|
|
4501
|
+
width, so the bar came out 4 px short at every fold level and folded everything behind \u22EE
|
|
4502
|
+
(Beatrina's window, 2026-09-23). */
|
|
4503
|
+
.cn-root .beatrina-md-bar-sp{flex:0 0 4px;width:4px;}
|
|
4471
4504
|
.cn-root .beatrina-md-count{
|
|
4472
4505
|
font-size:var(--cn-fs-sm);opacity:.55;font-variant-numeric:tabular-nums;margin-right:2px;
|
|
4473
4506
|
}
|
|
@@ -5207,11 +5240,11 @@ var BEATRINA_CSS = `
|
|
|
5207
5240
|
.cn-root .beatrina-tolatex.is-latex::before{content:"\u25CF ";}
|
|
5208
5241
|
.cn-root .beatrina-tolatex.is-latex:hover{filter:none;}
|
|
5209
5242
|
/* Write's one control of its own, and LaTeX's: the page beside the text. */
|
|
5210
|
-
.cn-root .beatrina-preview-toggle,.cn-root .beatrina-latex-
|
|
5211
|
-
.cn-root .beatrina-latex-
|
|
5243
|
+
.cn-root .beatrina-preview-toggle,.cn-root .beatrina-latex-page-toggle{height:28px;border:1px solid var(--beatrina-hdr-line-soft,rgba(255,255,255,.28));border-radius:6px;padding:0 12px;background:transparent;color:var(--beatrina-hdr-fg,#fff);font:500 var(--cn-fs-sm,12.5px) var(--cn-font);line-height:1;cursor:pointer;align-items:center;justify-content:center;box-sizing:border-box;}
|
|
5244
|
+
.cn-root .beatrina-latex-page-toggle{display:inline-flex;align-self:center;flex:0 0 auto;}
|
|
5212
5245
|
.cn-root .beatrina-preview-toggle{display:none;}
|
|
5213
5246
|
.cn-root[data-beatrina-mode="writing"] .beatrina-preview-toggle{display:inline-flex;}
|
|
5214
|
-
.cn-root .beatrina-preview-toggle.on,.cn-root .beatrina-latex-
|
|
5247
|
+
.cn-root .beatrina-preview-toggle.on,.cn-root .beatrina-latex-page-toggle.on{background:#fff;color:#141a3a;border-color:#fff;font-weight:600;}
|
|
5215
5248
|
/* A decision that replaces a file is not read at the size of a tooltip. */
|
|
5216
5249
|
.cn-root .beatrina-modal-panel.is-large{width:min(560px,94vw);}
|
|
5217
5250
|
.cn-root .beatrina-modal-panel.is-large .beatrina-modal-title{font-size:17px;}
|
|
@@ -10637,6 +10670,14 @@ var BEATRINA_CSS = `
|
|
|
10637
10670
|
@media(max-width:760px){
|
|
10638
10671
|
.cn-root.has-ai-left{padding-left:0;}
|
|
10639
10672
|
.cn-root.has-ai-right{padding-right:0;}
|
|
10673
|
+
/* \u2026and so a docked pane is a SHEET over the whole window, not a 400 px
|
|
10674
|
+
slab over the right half of the words. Reserving its width at this size
|
|
10675
|
+
would leave a column of ~260 px; overlaying half the text left every line
|
|
10676
|
+
cut off under it (journey windows, 720 px, 2026-09-24). One or the other
|
|
10677
|
+
is on screen; \xD7 gives the document back. !important beats the inline
|
|
10678
|
+
left/right/width ai-shell.js writes for the wide layout. */
|
|
10679
|
+
.cn-root .beatrina-ai-panel.is-side{left:0 !important;right:0 !important;width:auto !important;top:var(--beatrina-hdr-h,0px);}
|
|
10680
|
+
.cn-root .beatrina-ai-panel.is-side .beatrina-ai-sedge{display:none;}
|
|
10640
10681
|
}
|
|
10641
10682
|
|
|
10642
10683
|
.cn-root .beatrina-aiw-convbtn{flex:0 0 auto;width:24px;height:24px;
|
|
@@ -11777,7 +11818,7 @@ body:has(.cn-root[data-beatrina-mode="writing"] > .beatrina-writing-bar){--beatr
|
|
|
11777
11818
|
box-shadow:0 -1px 0 var(--cn-border,#e3e3e3);
|
|
11778
11819
|
}
|
|
11779
11820
|
.beatrina-dock[hidden]{display:none;}
|
|
11780
|
-
.beatrina-dock[data-mode="bottom"]{left:
|
|
11821
|
+
.beatrina-dock[data-mode="bottom"]{left:var(--beatrina-ai-edge-left,0px);right:var(--beatrina-ai-edge-right,0px);bottom:0;flex-direction:column;}
|
|
11781
11822
|
.beatrina-dock[data-mode="right"]{
|
|
11782
11823
|
top:0;right:0;bottom:0;flex-direction:row;
|
|
11783
11824
|
box-shadow:-1px 0 0 var(--cn-border,#e3e3e3);
|
|
@@ -14905,7 +14946,8 @@ ${out}</div>
|
|
|
14905
14946
|
if (!ctx.rawUrls && !safeImageSrc(url)) {
|
|
14906
14947
|
return `<span class="beatrina-md-badsrc">[image: ${alt || "unsupported source"}]</span>`;
|
|
14907
14948
|
}
|
|
14908
|
-
|
|
14949
|
+
const local = ctx.localImages && /^(?![a-z][a-z0-9+.-]*:|\/\/|#)/i.test(url.trim());
|
|
14950
|
+
let out = local ? `<img data-local-src="${attrValue(url)}" alt="${alt}"` : `<img src="${attrValue(encodeHref(url))}" alt="${alt}"`;
|
|
14909
14951
|
if (node2.title) out += ` title="${attrValue(node2.title)}"`;
|
|
14910
14952
|
if (!ctx.spec) out += ' class="beatrina-md-img" loading="lazy"';
|
|
14911
14953
|
return ctx.spec ? `${out} />` : `${out}>`;
|
|
@@ -14994,6 +15036,7 @@ function toHtml(tree, opts = {}) {
|
|
|
14994
15036
|
// survived a JSON round-trip or a parsed query, does not open it.
|
|
14995
15037
|
rawHtml: opts.allowDangerousHtml === true,
|
|
14996
15038
|
rawUrls: opts.allowDangerousProtocol === true,
|
|
15039
|
+
localImages: opts.localImages === true,
|
|
14997
15040
|
gfm: opts.gfm !== false,
|
|
14998
15041
|
softBreak: typeof opts.softBreak === "string" ? opts.softBreak : "\n",
|
|
14999
15042
|
state: opts.state || {},
|
|
@@ -18697,17 +18740,6 @@ function citeStyleOf(notebook) {
|
|
|
18697
18740
|
};
|
|
18698
18741
|
}
|
|
18699
18742
|
|
|
18700
|
-
// lib/engine-labels.js
|
|
18701
|
-
var KNOWN_ENGINES = Object.freeze([
|
|
18702
|
-
Object.freeze({ key: "r", label: "R" }),
|
|
18703
|
-
Object.freeze({ key: "python", label: "Python" }),
|
|
18704
|
-
Object.freeze({ key: "js", label: "JavaScript" }),
|
|
18705
|
-
Object.freeze({ key: "bash", label: "Bash" }),
|
|
18706
|
-
Object.freeze({ key: "sql", label: "SQL" })
|
|
18707
|
-
]);
|
|
18708
|
-
var BY_KEY = new Map(KNOWN_ENGINES.map((e) => [e.key, e.label]));
|
|
18709
|
-
var AUTHORING_ENGINES = Object.freeze(KNOWN_ENGINES.map((e) => e.key));
|
|
18710
|
-
|
|
18711
18743
|
// lib/doc-mode.js
|
|
18712
18744
|
var MODES = Object.freeze(["development", "writing", "latex"]);
|
|
18713
18745
|
var MODE_KEY = "beatrina-mode";
|
|
@@ -19671,6 +19703,10 @@ ${references}
|
|
|
19671
19703
|
`;
|
|
19672
19704
|
}
|
|
19673
19705
|
|
|
19706
|
+
// lib/chunk-meta.js
|
|
19707
|
+
init_engine_labels();
|
|
19708
|
+
init_engine_labels();
|
|
19709
|
+
|
|
19674
19710
|
// src/r-kernel.js
|
|
19675
19711
|
function normalizeTable(frame) {
|
|
19676
19712
|
const rows = Array.isArray(frame.rows) ? frame.rows : frame.rows ? [frame.rows] : [];
|
package/kernel/check/session.mjs
CHANGED
|
@@ -143,8 +143,14 @@ export function freePort() {
|
|
|
143
143
|
}
|
|
144
144
|
|
|
145
145
|
/**
|
|
146
|
-
* Start a supervisor on `port`, open a page socket, and wait for
|
|
147
|
-
*
|
|
146
|
+
* Start a supervisor on `port`, open a page socket, and wait for it to be ready.
|
|
147
|
+
*
|
|
148
|
+
* A host that speaks the handshake sends `host-ready` first (plans/
|
|
149
|
+
* getting_rid_of_R.md K2). When its R row says R is NOT coming — absent, or
|
|
150
|
+
* failed to start — there is no R `ready` to wait for, and the session is the
|
|
151
|
+
* HOST: `ready` stays null, `hostReady` is the frame, `noR` says why. An older
|
|
152
|
+
* host sends no host-ready and is waited on for R's ready, as before.
|
|
153
|
+
* @returns {Promise<object>} the session: frames, until, text, run, ask, ready, hostReady, noR, startupMs, log, shutdown
|
|
148
154
|
*/
|
|
149
155
|
export async function startSession({ command, port, cwd, env = {}, readyMs = 90000, logFile = "" }) {
|
|
150
156
|
const t0 = Date.now();
|
|
@@ -214,7 +220,7 @@ export async function startSession({ command, port, cwd, env = {}, readyMs = 900
|
|
|
214
220
|
else { try { process.kill(child.pid, "SIGKILL"); } catch { /* gone */ } }
|
|
215
221
|
}
|
|
216
222
|
};
|
|
217
|
-
const session = { child, port, announced, page: null, frames: [], ready: null, startupMs: null, log: () => hostLog, shutdown };
|
|
223
|
+
const session = { child, port, announced, page: null, frames: [], ready: null, hostReady: null, noR: "", startupMs: null, log: () => hostLog, shutdown };
|
|
218
224
|
if (!announced) return session;
|
|
219
225
|
|
|
220
226
|
const page = await openSocket(port, `http://127.0.0.1:${port}`);
|
|
@@ -250,7 +256,16 @@ export async function startSession({ command, port, cwd, env = {}, readyMs = 900
|
|
|
250
256
|
const reply = await session.until((f) => f.id === id && f.type !== "stdout" && f.type !== "stderr", ms, from);
|
|
251
257
|
return { reply, ms: Date.now() - t };
|
|
252
258
|
};
|
|
253
|
-
|
|
259
|
+
// host-ready is the first frame on open, or never (an older host): a short wait decides.
|
|
260
|
+
session.hostReady = await session.until((f) => f.type === "host-ready" || f.type === "ready", 15000);
|
|
261
|
+
if (session.hostReady && session.hostReady.type === "ready") { session.ready = session.hostReady; session.hostReady = null; }
|
|
262
|
+
const rRow = session.hostReady && Array.isArray(session.hostReady.engine_status)
|
|
263
|
+
? session.hostReady.engine_status.find((e) => e.engine === "r") : null;
|
|
264
|
+
if (session.hostReady && (!rRow || !rRow.available || rRow.error)) {
|
|
265
|
+
session.noR = (rRow && (rRow.error || rRow.detail)) || "this host has no R engine";
|
|
266
|
+
} else if (!session.ready) {
|
|
267
|
+
session.ready = await session.until((f) => f.type === "ready", readyMs);
|
|
268
|
+
}
|
|
254
269
|
session.startupMs = Date.now() - t0;
|
|
255
270
|
return session;
|
|
256
271
|
}
|
package/kernel/job-run.R
CHANGED
|
@@ -34,7 +34,10 @@ if (is.na(sentinel) || !nzchar(sentinel)) {
|
|
|
34
34
|
stop("job-run.R needs a sentinel argument", call. = FALSE)
|
|
35
35
|
}
|
|
36
36
|
suppressPackageStartupMessages(library(jsonlite))
|
|
37
|
-
|
|
37
|
+
# R writes a space in the --file= path as "~+~" (its front end's encoding), so a script
|
|
38
|
+
# installed under "Beatrina Note.app" read its own folder as "Beatrina~+~Note.app" and could not
|
|
39
|
+
# find its siblings: every Typst/render job died with exit 1 (owner, 2026-09-24). Decoded here.
|
|
40
|
+
here <- dirname(normalizePath(gsub("~+~", " ", sub("^--file=", "", grep("^--file=", commandArgs(FALSE), value = TRUE)[1L]), fixed = TRUE)))
|
|
38
41
|
source(file.path(here, "kernel.R"))
|
|
39
42
|
source(file.path(here, "jobs.R"))
|
|
40
43
|
source(file.path(here, "latex.R"))
|
package/kernel/kernel-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
1.0.1
|
package/kernel/worker-boot.R
CHANGED
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
local({
|
|
17
17
|
file_arg <- grep("^--file=", commandArgs(FALSE), value = TRUE)
|
|
18
18
|
here <- if (length(file_arg)) {
|
|
19
|
-
|
|
19
|
+
# R writes a space in --file= as "~+~" ("Beatrina Note.app"): decoded, or the folder is wrong.
|
|
20
|
+
dirname(normalizePath(gsub("~+~", " ", sub("^--file=", "", file_arg[1L]), fixed = TRUE), mustWork = FALSE))
|
|
20
21
|
} else getwd()
|
|
21
22
|
sys.source(file.path(here, "worker.R"), envir = globalenv(), keep.source = FALSE)
|
|
22
23
|
})
|
package/kernel/worker.R
CHANGED
|
@@ -201,7 +201,8 @@ import_sources <- local({
|
|
|
201
201
|
file_arg <- grep("^--file=", commandArgs(FALSE), value = TRUE)
|
|
202
202
|
here <- if (nzchar(env_dir)) env_dir
|
|
203
203
|
else if (length(file_arg)) {
|
|
204
|
-
|
|
204
|
+
# R writes a space in --file= as "~+~" ("Beatrina Note.app"): decoded, or the folder is wrong.
|
|
205
|
+
dirname(normalizePath(gsub("~+~", " ", sub("^--file=", "", file_arg[1L]), fixed = TRUE), mustWork = FALSE))
|
|
205
206
|
} else getwd()
|
|
206
207
|
file.path(here, c("fileio.R", "sniff.R", "project.R", "workspace-keep.R", "deployment.R"))
|
|
207
208
|
})
|
package/lib/engine-labels.js
CHANGED
|
@@ -118,6 +118,8 @@ export function versionMenuLabel(engine) {
|
|
|
118
118
|
*/
|
|
119
119
|
export function kernelLanguage(info) {
|
|
120
120
|
const declared = info && typeof info.language === "string" ? info.language.trim() : "";
|
|
121
|
+
// A host running no default engine (host-ready) runs no language: "".
|
|
122
|
+
if (!declared && info && info.host_only) return "";
|
|
121
123
|
return declared ? engineKey(declared) : DEFAULT_ENGINE;
|
|
122
124
|
}
|
|
123
125
|
|
|
@@ -154,6 +156,8 @@ export function kernelVersion(info) {
|
|
|
154
156
|
* has always shown for a listener that named no version.
|
|
155
157
|
*/
|
|
156
158
|
export function kernelDisplay(info) {
|
|
159
|
+
// The HOST is attached and no engine is its own (plans/getting_rid_of_R.md K2).
|
|
160
|
+
if (!kernelLanguage(info) && info) return "Beatrina";
|
|
157
161
|
const label = engineLabel(kernelLanguage(info));
|
|
158
162
|
const version = kernelVersion(info);
|
|
159
163
|
return version ? `${label} ${version}` : `${label} session`;
|
|
@@ -216,6 +220,8 @@ export function reportedEngines(info) {
|
|
|
216
220
|
if (row && !rows.some((seen) => seen.name === row.name)) rows.push(row);
|
|
217
221
|
});
|
|
218
222
|
if (!rows.length) return [];
|
|
223
|
+
// A host with no language of its own adds none.
|
|
224
|
+
if (!language) return rows;
|
|
219
225
|
// The kernel's own language is always one of its engines — a session that
|
|
220
226
|
// would not run its own language is not a thing, and a list that omitted it
|
|
221
227
|
// would strand every chunk in the notebook.
|
|
@@ -239,7 +245,7 @@ export function reportedEngines(info) {
|
|
|
239
245
|
*/
|
|
240
246
|
export function declaredEngines(info) {
|
|
241
247
|
const language = kernelLanguage(info);
|
|
242
|
-
const out = [language];
|
|
248
|
+
const out = language ? [language] : [];
|
|
243
249
|
reportedEngines(info).forEach((row) => { if (!out.includes(row.name)) out.push(row.name); });
|
|
244
250
|
return out;
|
|
245
251
|
}
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
<key>CFBundleExecutable</key><string>Beatrina Menu</string>
|
|
7
7
|
<key>CFBundleIdentifier</key><string>me.saqr.beatrina.menu</string>
|
|
8
8
|
<key>CFBundlePackageType</key><string>APPL</string>
|
|
9
|
-
<key>CFBundleShortVersionString</key><string>0.
|
|
10
|
-
<key>CFBundleVersion</key><string>0.
|
|
9
|
+
<key>CFBundleShortVersionString</key><string>1.0.0</string>
|
|
10
|
+
<key>CFBundleVersion</key><string>1.0.0</string>
|
|
11
11
|
<key>LSMinimumSystemVersion</key><string>13.0</string>
|
|
12
12
|
<!-- Menu bar only: no Dock tile, no window, nothing to close by accident. -->
|
|
13
13
|
<key>LSUIElement</key><true/>
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "beatrina",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Beatrina is a modern, powerful, feature-rich integrated development environment (IDE) for R, Python, and JavaScript.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -23,9 +23,10 @@
|
|
|
23
23
|
"failsafe",
|
|
24
24
|
"lib",
|
|
25
25
|
"menu",
|
|
26
|
+
"app",
|
|
26
27
|
"build-info.json",
|
|
27
|
-
"
|
|
28
|
-
"
|
|
28
|
+
"beatrina_V1.0.1.html",
|
|
29
|
+
"beatrina_V1.0.1.html.inputs.json",
|
|
29
30
|
"README.md",
|
|
30
31
|
"LICENSE",
|
|
31
32
|
"NOTICES"
|