Skip to content

feat: desktop app sidecar, connection screen, and 3D orb#452

Merged
jamiepine merged 2 commits into
mainfrom
desktop-improvements
Mar 19, 2026
Merged

feat: desktop app sidecar, connection screen, and 3D orb#452
jamiepine merged 2 commits into
mainfrom
desktop-improvements

Conversation

@jamiepine

@jamiepine jamiepine commented Mar 18, 2026

Copy link
Copy Markdown
Member

Summary

  • Tauri sidecar integration: The desktop app can now launch a bundled Spacebot server binary locally. bundle-sidecar.sh compiles the binary and copies it with the target-triple suffix Tauri expects. Shell permissions added for spawn/stdin/kill.
  • Connection screen: New ConnectionScreen gates the Tauri app until a server is reachable. Users can edit the server URL or start the bundled sidecar with one click. Server URL is persisted via Tauri IPC (connection.json) with localStorage fallback.
  • Dynamic API base URL: Refactored client.ts from a static API_BASE constant to getApiBase() / setServerUrl() so cross-origin requests from tauri://localhost work correctly. eventsUrl is now getEventsUrl().
  • useServer hook: New React context (ServerProvider) manages server lifecycle — health polling, sidecar process management, and connection state for Tauri mode. Same-origin mode (web/hosted/Vite dev) skips the connection screen entirely.
  • 3D Orb component: New WebGL orb (Orb.tsx) using the ogl library, shown on the connection screen.
  • Tauri build pipeline: beforeDevCommand and beforeBuildCommand in tauri.conf.json now automate sidecar bundling + frontend build. New just desktop-dev / just desktop-build recipes.
  • Tauri IPC commands: get_server_url / set_server_url in desktop/src-tauri/src/main.rs persist the server URL in the app data directory.
  • Docs: New docs/content/docs/(getting-started)/desktop.mdx covering connection screen, sidecar, building from source, architecture details, and CORS. Updated quickstart with desktop references.
  • Misc: Optimized ball.png (218K → 135K), corrected Vite dev server port in quickstart docs (3000 → 19840).

Note

This PR adds desktop application support to Spacebot via Tauri. The core achievement is bundling the Rust server as a sidecar binary that the desktop app can spawn locally, with a connection screen that handles server discovery and lifecycle. The new useServer hook abstracts server management (health checks, process spawning) from the UI, while the dynamic API base URL refactor makes cross-origin requests work seamlessly in Tauri's tauri:// protocol. Key frontend additions include a 3D orb visualization and persistent server URL configuration.
Written by Tembo for commit ddbe6ef. This will update automatically on new commits.

Add Tauri sidecar integration so the desktop app can launch a bundled
Spacebot server locally. Introduces a ConnectionScreen that gates the
app until a server is reachable, with options to connect to a remote
URL or start the bundled binary. Refactor the API client from a static
base URL to a dynamic getter so cross-origin Tauri requests work.

New components: ConnectionScreen, Orb (WebGL via ogl). New hook:
useServer for Tauri-aware server lifecycle management. Adds
bundle-sidecar.sh, desktop justfile recipes, and getting-started
desktop docs.
@coderabbitai

coderabbitai Bot commented Mar 18, 2026

Copy link
Copy Markdown
Contributor

Caution

Review failed

Pull request was closed or merged during review

Walkthrough

Adds desktop (Tauri) integration: persistent server URL IPC, client-side dynamic server handling and health polling, a Connection UI with sidecar lifecycle, Orb visual, docs, build scripts for bundling the sidecar, and minor gitignore updates for sidecar binaries.

Changes

