beatrina 0.9.83 → 1.0.0

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.
@@ -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";
@@ -4467,7 +4493,10 @@ var BEATRINA_CSS = `
4467
4493
  .cn-root .beatrina-md-tool:focus-visible{outline:0;box-shadow:var(--cn-focus-ring);}
4468
4494
  .cn-root .beatrina-md-tool.is-cite{color:var(--cn-accent);}
4469
4495
  .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;}
4496
+ /* width as well as flex-basis: WebKit leaves a basis-only item out of the bar's max-content
4497
+ width, so the bar came out 4 px short at every fold level and folded everything behind \u22EE
4498
+ (Beatrina's window, 2026-09-23). */
4499
+ .cn-root .beatrina-md-bar-sp{flex:0 0 4px;width:4px;}
4471
4500
  .cn-root .beatrina-md-count{
4472
4501
  font-size:var(--cn-fs-sm);opacity:.55;font-variant-numeric:tabular-nums;margin-right:2px;
4473
4502
  }
@@ -10637,6 +10666,14 @@ var BEATRINA_CSS = `
10637
10666
  @media(max-width:760px){
10638
10667
  .cn-root.has-ai-left{padding-left:0;}
10639
10668
  .cn-root.has-ai-right{padding-right:0;}
10669
+ /* \u2026and so a docked pane is a SHEET over the whole window, not a 400 px
10670
+ slab over the right half of the words. Reserving its width at this size
10671
+ would leave a column of ~260 px; overlaying half the text left every line
10672
+ cut off under it (journey windows, 720 px, 2026-09-24). One or the other
10673
+ is on screen; \xD7 gives the document back. !important beats the inline
10674
+ left/right/width ai-shell.js writes for the wide layout. */
10675
+ .cn-root .beatrina-ai-panel.is-side{left:0 !important;right:0 !important;width:auto !important;}
10676
+ .cn-root .beatrina-ai-panel.is-side .beatrina-ai-sedge{display:none;}
10640
10677
  }
10641
10678
 
10642
10679
  .cn-root .beatrina-aiw-convbtn{flex:0 0 auto;width:24px;height:24px;
@@ -11777,7 +11814,7 @@ body:has(.cn-root[data-beatrina-mode="writing"] > .beatrina-writing-bar){--beatr
11777
11814
  box-shadow:0 -1px 0 var(--cn-border,#e3e3e3);
11778
11815
  }
11779
11816
  .beatrina-dock[hidden]{display:none;}
11780
- .beatrina-dock[data-mode="bottom"]{left:0;right:0;bottom:0;flex-direction:column;}
11817
+ .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
11818
  .beatrina-dock[data-mode="right"]{
11782
11819
  top:0;right:0;bottom:0;flex-direction:row;
11783
11820
  box-shadow:-1px 0 0 var(--cn-border,#e3e3e3);
@@ -14905,7 +14942,8 @@ ${out}</div>
14905
14942
  if (!ctx.rawUrls && !safeImageSrc(url)) {
14906
14943
  return `<span class="beatrina-md-badsrc">[image: ${alt || "unsupported source"}]</span>`;
14907
14944
  }
14908
- let out = `<img src="${attrValue(encodeHref(url))}" alt="${alt}"`;
14945
+ const local = ctx.localImages && /^(?![a-z][a-z0-9+.-]*:|\/\/|#)/i.test(url.trim());
14946
+ let out = local ? `<img data-local-src="${attrValue(url)}" alt="${alt}"` : `<img src="${attrValue(encodeHref(url))}" alt="${alt}"`;
14909
14947
  if (node2.title) out += ` title="${attrValue(node2.title)}"`;
14910
14948
  if (!ctx.spec) out += ' class="beatrina-md-img" loading="lazy"';
14911
14949
  return ctx.spec ? `${out} />` : `${out}>`;
@@ -14994,6 +15032,7 @@ function toHtml(tree, opts = {}) {
14994
15032
  // survived a JSON round-trip or a parsed query, does not open it.
14995
15033
  rawHtml: opts.allowDangerousHtml === true,
14996
15034
  rawUrls: opts.allowDangerousProtocol === true,
15035
+ localImages: opts.localImages === true,
14997
15036
  gfm: opts.gfm !== false,
14998
15037
  softBreak: typeof opts.softBreak === "string" ? opts.softBreak : "\n",
14999
15038
  state: opts.state || {},
@@ -18697,17 +18736,6 @@ function citeStyleOf(notebook) {
18697
18736
  };
18698
18737
  }
18699
18738
 
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
18739
  // lib/doc-mode.js
18712
18740
  var MODES = Object.freeze(["development", "writing", "latex"]);
18713
18741
  var MODE_KEY = "beatrina-mode";
@@ -19671,6 +19699,10 @@ ${references}
19671
19699
  `;
19672
19700
  }
19673
19701
 
19702
+ // lib/chunk-meta.js
19703
+ init_engine_labels();
19704
+ init_engine_labels();
19705
+
19674
19706
  // src/r-kernel.js
19675
19707
  function normalizeTable(frame) {
19676
19708
  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
  }
@@ -1 +1 @@
1
- 0.9.83
1
+ 1.0.0
@@ -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.0",
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.0.html",
29
+ "beatrina_V1.0.0.html.inputs.json",
29
30
  "README.md",
30
31
  "LICENSE",
31
32
  "NOTICES"