beatrina 0.8.6 → 0.9.43

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 (112) hide show
  1. package/NOTICES +1 -1
  2. package/README.md +6 -0
  3. package/beatrina_V0.9.43.html +1885 -0
  4. package/beatrina_V0.9.43.html.inputs.json +1 -0
  5. package/bin/beatrina.mjs +156 -26
  6. package/bin/browser.mjs +31 -0
  7. package/bin/cli.mjs +64 -5
  8. package/bin/doctor-rows.mjs +31 -0
  9. package/bin/failsafe.mjs +8 -3
  10. package/bin/finder.mjs +74 -0
  11. package/bin/lsquery.swift +55 -0
  12. package/bin/pages.mjs +81 -0
  13. package/bin/python-setup.mjs +207 -0
  14. package/bin/runtime-dirs.mjs +27 -0
  15. package/bin/sessions.mjs +30 -10
  16. package/bin/shortcut.mjs +271 -35
  17. package/bin/update-check.mjs +2 -2
  18. package/build-info.json +1 -1
  19. package/engines/js/worker.mjs +6 -3
  20. package/engines/python/adapter.py +14 -13
  21. package/engines/python/analyze.py +4 -2
  22. package/engines/python/bootstrap.py +32 -31
  23. package/engines/python/debugger.py +10 -6
  24. package/engines/python/engine.json +1 -1
  25. package/engines/python/handoff.py +4 -2
  26. package/engines/python/worker.py +39 -10
  27. package/engines/r/engine.json +1 -1
  28. package/engines/r/handoff.R +4 -3
  29. package/failsafe/ai-policy.R +16 -16
  30. package/failsafe/ai-store.R +6 -6
  31. package/failsafe/cite.R +11 -11
  32. package/failsafe/journal.R +11 -11
  33. package/failsafe/plugins.R +27 -27
  34. package/failsafe/serve.R +364 -239
  35. package/failsafe/session-documents.R +104 -0
  36. package/host/ai-authority.mjs +55 -0
  37. package/host/ai-policy.mjs +48 -26
  38. package/host/bundle.mjs +194 -0
  39. package/host/deployment.mjs +25 -18
  40. package/host/engine-js.mjs +16 -17
  41. package/host/engine-pool.mjs +56 -8
  42. package/host/engine-python.mjs +80 -31
  43. package/host/engine-r.mjs +38 -32
  44. package/host/engine-stdio.mjs +45 -12
  45. package/host/env-names.mjs +148 -0
  46. package/host/gateway-token.mjs +51 -0
  47. package/host/journal-store.mjs +10 -9
  48. package/host/main.mjs +216 -76
  49. package/host/payload.mjs +131 -0
  50. package/host/planes/README.md +1 -1
  51. package/host/planes/ai-store.mjs +6 -5
  52. package/host/planes/ai.mjs +49 -29
  53. package/host/planes/analyze.mjs +6 -5
  54. package/host/planes/bundle.mjs +344 -0
  55. package/host/planes/choose.mjs +404 -0
  56. package/host/planes/cite.mjs +18 -17
  57. package/host/planes/files.mjs +0 -0
  58. package/host/planes/jobs.mjs +43 -20
  59. package/host/planes/latex.mjs +41 -5
  60. package/host/planes/mcp.mjs +80 -91
  61. package/host/planes/pair.mjs +24 -24
  62. package/host/planes/pipe-term.mjs +2 -2
  63. package/host/planes/plugins.mjs +1 -1
  64. package/host/planes/recent-documents.mjs +170 -0
  65. package/host/planes/sessions.mjs +230 -82
  66. package/host/planes/settings.mjs +4 -4
  67. package/host/planes/terminal.mjs +13 -11
  68. package/host/planes/test-file.mjs +2 -2
  69. package/host/planes/update.mjs +6 -13
  70. package/host/plugin-store.mjs +35 -52
  71. package/host/recent-documents.mjs +124 -0
  72. package/host/runtime-dir.mjs +47 -0
  73. package/host/server.mjs +123 -21
  74. package/host/session-keep.mjs +70 -0
  75. package/host/settings.mjs +85 -37
  76. package/host/update-record.mjs +3 -2
  77. package/host/user-dirs.mjs +60 -18
  78. package/host/which.mjs +39 -0
  79. package/host/windows-runtime.mjs +6 -3
  80. package/host/worker-plane.mjs +80 -7
  81. package/host/ws.mjs +9 -2
  82. package/host/zip.mjs +237 -0
  83. package/kernel/analyze.R +1 -1
  84. package/{check → kernel/check}/acceptance.mjs +1 -1
  85. package/kernel/check/knit-file.mjs +19639 -0
  86. package/{check → kernel/check}/session.mjs +49 -8
  87. package/kernel/deployment.R +20 -20
  88. package/kernel/examples/NOTICE.md +1 -1
  89. package/kernel/fileio.R +6 -6
  90. package/kernel/index.html +2 -2
  91. package/kernel/job-run.R +83 -22
  92. package/kernel/jobs.R +28 -12
  93. package/kernel/kernel-version +1 -1
  94. package/kernel/kernel.R +24 -21
  95. package/kernel/knitr-run.R +50 -9
  96. package/kernel/latex.R +429 -30
  97. package/kernel/mcp/{carmar-mcp.mjs → beatrina-mcp.mjs} +249 -59
  98. package/kernel/notebook-page.R +7 -7
  99. package/kernel/project.R +10 -10
  100. package/kernel/settings.R +105 -56
  101. package/kernel/sniff.R +4 -4
  102. package/kernel/typst.R +121 -0
  103. package/kernel/worker.R +602 -123
  104. package/kernel/workspace-keep.R +188 -0
  105. package/lib/agent-authoring-contract.js +28 -19
  106. package/lib/cell-kinds.js +3 -3
  107. package/lib/engine-labels.js +4 -4
  108. package/menu/Beatrina Menu.app/Contents/Info.plist +14 -0
  109. package/menu/Beatrina Menu.app/Contents/MacOS/Beatrina Menu +0 -0
  110. package/menu/Beatrina Menu.app/Contents/_CodeSignature/CodeResources +115 -0
  111. package/package.json +4 -3
  112. package/carmar_V0.8.6.html +0 -1310
