Skip to contents

RDesk AI Skill Reference File (v1.0.5)

This reference file serves as the definitive engineering and behavioral blueprint for AI coding agents developing, debugging, or maintaining RDesk applications. It maps out the package’s multi-directory structure, core contracts, API behaviors, design constraints, and common implementation pitfalls.


SECTION 1: Architecture & Process Model

RDesk applications utilize a dual-process desktop architecture:

[rdesk-launcher.exe] (Frontend Shell)  <--Standard I/O Pipes-->  [R Process] (Backend Logic)
   - WebView2 rendering control                                  - R6 event loop (App$run())
   - Virtual HTTPS folder mapping                                - Logic handlers (on_message())
   - Native windows, tray & menus                                - Background tasks (mirai)
   - Watchdog process cleanup                                    - Offline computation

Core Architectural Layers:

  • Layer 5 (Application Code): R logic (R/server.R) and UI assets (www/). Developers only write and modify this layer.
  • Layer 4 (High-Level Plugin API): Package utilities like async(), rdesk_auto_update(), and hot-reloading watchers.
  • Layer 3 (R Core Event Loop): The R6 App class that services messaging callbacks.
  • Layer 2 (Zero-Port IPC Bridge): Low-level message passing using WebView2’s native bridge (stdin/stdout pipes). No network sockets are ever opened.
  • Layer 1 (Native Launcher): Lightweight C++ shell compiled during package installation.

Key Development Rules:

  1. Process Isolation: Do not attempt to modify or recompile Layer 1 or Layer 2. Layer 1 C++ is compiled automatically during standard R package installation.
  2. Zero Network Exposure: RDesk operates fully offline and binds to no network ports (httpuv, WebSockets). Communication uses standard I/O streams. Never introduce code that opens local TCP/UDP ports.
  3. Explicit Messaging: State is not implicitly reactive. State synchronisation between R and the UI must be triggered via explicit message envelopes.

SECTION 2: Project Directory Mappings

Every RDesk application must adhere to this standardized file layout:

MyApp/
├── app.R              # Main application entry point (keep thin)
├── DESCRIPTION        # Application metadata and dependency list
├── R/                 # Backend R modules
│   ├── server.R       # Message handlers & initialization (primary logic file)
│   ├── data.R         # Data loading, extraction, and transformation
│   └── plots.R        # ggplot2 and base chart generation functions
└── www/               # Frontend UI assets
    ├── index.html     # HTML5 markup structure
    ├── css/
    │   └── style.css  # Application styling
    └── js/
        ├── app.js     # Client event handlers and UI updates
        └── rdesk.js   # RDesk frontend runtime bridge (NEVER EDIT THIS FILE)

Standard app.R Entry Point:

app_dir <- tryCatch(
  if (nzchar(Sys.getenv("R_BUNDLE_APP"))) getwd()
  else dirname(rstudioapi::getActiveDocumentContext()$path),
  error = function(e) getwd()
)

library(RDesk)

# Dynamically source all backend modules
lapply(
  list.files(file.path(app_dir, "R"), pattern = "\\.R$", full.names = TRUE),
  source
)

app <- App$new(
  title  = "My RDesk App",
  width  = 1100L,
  height = 740L,
  www    = file.path(app_dir, "www")
)

init_handlers(app)
app$run()

Directory & File Rules:

  • Thin Entry Point: Never place business logic directly in app.R. Sourcing modular R files and calling init_handlers(app) must be the only operations in app.R.
  • Dynamic Sourcing: Always source backend files using the lapply(list.files(...)) pattern. Individual explicit source() calls by filename break RDesk’s directory packaging and hot-reloading hooks.
  • Local Runtime File: www/js/rdesk.js is the framework client library. Never import it from a CDN or alter its contents.

SECTION 3: The IPC Message Envelope Contract

All messaging across the R-to-JS bridge uses this exact JSON envelope structure:

{
  "id": "msg_unique_hash",
  "type": "message_type",
  "version": "1.0",
  "payload": {},
  "timestamp": 1716382104.123
}

