Skip to content

feat(tools/computer_use): native per-OS desktop control (macOS / Linux / Windows) - #20660

Closed
Abd0r wants to merge 1 commit into
NousResearch:mainfrom
Abd0r:feat/computer-use
Closed

feat(tools/computer_use): native per-OS desktop control (macOS / Linux / Windows)#20660
Abd0r wants to merge 1 commit into
NousResearch:mainfrom
Abd0r:feat/computer-use

Conversation

@Abd0r

@Abd0r Abd0r commented May 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds three OS-specific desktop-control tools — computer_use_macos, computer_use_linux, computer_use_windows — sharing one JSON schema and one set of action semantics, but with native backends per platform. Per-OS skills teach the model platform-specific shortcuts and idioms.

This PR is complementary to the three existing computer-use efforts in flight; it owns a different deployment shape:

Effort Deployment shape
#15876 Linux inside Docker (containerised body, host-OS-agnostic, sandboxed)
#4562 macOS native, Anthropic computer-use protocol
#13308 macOS native, Hermes-protocol with approval gating
This PR Native host desktop on macOS + Linux + Windows — same agent-facing abstraction, native backends per OS

There's no overlap with #15876 (we're outside Docker, native to the host). There's surface overlap with #4562 / #13308 on macOS — but the value here is the consistent 3-OS surface using one schema and one grammar; the operator can run Hermes on any of the three majors and the model sees the same action vocabulary. Maintainers can land any combination of the four — they target genuinely different operator setups.

Problems this solves

Concretely, what does an operator gain by having computer_use_* available?

  1. Drive native desktop apps that have no CLI or web surface — Adobe apps, Office desktop, native installers, system settings panels, finance/CAD/DAW apps. Today the agent has to give up or ask the user to do it manually. With this PR, the agent screenshots, clicks, types, and gets the job done.

  2. Consistent abstraction across the three majors — same JSON schema, same action vocabulary, same grammar (Cmd+Tab parses on every host; the parser routes to the correct OS primitive). An operator running Hermes on a personal Mac, a Linux workstation, and a Windows VM at work doesn't have to retrain prompts or learn three different tool surfaces.

  3. Native host control without a container — the containerised path in Proposal: Optional desktop computer-use module (noVNC + screenshot + mouse/keyboard control) #15876 is excellent for VPS / always-on / sandboxed deployments, but it's the wrong answer when the operator wants Hermes to drive their actual desktop: their logged-in browser sessions, their installed apps, their multi-monitor setup. This PR fills that gap.

  4. Foundation for higher-level "GUI teaching" / record-and-replay efforts — issue [Feature]: GUI Teaching for Hermes Agent – Record mouse/keyboard workflows as executable skills (macOS) #19802 (Linka — record mouse/keyboard workflows on macOS, generate skills from demonstrations, replay them) needs a runtime primitive to execute recorded events. This PR provides exactly that primitive across all three OSes, so a Linka-style record-and-replay layer can sit on top without re-implementing per-OS native input.

  5. Keeps Hermes provider-neutral — by emitting a Hermes-internal tool spec rather than the Anthropic computer-use-tool block, this works across every provider Hermes supports (OpenRouter, OpenAI, Anthropic, custom local) without protocol-specific plumbing in the tool layer.

Issues referenced

This PR doesn't auto-close any single issue (computer-use is a new capability, not a bug fix). Issues it relates to:

Architecture

  • tools/computer_use_common.pyActionRequest / ActionResult dataclasses, shared JSON schema (mirrors Anthropic's computer_20251124 action set with practical additions: get_active_window, screen_size, cursor_position), parameter validation, screen-bounds enforcement.
  • tools/computer_use_safety.pyHERMES_COMPUTER_USE_ENABLED env gate (default off; tool refuses every action when unset), process-global kill-switch flag, append-only JSONL action log under \$HERMES_HOME/logs/computer_use.jsonl, screenshot redaction via PIL.
  • tools/computer_use_grammar.py — one parser, four targets. Cmd+Shift+T produces Quartz CGEvent flag mask + Carbon keycode on macOS, lowercase xdotool key argument on X11, Linux input event-code sequence on Wayland, Win32 VK codes for SendInput on Windows. Modifier aliases collapse cross-platform (Cmd/Win/Super/Meta → same canonical token), so a model trained against one OS's idiom routes correctly on the others.
  • tools/computer_use_macos.py — Quartz CGEventCreateMouseEvent / CGEventCreateKeyboardEvent for input, screencapture CLI for capture (always present on macOS), CGWindowListCopyWindowInfo for active-window queries. Only new dependency: pyobjc-framework-Quartz.
  • tools/computer_use_linux.py — runtime detection of X11 vs Wayland from \$WAYLAND_DISPLAY / \$XDG_SESSION_TYPE. X11 path uses xdotool + scrot / ImageMagick import. Wayland path uses ydotool + grim (wlroots) / gnome-screenshot / spectacle. Active-window queries: Sway IPC / hyprctl on Wayland, xdotool getactivewindow on X11.
  • tools/computer_use_windows.pyctypes wrapper over user32.SendInput (modern path; legacy keybd_event / mouse_event avoided). Per-monitor v2 DPI awareness set on import so click coordinates aren't scaled. Screenshot via mss if installed, falls back to a built-in ctypes BitBlt path so the tool works on a default Python install with no extra deps.

All three OS tools call registry.register() at module top so the AST tool-discovery picks them up. Each tool's check_fn returns False off the matching platform / when HERMES_COMPUTER_USE_ENABLED is unset / when required deps are missing — so on any given host the model only sees the one tool it can actually use, never the other two.

Skills

Per-OS skill teaches the model what's actually different on each host:

  • skills/computer-use/common/SKILL.md — when to reach for computer_use_* at all (vs browser_tool / terminal), screenshot-first discipline, cost discipline (token budget on base64 PNGs), redact_regions usage.
  • skills/computer-use/macos/SKILL.md — Cmd-vs-Ctrl, Spotlight (Cmd+Space) idiom, Mission Control, Accessibility + Screen Recording permission setup, multi-monitor caveats, things that won't work (Touch ID prompts, system password dialogs).
  • skills/computer-use/linux/SKILL.md — X11 vs Wayland detection, ydotoold + uinput setup, DE-specific shortcuts (GNOME / KDE / wlroots / Xfce), polkit / pkexec lockout warnings.
  • skills/computer-use/windows/SKILL.md — Win+S as Spotlight analogue, UAC / UIPI restrictions on synthetic input to elevated windows, DPI awareness, mss vs BitBlt screenshot tradeoff.

Validation

Unit tests: 56 / 56 passing (tests/tools/test_computer_use.py). Mocked Quartz, subprocess, and ctypes user32. Coverage:

  • Common: schema parse, validation rejection paths, screen-bounds enforcement.
  • Grammar: round-trip parsing, all four backend targets, modifier aliases, F1-F24 mapping, unknown-token rejection.
  • Safety: env-gate truthy/falsy values, kill-switch lifecycle, redact_image (PIL pixel-level assertion), JSONL log writes.
  • macOS: every action path against a stubbed Quartz, including screenshot-b64 round-trip and CGEvent flag emission.
  • Linux: X11 path (xdotool + scrot, xdpyinfo screen-size parsing) and Wayland path (ydotool + grim, wlr-randr screen-size).
  • Windows: SendInput call counts, screen-size, off-screen click rejection.

macOS integration on the author's MacBook (live, real Quartz, real screencapture): 11 / 11 cases pass —
```
✓ screen_size → {1470, 956}
✓ cursor_position → real cursor
✓ get_active_window → frontmost app correctly identified
✓ screenshot full → 575,589-byte real PNG round-trips through base64
✓ screenshot region → 6,171-byte cropped PNG (200×200)
✓ screenshot redact → redacted PNG (PIL fills rectangles black)
✓ wait → 50ms sleep returns clean result
✗ off-screen click (rejected) → validation error with screen bounds
✗ type empty (rejected) → validation error with field name
✗ unknown action (rejected) → validation error listing valid actions
✗ env-off refusal → 'refused: HERMES_COMPUTER_USE_ENABLED is not set'
```
Action log JSONL writes verified.

Linux + Windows: unit-test-only. Author has no Linux or Windows host immediately available for live integration validation. Mocked-subprocess and mocked-ctypes coverage exercises every code path, but I want to be honest that real-host validation hasn't happened yet. Happy to run live tests once a tester is available, or for maintainers to gate landing the Linux/Windows files on community validation.

Safety posture

  • Default-off env gate. `HERMES_COMPUTER_USE_ENABLED=true` required. Default behaviour: every action returns `{success: false, error: 'refused: …'}`. The operator opts in explicitly.
  • Action allowlist + per-action validation. Off-screen coordinates rejected before reaching the OS. Type strings capped at 10K chars. Wait values capped at 30s. Scroll amounts capped at 50 ticks. Unknown actions rejected with a list of valid actions in the error.
  • Process-global kill-switch. `set_kill_switch()` engages a flag checked before every action. Once engaged, subsequent actions refuse until `clear_kill_switch()` or gateway restart. OS backends that can register a global hotkey can wire it to set the flag; that's an obvious follow-up.
  • JSONL audit log. Every action attempt (including refusals) is logged to `$HERMES_HOME/logs/computer_use.jsonl` with timestamp, action name, params (screenshot bytes stripped), success bit, error if any. Browsable post-incident.
  • Screenshot redaction. `screenshot` action accepts `redact_regions=[[x1, y1, x2, y2], …]` to blank rectangles before the image reaches the model. Useful for hiding password fields, MFA codes, or sensitive UI zones.

Test plan

  • Reviewer runs the unit suite locally: `python -m unittest tests.tools.test_computer_use`
  • On macOS with `pyobjc-framework-Quartz` installed and `HERMES_COMPUTER_USE_ENABLED=true`, confirm tool registers and `screen_size` returns the host's resolution
  • On Linux with `xdotool` + `scrot` installed (X11 session), confirm registration and `screen_size`
  • On Linux Wayland with `ydotool` + `grim` installed and ydotoold running, confirm registration and `screen_size`
  • On Windows confirm registration and `screen_size` with default Python install (BitBlt fallback)
  • Verify the env gate refuses every action when unset
  • Verify the off-screen coordinate rejection on at least one host

🤖 Generated with Claude Code

@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/tools Tool registry, model_tools, toolsets labels May 6, 2026
@Abd0r
Abd0r force-pushed the feat/computer-use branch from 5b9956e to 525029f Compare May 15, 2026 19:32
…Windows)

Three OS-specific tools — `computer_use_macos`, `computer_use_linux`,
`computer_use_windows` — sharing one JSON schema and one set of action
semantics, but with native backends per platform. Complementary to
the containerised proposal in NousResearch#15876 (which targets the
"Hermes-runs-in-Docker" deployment shape) and the macOS-Anthropic-protocol
work in NousResearch#4562 / NousResearch#13308. This PR owns the "Hermes runs natively on the
host desktop, control any of the three majors with consistent abstraction"
shape.

Architecture
============

* All three tools register at module top via `registry.register()` so
  the AST tool-discovery picks them up. `check_fn` returns False off the
  matching platform / when `HERMES_COMPUTER_USE_ENABLED` is unset / when
  required deps are missing — so on a given host the model only sees the
  one tool it can actually use.
* `computer_use_common.py` — schema, `ActionRequest`, `ActionResult`,
  parameter validation, screen-bounds enforcement.
* `computer_use_safety.py` — env gate, kill-switch flag, JSONL action
  log under `$HERMES_HOME/logs/computer_use.jsonl`, screenshot
  redaction (PIL).
* `computer_use_grammar.py` — one parser, four targets. `Cmd+Shift+T`
  produces Quartz CGEvent flags+keycode on macOS, `xdotool key`
  string on X11, ydotool input event codes on Wayland, Win32 VK
  codes on Windows.
* `computer_use_macos.py` — Quartz `CGEvent` for input,
  `screencapture` CLI for capture, `CGWindowListCopyWindowInfo` for
  active window. pyobjc-framework-Quartz is the only new dep.
* `computer_use_linux.py` — runtime detection of X11 vs Wayland.
  X11 → `xdotool` + `scrot`/`import`. Wayland → `ydotool` + `grim`
  (wlroots) / `gnome-screenshot` / `spectacle`. Active-window queries
  via Sway IPC / hyprctl / xdotool depending on path.
* `computer_use_windows.py` — `ctypes` over `user32.SendInput` (modern
  path; avoids legacy `keybd_event`). DPI-aware on import. Screenshot
  via `mss` if installed, falls back to ctypes BitBlt + PIL otherwise.

Skills
======

Per-OS skill teaches the model what's actually different on each host:
Cmd-vs-Ctrl, Spotlight vs Win+S, X11 vs Wayland detection, UAC/UIPI,
accessibility / screen-recording perm setup, etc. The common skill
covers when to reach for `computer_use_*` at all (vs `browser_tool` /
`terminal`) and the screenshot-first discipline.

Validation
==========

* 56/56 unit tests passing (mocked Quartz / subprocess / user32 across
  all three backends + grammar + safety).
* macOS backend integration-tested live on the author's MacBook:
  screen_size, cursor_position, get_active_window, screenshot (full),
  screenshot (region crop), screenshot (with redact), wait, off-screen
  click validation, type-without-text validation, unknown-action
  validation, env-off refusal — all 11/11 cases pass.
* Linux + Windows are unit-test-only at the moment; author has no Linux
  or Windows host immediately available for end-to-end validation.
  Honest framing in the eventual PR body.

Safety posture
==============

* `HERMES_COMPUTER_USE_ENABLED=true` required. Default: refused.
* Action allowlist + per-action validation (no off-screen, no >10K
  type strings, no >30s waits, no unknown actions).
* Process-global kill-switch flag (`set_kill_switch()`) checked
  before every action — engaged once, all subsequent actions refuse
  until cleared.
* JSONL audit log of every attempt (action, params minus image bytes,
  success bit, error if any).
* `screenshot` action accepts `redact_regions` to blank rectangles
  (password manager, MFA codes) before the image reaches the model.
@Abd0r
Abd0r force-pushed the feat/computer-use branch from 525029f to 2d570cf Compare May 15, 2026 19:41
@teknium1

Copy link
Copy Markdown
Contributor

Tagging @f-trycua @ddupont808 @jamesmurdza for context — this PR proposes native per-OS desktop backends (macOS / Linux / Windows) inside Hermes, parallel to the cua-driver path we already integrate with (issue #24015, recently expanded for auxiliary.vision routing in #30126, and the bug-bundle cleanup in #24170).

Two questions where your perspective would be useful before we decide direction here:

  1. Windows + Linux roadmap on cua-driver. Is upstream cua-driver planning Windows/Linux backends, and on what timeline? If yes, we'd rather hold for those and keep one backend abstraction in Hermes rather than ship two parallel desktop-control surfaces.

  2. The Hermes-internal native Windows backend in this PR (tools/computer_use_windows.pyctypes over user32.SendInput, per-monitor v2 DPI, mss + BitBlt fallback). If cua-driver is going that direction independently, would the trycua team be open to absorbing this work as a cua-driver-Windows starting point so the abstraction stays unified? Or do you see independent value in a non-cua-driver Windows path?

No urgency — just want to make sure we don't fork the desktop-control surface unnecessarily. Happy to wait on your read before this lands.

f-trycua added a commit to trycua/hermes-agent that referenced this pull request May 23, 2026
The cua-driver backend was gated to macOS only:

    # tools/computer_use/tool.py
    def check_computer_use_requirements() -> bool:
        if sys.platform != "darwin":
            return False
        ...

But cua-driver itself has been Windows-feature-complete since cua-driver-rs
(the cross-platform Rust port) shipped its Windows backend. Every action
tool — click, type_text, hotkey, drag, scroll, screenshot, launch_app,
list_apps, list_windows, get_window_state, move_cursor, wait — is marked
VERIFIED on Windows in the cross-platform PARITY matrix:
https://github.com/trycua/cua/blob/main/libs/cua-driver-rs/PARITY.md

This PR widens the gate to `sys.platform in ("darwin", "win32")`. No new
code paths — the existing MCP stdio integration in cua_backend.py works
identically against cua-driver on Windows because cua-driver's tool
surface is uniform across OSes.

Linux is not in scope. cua-driver-rs Linux support exists in tree but is
alpha (most Linux rows in PARITY are OPEN, not VERIFIED) — keeping it gated
off here until upstream flips those to VERIFIED. The plumbing is
OS-agnostic so flipping the gate later is one-line.

Empirical verification on Windows 11 24H2 (2026-05-22 dogfood):

  - Built-in Administrator (RID 500) at High IL via cua-driver-rs
    RunLevel=Highest autostart task:
      `cua-driver call get_window_state` for Calculator UWP
      → element_count: 41

  - Regular admin (UAC-split, Medium IL primary token) running
    `cua-driver call` directly from PowerShell:
      `cua-driver call get_window_state` for Calculator UWP
      → element_count: 41

UWP / AppContainer UIA works at any IL for any user. No EV cert, no
uiAccess="true" manifest, no Program Files install requirement.

## Changes

- tools/computer_use/tool.py: replace `sys.platform != "darwin"`
  early-return with `sys.platform not in ("darwin", "win32")`. Update
  top-of-file docstring + vision-prompt phrasing ("macOS application" →
  "desktop application") so the model isn't told to expect a Mac UI when
  it's looking at a Windows screen.
- tools/computer_use/cua_backend.py: rewrite top-of-file docstring to
  cover macOS + Windows + the Linux-alpha caveat. `is_available()`
  matches the same `darwin/win32` allowlist. `cua_driver_install_hint()`
  returns the Windows installer (irm | iex) on Windows, the bash
  installer on macOS.
- tools/computer_use_tool.py: update registry description from "macOS
  desktop control" to "desktop control (macOS, Windows; Linux alpha)".

The macOS-specific bits in `cua_backend.py` (the `_is_arm_mac` helper, the
"macOS reports localized app names" warning) stay as-is — they're macOS
runtime details that are conditionally taken when running on macOS, not
gates that block other OSes.

## Install

Same one-liner story, OS-specific installer:

  macOS:
    /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/trycua/cua/main/libs/cua-driver/scripts/install.sh)"

  Windows (PowerShell):
    irm https://raw.githubusercontent.com/trycua/cua/main/libs/cua-driver/scripts/install.ps1 | iex

After install, `cua-driver` is on $PATH and Hermes's check_fn sees it.

## Related

Replies to @teknium1's question on NousResearch#20660 about whether cua-driver-rs
ships Windows + Linux backends and whether @Abd0r's per-OS Python work
should be absorbed into cua-driver as a starting point. Short answer:
the cua-driver-rs Rust impl is months ahead of a fresh Python port on
Windows. Linux is alpha and will get there. Several pieces of NousResearch#20660
(kill-switch, JSONL audit log, screenshot redact_regions, the per-OS
SKILL.md docs) are worth absorbing into cua-driver as follow-up work —
separate from this PR.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@f-trycua

Copy link
Copy Markdown
Contributor

Thanks for the tag, @teknium1.

Windows cua-driver has just shipped, pending some further tests in Hermes. Linux is still alpha.

cua-driver-rs v0.2.18 (cross-platform Rust port) is Windows-feature-complete — every action tool marked VERIFIED in PARITY.md, including UWP apps (Calculator, Settings, etc.). Linux is in tree but most rows are still OPEN; we'd suggest holding for VERIFIED there before Hermes flips it on.

Drafted what unified-backend looks like: #30660 - ~43 LOC across 3 files, widens the sys.platform != "darwin" gate to allow Windows. The existing cua_backend.py MCP stdio integration works identically on Windows because cua-driver's tool surface is uniform. Linux stays gated until cua-driver-rs Linux goes VERIFIED upstream (one-line flip when that happens).

Re: absorbing the Python computer_use_windows.py - probably does not make sense as a starting point; the Rust impl in cua-driver-rs/crates/platform-windows is weeks ahead (Chromium/Electron click routing, UWP CoreWindow fallback, UIA cache batching, autostart at RunLevel=Highest). But @Abd0r's safety primitives are great and worth absorbing into cua-driver as follow-up: kill-switch, JSONL audit log, modifier-aliasing grammar (Cmd/Win/Super/Meta → canonical), screenshot redact_regions for password/MFA blanking.

Thanks @Abd0r for the careful per-OS work!

cc @ddupont808

teknium1 pushed a commit that referenced this pull request Jun 15, 2026
The cua-driver backend was gated to macOS only:

    # tools/computer_use/tool.py
    def check_computer_use_requirements() -> bool:
        if sys.platform != "darwin":
            return False
        ...

But cua-driver itself has been Windows-feature-complete since cua-driver-rs
(the cross-platform Rust port) shipped its Windows backend. Every action
tool — click, type_text, hotkey, drag, scroll, screenshot, launch_app,
list_apps, list_windows, get_window_state, move_cursor, wait — is marked
VERIFIED on Windows in the cross-platform PARITY matrix:
https://github.com/trycua/cua/blob/main/libs/cua-driver-rs/PARITY.md

This PR widens the gate to `sys.platform in ("darwin", "win32")`. No new
code paths — the existing MCP stdio integration in cua_backend.py works
identically against cua-driver on Windows because cua-driver's tool
surface is uniform across OSes.

Linux is not in scope. cua-driver-rs Linux support exists in tree but is
alpha (most Linux rows in PARITY are OPEN, not VERIFIED) — keeping it gated
off here until upstream flips those to VERIFIED. The plumbing is
OS-agnostic so flipping the gate later is one-line.

Empirical verification on Windows 11 24H2 (2026-05-22 dogfood):

  - Built-in Administrator (RID 500) at High IL via cua-driver-rs
    RunLevel=Highest autostart task:
      `cua-driver call get_window_state` for Calculator UWP
      → element_count: 41

  - Regular admin (UAC-split, Medium IL primary token) running
    `cua-driver call` directly from PowerShell:
      `cua-driver call get_window_state` for Calculator UWP
      → element_count: 41

UWP / AppContainer UIA works at any IL for any user. No EV cert, no
uiAccess="true" manifest, no Program Files install requirement.

## Changes

- tools/computer_use/tool.py: replace `sys.platform != "darwin"`
  early-return with `sys.platform not in ("darwin", "win32")`. Update
  top-of-file docstring + vision-prompt phrasing ("macOS application" →
  "desktop application") so the model isn't told to expect a Mac UI when
  it's looking at a Windows screen.
- tools/computer_use/cua_backend.py: rewrite top-of-file docstring to
  cover macOS + Windows + the Linux-alpha caveat. `is_available()`
  matches the same `darwin/win32` allowlist. `cua_driver_install_hint()`
  returns the Windows installer (irm | iex) on Windows, the bash
  installer on macOS.
- tools/computer_use_tool.py: update registry description from "macOS
  desktop control" to "desktop control (macOS, Windows; Linux alpha)".

The macOS-specific bits in `cua_backend.py` (the `_is_arm_mac` helper, the
"macOS reports localized app names" warning) stay as-is — they're macOS
runtime details that are conditionally taken when running on macOS, not
gates that block other OSes.

## Install

Same one-liner story, OS-specific installer:

  macOS:
    /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/trycua/cua/main/libs/cua-driver/scripts/install.sh)"

  Windows (PowerShell):
    irm https://raw.githubusercontent.com/trycua/cua/main/libs/cua-driver/scripts/install.ps1 | iex

After install, `cua-driver` is on $PATH and Hermes's check_fn sees it.

## Related

Replies to @teknium1's question on #20660 about whether cua-driver-rs
ships Windows + Linux backends and whether @Abd0r's per-OS Python work
should be absorbed into cua-driver as a starting point. Short answer:
the cua-driver-rs Rust impl is months ahead of a fresh Python port on
Windows. Linux is alpha and will get there. Several pieces of #20660
(kill-switch, JSONL audit log, screenshot redact_regions, the per-OS
SKILL.md docs) are worth absorbing into cua-driver as follow-up work —
separate from this PR.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@teknium1

Copy link
Copy Markdown
Contributor

Superseded by #50552 — a comprehensive cross-platform cua-driver implementation (macOS/Windows/Linux) now merged to main. It covers the platform this PR targeted. Thanks for the contribution; closing as superseded.

@teknium1 teknium1 closed this Jun 22, 2026
@Abd0r
Abd0r deleted the feat/computer-use branch August 6, 2026 18:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/tools Tool registry, model_tools, toolsets P3 Low — cosmetic, nice to have type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants