Skip to content

rolebox v2.0.0

Choose a tag to compare

@EricMoin EricMoin released this 16 Oct 10:00
· 61 commits to main since this release

2.0.0 is the first stable release of the 2.x line. The breaking changes an upgrader from 1.x must act on are recorded under ## 2.0.0-alpha.1 and ## 2.0.0-alpha.2, and are not repeated here.

Features

  • The desktop computer-use family gains the observation and gesture tools it was missing — the family shipped screenshot, windows, click, move, type, key and permissions; this completes the twelve-tool surface the documentation now describes with computer_displays (each display's screenshot id, absolute screen-coordinate bounds, scale and primary, which is what makes a capture's screen origin knowable before converting image pixels to input coordinates), computer_focus (bring one computer_windows id to the foreground and confirm the OS actually focused it, refusing when the id cannot be matched unambiguously), computer_scroll (one to a hundred wheel ticks in a named direction, optionally after moving the pointer first), computer_drag (a left-button drag between two absolute positions over a bounded duration, with a best-effort mouse-release cleanup when the gesture is interrupted) and computer_status (plan every operation and report the platform, helper availability, refusals, the input grant and the screen-capture grant without taking a screenshot or injecting input). On macOS every pointer, text, scroll and drag gesture is now a native Quartz event posted through JXA: computer_click sends real left, right and middle mouse events instead of driving System Events plus the external cliclick helper, computer_type sends Quartz Unicode keyboard events, and computer_focus matches the window server's identity to one unambiguous Accessibility window — so cliclick is no longer required and its install hint is gone. Every tool also accepts timeout_ms (up to two minutes) on top of a queue that serializes operations within the host process, and the live-desktop checks stay opt-in — the native macOS capture tests run only under ROLEBOX_CUA_NATIVE_TESTS=1, with a fixture-app smoke runner under scripts/ — while the rewritten docs/computer-use.md documents the enablement gate, the arguments, the coordinate conversion and the per-platform limits.

  • The dsh Rolebox monitor draws an engine graph as a real node-and-edge figure — the pane rendered every engine graph as a strip of one small cell per node, which read as a single bar and said nothing about how nodes route to each other. A graph whose GET /rolebox/status payload carries its version-3 declaration now renders an orthogonal layered figure: nodes are boxes in rank rows, edges are orthogonal polylines with per-edge ports so two runs can never overlap, rank-skipping edges take a right rail, back edges become a U hairpin below the rank row, declared loop groups are drawn as labelled bands, and an outcome chip appears only where the source declares two or more outcomes or the edge is a back edge. Ranks come from the strongly-connected-component condensation, so a cycle shares one rank and can neither inflate the layout nor make it recurse, and the whole layout is a pure browser-safe function (layoutGraphTopology, src/platform/adapters/dsh/web-ui/graph-topology.ts) with no DOM, clock or randomness. The figure is a single image with an accessible name and a per-node title, beside a screen-reader-only routing list that states every edge in words; a rank wider than the docked sidebar scrolls inside the canvas instead of being squeezed, and a payload with no declaration nodes keeps the strip as its fallback.

Bug Fixes

  • A capture now checks the Screen Recording grant, and macOS input no longer breaks on the Accessibility prompt — computer_screenshot captured regardless of permission, so a host without the Screen Recording grant returned a wallpaper-only PNG as if it were a screenshot; the tool now runs the CGPreflightScreenCaptureAccess probe first and refuses with the grant to enable, computer_permissions and computer_status report the same screen-capture verdict beside the input verdict, and a capture larger than the attachment limit is refused with the limit stated instead of being read whole. The macOS probe passed a bare kAXTrustedCheckOptionPrompt reference into AXIsProcessTrustedWithOptions, which could not present the system dialog; the value is now cast into an Objective-C reference. computer_screenshot also refuses a call that names more than one of window_id, region and display instead of silently choosing one, a capture now reports metadata.origin and metadata.coordinate_space beside metadata.pixel_scale (with an explicit warning when a window or display capture's origin is unknown) and the pixel-scale note says to add that origin after dividing, and a command terminated by a signal is reported as such rather than as an exit status. A role's exact computer_<tool>: false now overrides the computer_*: true wildcard, computer_permissions is no longer classified concurrency-safe when it may open an OS dialog, and on Pi a tool context with no worktree falls back to its directory instead of resolving a capture to a path relative to the process.

  • Rolebox reads dsh's model catalog from the layer that actually holds it — dsh no longer keeps user settings in <dsh home>/settings.yaml: the settings package imports that file once into the active profile and renames it settings.yaml.imported, after which the active profile's Cordis patch carries the values. The dsh reader still looked only at the removed file, so it always answered with nothing and the dsh target silently fell back to the opencode seed catalog in the model picker and in the runtime model resolver alike. The reader now scans the home-level cordis.patch.yml, then every profiles/<name>/cordis.patch.yml in directory-name order, then the legacy settings.yaml, with the first occurrence of an id winning so a higher-precedence layer shadows a lower one. Extraction stays structural — a providers.<route>.models[] map yields <route>/<model id> from its dict key, while any other models[] takes its route from the same document's agent-default-model provider or stays a bare id — with no vendor-to-route table and no built-in model list. The !!js expressions dsh patch layers allow, which js-yaml's default schema rejects outright (discarding the whole layer and every model in it), are now registered as an opaque value that is parsed but never evaluated, and a model that resolves no provider no longer renders a dangling Name () in the picker. Reference: docs/model-aliases.md.

  • The dsh monitor and Logs panes scroll inside the host's tab body — the host's right Sidebar wraps every tab body in a pane-height clipping box (height: 100%; overflow: hidden), so a tab body root must be bounded and scroll inside itself; both rolebox panes were static blocks of natural height (the console measured 5145px and the log view 9629px inside an 855px box), so everything past the first viewport was clipped away and unreachable at any scroll position, and the pane went blank when scrolled. Both roots are now height: 100%; min-height: 0; overflow-y: auto with overscroll-behavior: contain and contain: paint, which also stops a pane's scrollable overflow from propagating into the host's .paneBody, where it created a phantom scroll range whose scrollbar dragged the pane out of view. Reference: docs/dsh-plugin-contract.md.

  • Graph workers dispatch again on dsh 0.2.x, which removed the subagent start seam they used — dsh replaced SubagentRuntime.start(name, request) with startActivation(spec) and narrowed SubagentCapabilities to agentOptions / agentPreset / outputSchema / depthLimit, dropping the start-request toolFilter and persona fields. rolebox still called the removed method, so dispatching a declared graph's node — or a loop round — threw before a worker existed. Every production call site now activates through startActivation, and the provider advertises the four booleans dsh actually declares. Because rolebox's provider declares prepareContinuable, dsh takes its LOCAL activation path and never calls the provider's own start(), so the role prompt, the per-session active-role override, the spawn context and the model override are composed at the call site by DshAgentRegistrar.composeSpawnRequest (wired into both the loop dispatch adapter and the graph outcome delivery) and a graph worker's complete prompt is prepended there. The activation's label still carries the stable idempotency key and dispose() is still the abort surface, and a graph worker's declared tool face is now enforced by rolebox's own execution guard plus the prompt rather than by a start-time filter.

  • rolebox declares the dsh line it needs, so an unsupported host is refused before it loads — rolebox established every subagent through startActivation and advertised the four-boolean capability shape, but package.json declared no dsh requirement at all: dsh's pre-flight reads engines.dsh first and otherwise only @deepseek-ai/dsh / @deepseek-ai/dsh-* PEER ranges, and rolebox's dsh packages are devDependencies, so every line passed the check and an unsupported one failed later at the dispatch seam. engines.dsh now states >=0.2.1-alpha.3, the first release whose subagent contract rolebox implements. Verified against dsh's own evaluatePluginCompatibility: 0.2.1-alpha.4 and 0.2.1-alpha.3 satisfy it, while 0.2.1-alpha.2, 0.2.1-alpha.1, 0.2.0-rc.2 and 0.1.5-rc.2 are refused with dsh's incompatibility message and the dsh plugin allow-version exemption path.

Documentation

  • The README leads with the captures that exist and states only what each one shows — the README before the rewrite carried the banner and the two demo gifs (assets/gifs/emperor-dispatch.gif, assets/gifs/loop-mode.gif) and no screenshots; this commit rewrites the prose, deletes the two now-unreferenced gifs and leads with the eight committed screenshots under assets/screenshots/ — the role picker over the loaded roles, a TypeScript Engineer declaring a graph, the parent reconciling accepted results against a recorded baseline, a one-shot worker settling its node, the monitor pane, the rolebox monitor TUI, the roles settings page and the channel-filtered logs — each with a caption stating only what that capture shows. Claims the captures or the source contradict were corrected: the computer-use family is twelve tools rather than seven, the language-server surface is exactly 32 lsp_* tools, the graph-declare section no longer asserts an execution budget no capture shows, the settings page is not described as reporting a role's origin, the TUI caption no longer claims a loops or dispatch view, the logs pane filters by channel and session rather than by code, and the upgrade note no longer says persisted legacy state is unreadable — a legacy state body stays readable and is simply never advanced. Reference: README.md.

  • README images resolve against the branch the README is read on — every repository-owned image pointed at raw.githubusercontent.com/<owner>/<repo>/HEAD/..., and HEAD there is the repository's default branch, so the eight screenshots that exist only on the development branch 404'd wherever the README was read on that branch, while the banner only appeared to work because that one file also exists on the default branch. All nine src values are now repository-relative, so each branch's copy of the README renders its own assets. Reference: README.md.

  • The published npm README gets images that resolve, without a broken image ever being publishable — npm stores the bytes of README.md as the registry readme and the npm site resolves relative URLs against its own domain, so the repository-relative paths that make GitHub render the branch being read left every screenshot broken on the package page; npm cannot be pointed at a second readme file (@npmcli/package-json globs {README,README.*} and reads those bytes, and readmeFilename is an output field, never an input). npm publish now rewrites the nine local image src values to commit-pinned raw URLs during prepack and restores them in postpack (scripts/npm-readme.ts); a path that is not present in the commit being packaged refuses the whole rewrite rather than emit a partially pinned README, and the inverse transform is deterministic so postpack needs no state from prepack. The README on disk stays repository-relative, which is what GitHub needs.

  • The package description and keywords name every supported harness, and the dsh install line can be copied for the web profile — package.json's description now presents the plugin as one for major harnesses (DeepSeek Harness, OpenCode, Pi and Codex) rather than an unnamed agent plugin, and it gains the discovery keywords dsh-plugin, deepseek-harness, dsh-bundle, cordis, cordis-plugin and deepseek-harness-plugin so the package is found by those searches; the dsh section of the README shows the copy-pasteable dsh plugin --profile web add rolebox command beside the generic profile-name example. Reference: README.md.

  • Every harness minimum is stated once, and the README badge row names the binding one — rolebox's four targets each carry a real version contract, but only dsh's is both hard and non-obvious: the subagent line moved twice inside 0.2.1-alpha, so startActivation and delivery appear in 0.2.1-alpha.2 while the four-boolean capability shape rolebox advertises (agentPreset in, toolFilter/persona out) only appears in 0.2.1-alpha.3 — and 0.2.0-rc.x has neither. docs/compatibility.md gains a Harness versions section carrying that release table together with the opencode 1.x / v2, Pi and Codex minimums and where each is declared, and the README badge row gains a dsh 0.2.1-alpha.3+ badge that links to it beside a pointer from the Install section. Reference: docs/compatibility.md.

Tests

  • The live opencode integration tests run only against a v1 CLI — the real-server tests use the v1 SDK's startup protocol and routes, but the shared helper treated any successful opencode --version as usable, so a v2 CLI satisfied the probe and then could not serve that contract. The probe is now version-aware (isOpencodeV1Version, tests/helpers/opencode.ts): a recognized v1 version runs the tests, a recognized incompatible version skips them, and an unrecognized version format — as well as a probe failure, a non-zero probe exit or a probe exceeding its five-second bound — fails explicitly instead of being read as available. Server startup cleanup also tolerates a server that never came up without hiding the original failure.