package/kernel/worker.R CHANGED
@@ -63,9 +63,9 @@ local({
63
63
  # interactive worker MUST read through R's console (stdin()), because that is
64
64
  # the one reader a native browser() prompt shares — a second buffered reader
65
65
  # on the same fd would steal bytes from the debugger.
66
- WORKER_MODE <- Sys.getenv("CARMAR_WORKER_MODE", "batch")
66
+ WORKER_MODE <- Sys.getenv("BEATRINA_WORKER_MODE", "batch")
67
67
  sentinel <- if (identical(WORKER_MODE, "interactive")) {
68
- Sys.getenv("CARMAR_SENTINEL", "")
68
+ Sys.getenv("BEATRINA_SENTINEL", "")
69
69
  } else {
70
70
  args <- commandArgs(trailingOnly = TRUE)
71
71
  if (length(args) >= 1L) args[1] else ""
@@ -75,13 +75,89 @@ stopifnot(nzchar(sentinel))
75
75
  # mode — a comment, so a line that ever reached R's raw top level would be
76
76
  # inert. The prefix is stripped here before parsing.
77
77
  CMD_PREFIX <- local({
78
- tag <- Sys.getenv("CARMAR_CMD_TAG", "")
78
+ tag <- Sys.getenv("BEATRINA_CMD_TAG", "")
79
79
  if (nzchar(tag)) paste0("#", tag, " ") else ""
80
80
  })
81
81
 
82
82
  PLOT_WIDTH <- 900L
83
83
  PLOT_HEIGHT <- 620L
84
84
  PLOT_RES <- 110L
85
+
86
+ #' knitr's OWN figure defaults, captured before any user code runs.
87
+ #'
88
+ #' The baseline that lets knitr_dim_defaults() tell "this document asked for a
89
+ #' 10-inch figure" from "knitr answered with its own 7-inch default". Read at
90
+ #' load time on purpose: after a setup chunk has run it is no longer pristine.
91
+ KNITR_FIG_BASE <- local({
92
+ if (!requireNamespace("knitr", quietly = TRUE)) return(NULL)
93
+ tryCatch(knitr::opts_chunk$get(c("fig.width", "fig.height", "dpi")),
94
+ error = function(e) NULL)
95
+ })
96
+
97
+ #' The document's own figure defaults, from knitr.
98
+ #'
99
+ #' `knitr::opts_chunk$set(fig.width = 10, dpi = 150)` in a setup chunk is how
100
+ #' every R author states what size their figures are, and until 2026-09-18 an
101
+ #' interactive run ignored it completely: the call looked like configuration
102
+ #' and did nothing, and the PNGs came back at this file's constants whatever
103
+ #' the document said. knitr sizes in INCHES and carries the resolution
104
+ #' separately, which is exactly a raster device's width = inches * dpi.
105
+ #'
106
+ #' Precedence, widest to narrowest: what the page sent for THIS run (the
107
+ #' chunk's own fig.width, or the editor's device settings) wins, because it is
108
+ #' more specific than a document default; then these; then the constants.
109
+ #'
110
+ #' @return A list with any of width/height/res in pixels, or an empty list.
111
+ knitr_dim_defaults <- function() {
112
+ if (!requireNamespace("knitr", quietly = TRUE)) return(list())
113
+ got <- tryCatch(knitr::opts_chunk$get(c("fig.width", "fig.height", "dpi")),
114
+ error = function(e) NULL)
115
+ if (!is.list(got)) return(list())
116
+ # Only what the DOCUMENT set. knitr always answers with something — its own
117
+ # defaults are 7in by 7in at 72 dpi — so honouring whatever it says would
118
+ # shrink every plot in every notebook that never mentioned figures, from 900
119
+ # pixels to 504. KNITR_FIG_BASE is knitr's pristine answer, captured at boot
120
+ # before a line of anyone's code has run; a value equal to it was not asked
121
+ # for by this document and is ignored.
122
+ base <- KNITR_FIG_BASE
123
+ if (is.list(base)) {
124
+ for (key in c("fig.width", "fig.height", "dpi")) {
125
+ if (!is.null(base[[key]]) && !is.null(got[[key]]) && isTRUE(all.equal(base[[key]], got[[key]]))) got[[key]] <- NULL
126
+ }
127
+ }
128
+ num <- function(x) if (is.numeric(x) && length(x) == 1L && is.finite(x) && x > 0) as.numeric(x) else NULL
129
+ dpi <- num(got$dpi)
130
+ w <- num(got$fig.width)
131
+ h <- num(got$fig.height)
132
+ out <- list()
133
+ if (!is.null(dpi)) out$res <- as.integer(round(dpi))
134
+ # Inches become pixels only when there is a resolution to multiply by; with
135
+ # no dpi set, knitr's own default of 72 is what a figure of that width means.
136
+ scale <- if (!is.null(dpi)) dpi else 72
137
+ if (!is.null(w)) out$width <- as.integer(round(w * scale))
138
+ if (!is.null(h)) out$height <- as.integer(round(h * scale))
139
+ out
140
+ }
141
+
142
+ #' The document's chosen display precision, or NULL if it never chose one.
143
+ #'
144
+ #' R's factory default is 7. A document that says `options(digits = 3)` means
145
+ #' its tables to read at three significant digits, and says it once, in a setup
146
+ #' chunk — so the value travels with every frame rather than being asked for
147
+ #' per table.
148
+ display_digits <- function() {
149
+ d <- getOption("digits", 7L)
150
+ if (!is.numeric(d) || length(d) != 1L || !is.finite(d) || d == 7L) return(NULL)
151
+ as.integer(max(1L, min(22L, round(d))))
152
+ }
153
+
154
+ #' One dimension, resolved: this run's value, else the document's, else ours.
155
+ dim_or <- function(value, key) {
156
+ if (!is.null(value)) return(value)
157
+ doc <- knitr_dim_defaults()
158
+ if (!is.null(doc[[key]])) return(doc[[key]])
159
+ switch(key, width = PLOT_WIDTH, height = PLOT_HEIGHT, res = PLOT_RES)
160
+ }
85
161
  # Hard ceilings for the data viewer. Server-side because the client is one
86
162
  # `limit: 1e9` typo away from asking for everything; the reply reports what was
87
163
  # clamped, so the UI never has to guess what it actually received.
@@ -114,13 +190,13 @@ try(utils::rc.settings(ipck = TRUE, func = TRUE, args = TRUE, files = TRUE),
114
190
  # is started by three different launchers (kernel.R, the Chrome bridge host,
115
191
  # and the packaged inst/app/kernel) and none of them guarantee a cwd.
116
192
  import_sources <- local({
117
- env_dir <- Sys.getenv("CARMAR_WORKER_DIR", "")
193
+ env_dir <- Sys.getenv("BEATRINA_WORKER_DIR", "")
118
194
  file_arg <- grep("^--file=", commandArgs(FALSE), value = TRUE)
119
195
  here <- if (nzchar(env_dir)) env_dir
120
196
  else if (length(file_arg)) {
121
197
  dirname(normalizePath(sub("^--file=", "", file_arg[1L]), mustWork = FALSE))
122
198
  } else getwd()
123
- file.path(here, c("fileio.R", "sniff.R", "project.R"))
199
+ file.path(here, c("fileio.R", "sniff.R", "project.R", "workspace-keep.R", "deployment.R"))
124
200
  })
125
201
  # environment(), not parent.frame(): inside a nested local() the parent frame
126
202
  # is the eval machinery's, not this file's private scope, and the functions
@@ -142,9 +218,9 @@ invisible(lapply(Filter(file.exists, import_sources),
142
218
  # user already set: their .Rprofile loads here (we deliberately do not use
143
219
  # --vanilla), and it is where r-universe and institutional mirrors live.
144
220
  local({
145
- managed_mirror <- trimws(Sys.getenv("CARMAR_CRAN_MIRROR", ""))
221
+ managed_mirror <- trimws(Sys.getenv("BEATRINA_CRAN_MIRROR", ""))
146
222
  if (nzchar(managed_mirror) && !grepl("^https://", managed_mirror)) {
147
- stop("CARMAR_CRAN_MIRROR must be an HTTPS URL.")
223
+ stop("BEATRINA_CRAN_MIRROR must be an HTTPS URL.")
148
224
  }
149
225
  repos <- getOption("repos")
150
226
  cran <- if (is.null(repos)) NA_character_ else unname(repos["CRAN"])
@@ -191,7 +267,7 @@ R_KEYWORDS <- c("if", "else", "repeat", "while", "function", "for", "in",
191
267
  #' CAIRO IS TRIED BEFORE QUARTZ, and that ORDER is a hang fix, not a
192
268
  #' preference. `png(type="quartz")` pulls in the macOS Aqua/AppKit graphics
193
269
  #' backend, which spawns a Cocoa event loop (an NSEventThread plus grDevices'
194
- #' own ELThread) the first time it opens. CarmaR's worker is a BackgroundOnly
270
+ #' own ELThread) the first time it opens. Beatrina's worker is a BackgroundOnly
195
271
  #' process with no window-server access, so when R pumps that event loop
196
272
  #' mid-evaluation — which it does from inside a long R-level loop, e.g. the
197
273
  #' permutation loop in `markov_order_test` — `ReceiveNextEventCommon` blocks
@@ -391,7 +467,10 @@ sys.source(file.path(here, "knitr-run.R"), envir = environment())
391
467
  # code and every package keep seeing base's own function, and removing the
392
468
  # attached frame restores the original behaviour exactly. The delegation is the
393
469
  # whole implementation — announce, then let R do what it already did.
394
- carmar_readline <- function(prompt = "") {
470
+ # The console reader itself, captured BEFORE the namespaces are patched below:
471
+ # the announcing wrapper must call the real one, never itself.
472
+ BASE_READLINE <- if (is.null(attr(base::readline, "beatrina"))) base::readline else attr(base::readline, "beatrina")
473
+ beatrina_readline <- function(prompt = "") {
395
474
  text <- tryCatch(as.character(prompt)[[1]], error = function(e) "")
396
475
  if (!length(text) || is.na(text)) text <- ""
397
476
  # The question belongs to the RUN that asked it, and says so.
@@ -413,7 +492,7 @@ carmar_readline <- function(prompt = "") {
413
492
  # id by finding the first `"id":…` in the worker's own bytes, so a prompt
414
493
  # containing that text would otherwise be rewritten instead of the id.
415
494
  emit(list(type = "input_request", id = RUN_STATE$id, prompt = text))
416
- answer <- base::readline(prompt)
495
+ answer <- BASE_READLINE(prompt)
417
496
  # Symmetry matters more than it looks: a page that opened an input row on the
418
497
  # request must be told to close it, INCLUDING when the answer arrived by some
419
498
  # other route (an interrupt, a second page). Without this the prompt row
@@ -438,7 +517,7 @@ carmar_readline <- function(prompt = "") {
438
517
  # RUN_STATE carries the current run's id from run_cell to the shadow.
439
518
  RUN_STATE <- new.env(parent = emptyenv())
440
519
  RUN_STATE$id <- NULL
441
- carmar_print <- function(x, ...) {
520
+ beatrina_print <- function(x, ...) {
442
521
  if (is.null(RUN_STATE$id) || length(list(...)) || sink.number() > (RUN_STATE$capture_depth %||% 0L)) {
443
522
  return(base::print(x, ...))
444
523
  }
@@ -457,8 +536,8 @@ carmar_print <- function(x, ...) {
457
536
  }
458
537
  base::print(x, ...)
459
538
  }
460
- INPUT_SHADOW <- "carmar:input"
461
- carmar_flush_console <- function() {
539
+ INPUT_SHADOW <- "beatrina:input"
540
+ beatrina_flush_console <- function() {
462
541
  if (!is.null(KNIT_CAPTURE)) evaluate::flush_console()
463
542
  else utils::flush.console()
464
543
  invisible(NULL)
@@ -467,10 +546,136 @@ if (!(INPUT_SHADOW %in% search())) {
467
546
  # warn.conflicts = FALSE: masking `readline` (and `print`) is the entire
468
547
  # point, and a startup warning about it would be printed into the user's
469
548
  # first cell.
470
- attach(list(readline = carmar_readline, print = carmar_print, flush.console = carmar_flush_console),
549
+ attach(list(readline = beatrina_readline, print = beatrina_print, flush.console = beatrina_flush_console),
471
550
  name = INPUT_SHADOW, warn.conflicts = FALSE)
472
551
  }
473
552
 
553
+ # Console reads R's OWN helpers make are announced too (2026-09-19). The shadow
554
+ # above only answers for readline() typed in the user's code: askYesNo(),
555
+ # menu(), select.list(), scan("") and every package reach base and utils
556
+ # through their NAMESPACES, where a search-path shadow is invisible — measured:
557
+ # menu() printed its choices and the run sat blocked with no prompt on the
558
+ # page, and so did askYesNo, select.list and scan. So the readers are announced
559
+ # where everything funnels through them:
560
+ # - base::readline is the announcing reader in base's namespace and package
561
+ # env, so askYesNo() and every package's readline() ride it;
562
+ # - utils::menu's console loop is re-expressed over it (the original reads
563
+ # the console from C, C_menu, which nothing at R level can observe);
564
+ # - scan() from the console collects its lines through it and hands them to
565
+ # the real scan() as `text`.
566
+ # Each READ is announced, so a menu that re-asks after a bad answer opens a
567
+ # fresh prompt instead of having its answer refused by the one-line gate.
568
+ # The originals are kept (as the "beatrina" attribute, BASE_*), and the
569
+ # patching is idempotent.
570
+
571
+ #' Put `value` in place of `name` in a package's namespace and its attached env.
572
+ #' @return Invisibly, whether every present binding was replaced.
573
+ beatrina_patch <- function(name, value, pkg) {
574
+ envs <- Filter(Negate(is.null), list(asNamespace(pkg),
575
+ if (paste0("package:", pkg) %in% search()) as.environment(paste0("package:", pkg)) else NULL))
576
+ done <- vapply(envs, function(env) {
577
+ if (!exists(name, envir = env, inherits = FALSE)) return(TRUE)
578
+ locked <- bindingIsLocked(name, env)
579
+ if (locked) unlockBinding(name, env)
580
+ on.exit(if (locked) lockBinding(name, env), add = TRUE)
581
+ assign(name, value, envir = env)
582
+ TRUE
583
+ }, logical(1))
584
+ invisible(all(done))
585
+ }
586
+
587
+ #' menu() over the announced reader, faithful to utils::menu's console path:
588
+ #' the same layout, a number or an exact choice, 0 to exit, and the same
589
+ #' sentence on a bad answer. The graphics path is left to the original.
590
+ #' @return The chosen index, or 0.
591
+ BASE_MENU <- if (is.null(attr(utils::menu, "beatrina"))) utils::menu else attr(utils::menu, "beatrina")
592
+ beatrina_menu <- function(choices, graphics = FALSE, title = NULL) {
593
+ if (!interactive()) stop("menu() cannot be used non-interactively")
594
+ if (isTRUE(graphics)) return(BASE_MENU(choices, graphics = graphics, title = title))
595
+ choices <- as.character(choices)
596
+ nc <- length(choices)
597
+ if (length(title) && nzchar(title[1L])) cat(title[1L], "\n")
598
+ op <- paste0(format(seq_len(nc)), ": ", choices)
599
+ if (nc > 10L) {
600
+ fop <- format(op)
601
+ nw <- nchar(fop[1L], "w") + 2L
602
+ ncol <- getOption("width") %/% nw
603
+ if (ncol > 1L) op <- paste0(fop, c(rep.int(" ", min(nc, ncol) - 1L), "\n"), collapse = "")
604
+ }
605
+ cat("", op, "", sep = "\n")
606
+ # Recursion, not a loop: ask until the answer names an item (R's own menu
607
+ # repeats the same way; a person typing nonsense is the only way round).
608
+ ask <- function() {
609
+ ans <- trimws(beatrina_readline("Selection: "))
610
+ ind <- if (grepl("^[0-9]", ans)) suppressWarnings(as.integer(sub("^([0-9]+).*$", "\\1", ans)))
611
+ else match(ans, choices, nomatch = nc + 1L)
612
+ if (!is.na(ind) && ind <= nc) return(ind)
613
+ # gettext() trims the trailing newline off the message; it is said apart.
614
+ cat(gettext("Enter an item from the menu, or 0 to exit", domain = "R-utils"), "\n", sep = "")
615
+ ask()
616
+ }
617
+ ask()
618
+ }
619
+ attr(beatrina_menu, "beatrina") <- BASE_MENU
620
+
621
+ #' scan() from the console, through the announced reader. Lines are read the
622
+ #' way the console scan reads them — one prompt per line, numbered by the next
623
+ #' item, until a blank line, `nlines` lines, or `n`/`nmax` items — then the real
624
+ #' scan() parses them as `text`, so every argument keeps its meaning.
625
+ BASE_SCAN <- if (is.null(attr(base::scan, "beatrina"))) base::scan else attr(base::scan, "beatrina")
626
+ beatrina_scan <- BASE_SCAN
627
+ body(beatrina_scan) <- quote({
628
+ call <- match.call()
629
+ call[[1L]] <- BASE_SCAN
630
+ from_console <- is.character(file) && identical(file, "") && missing(text)
631
+ if (!from_console) return(eval(call, parent.frame()))
632
+ per_line <- function(line) {
633
+ parts <- if (nzchar(sep)) strsplit(line, sep, fixed = TRUE)[[1L]] else strsplit(trimws(line), "[ \t]+")[[1L]]
634
+ length(parts[nzchar(parts)])
635
+ }
636
+ want <- max(c(n, nmax, 0L)[c(n, nmax, 0L) > 0L][1L], 0L, na.rm = TRUE)
637
+ collect <- function(lines, items) {
638
+ if (nlines > 0L && length(lines) >= nlines) return(lines)
639
+ if (want > 0L && items >= want) return(lines)
640
+ line <- beatrina_readline(sprintf("%d: ", items + 1L))
641
+ if (!nzchar(trimws(line))) return(lines)
642
+ collect(c(lines, line), items + per_line(line))
643
+ }
644
+ call$file <- NULL
645
+ call$text <- paste(collect(character(), 0L), collapse = "\n")
646
+ eval(call, parent.frame())
647
+ })
648
+ attr(beatrina_scan, "beatrina") <- BASE_SCAN
649
+ attr(beatrina_readline, "beatrina") <- BASE_READLINE
650
+ environment(beatrina_scan) <- environment()
651
+
652
+ beatrina_patch("readline", beatrina_readline, "base")
653
+ beatrina_patch("scan", beatrina_scan, "base")
654
+ beatrina_patch("menu", beatrina_menu, "utils")
655
+
656
+ #' Never ask "Hit <Return> to see next plot".
657
+ #'
658
+ #' This worker is interactive, so a plot that turns page prompting on —
659
+ #' tna's plot() for cliques does `par(ask = TRUE)` by default — makes the
660
+ #' graphics engine read the console before each new page. That read is not
661
+ #' readline(), so nothing announces it: the chunk spun forever and every
662
+ #' chunk behind it queued (test/lab-rmd.e2e, the lab's `plot(cliques_of_two,
663
+ #' 4)`). Every page is captured as its own figure here, so there is no page to
664
+ #' wait for. The hooks run before the engine's check on every new page, base
665
+ #' graphics and grid alike; a package's own `par(ask = TRUE)` is left in place
666
+ #' and simply never gets to ask.
667
+ #'
668
+ #' @return Invisibly NULL.
669
+ beatrina_no_page_prompt <- function() {
670
+ if (isTRUE(grDevices::devAskNewPage())) grDevices::devAskNewPage(FALSE)
671
+ invisible(NULL)
672
+ }
673
+ local({
674
+ already <- function(hook) any(vapply(getHook(hook), identical, logical(1), beatrina_no_page_prompt))
675
+ if (!already("before.plot.new")) setHook("before.plot.new", beatrina_no_page_prompt)
676
+ if (!already("before.grid.newpage")) setHook("before.grid.newpage", beatrina_no_page_prompt)
677
+ })
678
+
474
679
  #' A function's formals as one display string, defaults included.
475
680
  #'
476
681
  #' `formals()` is NULL for primitives like `sum`; `args()` still knows their
@@ -651,9 +856,9 @@ emit_struct <- function(id, name = NULL, path = NULL) {
651
856
  child_get(o, k)
652
857
  }
653
858
  node <- tryCatch(Reduce(step, keys, init = get(name, envir = globalenv())),
654
- error = function(e) structure(class = "carmar_fail",
859
+ error = function(e) structure(class = "beatrina_fail",
655
860
  list(msg = conditionMessage(e))))
656
- if (inherits(node, "carmar_fail")) {
861
+ if (inherits(node, "beatrina_fail")) {
657
862
  emit(list(type = "struct", id = id, name = name, path = as.list(keys),
658
863
  error = node$msg))
659
864
  return(invisible(NULL))
@@ -955,6 +1160,88 @@ emit_colstats <- function(id, name, column, query = NULL, filters = NULL) {
955
1160
  )
956
1161
  }
957
1162
 
1163
+ #' The grid's copy door, R's half: one column, every row, as text.
1164
+ #'
1165
+ #' `Copy Column (all rows, through R)` (lib/panel.js `columnValues`,
1166
+ #' lib/command-system.js): the page holds one window of rows, so a whole
1167
+ #' column has to come from here. Same object resolution, same query and
1168
+ #' filters as `view` and `colstats`, so what is copied is the column of the
1169
+ #' rows the grid is showing — every one of them, not one page. Written the way
1170
+ #' `write.csv` writes it (header quoted, text quoted, numbers at full
1171
+ #' precision, NA bare, no row names) to a temporary file and read back: no
1172
+ #' sink, nothing evaluated but the name. Read-only.
1173
+ #'
1174
+ #' Bounded at COLUMN_VALUES_MAX values and refused above it with a sentence:
1175
+ #' a twenty-million-line clipboard is not a copy, it is an export, and the
1176
+ #' Export door sits one row down in the same menu.
1177
+ #'
1178
+ #' @param id Request id.
1179
+ #' @param name Object name or expression, as for `view`.
1180
+ #' @param column The column to copy.
1181
+ #' @param query,filters The viewer's active search and column filters.
1182
+ COLUMN_VALUES_MAX <- 1000000L
1183
+
1184
+ emit_column_values <- function(id, name, column, query = NULL, filters = NULL) {
1185
+ column <- if (is.character(column) && length(column)) column[[1L]] else ""
1186
+ reply <- function(...) emit(list(type = "column_values", id = id, name = name,
1187
+ column = column, ...))
1188
+ if (!is.character(name) || length(name) != 1L || is.na(name) || !nzchar(name)) {
1189
+ reply(error = "bad name")
1190
+ return(invisible(NULL))
1191
+ }
1192
+ obj <- tryCatch(eval(parse(text = name), globalenv()), error = function(e) NULL)
1193
+ # NULL is checked BEFORE coercion: as.data.frame(NULL) is an empty frame,
1194
+ # not NULL, and a missing object would otherwise read as "no such column".
1195
+ if (is.null(obj)) {
1196
+ reply(error = "not found")
1197
+ return(invisible(NULL))
1198
+ }
1199
+ if (!is.data.frame(obj)) obj <- tryCatch(as.data.frame(obj, stringsAsFactors = FALSE),
1200
+ error = function(e) NULL)
1201
+ if (is.null(obj) || !nzchar(column)) {
1202
+ reply(error = "not found")
1203
+ return(invisible(NULL))
1204
+ }
1205
+ total <- nrow(obj)
1206
+ narrowed <- view_filter(obj, query, filters)
1207
+ obj <- narrowed$obj
1208
+ hit <- match(column, names(obj))
1209
+ if (is.na(hit)) hit <- match(column, substr(names(obj), 1L, MAX_VIEW_LABEL_CHARS))
1210
+ if (is.na(hit)) {
1211
+ reply(error = "no such column")
1212
+ return(invisible(NULL))
1213
+ }
1214
+ n <- nrow(obj)
1215
+ if (n > COLUMN_VALUES_MAX) {
1216
+ reply(error = sprintf("%s has %s values; copying through R stops at %s. Export the table instead.",
1217
+ column, format(n, big.mark = ","),
1218
+ format(COLUMN_VALUES_MAX, big.mark = ",")),
1219
+ rows = n, totalRows = total)
1220
+ return(invisible(NULL))
1221
+ }
1222
+ # A temporary file, not a textConnection: the connection grows its vector a
1223
+ # line at a time and took 16 s for 100,000 rows where the file takes 50 ms
1224
+ # (measured 2026-09-17, identical text). UTF-8 both ways, so Windows' native
1225
+ # code page never enters the text. as.data.frame() first: a tibble or
1226
+ # data.table has its own `[`, and this must be base's one-column frame.
1227
+ tf <- tempfile("column-values-", fileext = ".csv")
1228
+ on.exit(unlink(tf), add = TRUE)
1229
+ written <- tryCatch({
1230
+ utils::write.csv(as.data.frame(obj)[hit], tf, row.names = FALSE, fileEncoding = "UTF-8")
1231
+ TRUE
1232
+ }, interrupt = function(i) FALSE, error = function(e) e)
1233
+ if (isTRUE(written)) {
1234
+ reply(text = paste(readLines(tf, encoding = "UTF-8", warn = FALSE), collapse = "\n"),
1235
+ rows = n, totalRows = total,
1236
+ filtered = nzchar(narrowed$query) || narrowed$count > 0L)
1237
+ } else if (isFALSE(written)) {
1238
+ reply(error = "interrupted")
1239
+ } else {
1240
+ reply(error = conditionMessage(written))
1241
+ }
1242
+ invisible(NULL)
1243
+ }
1244
+
958
1245
  #' Autocomplete via R's own engine — the one behind TAB in the console,
959
1246
  #' RStudio and Jupyter. Not hand-rolled: the engine already understands `$`
960
1247
  #' and `@` access, argument names inside a call, `::` namespaces, library()
@@ -1051,15 +1338,125 @@ emit_complete <- function(id, line = NULL, cursor = NULL, max_items = MAX_COMPLE
1051
1338
  }
1052
1339
  list(value = v, kind = "variable", detail = detail)
1053
1340
  }
1341
+ comps <- c(st$comps, case_blind_extras(st$token, st$comps, line, st$start, quoted))
1342
+ # R answers a `df$xyz` that matches nothing with the bare stub `df$`. As a
1343
+ # completion that is worse than none: accepting it REPLACES the token, so it
1344
+ # deletes the `xyz` the user typed and leaves incomplete syntax behind. It is
1345
+ # dropped only when the token already carries the separator — offering `x$`
1346
+ # for a token of `x`, or `stats::` for `stat`, is the useful "there is more
1347
+ # inside this" hint RStudio gives, and those stay. `pkg::` behaves the same
1348
+ # way as `$` and `@` here, so all three are one rule.
1349
+ SEP_END <- "([$@]|:::?)$"
1350
+ if (grepl("([$@]|::)", st$token) && !grepl(SEP_END, st$token)) {
1351
+ comps <- comps[!grepl(SEP_END, comps)]
1352
+ }
1054
1353
  frame <- list(type = "complete", id = id, start = st$start, end = cursor,
1055
1354
  token = st$token,
1056
- items = lapply(utils::head(st$comps, max_items), describe_item),
1057
- truncated = length(st$comps) > max_items)
1355
+ items = lapply(utils::head(comps, max_items), describe_item),
1356
+ truncated = length(comps) > max_items)
1058
1357
  frame$args <- I(completion_call_args(fn))
1059
1358
  frame$columns <- completion_columns(data, max_items)
1060
1359
  emit(frame)
1061
1360
  }
1062
1361
 
1362
+ #' Names that match the typed token when case is ignored.
1363
+ #'
1364
+ #' R's own completion engine (`utils:::.completeToken`) matches case-SENSITIVELY
1365
+ #' and has no option not to: typing `rda` offers nothing at all for an object
1366
+ #' called `Rdata`, and `myv` offers nothing for `myVar`. The page's own
1367
+ #' candidates — the names it parses out of the notebook's source — have always
1368
+ #' matched case-blind (`lib/completion-merge.js`), so the same keystroke
1369
+ #' completed a name written in a chunk and did NOT complete the same name once
1370
+ #' it existed in the session. One editor, two rules, decided by where the name
1371
+ #' happened to live.
1372
+ #'
1373
+ #' Case is ignored for EVERY token, `myV` as much as `rda`. A first shape of
1374
+ #' this left mixed-case tokens to R's exact matching on the theory that a
1375
+ #' capital typed mid-word was deliberate — but the page's parsed candidates
1376
+ #' never made that distinction, so it would have rebuilt the same split this
1377
+ #' function exists to close, one spelling further along. Completion is case
1378
+ #' insensitive; there is no second rule.
1379
+ #'
1380
+ #' These are EXTRAS, appended: an exact-case match is still found the way it
1381
+ #' always was, and `lib/completion-merge.js` already sorts a prefix match that
1382
+ #' differs in case below one that does not, so nothing moves in a list that was
1383
+ #' already right.
1384
+ #'
1385
+ #' Refused where a dotted or quoted context means the names are not the search
1386
+ #' path's: `df$`, `obj@`, `pkg::` and a path inside a string are R's to answer,
1387
+ #' and sweeping globals into them would offer names that cannot be used there.
1388
+ #'
1389
+ #' Measured at 2.4 ms over a 2,482-name search path — one sweep per keystroke
1390
+ #' that reaches the kernel, against a round trip that costs more than that.
1391
+ #'
1392
+ #' Every context R answers is covered, because "autocomplete is case
1393
+ #' insensitive" with an exception is two rules again: a bare name against the
1394
+ #' search path, `df$col`, `obj@slot`, and `pkg::name`. Each gets its OWN
1395
+ #' candidate set — the object's names, the object's slots, the namespace's
1396
+ #' exports — so nothing is offered where it could not be used.
1397
+ #'
1398
+ #' NOTHING IS EVALUATED to answer a keystroke. The head of a `$`/`@`/`::` is
1399
+ #' accepted only as a plain name and looked up with `get0`, and a namespace is
1400
+ #' read only when it is ALREADY loaded — the same rule
1401
+ #' `completion_call_args()` follows, for the same reason.
1402
+ #'
1403
+ #' @param token The token R guessed, as typed. R reports the WHOLE token for a
1404
+ #' dotted context (`x$al`, not `al`), which is what makes splitting it here
1405
+ #' the right place rather than re-deriving the head from the line.
1406
+ #' @param comps What R already matched, so nothing is offered twice.
1407
+ #' @param line The whole line, for the character before the token.
1408
+ #' @param start R's own start offset for the token (1-based, before the token).
1409
+ #' @param quoted Whether the token sits inside a string.
1410
+ #' @return Character vector of extra completions, spelled as R spells them
1411
+ #' (prefix included for a dotted context); empty when the context is one this
1412
+ #' cannot answer.
1413
+ case_blind_extras <- function(token, comps, line, start, quoted) {
1414
+ if (isTRUE(quoted)) return(character(0))
1415
+ if (!is.character(token) || length(token) != 1L || is.na(token)) return(character(0))
1416
+ # A prefix is safe to splice back only because `is_completion_name` vets the
1417
+ # head; the one regex metacharacter either half can carry is then the dot.
1418
+ starts_with <- function(pool, tail) {
1419
+ if (!length(pool)) return(character(0))
1420
+ pattern <- paste0("^", gsub(".", "\\.", tail, fixed = TRUE))
1421
+ pool[grepl(pattern, pool, ignore.case = TRUE)]
1422
+ }
1423
+ # `x$al` / `obj@sl` / `pkg::na` — R hands the whole thing over as one token.
1424
+ dotted <- regmatches(token, regexec("^(.*?)(\\$|@|:::?)([^$@:]*)$", token))[[1L]]
1425
+ if (length(dotted) == 4L) {
1426
+ head <- dotted[[2L]]; sep <- dotted[[3L]]; tail <- dotted[[4L]]
1427
+ if (!is_completion_name(head)) return(character(0))
1428
+ pool <- if (sep %in% c("::", ":::")) {
1429
+ if (head %in% loadedNamespaces())
1430
+ tryCatch(ls(asNamespace(head)), error = function(...) character(0))
1431
+ else character(0)
1432
+ } else {
1433
+ obj <- tryCatch(get0(head, envir = globalenv()), error = function(...) NULL)
1434
+ if (is.null(obj)) character(0)
1435
+ else if (identical(sep, "@")) tryCatch(methods::slotNames(obj), error = function(...) character(0))
1436
+ else if (is.environment(obj)) tryCatch(ls(obj), error = function(...) character(0))
1437
+ else tryCatch(names(obj), error = function(...) character(0))
1438
+ }
1439
+ matched <- starts_with(pool, tail)
1440
+ # `paste0("x", "$", character(0))` is "x$", not nothing — paste recycles a
1441
+ # zero-length argument as "". Without this the empty answer for an object
1442
+ # that does not exist came back as a completion to the bare prefix.
1443
+ if (!length(matched)) return(character(0))
1444
+ hits <- paste0(head, sep, matched)
1445
+ return(setdiff(hits, c(comps, sub("\\($", "", comps))))
1446
+ }
1447
+ if (!is_completion_name(token)) return(character(0))
1448
+ # A bare tail sitting right after a separator is a shape this cannot place —
1449
+ # the head is not in the token, so refuse rather than sweep in globals that
1450
+ # could not be used there.
1451
+ before <- if (start > 1L) substr(line, start - 1L, start - 1L) else ""
1452
+ if (before %in% c("$", "@", ":")) return(character(0))
1453
+ names <- unique(unlist(lapply(search(), function(e)
1454
+ tryCatch(ls(e), error = function(...) character(0)))))
1455
+ # Drop what R already offered, in either of the two spellings it ships
1456
+ # (`name` and `name(` for a function).
1457
+ setdiff(starts_with(names, token), c(comps, sub("\\($", "", comps)))
1458
+ }
1459
+
1063
1460
  #' A bare R name, or `pkg::name` — the only shape a completion context may
1064
1461
  #' name. Anything else (a call, a subset, an expression) is refused, because
1065
1462
  #' these names are looked up, and a lookup must never become an evaluation.
@@ -1110,6 +1507,57 @@ completion_columns <- function(data, max_items = MAX_COMPLETIONS) {
1110
1507
  lapply(cols, function(nm) list(value = nm, type = class(df[[nm]])[1L]))
1111
1508
  }
1112
1509
 
1510
+ #' A progress callback for keep_save()/keep_resume() that emits id-less
1511
+ #' `keep_progress` frames — the supervisor broadcasts an id-less frame to every
1512
+ #' page, which is what lets the page that asked AND a page that follows a
1513
+ #' handoff both draw it. Throttled to four a second; the last one always goes.
1514
+ #' @param phase "save" or "restore".
1515
+ #' @return function(done, total, bytes, name).
1516
+ keep_progress_emitter <- function(phase) {
1517
+ last <- 0
1518
+ function(done, total, bytes = 0, name = "") {
1519
+ now <- as.numeric(Sys.time())
1520
+ if (done < total && now - last < 0.25) return(invisible(NULL))
1521
+ last <<- now
1522
+ emit(list(type = "keep_progress", phase = phase, done = done, total = total,
1523
+ bytes = as.numeric(bytes %||% 0), name = as.character(name)[1L]))
1524
+ }
1525
+ }
1526
+
1527
+ #' "Restart, keep variables", step one: save the global environment
1528
+ #' (spike/workspace-keep.R). No deadline — Stop (the supervisor's
1529
+ #' suspend_cancel) interrupts it, and the partial copy is removed.
1530
+ #' @param id Request id.
1531
+ #' @return Invisibly NULL. Emits one `suspend` frame: token, saved, bytes,
1532
+ #' skipped — or cancelled, or error.
1533
+ emit_suspend <- function(id) {
1534
+ out <- tryCatch(keep_save(globalenv(), progress = keep_progress_emitter("save")),
1535
+ error = function(e) list(error = conditionMessage(e)),
1536
+ interrupt = function(i) list(cancelled = TRUE))
1537
+ emit(c(list(type = "suspend", id = id), out))
1538
+ }
1539
+
1540
+ #' "Restart, keep variables", step two, in the NEW worker: load the kept
1541
+ #' objects back and restore the working directory. Asked by the supervisor
1542
+ #' only, never by a page (it is not in FORWARDED), with a token the save minted.
1543
+ #' @param id Request id.
1544
+ #' @param token The kept workspace's token.
1545
+ #' @return Invisibly NULL. Emits one `resume` frame: restored, failed, skipped,
1546
+ #' cwd — or error.
1547
+ emit_resume <- function(id, token) {
1548
+ out <- tryCatch(keep_resume(token, globalenv(), progress = keep_progress_emitter("restore")),
1549
+ error = function(e) list(error = conditionMessage(e)),
1550
+ interrupt = function(i) list(error = "The restore was interrupted."))
1551
+ # The directory R was in comes back too — unless it is gone (the old R's
1552
+ # tempdir() dies with the old R), which the report then says in words.
1553
+ if (is.character(out$wd) && nzchar(out$wd)) {
1554
+ moved <- dir.exists(out$wd) && !inherits(try(setwd(out$wd), silent = TRUE), "try-error")
1555
+ if (!moved) out$wd_missing <- out$wd
1556
+ }
1557
+ out$wd <- NULL
1558
+ emit(c(list(type = "resume", id = id, cwd = getwd()), out))
1559
+ }
1560
+
1113
1561
  #' Coerce a wire-supplied count, falling back rather than erroring: a malformed
1114
1562
  #' offset from a client must degrade to the default, not kill the pane.
1115
1563
  #'
@@ -1397,14 +1845,14 @@ emit_sniff <- function(id, path = NULL, opts = NULL) {
1397
1845
  if (!file.exists(p) || dir.exists(p)) return(fail("not a readable file"))
1398
1846
  }
1399
1847
  if (!exists("sniff_file", inherits = TRUE)) {
1400
- return(fail("this kernel is too old for the import wizard - restart CarmaR"))
1848
+ return(fail("this kernel is too old for the import wizard - restart Beatrina"))
1401
1849
  }
1402
1850
  res <- tryCatch(
1403
1851
  sniff_file(p, if (is.list(opts)) opts else list()),
1404
- error = function(e) structure(class = "carmar_fail", list(msg = conditionMessage(e))),
1405
- interrupt = function(i) structure(class = "carmar_fail", list(msg = "interrupted"))
1852
+ error = function(e) structure(class = "beatrina_fail", list(msg = conditionMessage(e))),
1853
+ interrupt = function(i) structure(class = "beatrina_fail", list(msg = "interrupted"))
1406
1854
  )
1407
- if (inherits(res, "carmar_fail")) return(fail(res$msg))
1855
+ if (inherits(res, "beatrina_fail")) return(fail(res$msg))
1408
1856
  emit(c(list(type = "sniff", id = id), res))
1409
1857
  }
1410
1858
 
@@ -1443,10 +1891,10 @@ emit_import <- function(id, path = NULL, name = NULL) {
1443
1891
  }
1444
1892
  obj <- tryCatch(
1445
1893
  if (requireNamespace("rio", quietly = TRUE)) rio::import(p) else read_by_ext(p),
1446
- error = function(e) structure(class = "carmar_fail", list(msg = conditionMessage(e))),
1447
- interrupt = function(i) structure(class = "carmar_fail", list(msg = "interrupted"))
1894
+ error = function(e) structure(class = "beatrina_fail", list(msg = conditionMessage(e))),
1895
+ interrupt = function(i) structure(class = "beatrina_fail", list(msg = "interrupted"))
1448
1896
  )
1449
- if (inherits(obj, "carmar_fail")) return(fail(obj$msg))
1897
+ if (inherits(obj, "beatrina_fail")) return(fail(obj$msg))
1450
1898
  nm <- if (is.character(name) && length(name) == 1L && nzchar(name)) name
1451
1899
  else make.names(tools::file_path_sans_ext(basename(p)))
1452
1900
  assign(nm, obj, envir = globalenv())
@@ -1709,14 +2157,14 @@ emit_package_action <- function(id, action, name, lib = NULL) {
1709
2157
 
1710
2158
  #' Project and renv state, without paths or lockfile contents on the wire.
1711
2159
  emit_project_status <- function(id) {
1712
- result <- tryCatch(carmar_project_status(), error = function(e)
2160
+ result <- tryCatch(beatrina_project_status(), error = function(e)
1713
2161
  list(error = conditionMessage(e)))
1714
2162
  emit(c(list(type = "project_status", id = id), result))
1715
2163
  }
1716
2164
 
1717
2165
  #' One explicit, visible environment action: install renv or restore its lock.
1718
2166
  emit_project_action <- function(id, action) {
1719
- result <- tryCatch(carmar_project_action(action), error = function(e)
2167
+ result <- tryCatch(beatrina_project_action(action), error = function(e)
1720
2168
  list(ok = FALSE, error = conditionMessage(e)))
1721
2169
  emit(c(list(type = "project_action", id = id, action = action), result))
1722
2170
  }
@@ -1777,7 +2225,7 @@ emit_help <- function(id, topic) {
1777
2225
  } else {
1778
2226
  txt <- paste(utils::capture.output(base::print(paths)), collapse = "\n")
1779
2227
  if (!nzchar(trimws(txt))) NULL
1780
- else paste0("<pre class=\"carmar-help-text\">",
2228
+ else paste0("<pre class=\"beatrina-help-text\">",
1781
2229
  gsub("<", "&lt;", gsub("&", "&amp;", txt), fixed = TRUE), "</pre>")
1782
2230
  }
1783
2231
  }, error = function(e) NULL)
@@ -1927,7 +2375,7 @@ emit_wd <- function(id, path = NULL) {
1927
2375
 
1928
2376
  # The file system lives in fileio.R (shared with the supervisor). Each op is
1929
2377
  # `emit(fs_x(...))`: fileio.R returns the frame, this process sends it. The
1930
- # worker still answers them when the supervisor forwards (CARMAR_FILE_OPS=worker,
2378
+ # worker still answers them when the supervisor forwards (BEATRINA_FILE_OPS=worker,
1931
2379
  # the transition flag), so nothing here may diverge from fileio.R.
1932
2380
  emit_files <- function(id, path = NULL, all = FALSE) emit(fs_files(id, path, all, wd = getwd()))
1933
2381
  emit_mkdir <- function(id, path = NULL) emit(fs_mkdir(id, path, wd = getwd()))
@@ -1940,7 +2388,7 @@ emit_writefile <- function(id, path = NULL, text = "", expected = NULL, encoding
1940
2388
  emit(fs_writefile(id, path, text, expected, encoding, base64, wd = getwd()))
1941
2389
  emit_writefiles_atomic <- function(id, files = NULL) emit(fs_writefiles_atomic(id, files, wd = getwd()))
1942
2390
 
1943
- #' Check whether CarmaR's current R session is usable without collecting user
2391
+ #' Check whether Beatrina's current R session is usable without collecting user
1944
2392
  #' material.
1945
2393
  #'
1946
2394
  #' This reply is deliberately a FACT ALLOW-LIST. It never contains getwd(),
@@ -1954,12 +2402,12 @@ emit_writefiles_atomic <- function(id, files = NULL) emit(fs_writefiles_atomic(i
1954
2402
  emit_doctor <- function(id) {
1955
2403
  can_create_in <- function(dir) {
1956
2404
  if (!is.character(dir) || length(dir) != 1L || !dir.exists(dir)) return(FALSE)
1957
- probe <- tempfile(".carmar-doctor-", tmpdir = dir)
2405
+ probe <- tempfile(".beatrina-doctor-", tmpdir = dir)
1958
2406
  on.exit(unlink(probe, force = TRUE), add = TRUE)
1959
2407
  isTRUE(tryCatch(file.create(probe, showWarnings = FALSE), error = function(e) FALSE))
1960
2408
  }
1961
2409
 
1962
- q_configured <- trimws(Sys.getenv("CARMAR_QUARTO", ""))
2410
+ q_configured <- trimws(Sys.getenv("BEATRINA_QUARTO", ""))
1963
2411
  q_path <- if (nzchar(q_configured)) q_configured else unname(Sys.which("quarto"))
1964
2412
  q_available <- is.character(q_path) && length(q_path) == 1L && nzchar(q_path) && file.exists(q_path)
1965
2413
  q_version <- ""
@@ -2005,16 +2453,26 @@ emit_doctor <- function(id) {
2005
2453
  version = q_version
2006
2454
  ),
2007
2455
  required_packages = required,
2008
- deployment = list(
2009
- loopback = Sys.getenv("CARMAR_BIND", "127.0.0.1") %in% c("", "127.0.0.1", "localhost", "::1"),
2010
- confinement = nzchar(Sys.getenv("CARMAR_ROOT", "")),
2011
- audit = nzchar(Sys.getenv("CARMAR_LOG", "")),
2012
- ai_local_only = identical(Sys.getenv("CARMAR_AI_LOCAL_ONLY", ""), "1"),
2013
- ai_policy = nzchar(Sys.getenv("CARMAR_AI_POLICY", "")) || nzchar(Sys.getenv("CARMAR_AI_PROVIDERS", "")),
2014
- origin_required = !identical(Sys.getenv("CARMAR_REQUIRE_ORIGIN", "1"), "0"),
2015
- trusted_proxy = identical(Sys.getenv("CARMAR_TRUST_PROXY", ""), "1"),
2016
- unauthenticated = identical(Sys.getenv("CARMAR_ALLOW_UNAUTHENTICATED", ""), "1")
2017
- ),
2456
+ # ASKED, not re-derived. Doctor's own copy of the posture rule had drifted:
2457
+ # `!identical(Sys.getenv("BEATRINA_REQUIRE_ORIGIN", "1"), "0")` is TRUE
2458
+ # whenever the variable is unset, while the real rule (deployment.R, and
2459
+ # host/deployment.mjs, which agree) is `== "1" || !loopback` — FALSE on a
2460
+ # default desktop session. So Doctor reported an origin check the host was
2461
+ # not enforcing, and lib/doctor.js computes its managed-remote verdict from
2462
+ # exactly these fields. The loopback list had drifted too, missing "[::1]".
2463
+ deployment = local({
2464
+ d <- beatrina_deployment(as.integer(Sys.getenv("BEATRINA_PORT", "0")))
2465
+ list(
2466
+ loopback = isTRUE(d$loopback),
2467
+ confinement = nzchar(Sys.getenv("BEATRINA_ROOT", "")),
2468
+ audit = nzchar(Sys.getenv("BEATRINA_LOG", "")),
2469
+ ai_local_only = identical(Sys.getenv("BEATRINA_AI_LOCAL_ONLY", ""), "1"),
2470
+ ai_policy = nzchar(Sys.getenv("BEATRINA_AI_POLICY", "")) || nzchar(Sys.getenv("BEATRINA_AI_PROVIDERS", "")),
2471
+ origin_required = isTRUE(d$require_origin),
2472
+ trusted_proxy = isTRUE(d$trust_proxy),
2473
+ unauthenticated = isTRUE(d$allow_unauthenticated)
2474
+ )
2475
+ }),
2018
2476
  proxy = list(
2019
2477
  http = nzchar(Sys.getenv("HTTP_PROXY", "")) || nzchar(Sys.getenv("http_proxy", "")),
2020
2478
  https = nzchar(Sys.getenv("HTTPS_PROXY", "")) || nzchar(Sys.getenv("https_proxy", "")),
@@ -2033,9 +2491,9 @@ emit_doctor <- function(id) {
2033
2491
  #' @param seq Index of the top-level expression, to keep filenames unique.
2034
2492
  #' @return Invisibly NULL.
2035
2493
  open_plot_device <- function(dir, seq, dims = NULL) {
2036
- width <- if (!is.null(dims$width)) dims$width else PLOT_WIDTH
2037
- height <- if (!is.null(dims$height)) dims$height else PLOT_HEIGHT
2038
- res <- if (!is.null(dims$res)) dims$res else PLOT_RES
2494
+ width <- dim_or(dims$width, "width")
2495
+ height <- dim_or(dims$height, "height")
2496
+ res <- dim_or(dims$res, "res")
2039
2497
  if (plot_is_svg(dims)) {
2040
2498
  # A vector device is sized in INCHES and has no resolution at all — which
2041
2499
  # is the whole point: its cost does not grow with dpi. The client sends
@@ -2088,9 +2546,9 @@ emit_plot_frame <- function(id, f, dims = NULL) {
2088
2546
  # older client would have rendered nothing at all.
2089
2547
  svg <- grepl("\\.svg$", f)
2090
2548
  emit(list(type = "plot", id = id, mime = if (svg) "image/svg+xml" else "image/png",
2091
- width = if (!is.null(dims$width)) dims$width else PLOT_WIDTH,
2092
- height = if (!is.null(dims$height)) dims$height else PLOT_HEIGHT,
2093
- res = if (!is.null(dims$res)) dims$res else PLOT_RES,
2549
+ width = dim_or(dims$width, "width"),
2550
+ height = dim_or(dims$height, "height"),
2551
+ res = dim_or(dims$res, "res"),
2094
2552
  data = jsonlite::base64_enc(readBin(f, "raw", file.info(f)$size))))
2095
2553
  invisible(NULL)
2096
2554
  }
@@ -2121,9 +2579,9 @@ emit_plot_frame <- function(id, f, dims = NULL) {
2121
2579
  .blank_png_cache <- new.env(parent = emptyenv())
2122
2580
  is_blank_plot <- function(f, dims) {
2123
2581
  if (!identical(RASTER_DEVICE, "ragg")) return(FALSE)
2124
- w <- if (!is.null(dims$width)) dims$width else PLOT_WIDTH
2125
- h <- if (!is.null(dims$height)) dims$height else PLOT_HEIGHT
2126
- r <- if (!is.null(dims$res)) dims$res else PLOT_RES
2582
+ w <- dim_or(dims$width, "width")
2583
+ h <- dim_or(dims$height, "height")
2584
+ r <- dim_or(dims$res, "res")
2127
2585
  key <- paste(w, h, r, sep = "x")
2128
2586
  blank <- .blank_png_cache[[key]]
2129
2587
  if (is.null(blank)) {
@@ -2315,6 +2773,16 @@ emit_dataframe <- function(id, df, source = NULL) {
2315
2773
  # jsonlite drops a NULL, so a frame with no name is byte-identical to what
2316
2774
  # this emitted before the field existed.
2317
2775
  source = source,
2776
+ # How many significant digits this document asked its numbers to be SHOWN
2777
+ # with. The values on the wire stay full precision — the page exports and
2778
+ # copies from them, and rounding here would write 0 for 1.2e-5 the way
2779
+ # jsonlite's own digits = 4 once did. So `options(digits = 3)` becomes a
2780
+ # display instruction the table renderer honours, instead of a call that
2781
+ # looks like configuration and changes nothing: auto-printing a data.frame
2782
+ # never passes through print.data.frame, which is what would have applied
2783
+ # it. NULL (dropped by jsonlite) whenever the document left R's default of
2784
+ # 7 alone, so an untouched notebook's frames are unchanged.
2785
+ digits = display_digits(),
2318
2786
  rows = head_df
2319
2787
  ))
2320
2788
  invisible(NULL)
@@ -2383,7 +2851,7 @@ widget_standalone_html <- function(w, fit = FALSE) {
2383
2851
  "<style>html,body{margin:0;padding:0;height:100%;}</style>"
2384
2852
  }
2385
2853
  fit_js <- if (fit) paste0(
2386
- "<script>(function(){var r=function(){try{parent.postMessage({carmarHtmlHeight:",
2854
+ "<script>(function(){var r=function(){try{parent.postMessage({beatrinaHtmlHeight:",
2387
2855
  "Math.ceil(document.documentElement.getBoundingClientRect().height)},'*')}catch(e){}};",
2388
2856
  "window.addEventListener('load',r);if(window.ResizeObserver){new ResizeObserver(r)",
2389
2857
  ".observe(document.documentElement)}setTimeout(r,0);setTimeout(r,250)})()</script>") else ""
@@ -2411,7 +2879,7 @@ widget_standalone_html <- function(w, fit = FALSE) {
2411
2879
  #' when `v` is not HTML.
2412
2880
  rich_html_of <- function(v) {
2413
2881
  if (inherits(v, "htmlwidget")) return(list(x = v, kind = "widget"))
2414
- if (inherits(v, "carmar_html")) return(list(x = htmltools::HTML(unclass(v)), kind = "html"))
2882
+ if (inherits(v, "beatrina_html")) return(list(x = htmltools::HTML(unclass(v)), kind = "html"))
2415
2883
  if (inherits(v, c("shiny.tag", "shiny.tag.list", "html"))) return(list(x = v, kind = "html"))
2416
2884
  if (inherits(v, "knit_asis")) return(list(x = htmltools::HTML(paste(as.character(v), collapse = "\n")), kind = "html"))
2417
2885
  if (inherits(v, "knitr_kable") && identical(attr(v, "format"), "html")) {
@@ -2593,7 +3061,7 @@ apply_fn_breaks <- function() {
2593
3061
  suppressMessages(trace(
2594
3062
  h$name,
2595
3063
  tracer = bquote({
2596
- .carmar_debug_entered(.(file), .(line), "breakpoint")
3064
+ .beatrina_debug_entered(.(file), .(line), "breakpoint")
2597
3065
  browser()
2598
3066
  }),
2599
3067
  at = h$at, where = h$env, print = FALSE))
@@ -2657,7 +3125,7 @@ debug_stack <- function(calls, frames, max_frames = 40L) {
2657
3125
  # frame's environment holds the TRACER EXPRESSION as an unforced promise —
2658
3126
  # mget() forces what it reads, so describing that frame re-entered the
2659
3127
  # tracer recursively (a second paused frame, a browser "Called from: mget").
2660
- ours <- "^(\\.carmar_debug_entered|\\.carmar_debug_where|\\.doTrace|browser\\(|eval\\(expr|eval\\.parent|Reduce\\(|\\{)"
3128
+ ours <- "^(\\.beatrina_debug_entered|\\.beatrina_debug_where|\\.doTrace|browser\\(|eval\\(expr|eval\\.parent|Reduce\\(|\\{)"
2661
3129
  while (to >= from && grepl(ours, texts[[to]])) to <- to - 1L
2662
3130
  if (to < from) return(list())
2663
3131
  idx <- seq.int(from, to)
@@ -2684,7 +3152,7 @@ debug_stack <- function(calls, frames, max_frames = 40L) {
2684
3152
  #'
2685
3153
  #' THE HARNESS IS TRIMMED. The bottom frames are this file's own
2686
3154
  #' (`run_cell`, `withCallingHandlers`, the `lapply` over expressions) and
2687
- #' showing them teaches the reader that their error came from CarmaR. Frames
3155
+ #' showing them teaches the reader that their error came from Beatrina. Frames
2688
3156
  #' are dropped up to and including the last `eval(e, globalenv())`, which is
2689
3157
  #' the boundary between our code and theirs.
2690
3158
  #'
@@ -2748,7 +3216,7 @@ capture_trace <- function(max_frames = 40L) {
2748
3216
  #' The narrow message fallback covers older R releases. In both cases the
2749
3217
  #' allow-list is deliberately the same one used by `package_action`, so an
2750
3218
  #' error message can never become code or an arbitrary install target.
2751
- carmar_missing_package <- function(error) {
3219
+ beatrina_missing_package <- function(error) {
2752
3220
  valid <- function(value) {
2753
3221
  is.character(value) && length(value) == 1L && !is.na(value) &&
2754
3222
  grepl("^[A-Za-z][A-Za-z0-9.]*$", value)
@@ -2776,12 +3244,12 @@ run_cell <- function(id, source, dims = NULL, srcname = NULL) {
2776
3244
  # one established outside the cell's own tryCatch, so the cell's exiting
2777
3245
  # handlers, being inner, are still the ones that take a real Stop.
2778
3246
  withCallingHandlers(run_cell_impl(id, source, dims, srcname),
2779
- interrupt = function(i) if (carmar_interrupt_is_stray(id)) carmar_resume_if_stray(i))
3247
+ interrupt = function(i) if (beatrina_interrupt_is_stray(id)) beatrina_resume_if_stray(i))
2780
3248
  }
2781
3249
  run_cell_impl <- function(id, source, dims = NULL, srcname = NULL) {
2782
3250
  stopifnot(is.character(source), length(source) == 1L)
2783
3251
  # First: a signal that arrived between commands must not end THIS cell.
2784
- carmar_drain_stray_interrupt(id)
3252
+ beatrina_drain_stray_interrupt(id)
2785
3253
  # The srcname is the chunk's identity in the debugger: functions defined
2786
3254
  # here carry it in their srcrefs, which is what lets findLineNum resolve
2787
3255
  # "chunk:<id>#line" to a function and a step. Wire-supplied; anything not a
@@ -2818,7 +3286,7 @@ run_cell_impl <- function(id, source, dims = NULL, srcname = NULL) {
2818
3286
  status <- "ok"
2819
3287
  detail <- NULL
2820
3288
  missing_package <- NULL
2821
- plot_dir <- tempfile("carmar-plots-")
3289
+ plot_dir <- tempfile("beatrina-plots-")
2822
3290
  dir.create(plot_dir)
2823
3291
  seen <- character(0)
2824
3292
  # Asked for vector, cannot give it: SAY SO. Falling back to a raster the
@@ -2829,7 +3297,7 @@ run_cell_impl <- function(id, source, dims = NULL, srcname = NULL) {
2829
3297
  emit(list(type = "stream", id = id, kind = "warning",
2830
3298
  text = paste("Vector output is not available in this R —",
2831
3299
  "install the svglite package (or XQuartz, for Cairo)",
2832
- "and restart. Drawing at", dims$res %||% PLOT_RES,
3300
+ "and restart. Drawing at", dim_or(dims$res, "res"),
2833
3301
  "dpi instead.\n")))
2834
3302
  }
2835
3303
 
@@ -2876,9 +3344,9 @@ run_cell_impl <- function(id, source, dims = NULL, srcname = NULL) {
2876
3344
  } else {
2877
3345
  NA_integer_
2878
3346
  }
2879
- # Stop, read at the boundary (carmar_stop_requested): the line after a
3347
+ # Stop, read at the boundary (beatrina_stop_requested): the line after a
2880
3348
  # busy call that no signal could interrupt does not run.
2881
- carmar_stop_if_requested(id)
3349
+ beatrina_stop_if_requested(id)
2882
3350
  # Where R is, for the page's progress bar (RStudio's green line
2883
3351
  # down the chunk): one frame per top-level expression, BEFORE it
2884
3352
  # runs, naming the lines it spans — so a 40-second model fit shows
@@ -2896,7 +3364,7 @@ run_cell_impl <- function(id, source, dims = NULL, srcname = NULL) {
2896
3364
  res <- if (!is.na(at_line) && at_line %in% bp_lines) {
2897
3365
  wrapped <- as.call(list(
2898
3366
  as.name("{"),
2899
- bquote(.carmar_debug_entered(.(srcname), .(at_line), "breakpoint")),
3367
+ bquote(.beatrina_debug_entered(.(srcname), .(at_line), "breakpoint")),
2900
3368
  quote(browser()),
2901
3369
  e))
2902
3370
  withVisible(eval(wrapped, globalenv()))
@@ -2934,11 +3402,11 @@ run_cell_impl <- function(id, source, dims = NULL, srcname = NULL) {
2934
3402
  }
2935
3403
  }))
2936
3404
  # …and after the last expression: a Stop that arrived while it ran ends the cell interrupted.
2937
- carmar_stop_if_requested(id)
3405
+ beatrina_stop_if_requested(id)
2938
3406
  },
2939
3407
  # The stray-signal rule (see run_cell), established INSIDE the cell's own
2940
3408
  # tryCatch so it is asked before the exiting handler below can unwind.
2941
- interrupt = function(i) if (carmar_interrupt_is_stray(id)) carmar_resume_if_stray(i),
3409
+ interrupt = function(i) if (beatrina_interrupt_is_stray(id)) beatrina_resume_if_stray(i),
2942
3410
  warning = function(w) {
2943
3411
  emit(list(type = "stream", id = id, kind = "warning",
2944
3412
  text = conditionMessage(w)))
@@ -2961,7 +3429,7 @@ run_cell_impl <- function(id, source, dims = NULL, srcname = NULL) {
2961
3429
  # dispatch loop and zombie the worker; invoking this restart instead
2962
3430
  # unwinds the browser and the rest of the cell, lands here, and the cell
2963
3431
  # reports itself stopped like any interrupt.
2964
- carmar_abort_cell = function() {
3432
+ beatrina_abort_cell = function() {
2965
3433
  status <<- "interrupted"
2966
3434
  detail <<- "Stopped from the debugger"
2967
3435
  }
@@ -2969,7 +3437,7 @@ run_cell_impl <- function(id, source, dims = NULL, srcname = NULL) {
2969
3437
  error = function(e) {
2970
3438
  status <<- "error"
2971
3439
  detail <<- conditionMessage(e)
2972
- missing_package <<- carmar_missing_package(e)
3440
+ missing_package <<- beatrina_missing_package(e)
2973
3441
  # A parse error has no stack and no expression index; its position comes
2974
3442
  # from the message instead, exactly as the analyzer reads it.
2975
3443
  emit(list(type = "traceback", id = id,
@@ -3008,13 +3476,13 @@ run_cell_impl <- function(id, source, dims = NULL, srcname = NULL) {
3008
3476
  #' on to the next line unless a second signal happens to land in the instant
3009
3477
  #' between system() returning and that line starting — on Linux it never did
3010
3478
  #' (`beatrina check`, 2026-09-16). So the supervisor also writes a flag file,
3011
- #' `stop-<id>` in CARMAR_STOP_DIR, and the cell reads it at every top-level
3479
+ #' `stop-<id>` in BEATRINA_STOP_DIR, and the cell reads it at every top-level
3012
3480
  #' expression boundary. The flag is removed when read; no directory, no flag.
3013
3481
  #'
3014
3482
  #' @param id The wire id of the running cell.
3015
3483
  #' @return TRUE when a stop was requested (and the flag consumed), else FALSE.
3016
- carmar_stop_requested <- function(id) {
3017
- dir <- Sys.getenv("CARMAR_STOP_DIR", "")
3484
+ beatrina_stop_requested <- function(id) {
3485
+ dir <- Sys.getenv("BEATRINA_STOP_DIR", "")
3018
3486
  if (!nzchar(dir) || !is.character(id) || length(id) != 1L || is.na(id)) return(FALSE)
3019
3487
  flag <- file.path(dir, paste0("stop-", id))
3020
3488
  if (!file.exists(flag)) return(FALSE)
@@ -3026,8 +3494,8 @@ carmar_stop_requested <- function(id) {
3026
3494
  #' through the same `interrupt` handler a signal reaches.
3027
3495
  #' @param id The wire id of the running cell.
3028
3496
  #' @return NULL, invisibly, when no stop was requested.
3029
- carmar_stop_if_requested <- function(id) {
3030
- if (carmar_stop_requested(id)) {
3497
+ beatrina_stop_if_requested <- function(id) {
3498
+ if (beatrina_stop_requested(id)) {
3031
3499
  signalCondition(structure(class = c("interrupt", "condition"),
3032
3500
  list(message = "Execution interrupted", call = NULL)))
3033
3501
  }
@@ -3037,23 +3505,23 @@ carmar_stop_if_requested <- function(id) {
3037
3505
  #' Is a stop flag for run `id` on disk? Read without consuming it.
3038
3506
  #' @param id The wire id of the running cell.
3039
3507
  #' @return TRUE when the supervisor has asked this run to stop.
3040
- carmar_stop_pending <- function(id) {
3041
- dir <- Sys.getenv("CARMAR_STOP_DIR", "")
3508
+ beatrina_stop_pending <- function(id) {
3509
+ dir <- Sys.getenv("BEATRINA_STOP_DIR", "")
3042
3510
  if (!nzchar(dir) || !is.character(id) || length(id) != 1L || is.na(id)) return(FALSE)
3043
3511
  file.exists(file.path(dir, paste0("stop-", id)))
3044
3512
  }
3045
3513
 
3046
3514
  #' Is an interrupt raised inside run `id` NOT this run's Stop?
3047
3515
  #'
3048
- #' Under a supervisor that writes stop flags (CARMAR_STOP_DIR set), every real
3516
+ #' Under a supervisor that writes stop flags (BEATRINA_STOP_DIR set), every real
3049
3517
  #' Stop writes `stop-<id>` BEFORE it sends a signal, so a signal-borne interrupt
3050
3518
  #' with no flag for this run was sent for another — one that has already ended.
3051
3519
  #' Without a flag folder (an older supervisor) nothing can be told apart, and
3052
3520
  #' every interrupt is this run's, as it always was.
3053
3521
  #' @param id The wire id of the running cell.
3054
3522
  #' @return TRUE only when a flag folder exists and holds no flag for `id`.
3055
- carmar_interrupt_is_stray <- function(id) {
3056
- nzchar(Sys.getenv("CARMAR_STOP_DIR", "")) && !carmar_stop_pending(id)
3523
+ beatrina_interrupt_is_stray <- function(id) {
3524
+ nzchar(Sys.getenv("BEATRINA_STOP_DIR", "")) && !beatrina_stop_pending(id)
3057
3525
  }
3058
3526
 
3059
3527
  #' Resume from an interrupt that belongs to a run that has already ended.
@@ -3081,7 +3549,7 @@ carmar_interrupt_is_stray <- function(id) {
3081
3549
  #'
3082
3550
  #' @param i The interrupt condition.
3083
3551
  #' @return Does not return when a `resume` restart exists; NULL otherwise.
3084
- carmar_resume_if_stray <- function(i) {
3552
+ beatrina_resume_if_stray <- function(i) {
3085
3553
  if (!is.null(findRestart("resume"))) invokeRestart("resume")
3086
3554
  invisible(NULL)
3087
3555
  }
@@ -3102,13 +3570,13 @@ carmar_resume_if_stray <- function(i) {
3102
3570
  #' has nothing worth resuming anyway.
3103
3571
  #' @param id The wire id of the cell about to run.
3104
3572
  #' @return NULL, invisibly.
3105
- carmar_drain_stray_interrupt <- function(id) {
3573
+ beatrina_drain_stray_interrupt <- function(id) {
3106
3574
  raised <- tryCatch({
3107
3575
  n <- 0L
3108
3576
  for (k in seq_len(1200L)) n <- n + 1L
3109
3577
  FALSE
3110
3578
  }, interrupt = function(i) TRUE)
3111
- if (isTRUE(raised) && carmar_stop_pending(id)) {
3579
+ if (isTRUE(raised) && beatrina_stop_pending(id)) {
3112
3580
  signalCondition(structure(class = c("interrupt", "condition"),
3113
3581
  list(message = "Execution interrupted", call = NULL)))
3114
3582
  }
@@ -3125,12 +3593,12 @@ carmar_drain_stray_interrupt <- function(id) {
3125
3593
  #' whole worker down with it, ending the session.
3126
3594
  #' @param line One line as read_command returned it.
3127
3595
  #' @return The command list, or NULL.
3128
- carmar_parse_command <- function(line) {
3596
+ beatrina_parse_command <- function(line) {
3129
3597
  withCallingHandlers({
3130
3598
  cmd <- tryCatch(jsonlite::fromJSON(line), error = function(e) NULL)
3131
3599
  if (!is.list(cmd) || !is.character(cmd$type) || length(cmd$type) != 1L) return(NULL)
3132
3600
  resolve_cmdfile(cmd)
3133
- }, interrupt = carmar_resume_if_stray)
3601
+ }, interrupt = beatrina_resume_if_stray)
3134
3602
  }
3135
3603
 
3136
3604
  #' Read one command line from stdin, tolerating an interrupt that lands while
@@ -3157,14 +3625,14 @@ read_command <- function(con) {
3157
3625
  # half-way through when its interrupt fired — the supervisor's `ack` deadline
3158
3626
  # (host/worker-plane.mjs) is the answer to that one.
3159
3627
  line <- tryCatch(
3160
- withCallingHandlers(readLines(con, n = 1L, warn = FALSE), interrupt = carmar_resume_if_stray),
3628
+ withCallingHandlers(readLines(con, n = 1L, warn = FALSE), interrupt = beatrina_resume_if_stray),
3161
3629
  interrupt = function(i) NA_character_
3162
3630
  )
3163
3631
  # Strip the supervisor's comment tag; a line without it (older supervisor,
3164
3632
  # test harness writing bare NDJSON) passes through untouched.
3165
3633
  if (length(line) == 1L && !is.na(line) && nzchar(CMD_PREFIX) &&
3166
3634
  startsWith(line, CMD_PREFIX)) {
3167
- line <- substring(line, nchar(CMD_PREFIX) + 1L)
3635
+ line <- substring(line, nchar(CMD_PREFIX) + 1L, nchar(line))
3168
3636
  }
3169
3637
  line
3170
3638
  }
@@ -3197,13 +3665,13 @@ resolve_cmdfile <- function(cmd) {
3197
3665
  # `View(df)` must do what it does in RStudio. Overriding it on the SEARCH PATH
3198
3666
  # rather than in globalenv keeps the user's environment clean (the Environment
3199
3667
  # pane shows their objects, not ours) while still shadowing utils::View.
3200
- carmar_tools <- new.env()
3668
+ beatrina_tools <- new.env()
3201
3669
  # Debug hooks live on the SEARCH PATH, not in this file's private scope, for a
3202
3670
  # reason of visibility: a trace() tracer evaluates in the traced function's
3203
3671
  # frame, whose lexical chain ends at globalenv() and then the search path —
3204
3672
  # the worker's private local() is not on it. Same for expressions typed at a
3205
- # Browse prompt, which is how .carmar_debug_where is called.
3206
- carmar_tools$.carmar_debug_entered <- function(file, line, reason = "breakpoint") {
3673
+ # Browse prompt, which is how .beatrina_debug_where is called.
3674
+ beatrina_tools$.beatrina_debug_entered <- function(file, line, reason = "breakpoint") {
3207
3675
  # Snapshot FIRST, then drop this function's own frame. Taking sys.calls()
3208
3676
  # lazily inside the emit arguments would extend the stack through
3209
3677
  # debug_stack/frame_vars themselves, defeat the machinery trimming, and put
@@ -3217,7 +3685,7 @@ carmar_tools$.carmar_debug_entered <- function(file, line, reason = "breakpoint"
3217
3685
  locals = frame_vars(parent.frame())))
3218
3686
  invisible(NULL)
3219
3687
  }
3220
- carmar_tools$.carmar_debug_where <- function() {
3688
+ beatrina_tools$.beatrina_debug_where <- function() {
3221
3689
  calls <- sys.calls()
3222
3690
  frames <- sys.frames()
3223
3691
  n <- length(calls)
@@ -3232,7 +3700,7 @@ carmar_tools$.carmar_debug_where <- function() {
3232
3700
  #' @param x HTML as a character vector (joined with newlines) or any value
3233
3701
  #' `rich_html_of()` understands.
3234
3702
  #' @return Invisibly `x`.
3235
- carmar_tools$display_html <- function(x) {
3703
+ beatrina_tools$display_html <- function(x) {
3236
3704
  rich <- if (is.character(x)) {
3237
3705
  list(x = htmltools::HTML(paste(x, collapse = "\n")), kind = "html")
3238
3706
  } else {
@@ -3247,16 +3715,16 @@ carmar_tools$display_html <- function(x) {
3247
3715
  emit_rich(RUN_STATE$id, rich)
3248
3716
  invisible(x)
3249
3717
  }
3250
- carmar_tools$View <- function(x, title = NULL) {
3718
+ beatrina_tools$View <- function(x, title = NULL) {
3251
3719
  # The LABEL is what the user typed; the FETCH NAME is where the viewer reads
3252
- # it from. Conflating them titled `View(mtcars)` as `.carmar_view`, because
3720
+ # it from. Conflating them titled `View(mtcars)` as `.beatrina_view`, because
3253
3721
  # mtcars lives in the datasets package rather than the global environment.
3254
3722
  label <- if (!is.null(title)) title else deparse(substitute(x))
3255
- assign(".carmar_view", x, envir = globalenv())
3256
- emit_view("view-request", ".carmar_view", label = label)
3723
+ assign(".beatrina_view", x, envir = globalenv())
3724
+ emit_view("view-request", ".beatrina_view", label = label)
3257
3725
  invisible(NULL)
3258
3726
  }
3259
- attach(carmar_tools, name = "carmar:tools", warn.conflicts = FALSE)
3727
+ attach(beatrina_tools, name = "beatrina:tools", warn.conflicts = FALSE)
3260
3728
 
3261
3729
  # Interactive mode reads the console; batch mode opens fd 0 as a connection.
3262
3730
  # The console read is what lets browser() prompts and the dispatch loop share
@@ -3277,7 +3745,7 @@ if (identical(WORKER_MODE, "interactive")) {
3277
3745
  }
3278
3746
 
3279
3747
  # macOS 26 registers command-line R as a foreground application after Aqua or
3280
- # AppKit initializes, even when the process was launched by CarmaR's UIElement
3748
+ # AppKit initializes, even when the process was launched by Beatrina's UIElement
3281
3749
  # helper. The supervisor loads the packaged marker before sourcing this file
3282
3750
  # (preventing an initial Dock flash); repeat the transition here, after all
3283
3751
  # worker initialization and immediately before `ready`, so any framework that
@@ -3286,11 +3754,11 @@ if (identical(WORKER_MODE, "interactive")) {
3286
3754
  # or restarted worker reaches this point automatically.
3287
3755
  # OPT-IN — see macos_background_boot in kernel.R. The background transform hides
3288
3756
  # the Dock icon but wedges R's console read on a GUI-launched macOS 26 worker,
3289
- # so it is off unless CARMAR_MARK_BACKGROUND=1 is set.
3757
+ # so it is off unless BEATRINA_MARK_BACKGROUND=1 is set.
3290
3758
  if (identical(unname(Sys.info()[["sysname"]]), "Darwin") &&
3291
- identical(Sys.getenv("CARMAR_MARK_BACKGROUND", "0"), "1") &&
3292
- is.loaded("carmar_mark_background")) {
3293
- invisible(try(.C("carmar_mark_background", result = integer(1)), silent = TRUE))
3759
+ identical(Sys.getenv("BEATRINA_MARK_BACKGROUND", "0"), "1") &&
3760
+ is.loaded("beatrina_mark_background")) {
3761
+ invisible(try(.C("beatrina_mark_background", result = integer(1)), silent = TRUE))
3294
3762
  }
3295
3763
 
3296
3764
  # The ready frame says WHICH R this is. A session that silently uses the wrong
@@ -3308,13 +3776,13 @@ if (identical(unname(Sys.info()[["sysname"]]), "Darwin") &&
3308
3776
  # Advertising the vocabulary lets a client know instantly. Kernels older than
3309
3777
  # this simply omit the field, and clients fall back to probing.
3310
3778
  #
3311
- # A fresh worker is a fresh R, always. Until 7.59 a "Restart into" handoff
3312
- # asked this worker to `save.image()` and the successor's first worker loaded
3313
- # it back (`workspace_save` / CARMAR_RESTORE_WORKSPACE / a `restored` field
3314
- # here). That is gone by decision — the owner's "restore work has been
3315
- # abysmally bad and almost invariably a failure" — and the rule is: a session
3316
- # is a process you can see; open → R starts, close → R stops, restart → a
3317
- # fresh R. The saved document is the record. Do not add a restore path back.
3779
+ # A worker starts as a fresh R. Until 7.59 a "Restart into" handoff asked this
3780
+ # worker to `save.image()` and the successor loaded one file back inside a
3781
+ # deadline; that failed on large sessions and was removed ("restore work has
3782
+ # been abysmally bad"). Since 0.8.5 "Restart, keep variables" is an explicit
3783
+ # choice built differently (spike/workspace-keep.R): the page asks `suspend`
3784
+ # with no deadline, the new worker is asked `resume` by the supervisor after
3785
+ # `ready`, one file per object, and the report names what did not come back.
3318
3786
  emit(list(type = "ready", pid = Sys.getpid(), r = R.version.string, cwd = getwd(),
3319
3787
  # I(): a single library path must still ship as an array.
3320
3788
  home = R.home(), libs = I(.libPaths()),
@@ -3328,8 +3796,9 @@ emit(list(type = "ready", pid = Sys.getpid(), r = R.version.string, cwd = getwd(
3328
3796
  "project_status", "project_action",
3329
3797
  "help", "hover", "wd", "files", "sniff",
3330
3798
  "import", "readfile", "writefile", "writefiles_atomic", "view", "colstats",
3799
+ "column_values",
3331
3800
  "mkdir", "renamepath", "deletepath", "copypath", "revealpath",
3332
- "rm",
3801
+ "rm", "suspend",
3333
3802
  if (identical(WORKER_MODE, "interactive")) "debug_breaks"))))
3334
3803
  # (No package count here on purpose: installed.packages() reads every
3335
3804
  # package's DESCRIPTION — 0.3–1.8 s on a big library — and no client ever
@@ -3342,7 +3811,7 @@ emit(list(type = "ready", pid = Sys.getpid(), r = R.version.string, cwd = getwd(
3342
3811
  # parser's (a late signal, resumed), the dispatcher's (Stop as the cell's own
3343
3812
  # outcome) — and the loop around them has the last word: an interrupt that
3344
3813
  # escapes an iteration ends that iteration, never the loop, and one that escapes
3345
- # the loop itself re-enters it through the `carmar_serve_again` restart from a
3814
+ # the loop itself re-enters it through the `beatrina_serve_again` restart from a
3346
3815
  # global calling handler. Before 2026-09-16 the parse step had no handler at all,
3347
3816
  # and a late Stop signal raised there unwound the whole worker script into R's
3348
3817
  # top-level prompt — where the `options(error=)` guard ended the process, or,
@@ -3365,7 +3834,7 @@ emit(list(type = "ready", pid = Sys.getpid(), r = R.version.string, cwd = getwd(
3365
3834
  # view) catch it themselves, with meaning. The outer handler exists for the
3366
3835
  # narrow window where one lands outside those — an uncaught interrupt ends
3367
3836
  # the worker script, and the supervisor treats a dead worker as fatal.
3368
- carmar_dispatch <- function(cmd) {
3837
+ beatrina_dispatch <- function(cmd) {
3369
3838
  if (identical(cmd$type, "exec")) {
3370
3839
  if (is.null(cmd$document)) run_cell(cmd$id, cmd$source, cmd$dims, cmd$srcname)
3371
3840
  else run_document_cell(cmd$id, cmd$source, cmd$document, cmd$dims, cmd$srcname)
@@ -3379,6 +3848,8 @@ carmar_dispatch <- function(cmd) {
3379
3848
  if (identical(cmd$type, "doctor")) emit_doctor(cmd$id)
3380
3849
  if (identical(cmd$type, "complete")) emit_complete(cmd$id, cmd$line, cmd$cursor,
3381
3850
  fn = cmd$fn, data = cmd$data)
3851
+ if (identical(cmd$type, "suspend")) emit_suspend(cmd$id)
3852
+ if (identical(cmd$type, "resume")) emit_resume(cmd$id, cmd$token)
3382
3853
  if (identical(cmd$type, "packages")) emit_packages(cmd$id, cmd$scope)
3383
3854
  if (identical(cmd$type, "package_action")) emit_package_action(cmd$id, cmd$action, cmd$name, cmd$lib)
3384
3855
  if (identical(cmd$type, "package_help")) emit_package_help(cmd$id, cmd$name)
@@ -3411,6 +3882,9 @@ carmar_dispatch <- function(cmd) {
3411
3882
  if (identical(cmd$type, "colstats")) emit_colstats(cmd$id, cmd$name, cmd$column,
3412
3883
  query = cmd$query,
3413
3884
  filters = cmd$filters)
3885
+ if (identical(cmd$type, "column_values")) emit_column_values(cmd$id, cmd$name, cmd$column,
3886
+ query = cmd$query,
3887
+ filters = cmd$filters)
3414
3888
  if (identical(cmd$type, "rm")) emit_rm(cmd$id, cmd$names)
3415
3889
  invisible(NULL)
3416
3890
  }
@@ -3424,7 +3898,7 @@ carmar_dispatch <- function(cmd) {
3424
3898
  # interrupts around the eval, but one landing in its epilogue (unlink,
3425
3899
  # flush, the emit itself) used to escape this loop, end the worker script,
3426
3900
  # and take the supervisor's event loop down with it.
3427
- carmar_fail_frame <- function(cmd, text, outcome = "error") {
3901
+ beatrina_fail_frame <- function(cmd, text, outcome = "error") {
3428
3902
  id_ok <- is.character(cmd$id) && length(cmd$id) == 1L
3429
3903
  if (identical(cmd$type, "exec")) {
3430
3904
  if (id_ok) emit(list(type = "done", id = cmd$id, status = outcome,
@@ -3438,11 +3912,11 @@ carmar_fail_frame <- function(cmd, text, outcome = "error") {
3438
3912
  #' Serve one command. Returns "eof" when the supervisor has gone, "shutdown"
3439
3913
  #' when it said so, NULL after a command or a line that was not one.
3440
3914
  #' @param con The command connection (the console in interactive mode).
3441
- carmar_serve_one <- function(con) {
3915
+ beatrina_serve_one <- function(con) {
3442
3916
  line <- read_command(con)
3443
3917
  if (length(line) == 0L) return("eof") # EOF: supervisor went away
3444
3918
  if (is.na(line) || !nzchar(trimws(line))) return(NULL)
3445
- cmd <- carmar_parse_command(line)
3919
+ cmd <- beatrina_parse_command(line)
3446
3920
  if (is.null(cmd)) return(NULL)
3447
3921
  if (identical(cmd$type, "shutdown")) return("shutdown")
3448
3922
  # The acknowledgement: the supervisor wrote this command to an idle worker and
@@ -3453,10 +3927,10 @@ carmar_serve_one <- function(con) {
3453
3927
  # only wait forever — an exec has no deadline, by design. With it, a command
3454
3928
  # that is never acknowledged is failed with a sentence and the queue moves on.
3455
3929
  if (is.character(cmd$id) && length(cmd$id) == 1L && !is.na(cmd$id)) emit(list(type = "ack", id = cmd$id))
3456
- tryCatch(carmar_dispatch(cmd),
3457
- error = function(e) carmar_fail_frame(cmd, paste("command failed:", conditionMessage(e))),
3930
+ tryCatch(allowInterrupts(beatrina_dispatch(cmd)),
3931
+ error = function(e) beatrina_fail_frame(cmd, paste("command failed:", conditionMessage(e))),
3458
3932
  # Cleanup interrupts mean the same thing as interrupts during eval.
3459
- interrupt = function(i) carmar_fail_frame(cmd, "Execution interrupted", "interrupted"))
3933
+ interrupt = function(i) beatrina_fail_frame(cmd, "Execution interrupted", "interrupted"))
3460
3934
  NULL
3461
3935
  }
3462
3936
 
@@ -3464,9 +3938,9 @@ carmar_serve_one <- function(con) {
3464
3938
  #' iteration and nothing else.
3465
3939
  #' @param con The command connection.
3466
3940
  #' @return "eof" or "shutdown", invisibly.
3467
- carmar_serve <- function(con) {
3941
+ beatrina_serve <- function(con) {
3468
3942
  repeat {
3469
- outcome <- tryCatch(carmar_serve_one(con), interrupt = function(i) NULL)
3943
+ outcome <- tryCatch(beatrina_serve_one(con), interrupt = function(i) NULL)
3470
3944
  if (identical(outcome, "eof") || identical(outcome, "shutdown")) return(invisible(outcome))
3471
3945
  }
3472
3946
  }
@@ -3479,13 +3953,18 @@ carmar_serve <- function(con) {
3479
3953
  # the process, as the `options(error=)` guard does for an error: a worker at
3480
3954
  # R's top-level prompt is a zombie the supervisor cannot tell from an idle one.
3481
3955
  globalCallingHandlers(interrupt = function(i) {
3482
- if (!is.null(findRestart("carmar_serve_again"))) invokeRestart("carmar_serve_again")
3956
+ if (!is.null(findRestart("beatrina_serve_again"))) invokeRestart("beatrina_serve_again")
3483
3957
  quit(save = "no", status = 70L)
3484
3958
  })
3485
- repeat {
3486
- outcome <- withRestarts(carmar_serve(con), carmar_serve_again = function() NULL)
3487
- if (!is.null(outcome)) break
3488
- }
3959
+ # Signals between commands must not unwind a consumed line or the loop's
3960
+ # restart setup. Only dispatch enables interrupts; run_cell distinguishes a
3961
+ # flagged Stop from a late signal, and its handlers are installed before entry.
3962
+ suspendInterrupts({
3963
+ repeat {
3964
+ outcome <- withRestarts(beatrina_serve(con), beatrina_serve_again = function() NULL)
3965
+ if (!is.null(outcome)) break
3966
+ }
3967
+ })
3489
3968
 
3490
3969
  # stdin() is R's console, not ours to close. And an interactive R does not
3491
3970
  # exit when the script does — it would sit at the top-level prompt waiting for