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.
Files changed (40) hide show
  1. package/app/Beatrina.app/Contents/Info.plist +86 -0
  2. package/app/Beatrina.app/Contents/MacOS/beatrina +0 -0
  3. package/app/Beatrina.app/Contents/Resources/icon.icns +0 -0
  4. package/app/beatrina.png +0 -0
  5. package/app/linux-arm64/beatrina +0 -0
  6. package/app/win-x64/Beatrina-setup.exe +0 -0
  7. package/beatrina_V1.0.1.html +1906 -0
  8. package/beatrina_V1.0.1.html.inputs.json +1 -0
  9. package/bin/beatrina.mjs +200 -12
  10. package/bin/cli.mjs +8 -3
  11. package/bin/finder.mjs +1 -1
  12. package/bin/shortcut.mjs +15 -15
  13. package/build-info.json +1 -1
  14. package/host/engine-node-job.mjs +35 -0
  15. package/host/engine-pool.mjs +153 -26
  16. package/host/engine-r.mjs +8 -1
  17. package/host/jobs/document-child.mjs +271 -0
  18. package/host/latex-compile.mjs +673 -0
  19. package/host/main.mjs +58 -18
  20. package/host/planes/analyze.mjs +18 -2
  21. package/host/planes/jobs.mjs +38 -5
  22. package/host/planes/sessions.mjs +5 -1
  23. package/host/pwa/icon-192.png +0 -0
  24. package/host/pwa/icon-512.png +0 -0
  25. package/host/quarto.mjs +118 -0
  26. package/host/server.mjs +83 -2
  27. package/host/worker-plane.mjs +7 -1
  28. package/kernel/check/acceptance.mjs +43 -12
  29. package/kernel/check/knit-file.mjs +55 -19
  30. package/kernel/check/session.mjs +19 -4
  31. package/kernel/job-run.R +4 -1
  32. package/kernel/kernel-version +1 -1
  33. package/kernel/worker-boot.R +2 -1
  34. package/kernel/worker.R +2 -1
  35. package/lib/engine-labels.js +7 -1
  36. package/menu/Beatrina Menu.app/Contents/Info.plist +2 -2
  37. package/menu/Beatrina Menu.app/Contents/MacOS/Beatrina Menu +0 -0
  38. package/package.json +4 -3
  39. package/beatrina_V0.9.83.html +0 -1903
  40. 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
- record("startup", `R ready (${ready.r || "?"}, worker mode ${ready.mode || "?"})`, "pass", "", session.startupMs, { r: ready.r, mode: ready.mode });
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 engines = Array.isArray(ready.engines) ? ready.engines : ["r"];
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 (want("run")) {
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 (want("stop")) {
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 (want("readline")) {
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 (want("busy")) {
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 (want("force")) {
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 (want("crash")) {
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 (want("memory")) {
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(ready.engine_status) ? ready.engine_status.find((e) => e.engine === engine) : null;
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:820;margin:0;box-shadow:var(--cn-shadow-lg);
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:820;margin:0;border-radius:0;
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
- .cn-root .beatrina-md-bar-sp{flex:0 0 4px;}
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-pdf{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;}
5211
- .cn-root .beatrina-latex-pdf{display:inline-flex;align-self:center;flex:0 0 auto;}
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-pdf.on{background:#fff;color:#141a3a;border-color:#fff;font-weight:600;}
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:0;right:0;bottom:0;flex-direction:column;}
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
- let out = `<img src="${attrValue(encodeHref(url))}" alt="${alt}"`;
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] : [];
@@ -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 R's ready.
147
- * @returns {Promise<object>} the session: frames, until, text, run, ask, ready, startupMs, log, shutdown
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
- session.ready = await session.until((f) => f.type === "ready", readyMs);
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
- here <- dirname(normalizePath(sub("^--file=", "", grep("^--file=", commandArgs(FALSE), value = TRUE)[1L])))
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"))
@@ -1 +1 @@
1
- 0.9.83
1
+ 1.0.1
@@ -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
- dirname(normalizePath(sub("^--file=", "", file_arg[1L]), mustWork = FALSE))
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
- dirname(normalizePath(sub("^--file=", "", file_arg[1L]), mustWork = FALSE))
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
  })
@@ -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.9.83</string>
10
- <key>CFBundleVersion</key><string>0.9.83</string>
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/>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "beatrina",
3
- "version": "0.9.83",
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
- "beatrina_V0.9.83.html",
28
- "beatrina_V0.9.83.html.inputs.json",
28
+ "beatrina_V1.0.1.html",
29
+ "beatrina_V1.0.1.html.inputs.json",
29
30
  "README.md",
30
31
  "LICENSE",
31
32
  "NOTICES"