v2.1.0 — Scripting, variable resolver, throughput surface
v2.1 turns Bowire from a discovery + invoke workbench into a real composition surface. The new Compose rail carries a Hoppscotch-style request builder with a protocol picker, per-protocol layouts, history persistence, binary uploads, and full Collections + Presets integration. Underneath, the workbench is now pluggable: rails and modules ship as package contributions, so the standalone Tool, embedded MapBowire(), and operator-built distributions can pick exactly the surface they want. The new interceptor middleware lets a host hand Bowire every inbound request without client setup. A MapLibre extension + a stand-alone TacticalApi sample demo coordinate-bearing payloads on a live map with bi-directional JSON↔map sync. Streaming subscriptions are now legible, the JSON viewer reaches Hoppscotch parity, and the guided tour walks first-time operators through every rail.
300+ commits since v2.0.1. Three packaging breaking changes for embedded hosts: the standalone Kuestenlogik.Bowire.Rail.Collections package is retired, the whole Rail. package prefix dropped (e.g. Rail.Compose → Compose, Rail.Benchmarks → Benchmarking), and the interceptor surface (Proxy + Intercepted + Traffic rails + the middleware that lived in Core) consolidated into a single Kuestenlogik.Bowire.Interceptor package. See Breaking changes below for the one-to-one mapping table. Existing workspaces, collections, recordings, benchmarks load identically; operator-saved railMode / enabled-rails state migrates on first boot.
Highlights
Compose rail (#293, #289, #290, #291, #295)
The old Design rail is rebranded and rebuilt as Compose — a single-line Hoppscotch-style request bar at the top, per-protocol layout below, history + collections + presets on the side. The protocol picker switches the layout in place: REST gets a URL + KV-table chrome, gRPC gets service / method / JSON body, MCP gets tool / arguments, MQTT gets topic / QoS, WebSocket / SSE get connect / subscribe shells. History persists across reloads, binary uploads ride a base64 side-channel through the wire, and benchmarks can re-run any historical request directly. A Compose tab supports right-click → Duplicate tab so you can fan out a request without losing the original. The toolbar lives where you'd expect (no toolbar in Raw mode, of course).
Pluggable workbench — rails + modules as package contributions (#294)
Bundle.Workbench is now a meta-package: every rail, every module, every extension is a separate project that registers itself via [BowirePlugin] / [BowireExtension] attributes. The standalone Tool depends on Bundle.Workbench; embedded hosts can drop the bundle and pick per-package references for a smaller surface (e.g. just REST + Compose without the AI module). Bundle.Minimal carries the Core + Home + Discover only — useful for "tiny embedded probe" deployments. Settings → Plugins now lists every loaded protocol AND every UI extension with kind / capability chips so the operator can see what's installed at a glance.
Transparent in-process interceptor (#153)
app.UseBowireInterceptor() registers a pass-through middleware that records method / path / headers / request body / response status / response headers / response body / latency for every request the host receives — from any client, with zero client-side setup, no cert trust, no separate process. Captured flows land in the workbench's new Intercepted rail live over SSE. When the operator starts a recording, intercepted flows auto-append as recording steps — point any client at the host, click stop, replay.
MapLibre extension + TacticalApi integration (#new)
A new Kuestenlogik.Bowire.Map extension auto-mounts a MapLibre GL JS viewer whenever a response carries the coordinate.wgs84 semantic kind. The Wgs84CoordinateDetector picks up {lat, lon}, {latitude, longitude}, AND {latitudeCoordinate, longitudeCoordinate} shapes (TacticalAPI naming) anchored by anchored case-insensitive regex. Bidirectional sync: hover on a JSON {lat,lon} block highlights the matching map pin; hover on a pin highlights the JSON. Click on a pin scrolls the JSON to the line and auto-expands collapsed ancestors. Right-click on a coordinate offers Center on map; double-click on either side copies the path to clipboard. A right-side gutter hint surfaces the semantic kind on hover (wgs84 coordinate).
The new Sample.TacticalApi server (separate samples/ project) hosts Rheinmetall's Situation gRPC service with eight NATO-phonetic-callsign tactical entities (Alpha-1 Recon UAV, Bravo-2 Air Defence Btry, …) drifting on a per-object sine-wave so subscriptions feel live. Bundle.Workbench ships the Kuestenlogik.Bowire.Protocol.TacticalApi plugin out of the box.
Streaming subscriptions, finally legible (#new)
The action button adapts to the active method type: Execute for unary, Subscribe for server-streaming (changes to Stop while a stream is live), Connect / Disconnect for bidirectional and client-streaming. A live state badge in the response pane shows ● Subscribed — 0 msgs → ● Receiving — 42 msgs → ○ Idle — 42 msgs after 5 s without a frame → ○ Closed on stop → × Error on stream failure. A statusbar pill lists every active subscription across methods; click → dropdown with switch / stop / copy-path / "stop all". The action button keeps a stable footprint so layout doesn't jitter when methods change.
Hoppscotch-parity JSON response viewer + toolbar (#302, #new)
The response viewer renders one JSON line per row with a sticky left gutter carrying line numbers AND the collapse chevron — chevrons line up on the same vertical column regardless of indent depth. Click anywhere on an opener line OR the gutter chevron to toggle. Click = toggle, double-click = Copy path, right-click = unified menu with Copy ${response.X}, Copy path, plus extension-contributed items (Center on map when the map plugin resolves the target). A dedicated JSON toolbar — Expand all, Collapse all, Wrap long lines, Search (Ctrl/Cmd+F), Copy response, Download — sits above the viewer and only renders when the JSON tab is active in tab mode (so the map widget gets its own toolbar real estate). Expand / Collapse rebuild the viewer in place — no full render, so the map's WebGL canvas survives.
Tab ↔ Split layout for response viewers (#new)
Server-streaming and unary response panes get a tab-strip with JSON + per-extension widget tab + a layout toggle. Click the toggle → split-pane with JSON on one side, widget on the other; click again → tabs. Per-method layout persists in localStorage so the operator's preference for each method is remembered. The split-layout decision is extension-driven — Core asks the framework preferredSplitExtensionForMethod(svc, method) rather than hard-coding the coordinate.wgs84 kind. Layering stays clean: Map-specific code in Kuestenlogik.Bowire.Map, Core stays generic.
Guided tour — page-navigation, alternative paths, per-rail empties (#281, #303)
A spotlight-overlay tour engine walks first-time operators through every rail: workspace creation, source-URL add, method invoke, recording, collection composition, flow building, traffic capture, security scan. Each rail's empty state surfaces its own secondary tour. The tour engine handles dialog-modal yields, alternative paths (e.g. operator already has a workspace), advance modes (on-cta / on-event / next-button), saved-once dismissal, and reusable fragment includes (_createWorkspaceSteps). Six rail-specific tours land: bowireStartCaptureRecordingTour, bowireStartBuildCollectionTour, bowireStartBuildFlowTour, bowireStartComposeRequestTour, bowireStartCaptureTrafficTour, bowireStartSecurityScanTour.
REST auto-probing of well-known OpenAPI paths
When the operator types a bare URL like http://localhost:5181, the REST plugin now probes a short list of well-known OpenAPI document paths: /openapi.json, /openapi/v1.json, /swagger/v1/swagger.json, /swagger.json, /v3/api-docs, /v3/api-docs.yaml, /api-docs, /openapi.yaml. First valid OpenAPI document wins; the resolved spec URL is cached per origin for the next call. Services are tagged with the operator-supplied URL (not the probed spec URL) so the Discover sidebar groups everything under the URL the operator typed. Skips probing when the supplied URL already looks like a spec URL.
Discovery self-origin gate for SSE / WebSocket / SignalR
Three protocol plugins (SSE, WebSocket, SignalR) used to scan the workbench host's own EndpointDataSource and return locally-registered endpoints regardless of the source URL the operator asked about. With the Tool's --enable-mcp-adapter flag, the workbench's own /mcp route leaked into every external serverUrl the operator added — read as a phantom "SSE Endpoints" service tagged to the wrong source. The new SelfOriginCheck.IsSelfOrigin(serverUrl, sp) helper compares against IServer.Features.IServerAddressesFeature (with loopback aliases + Kestrel wildcards). Each protocol's discovery now gates its local scan on this check — operator-registered endpoints keep working in both directions.
Catalogue plugin — provider seam (#136)
Kuestenlogik.Bowire.Catalogue lands as a provider abstraction with three first-party providers: local file (JSON catalogue), HTTP (fetch + parse remote catalogue), and Consul (service registry integration). Workspaces can pin a catalogue source so the Discover rail enumerates services from a curated list instead of probing one URL at a time.
Topbar — undo / redo / aggregated trash (#296), responsive overflow (#297)
The topbar's right cluster grows Undo / Redo buttons backed by the action log (cross-reload undo from #194 Phase 2) and an aggregated Trash drawer that lists every soft-deleted workspace / recording / collection / mock / flow / environment with a one-click restore. The right cluster also gains horizontal overflow handling — buttons collapse into a … overflow popover when the viewport gets tight, preserving the most recent items.
Workspaces — sort + manual ordering (#279)
The Workspaces rail picks up a sort dropdown (last-used / created / alphabetical / manual) and a drag-handle on each row for explicit manual ordering. Per-user preference persists; manual ordering survives a rebuild of the underlying workspace list.
Benchmarks — random targets + diff banner + exports (#231, #233, #234)
Benchmark target shapes get a new random option that picks a target from a pool per iteration — useful when a downstream service partitions by URL. The previous-run diff banner shows p95 / rps / status histogram delta against the last run for the same benchmark. Results export as CSV (per-method or per-iteration), k6-summary JSON, and OTLP metrics files for ingestion into Grafana / Tempo / Prometheus.
MCP-over-MCP forwarder (#286)
bowire mcp serve --attach localhost:5198 --port 5199 boots a thin Bowire process that relays every incoming MCP tool call to a heavier Bowire running on the operator's workstation. tools/list, tools/call, prompts, resources, resource templates — all marshalled to the parent. The parent gains --token <secret> bearer-auth (--bind http only); the child passes the secret with --attach-token <secret>. Useful when an LLM agent on a CI runner / container should drive the workstation Bowire without sharing the parent's MCP socket directly.
AI discoverability — <link rel="mcp">, Reader Mode suppression
Bowire emits <link rel="mcp" href="/mcp"> + <meta name="mcp-endpoint" content="/mcp"> in the workbench HTML so MCP-aware crawlers and AI agents can discover the tool surface. Prefix-aware so embedded MapBowire("/bowire") emits /bowire/mcp correctly. Separately, Edge Reader Mode used to extract stale JSON snippets and render them as "the article" — og:type=website + role="application" on the workbench root signal "app, not document" so Reader Mode stays out of the way. Deliberately no robots=noai — Bowire is meant to be AI-driven.
Mock host + Mocks rail consolidated into one package
Kuestenlogik.Bowire.Mock now ships both the mock-host runtime
(MockServer, MockHandler, MockOptions, MockServerOptions) and the
Mocks rail contribution (BowireMocksRailContribution) in a single
package — same pluggable pattern Help follows after #324. The
provisional Kuestenlogik.Bowire.Rail.Mocks package is retired
before its first NuGet release, so existing operators on
Kuestenlogik.Bowire.Mock 1.0.x see no migration. Bundle.Workbench
keeps referencing Mock; embedded hosts that want the Mocks rail
simply reference Kuestenlogik.Bowire.Mock and the contribution
self-registers.
Welle 2 — Rail. prefix dropped, interceptor consolidated (#325)
The Mock + Help collapses above were Welle 1 of a v2.1 package-shape
cleanup. Welle 2 finishes the job by dropping the Rail. prefix
from every remaining package id, folding two descriptor-only rails
into Core, and consolidating the three deprecated interceptor
descriptors AND the interceptor runtime into a single
Kuestenlogik.Bowire.Interceptor package.
Renames (package ids change, contribution Id strings keep verbatim
so saved railMode + bowire_enabled_rails keep dispatching):
| Old package | New package | Notes |
|---|---|---|
Kuestenlogik.Bowire.Rail.Home |
(folded into Core) | descriptor-only, no per-rail JS |
Kuestenlogik.Bowire.Rail.Discover |
(folded into Core) | descriptor-only, no per-rail JS |
Kuestenlogik.Bowire.Rail.Workspaces + …Rail.Environments |
Kuestenlogik.Bowire.Workspaces |
env vars are workspace-scoped — same package |
Kuestenlogik.Bowire.Rail.Compose |
Kuestenlogik.Bowire.Compose |
rename only |
Kuestenlogik.Bowire.Rail.Recordings |
Kuestenlogik.Bowire.Recordings |
rename only (plural stem kept) |
Kuestenlogik.Bowire.Rail.Flows |
Kuestenlogik.Bowire.Flows |
rename only (plural stem kept) |
Kuestenlogik.Bowire.Rail.Proxy + …Rail.Intercepted + …Rail.Traffic |
Kuestenlogik.Bowire.Interceptor |
three rails + middleware + reverse-proxy host + /api/intercepted/* + /api/tools/reverse-proxy/* all in one package |
Kuestenlogik.Bowire.Rail.Benchmarks |
Kuestenlogik.Bowire.Benchmarking |
gerund matches the activity-rail pattern (Compose / Mock / Discover / Help / Telemetry) |
The Interceptor consolidation is the biggest shape change in
v2.1. The interceptor middleware (UseBowireInterceptor), the
reverse-proxy host (BowireReverseProxyHost), the stores
(InterceptedFlowStore, InterceptorMockStore), the
reverse-proxy registry (ReverseProxyRegistry), and the
/api/intercepted/* + /api/tools/reverse-proxy/* endpoints all
moved out of Core into the new Kuestenlogik.Bowire.Interceptor
package. Two new contribution seams (IBowireServiceContribution,
IBowireEndpointContribution) let Core discover the package's
DI registrations + endpoint mounts without taking a compile-time
reference on its types. Embedded hosts that don't reference
Kuestenlogik.Bowire.Interceptor lose the entire interceptor stack
— no middleware, no rails, no admin endpoints. The standalone Tool
keeps the same shape because Bundle.Workbench pulls the package in
transitively.
Settings IA tightened in the same wave: the top-level
Rail modes row renames to Rails. See the dedicated
"Settings tree organized by extension point" highlight below for
the full new layout — operator review caught that the first-pass
"collapse Modules + Plugins + Assistant into one bucket" was too
coarse and that Data is app-wide rather than workspace-scoped, so
the IA was reworked around extension points.
Settings tree organized by extension point
Settings groups per-plugin configuration under each plugin's
extension point — protocols, UI widgets, modules, formats, tools,
discovery providers — instead of one big bucket. A plugin
contributing at multiple points shows up under each with its
specific settings. Plugin lifecycle (load, unload, restart, reset
storage, install) lives in its own Plugins section; load actions
surface a v2.2 placeholder while the listing + per-plugin status
work today. Data management stays at the top level (it's app-wide,
not workspace-scoped — the workspace list itself lives in app
storage).
Breaking changes
- Collections rail retired. Default-off since #304; Compose rail's side panel (#295) is the canonical surface for collections + presets. Operators with
railMode === 'collections'saved are migrated to Compose on next boot. The standalone Collections package (Kuestenlogik.Bowire.Rail.Collections) is removed fromBundle.Workbench; embedded hosts that referenced it explicitly need to drop the reference. TherenderCollectionDetail+renderSaveToCollectionDropdown+importPostmanCollection+runCollectionItemhelpers + the Parallel-sessions panel moved back into core so the Workspaces-rail collection-detail leaf and the Recordings-rail parallel-sessions panel keep working without the package. Rail.package prefix dropped (#325). Every standalone rail NuGet renamed:Kuestenlogik.Bowire.Rail.Compose→Kuestenlogik.Bowire.Compose,Rail.Recordings→Recordings,Rail.Flows→Flows,Rail.Workspaces(now also carrying Environments) →Workspaces,Rail.Benchmarks→Benchmarking. The descriptor-onlyRail.Home+Rail.Discoverpackages fold into Core. Embedded hosts that pinned the old package ids need to swap the<PackageReference>lines (one-to-one mapping in the table above). ContributionIdstrings,IconKey,SortIndex,Group,SidebarKindkeep verbatim so saved operator settings,railModedeep links, andbowire_enabled_railsfilters survive unchanged.- Interceptor surface consolidated into
Kuestenlogik.Bowire.Interceptor(#325). The three v2.0 packages (Rail.Proxy+Rail.Intercepted+Rail.Traffic) AND the interceptor middleware that lived in Core (UseBowireInterceptor,InterceptedFlowStore,InterceptorMockStore,BowireReverseProxyHost,ReverseProxyRegistry,BowireInterceptorEndpoints,BowireToolsEndpoints) all moved into a single new package. Embedded hosts that didn't reference any of the old packages were already losing the deprecated rails; now they also lose the middleware + reverse-proxy host +/api/intercepted/*+/api/tools/reverse-proxy/*endpoints unless they addKuestenlogik.Bowire.Interceptor. Existingapp.UseBowireInterceptor()call sites compile unchanged — the namespace stayed atKuestenlogik.Bowire.Interceptor, only the assembly moved. The standalone Tool ships the package transitively throughBundle.Workbenchso CLI users see no behavioural change.
Bug fixes
- Sources tree right-click
Remove URLwas a silent no-op — handler called an undefinedremoveServerUrlfunction; inlined the same splice + persist + meta-clear + selection-reset fallback the inline trash button uses (3d9b265). bowireConfirmcallback vs. Promise shape mismatch — three Promise-style callers (Remove URL on workspace detail, preset delete, benchmark delete) silently TypeError'd inside.then(). The function now polymorphic: detect arg2's type, route both shapes through a sharedsettle()(9ae9bf1).- REST probe-resolved services were tagged with the spec URL, not the operator URL — Discover sidebar grouped a single source as two panels. Fixed via
RetagOriginUrl+AliasSchemaCacheso the cache works under both keys (a1c5831,2b1c045). - Bundle.Workbench was missing the Map extension — standalone Tool surfaced "Install Kuestenlogik.Bowire.Map" placeholder cards instead of mounting the viewer (
76ed326). - Settings → Plugins didn't list UI extensions — only protocols showed. Added
installedExtensionsarray to/api/plugins+ a parallel "Installed UI extensions" section; clicking an extension routes to its own detail tab (0182a20,b3b98ba). - Sample.TacticalApi gRPC discovery failed for plain URLs — Kestrel defaulted to HTTP/1.1,
Http1AndHttp2only upgrades via TLS+ALPN. Pinned to HTTP/2 only so the genericgrpc@http://...URL works via Server Reflection;tacticalapi@...still uses bundled descriptors (61ba0ef,e83dce7). - Map widget disappeared on Tab ↔ Split toggle — extension bootstrap was fire-and-forget, so
preferredSplitExtensionForMethodreturned null at first render. Chainedrender()onto the load promise; stamped distinct host IDs so morphdom replaces wrapper subtrees wholesale (a9d403f,a00b534). - JSON viewer click toggle didn't actually toggle — viewer mutated
togglesByPathSet but waited on a caller'sonToggleto trigger render; render-main.js callers don't pass one. Viewer now self-rebuilds in place via_rebuildViewerLines(df3344c). <details>-based JSON viewer lost line numbers + gutter chevron after refactor — Hoppscotch-stylerenderJsonViewerre-adopted in both response paths so line numbers + gutter chevron come back. Extension hooks (gestures, decorators) preserved (0aea6f8).bowire.io"Recently shipped" stuck on v1.5.0 —scripts/site/build-activity-snapshot.mjswrote toscripts/site/site/_data/activity.json(one..short). Documentation workflow ran daily refreshing the wrong file (2ed7ef5).
Polish
- Action button width stabilised across method types —
min-width: 132pxaccommodates "Disconnect" + icon so switching unary ↔ streaming doesn't reflow the action bar. - JSON validation pill moved from below the editor to the pane header toolbar — a long payload no longer pushes the
✓ Valid JSONindicator off-screen. - Map widget pin gestures — hover highlights JSON (no scroll); click scrolls + auto-expands collapsed ancestors; double-click copies path.
- Workbench sub-tab strip for response viewers — JSON + Map tabs share the same chrome as the rail tab strip.
- Splitter Hover-Intent gate (250 ms dwell before highlight) so a casual mouseover near the rail edge doesn't flash a splitter accent.
- Toast pattern with leading icons + undo / log actions hook.
- Tour buttons icon-only with stable footprint + tooltip on hover.
- Welcome card icons fixed (home → house, mocks → mock, discover → discover icon).
- Statusbar pills are now rounded (matching GET/POST chips).
- Tactical Sample uses real Guid UUIDs + NATO-phonetic callsigns instead of slug ids + civilian transport hubs.
Acknowledgements
Operator review during this release surfaced more than fifty UX issues — from "the chevron isn't in the gutter" to "Subscribe semantics are invisible". Every Highlights and Polish entry above traces back to feedback received and validated. Special thanks for the patience holding rc-grade work in production until v2.1.
The full commit list, contributors, and compare-URL diff are auto-generated below.
What's Changed
- chore: bump floor to 2.0.1-dev (after v2.0.1) by @thomas-stegemann in #255
- chore(npm): bump @playwright/test from 1.61.0 to 1.61.1 by @dependabot[bot] in #317
- chore(deps): Bump the xunit group with 1 update by @dependabot[bot] in #318
- chore(deps): Bump Microsoft.Identity.Web from 4.10.0 to 4.11.0 by @dependabot[bot] in #319
- chore(deps): Bump OpenTelemetry.Instrumentation.AspNetCore from 1.15.2 to 1.16.0 by @dependabot[bot] in #320
- chore(deps): Bump YamlDotNet from 18.0.0 to 18.1.0 by @dependabot[bot] in #322
- chore(deps): Bump OpenTelemetry.Instrumentation.Http from 1.15.1 to 1.16.0 by @dependabot[bot] in #321
Full Changelog: v2.0.1...v2.1.0