Protocol & Handler Rules:

  1. Response Routing: A message sent from JS of type "foo" automatically returns its response payload to a JS listener registered for "foo_result".
  2. Handler Output: R on_message() handlers must return a standard R list or NULL. The framework automatically serialises and wraps the output in the envelope. Do not wrap return values in rdesk_message() inside handlers.
  3. Envelope Serialization: Never construct JSON strings manually. Use app$send() on the R side, and rdesk.send() on the JS side.
  4. System Messaging: Message types starting with a double underscore (e.g., __loading__, __progress__, __reload_ui__) are reserved for core framework signaling. Never define custom application messages with the __ prefix.

SECTION 4: Backend Event Handlers (R/server.R)

Define event handlers inside init_handlers(app) in R/server.R.

init_handlers <- function(app) {
  
  # Triggered once when the UI window is fully initialized
  app$on_ready(function() {
    # Define native window menu
    app$set_menu(list(
      File = list(
        "Open..." = function() app$send("trigger_open", list()),
        "---",
        "Exit"    = app$quit
      ),
      Help = list(
        "About"   = function() app$toast("RDesk v1.0.5", type = "info")
      )
    ))
    
    # Push initial dataset
    df <- load_initial_data()
    app$send("data_ready", rdesk_df_to_list(df))
  })
  
  # Standard request-response handler
  app$on_message("get_filtered_data", function(payload) {
    # payload matches the JS payload object
    df <- load_subset(payload$filter_val)
    rdesk_df_to_list(df)
  })
}

Low-Level Native APIs:

  • Toasts: app$toast("message", type = "success" | "error" | "info" | "warning")
  • Native Dialogs:
    • app$dialog_open(title = "Open", filters = "CSV (*.csv)|*.csv") -> Returns character path or NULL
    • app$dialog_save(title = "Save", filters = "CSV (*.csv)|*.csv", default = "export.csv")
    • app$dialog_folder(title = "Select Folder")
  • Loading Overlays: app$loading_start("text", cancellable = TRUE), app$loading_progress(50), app$loading_done()

SECTION 5: Asynchronous Execution & Task Tiers

To prevent blocking the single-threaded R event loop during computation, RDesk utilizes a three-tier async architecture:

Tier 1: Asynchronous Handler Wrapper (async())

Applies to 95% of background processing needs. Automatically manages loading overlays, routing, and task cancellation.

app$on_message("compute_stats", async(function(payload) {
  # Executed inside an isolated background worker process
  # DO NOT call app$ methods inside this closure
  # DO NOT access global variables; pass them via payload or capture them explicitly
  df <- load_raw_data(payload$source)
  result <- run_calculation(df)
  list(stats = result)
}, app = app, loading_message = "Processing calculation..."))

Asynchronous Progress Updates:

To push real-time progress updates from a background worker:

app$on_message("heavy_loop", async(function(payload) {
  steps <- payload$steps
  for (i in seq_along(steps)) {
    process_step(steps[[i]])
    async_progress(
      value   = round(i / length(steps) * 100),
      message = paste("Processing step", i, "of", length(steps))
    )
  }
  list(complete = TRUE)
}, app = app))

Tier 2: Explicit Async Control (rdesk_async())

Used when fine-grained callback control is required.

job_id <- rdesk_async(
  task     = function(x) run_heavy(x),
  args     = list(x = data),
  on_done  = function(res) app$send("done", res),
  on_error = function(err) app$toast(err$message, type = "error")
)
app$loading_start("Running...", cancellable = TRUE, job_id = job_id)

Tier 3: Direct Asynchronous Workers (mirai)

For expert use cases requiring direct interaction with background processes.

m <- mirai::mirai(
  slow_function(x),
  slow_function = slow_function,
  x = input_data
)

SECTION 6: Client-Side JavaScript Integration

Communicate with the backend R process using the globally available rdesk object:

// Wrap initialization inside ready() to ensure bridge is fully established
rdesk.ready(function() {
  rdesk.send("get_initial_state", {})
    .then(res => renderUI(res))
    .catch(err => console.error(err));
});

// Listen for push notifications from R
rdesk.on("update_data", function(data) {
  updateChart(data);
});

// Clean up listeners
rdesk.off("update_data");

Standard DOM Mapping Patterns:

  • Base64 Charts:

    rdesk.on("render_chart_result", data => {
      document.getElementById("chart-img").src = "data:image/png;base64," + data.chart;
    });
  • Structured Tables:

    rdesk.on("render_table_result", data => {
      const rows = data.rows.map(r => `<tr>${data.cols.map(c => `<td>${r[c]}</td>`).join('')}</tr>`).join('');
      document.getElementById("table-body").innerHTML = rows;
    });

JS Bridging Rules:

  1. Always wrap initial page-load send() calls inside rdesk.ready(). Calling send() before the native bridge is established throws silent errors.
  2. Never attempt to use fetch(), XMLHttpRequest, or WebSocket to communicate with the R process. Always use rdesk.send() and rdesk.on().

SECTION 7: Charts & Data Serialisation

1. Generating Base64 Plot Strings

Convert plots to strings for transmission over the standard input/output pipe:

# In R/plots.R
generate_trend_plot <- function(df) {
  p <- ggplot2::ggplot(df, ggplot2::aes(x = date, y = value)) +
    ggplot2::geom_line(colour = "#1a1a2e", linewidth = 1) +
    ggplot2::theme_minimal()
  rdesk_plot_to_base64(p)
}

# In R/server.R handler
app$on_message("get_trend", function(payload) {
  df <- load_data()
  list(chart = generate_trend_plot(df))
})

2. Formatting Data Frames for JSON

Data frames must be converted to $rows (list of row-lists) and $cols (column names) for clean client-side parsing:

# In R/server.R handler
app$on_message("get_raw_table", function(payload) {
  df <- head(mtcars)
  rdesk_df_to_list(df)
})

SECTION 8: Shiny Migration Guide

Side-by-Side Architectural Mapping

Shiny Concept RDesk Desktop Equivalent
input$x payload$x inside app$on_message("x_changed", ...)
reactive({...}) Standard R function called explicitly by the handler
renderPlot({...}) rdesk_plot_to_base64() returned inside a list
renderTable({...}) rdesk_df_to_list() returned inside a list
observeEvent(input$x, {...}) app$on_message("x_changed", function(payload) {...})
showNotification() app$toast()
withProgress() async(..., loading_message = "...")
fileInput() app$dialog_open()
downloadHandler() app$dialog_save()

Complete Migration Blueprint:

# --- 1. SHINY ORIGINAL (server.R) ---
server <- function(input, output, session) {
  filtered <- reactive({
    mtcars[mtcars$cyl == input$cyl_filter, ]
  })
  output$scatter <- renderPlot({
    ggplot2::ggplot(filtered(), ggplot2::aes(wt, mpg)) + ggplot2::geom_point()
  })
  output$table <- renderTable({ filtered() })
}
# --- 2. RDESK REWRITE (R/server.R) ---
init_handlers <- function(app) {
  app$on_ready(function() {
    app$send("data_ready", rdesk_df_to_list(mtcars))
  })

  app$on_message("filter_changed", async(function(payload) {
    df <- mtcars[mtcars$cyl == payload$cyl_filter, ]
    p  <- ggplot2::ggplot(df, ggplot2::aes(wt, mpg)) + ggplot2::geom_point()
    list(
      chart = rdesk_plot_to_base64(p),
      table = rdesk_df_to_list(df)
    )
  }, app = app))
}
// --- 3. RDESK FRONTEND (www/js/app.js) ---
rdesk.ready(function() {
  rdesk.send("get_data", {});
});

document.getElementById("cyl-filter").addEventListener("change", function() {
  rdesk.send("filter_changed", { cyl_filter: parseInt(this.value) });
});

rdesk.on("filter_changed_result", function(data) {
  document.getElementById("chart").src = "data:image/png;base64," + data.chart;
  renderTable(data.table.rows, data.table.cols);
});

SECTION 9: Common Pitfalls & Solutions

Mistake 1: Invoking main-process APIs inside async() workers

# WRONG
app$on_message("task", async(function(payload) {
  res <- run_calculation(payload)
  app$toast("Task Completed!")  # ERROR: app$ is not available in isolated worker processes
  list(data = res)
}, app = app))

# CORRECT
app$on_message("task", async(function(payload) {
  res <- run_calculation(payload)
  list(data = res)  # Return value is pushed automatically to task_result
}, app = app))

Mistake 2: Accessing unscoped global variables in workers

# WRONG
dataset <- load_heavy_data()
app$on_message("run", async(function(payload) {
  process(dataset)  # ERROR: dataset is not bound in the worker process environment
}, app = app))

# CORRECT
dataset <- load_heavy_data()
app$on_message("run", async(function(payload) {
  process(payload$data)  # Pass the object explicitly via payload
}, app = app))

# OR
local_data <- dataset
app$on_message("run", async(function(payload) {
  process(local_data)  # Captured in the closure, serialised to the worker
}, app = app))

Mistake 3: Forgetting to flush the output stream

# WRONG - Output might buffer in compiled desktop mode
cat(jsonlite::toJSON(msg), "\n")

# CORRECT - Always flush stdout after writing low-level console logs
cat(jsonlite::toJSON(msg), "\n")
flush(stdout())

Mistake 4: Writing files to the working directory in examples

# WRONG - Violates CRAN policy
RDesk::build_app(app_dir = "MyApp", out_dir = "dist")

# CORRECT - Always write files inside tempdir()
RDesk::build_app(app_dir = "MyApp", out_dir = file.path(tempdir(), "dist"))

Mistake 5: Using installed.packages() to check dependencies

# WRONG - Extremely slow and violates CRAN guidelines
if ("ggplot2" %in% installed.packages()[,"Package"]) { ... }

# CORRECT - Fact-based, fast package checking
if (requireNamespace("ggplot2", quietly = TRUE)) { ... }

Mistake 6: Changing working directory without restoring state

# WRONG
setwd(build_dir)
# ... tasks ...
setwd(original_dir) # Never executes if an error occurs mid-task

# CORRECT
old_wd <- getwd()
on.exit(setwd(old_wd), add = TRUE)
setwd(build_dir)

SECTION 10: Development, Testing & Build Verification

Local Hot-Reload Workflows

  • Manual Development Loop: Sourcing app.R launches the application window. Close the window, edit files in R/server.R or www/, and source app.R again.
  • Automatic reloading: Run rdesk_watch(app) during development to live-reload UI assets when saved.

Headless Verification Mode (ci_mode)

To run unit tests on message handlers without spawning a browser window:

options(rdesk.ci_mode = TRUE)
library(RDesk)

# Source Server handlers and mock dynamic events
source("R/server.R")
# Handlers are initialized without spawning a physical WebView2 window

Reproducible Bundles via renv

  • If renv is active, RDesk automatically detects the local environment.
  • build_app() writes a frozen renv.lock into the distributable, copying your exact package versions to ensure ABI compatibility and prevent package-mismatch crashes.

SECTION 11: Quick Reference Card

Core R API Functions:

# Create scaffolding
rdesk_create_app("MyApp")

# Core Window Setup
app <- App$new(title = "App", width = 1100L, height = 740L, www = "www/")
app$on_ready(function() { ... })
app$on_message("msg_type", function(payload) { list(...) })
app$run()

# Server-Side Push
app$send("type", list(key = value))

# Dialogs
app$dialog_open(title = "Open File", filters = "CSV (*.csv)|*.csv")
app$dialog_save(title = "Save File", filters = "CSV (*.csv)|*.csv", default = "data.csv")
app$dialog_folder(title = "Select Export Folder")

# System Control
app$toast("message", type = "success|error|info|warning")
app$notify("Title", "Body Text")
app$maximize()
app$quit()

# Loading States
app$loading_start("Message...", cancellable = TRUE, job_id = "job_1")
app$loading_progress(50)
app$loading_done()

# Async Operations
rdesk_async(task, args, on_done, on_error)
rdesk_cancel_job("job_id")
rdesk_jobs_pending()

Core JavaScript Client Functions:

// Send data to R, returns Promise
rdesk.send("msg_type", { payload_key: "value" })
  .then(res => { ... })
  .catch(err => { ... });

// Listen for push notifications from R
rdesk.on("push_type", function(data) { ... });

// Run when the browser bridge is active
rdesk.ready(function() { ... });

// Deregister listener
rdesk.off("push_type");