Cohort / File(s) Summary
Gitignore Configuration
\.gitignore, desktop/src-tauri/.gitignore
Added ignore rule to exclude desktop/src-tauri/binaries/ (sidecar binaries) from version control.
Tauri Desktop IPC
desktop/src-tauri/src/main.rs
Added get_server_url and set_server_url Tauri commands to persist/retrieve connection.json in the app data directory (with default fallback URL).
Frontend API Client
interface/src/api/client.ts
Added exported setServerUrl/getServerUrl, introduced getApiBase() dynamic base resolution, replaced static API_BASE usage, and renamed eventsUrl to getEventsUrl.
Server Connection Context & Hooks
interface/src/hooks/useServer.tsx, interface/src/hooks/useLiveContext.tsx
New ServerProvider/useServer for server URL state, health checks, Tauri IPC sync, polling and connectivity state; LiveContextProvider updated to accept onBootstrapped and use dynamic events URL with conditional SSE activation.
App Shell Composition
interface/src/App.tsx
Introduced AppShell and restructured top-level to wrap app in ServerProvider, gating main app rendering behind connection/bootstrapping and moving LiveContextProvider instantiation.
Connection UI & Visuals
interface/src/components/ConnectionScreen.tsx, interface/src/components/Orb.tsx, interface/src/components/Orb.css
Added ConnectionScreen component (connect input, sidecar start via Tauri shell sidecar spawn, sidecar lifecycle handling) and new Orb WebGL visual component with supporting styles.
Documentation
docs/content/docs/(getting-started)/desktop.mdx, docs/content/docs/(getting-started)/quickstart.mdx
Added desktop app documentation covering connection flows, sidecar behavior, build/dev workflows; updated quickstart dev URL and added Desktop app references.
Build Tooling & Scripts
justfile, scripts/bundle-sidecar.sh
Added bundle-sidecar, desktop-dev, desktop-build just targets and scripts/bundle-sidecar.sh to build and copy the Rust sidecar binary into Tauri binaries directory (handles target triple and .exe naming).
Server Code Comment
src/api/server.rs
Added an explanatory inline comment about intentionally disabling CORS credentials and the security implications; no functional CORS change was made.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 68.18% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main changes: desktop app sidecar integration, connection screen UI, and 3D orb component—the primary new features in this changeset.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, detailing sidecar integration, connection screen, API refactoring, useServer hook, orb component, build pipeline, IPC commands, and documentation updates.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch desktop-improvements
📝 Coding Plan
  • Generate coding plan for human review comments

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ast-grep (0.41.1)
src/api/server.rs

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread scripts/bundle-sidecar.sh Outdated
Comment on lines +17 to +32
# Determine Rust target triple
TARGET_TRIPLE="${TAURI_ENV_TARGET_TRIPLE:-$(rustc -vV | awk '/^host:/ {print $2}')}"

# Build mode
BUILD_MODE="release"
CARGO_FLAGS="--release"
if [[ "${1:-}" != "--release" ]]; then
BUILD_MODE="debug"
CARGO_FLAGS=""
fi

echo "Building spacebot ($BUILD_MODE) for $TARGET_TRIPLE..."
cargo build $CARGO_FLAGS --manifest-path "$REPO_ROOT/Cargo.toml"

# Source binary path
SRC_BIN="$REPO_ROOT/target/$BUILD_MODE/spacebot"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TAURI_ENV_TARGET_TRIPLE can point at a non-host target, but the script always builds the host binary (no --target) and then copies it under the target triple name. That seems like it could silently ship the wrong binary when cross-building.

Suggested change
# Determine Rust target triple
TARGET_TRIPLE="${TAURI_ENV_TARGET_TRIPLE:-$(rustc -vV | awk '/^host:/ {print $2}')}"
# Build mode
BUILD_MODE="release"
CARGO_FLAGS="--release"
if [[ "${1:-}" != "--release" ]]; then
BUILD_MODE="debug"
CARGO_FLAGS=""
fi
echo "Building spacebot ($BUILD_MODE) for $TARGET_TRIPLE..."
cargo build $CARGO_FLAGS --manifest-path "$REPO_ROOT/Cargo.toml"
# Source binary path
SRC_BIN="$REPO_ROOT/target/$BUILD_MODE/spacebot"
# Determine Rust target triple
HOST_TRIPLE="$(rustc -vV | awk '/^host:/ {print $2}')"
TARGET_TRIPLE="${TAURI_ENV_TARGET_TRIPLE:-$HOST_TRIPLE}"
# Build mode
BUILD_MODE="release"
CARGO_FLAGS="--release"
if [[ "${1:-}" != "--release" ]]; then
BUILD_MODE="debug"
CARGO_FLAGS=""
fi
echo "Building spacebot ($BUILD_MODE) for $TARGET_TRIPLE..."
if [[ "$TARGET_TRIPLE" != "$HOST_TRIPLE" ]]; then
cargo build $CARGO_FLAGS --target "$TARGET_TRIPLE" --manifest-path "$REPO_ROOT/Cargo.toml"
SRC_BIN="$REPO_ROOT/target/$TARGET_TRIPLE/$BUILD_MODE/spacebot"
else
cargo build $CARGO_FLAGS --manifest-path "$REPO_ROOT/Cargo.toml"
SRC_BIN="$REPO_ROOT/target/$BUILD_MODE/spacebot"
fi

{
"name": "binaries/spacebot",
"sidecar": true,
"args": true

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the webview code always calls the sidecar with a fixed arg list, it’s safer to scope args down to just those values instead of true.

Suggested change
"args": true
"args": ["start", "--foreground"]

Comment on lines +8 to +41
/// Resolve the path to the connection settings file in the app data directory.
fn settings_path(app: &tauri::AppHandle) -> PathBuf {
let dir = app
.path()
.app_data_dir()
.expect("failed to resolve app data dir");
dir.join("connection.json")
}

/// Read the saved server URL, or return the default.
#[tauri::command]
fn get_server_url(app: tauri::AppHandle) -> String {
let path = settings_path(&app);
if let Ok(contents) = fs::read_to_string(&path) {
if let Ok(value) = serde_json::from_str::<serde_json::Value>(&contents) {
if let Some(url) = value.get("server_url").and_then(|v| v.as_str()) {
return url.to_string();
}
}
}
"http://localhost:19898".to_string()
}

/// Persist the server URL to disk.
#[tauri::command]
fn set_server_url(app: tauri::AppHandle, url: String) -> Result<(), String> {
let path = settings_path(&app);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).map_err(|e| e.to_string())?;
}
let value = serde_json::json!({ "server_url": url });
fs::write(&path, serde_json::to_string_pretty(&value).unwrap()).map_err(|e| e.to_string())?;
Ok(())
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor robustness: app_data_dir().expect(...) and to_string_pretty(...).unwrap() will hard-crash the desktop app on what should probably be recoverable errors.

Suggested change
/// Resolve the path to the connection settings file in the app data directory.
fn settings_path(app: &tauri::AppHandle) -> PathBuf {
let dir = app
.path()
.app_data_dir()
.expect("failed to resolve app data dir");
dir.join("connection.json")
}
/// Read the saved server URL, or return the default.
#[tauri::command]
fn get_server_url(app: tauri::AppHandle) -> String {
let path = settings_path(&app);
if let Ok(contents) = fs::read_to_string(&path) {
if let Ok(value) = serde_json::from_str::<serde_json::Value>(&contents) {
if let Some(url) = value.get("server_url").and_then(|v| v.as_str()) {
return url.to_string();
}
}
}
"http://localhost:19898".to_string()
}
/// Persist the server URL to disk.
#[tauri::command]
fn set_server_url(app: tauri::AppHandle, url: String) -> Result<(), String> {
let path = settings_path(&app);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).map_err(|e| e.to_string())?;
}
let value = serde_json::json!({ "server_url": url });
fs::write(&path, serde_json::to_string_pretty(&value).unwrap()).map_err(|e| e.to_string())?;
Ok(())
}
/// Resolve the path to the connection settings file in the app data directory.
fn settings_path(app: &tauri::AppHandle) -> Result<PathBuf, String> {
let dir = app
.path()
.app_data_dir()
.map_err(|e| e.to_string())?;
Ok(dir.join("connection.json"))
}
/// Read the saved server URL, or return the default.
#[tauri::command]
fn get_server_url(app: tauri::AppHandle) -> String {
let Ok(path) = settings_path(&app) else {
return "http://localhost:19898".to_string();
};
if let Ok(contents) = fs::read_to_string(&path) {
if let Ok(value) = serde_json::from_str::<serde_json::Value>(&contents) {
if let Some(url) = value.get("server_url").and_then(|v| v.as_str()) {
return url.to_string();
}
}
}
"http://localhost:19898".to_string()
}
/// Persist the server URL to disk.
#[tauri::command]
fn set_server_url(app: tauri::AppHandle, url: String) -> Result<(), String> {
let path = settings_path(&app)?;
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).map_err(|e| e.to_string())?;
}
let value = serde_json::json!({ "server_url": url });
let contents = serde_json::to_string_pretty(&value).map_err(|e| e.to_string())?;
fs::write(&path, contents).map_err(|e| e.to_string())?;
Ok(())
}

Comment on lines +57 to +69
async function checkHealth(baseUrl: string): Promise<boolean> {
try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 3000);
const response = await fetch(`${baseUrl}/api/health`, {
signal: controller.signal,
});
clearTimeout(timeout);
return response.ok;
} catch {
return false;
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small thing: if fetch throws before clearTimeout(timeout), the timeout stays live until it fires. Wrapping clearTimeout in a finally keeps it tidy (especially since this runs on an interval).

Suggested change
async function checkHealth(baseUrl: string): Promise<boolean> {
try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 3000);
const response = await fetch(`${baseUrl}/api/health`, {
signal: controller.signal,
});
clearTimeout(timeout);
return response.ok;
} catch {
return false;
}
}
async function checkHealth(baseUrl: string): Promise<boolean> {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 3000);
try {
const response = await fetch(`${baseUrl}/api/health`, {
signal: controller.signal,
});
return response.ok;
} catch {
return false;
} finally {
clearTimeout(timeout);
}
}

Comment thread src/api/server.rs Outdated
])
.allow_headers([header::CONTENT_TYPE, header::AUTHORIZATION, header::ACCEPT]);
.allow_headers([header::CONTENT_TYPE, header::AUTHORIZATION, header::ACCEPT])
.allow_credentials(true);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth double-checking the implications of .allow_credentials(true) together with AllowOrigin::mirror_request(): this effectively opts into credentialed CORS for any requesting origin. If auth ever moves to cookies (or there are other ambient credentials), that’s a pretty sharp edge; consider keeping credentials off unless it’s explicitly needed, or restricting allow_origin to an allowlist/predicate.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@desktop/src-tauri/src/main.rs`:
- Around line 9-14: The code currently panics via expect/unwrap in settings_path
and set_server_url; change settings_path(app: &tauri::AppHandle) to return
Result<PathBuf, String> and remove the expect by mapping the Option from
app.path().app_data_dir() into an Err(String) on None; update get_server_url to
call settings_path and handle the Err by returning the existing fallback URL (or
converting the error into the Result flow), and update set_server_url to
propagate both path resolution errors and serde_json::to_string / std::fs::write
errors by returning a tauri::Result (use ? or map_err to convert errors into
tauri::Error/String) instead of calling unwrap, ensuring functions mentioned
(settings_path, get_server_url, set_server_url) no longer call expect/unwrap and
surface recoverable errors to the Tauri command handler.

In `@interface/src/App.tsx`:
- Around line 27-31: The code uses hasConnected (from useServer) as a one-way
health latch which hides ConnectionScreen too early; instead introduce and use a
real bootstrap/ready signal that only flips after the app's initial data queries
succeed. Update useServer.tsx to expose a bootstrapped/serverReady boolean
(e.g., hasBootstrapped or isServerReady) that is set true only when initial
bootstrap queries complete, and in App.tsx replace the check (state !==
"connected" && !hasConnected) with (state !== "connected" && !hasBootstrapped)
or equivalent; ensure LiveContextProvider consumes the same bootstrap signal or
exposes a promise so ConnectionScreen remains until LiveContextProvider's
initial queries finish.

In `@interface/src/components/ConnectionScreen.tsx`:
- Around line 55-72: The close handler for command.on("close") currently only
treats non-zero codes as failures; update that handler to also handle clean
exits (code === 0) and null codes that occur before the readiness log is
observed by checking the current sidecar state (e.g., if sidecarState is
"starting" or not yet "running") and transitioning to a recoverable state (e.g.,
call setSidecarState("stopped") or setSidecarState("error") as appropriate) and
set a user-visible message via setSidecarError (include the code or "null" in
the message); make this change inside the command.on("close") callback so the
start button is re-enabled and users can retry if the sidecar exits before
command.stdout.on("data") ever reports "HTTP server listening".

In `@interface/src/hooks/useServer.tsx`:
- Around line 86-96: In loadPersistedUrl(): when running under IS_TAURI, treat
the Tauri IPC result value that acts as a default sentinel (e.g.
"http://localhost:19898") or empty string as “not persisted” so the code falls
back to localStorage; update the invoke handling in loadPersistedUrl to check
the returned url for truthiness AND that it is not the sentinel value (and not
the empty string) before returning it, otherwise continue to return
localStorage.getItem(STORAGE_KEY) so a real persisted URL in localStorage is
used.
- Around line 46-52: The normalizeUrl function currently preserves user-entered
paths which causes downstream desktop connections to become origin+path; update
normalizeUrl so it returns only the origin (scheme + host [+ port]) for a given
input: trim and default to DEFAULT_SERVER_URL, ensure protocol (add http:// when
missing), then parse the URL and return its origin (not pathname/search/hash).
Refer to the normalizeUrl function to implement this change so desktop
connections use origin-only URLs.

In `@scripts/bundle-sidecar.sh`:
- Around line 18-33: The script computes TARGET_TRIPLE but doesn't pass it to
cargo or use it in SRC_BIN/DEST_BIN; update the cargo invocation (the cargo
build call where CARGO_FLAGS and BUILD_MODE are used) to include --target
"$TARGET_TRIPLE" when TARGET_TRIPLE is set, and change SRC_BIN (and any
subsequent DEST_BIN references) to point to
target/$TARGET_TRIPLE/$BUILD_MODE/spacebot instead of
target/$BUILD_MODE/spacebot so the built binary path matches cross-compile
output; ensure this logic respects BUILD_MODE and CARGO_FLAGS variables and only
adds --target when TARGET_TRIPLE is non-empty.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9855dc36-50d9-4c3f-b55c-77f723461437

📥 Commits

Reviewing files that changed from the base of the PR and between ed3aebe and ddbe6ef.

⛔ Files ignored due to path filters (7)
  • desktop/src-tauri/capabilities/default.json is excluded by !**/*.json
  • desktop/src-tauri/gen/schemas/capabilities.json is excluded by !**/gen/**, !**/*.json, !**/gen/**
  • desktop/src-tauri/tauri.conf.json is excluded by !**/*.json
  • docs/content/docs/(getting-started)/meta.json is excluded by !**/*.json
  • interface/bun.lock is excluded by !**/*.lock, !**/*.lock
  • interface/package.json is excluded by !**/*.json
  • interface/public/ball.png is excluded by !**/*.png, !**/*.png
📒 Files selected for processing (15)
  • .gitignore
  • desktop/src-tauri/.gitignore
  • desktop/src-tauri/src/main.rs
  • docs/content/docs/(getting-started)/desktop.mdx
  • docs/content/docs/(getting-started)/quickstart.mdx
  • interface/src/App.tsx
  • interface/src/api/client.ts
  • interface/src/components/ConnectionScreen.tsx
  • interface/src/components/Orb.css
  • interface/src/components/Orb.tsx
  • interface/src/hooks/useLiveContext.tsx
  • interface/src/hooks/useServer.tsx
  • justfile
  • scripts/bundle-sidecar.sh
  • src/api/server.rs

Comment thread desktop/src-tauri/src/main.rs Outdated
Comment thread interface/src/App.tsx Outdated
Comment on lines +27 to +31
// Show connection screen if we've never connected, or if we lost
// connection before any data was loaded.
if (state !== "connected" && !hasConnected) {
return <ConnectionScreen />;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use a real bootstrap signal here.

hasConnected is only a one-way health-check latch in interface/src/hooks/useServer.tsx, while LiveContextProvider immediately starts real data queries once this branch mounts. A brief /api/health success is enough to hide ConnectionScreen even if the first app queries fail, which leaves the user in the main shell with no way to change the server URL or restart the sidecar.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@interface/src/App.tsx` around lines 27 - 31, The code uses hasConnected (from
useServer) as a one-way health latch which hides ConnectionScreen too early;
instead introduce and use a real bootstrap/ready signal that only flips after
the app's initial data queries succeed. Update useServer.tsx to expose a
bootstrapped/serverReady boolean (e.g., hasBootstrapped or isServerReady) that
is set true only when initial bootstrap queries complete, and in App.tsx replace
the check (state !== "connected" && !hasConnected) with (state !== "connected"
&& !hasBootstrapped) or equivalent; ensure LiveContextProvider consumes the same
bootstrap signal or exposes a promise so ConnectionScreen remains until
LiveContextProvider's initial queries finish.

Comment thread interface/src/components/ConnectionScreen.tsx
Comment thread interface/src/hooks/useServer.tsx Outdated
Comment thread interface/src/hooks/useServer.tsx
Comment thread scripts/bundle-sidecar.sh Outdated
- Remove expect/unwrap panics in Tauri command handlers; settings_path
  now returns Result and errors propagate gracefully
- Handle clean (code 0) and null sidecar exits before readiness so the
  start button re-enables and users can retry
- Replace hasConnected one-way health latch with hasBootstrapped signal
  that waits for LiveContextProvider's initial queries to resolve
- Strip normalizeUrl to origin-only so user-entered paths don't break
  desktop API connections
- Treat Tauri IPC default sentinel as 'not persisted' so localStorage
  fallback is reachable
- Pass --target to cargo and fix SRC_BIN path in bundle-sidecar.sh for
  cross-compilation correctness
- Move clearTimeout into finally block in checkHealth
- Scope sidecar shell args to ["start", "--foreground"] instead of true
- Remove allow_credentials(true) from CORS layer (Bearer auth only)
@jamiepine jamiepine merged commit 769452f into main Mar 19, 2026
4 of 5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant