diff --git a/docs/agent/feature-map.md b/docs/agent/feature-map.md
index d8772d43..3a2226af 100644
--- a/docs/agent/feature-map.md
+++ b/docs/agent/feature-map.md
@@ -6,7 +6,7 @@ This file maps consumer-visible capabilities to implementation anchors, executab
|---|---|---|---|
| Build, publication, Java compatibility, nested controller distribution | `build.gradle`; `settings.gradle`; `pickleball-control-protocol/build.gradle`; `pickleball-workbench/build.gradle`; `src/main/aspectj`; search `verifyStrictControllerIsolation`, `verifyEmbeddedWorkbench`, `shadowJar`, `JavaLanguageVersion` | root tests; protocol `check`; `:pickleball-workbench:test`; artifact/dependency verification; Maven consumer build | `README.md`; `docs/getting-started.md`; `docs/cucumber-compatibility.md`; `docs/consumer-project.md`; `docs/pickleball-workbench.md` |
| Neutral controller/worker wire protocol | `pickleball-control-protocol/src/main/java/tools/dscode/control/protocol`; `ControlProtocol`; `ControlBridgeRequests`; `ControlBridgeResponses`; immutable `ControlBridge*` records | `verifyProtocolIsolation`; `ControlBridgeClientTest`; consumer `@control-bridge`; protocol/version/capability assertions | `docs/pickleball-workbench.md`; `pickleball-workbench/AGENTS.md` |
-| Pickleball Workbench synchronization / persistent live worker / MCP stdio / player-style Swing+WebView UI / watched-agent control lease | `pickleball-workbench`; `WorkbenchApplication`; `WorkbenchRuntimeBoundary`; `WorkbenchServices`; `WorkbenchController`; `tools.dscode.workbench.lease`; `WorkbenchAttachServer`; `tools.dscode.workbench.sync`; `WorkbenchWorkerManager`; `WorkbenchLiveSession`; `tools.dscode.workbench.bridge.ControlBridgeClient`; `tools.dscode.workbench.mcp`; `tools.dscode.workbench.player`; `tools.dscode.workbench.catalog`; `tools.dscode.workbench.mapping`; `tools.dscode.workbench.terminal`; `tools.dscode.workbench.diagnostics`; `tools.dscode.workbench.ui`; OpenJFX `WebView` / `JFXPanel`; protocol `ControlProtocol.WORKER_MAIN_CLASS` | `:pickleball-workbench:test`; `WorkbenchRuntimeBoundaryTest`; `ConsumerFeatureCatalogTest`; `GherkinBlockDocumentTest`; `LivePlaybackCoordinatorTest`; `LiveFeatureSaveTest`; `WorkbenchControlLeaseTest`; `WorkbenchControllerLeaseTest`; `WorkbenchAttachServerTest`; `MappingValueCodecTest`; `WorkerLogBufferTest`; `DiagnosticEvidenceNavigatorTest`; `WorkbenchUiControllerTest`; `LiveScenarioPlayerTest`; packaged UI/MCP probes | `docs/pickleball-workbench.md`; `docs/pickleball-workbench-player.md`; `pickleball-workbench/AGENTS.md` |
+| Pickleball Workbench synchronization / persistent live worker / MCP stdio / player-style Swing+WebView UI / watched-agent control lease / scenario name-tag filter / Text-Blocks editor toggle | `pickleball-workbench`; `WorkbenchApplication`; `WorkbenchRuntimeBoundary`; `WorkbenchServices`; `WorkbenchController`; `tools.dscode.workbench.lease`; `WorkbenchAttachServer`; `tools.dscode.workbench.sync`; `WorkbenchWorkerManager`; `WorkbenchLiveSession`; `tools.dscode.workbench.bridge.ControlBridgeClient`; `tools.dscode.workbench.mcp`; `tools.dscode.workbench.player`; `LiveEditorView`; `tools.dscode.workbench.catalog`; `ScenarioFilter`; `ConsumerFeatureCatalog`; `tools.dscode.workbench.mapping`; `tools.dscode.workbench.terminal`; `tools.dscode.workbench.diagnostics`; `tools.dscode.workbench.ui`; `FeaturePickerPanel`; OpenJFX `WebView` / `JFXPanel`; protocol `ControlProtocol.WORKER_MAIN_CLASS` | `:pickleball-workbench:test`; `WorkbenchRuntimeBoundaryTest`; `ConsumerFeatureCatalogTest`; `ScenarioFilterTest`; `GherkinBlockDocumentTest`; `LivePlaybackCoordinatorTest`; `LiveFeatureSaveTest`; `LiveEditorViewTest`; `WorkbenchControlLeaseTest`; `WorkbenchControllerLeaseTest`; `WorkbenchAttachServerTest`; `MappingValueCodecTest`; `WorkerLogBufferTest`; `DiagnosticEvidenceNavigatorTest`; `WorkbenchUiControllerTest`; `LiveScenarioPlayerTest`; packaged UI/MCP probes | `docs/pickleball-workbench.md`; `docs/pickleball-workbench-player.md`; `pickleball-workbench/AGENTS.md` |
| Consumer-side Control Bridge and live investigation | worker-side `pickleball-control-api/src/main/java/tools/dscode/control/bridge`; core `ControlRuntime`; protocol `tools.dscode.control.protocol`; controller client `tools.dscode.workbench.bridge.ControlBridgeClient`; endpoints `/v1/status`, `/v1/scenarios`, `/v1/events`, `/v1/pause`, `/v1/resume`, `/v1/steps/execute`, `/v1/mappings/*`, `/v1/browser/*`, `/v1/services/call`, `/v1/breakpoints*`, `/v1/step-overrides*` | controller-only fake-server `ControlBridgeClientTest`; `control-bridge.feature` tagged `@control-bridge`; `ControlBridgeTestSteps.java`; Workbench `worker-check` / `live-check` | `docs/dynamic-control-api.md`; `docs/pickleball-workbench.md`; `pickleball-workbench/AGENTS.md` |
| Consumer Workbench launcher/extractor | `tools.dscode.launcher.PickleballWorkbenchLauncher`; `ControlProtocol.EMBEDDED_WORKBENCH_RESOURCE`; root `shadowJar`; `verifyEmbeddedWorkbench`; content-addressed `.pickleball/workbench/controller//` extraction | `PickleballWorkbenchLauncherTest`; `verifyEmbeddedWorkbench`; `verifyWorkbenchEntrypoint` | `docs/getting-started.md`; `docs/consumer-project.md`; `docs/pickleball-workbench.md` |
| Step Override runtime and Workbench authoring | `src/main/java/tools/dscode/control/override`; `src/main/java/io/cucumber/core/runner/StepOverrideDispatcher.java`; `NPickleStepTestStepFactory`; `src/main/aspectj/tools/dscode/control/override/StepOverrideLifecycleAspect.aj`; bridge `/v1/step-overrides*`; `ControlBridgeClient`; `WorkbenchLiveSession`; MCP `workbench_step_override_*` | `StepOverrideCompilerTest`; `StepOverrideChecks`; `StepOverrideBridgeTestSteps`; `@step-override`; Workbench `live-check`; `WorkbenchMcpServerTest`; `WorkbenchUiControllerTest` | `docs/step-overrides.md`; `docs/pickleball-workbench.md`; `pickleball-workbench/AGENTS.md` |
@@ -47,6 +47,6 @@ The canonical worker bootstrap environment is `PKB_CONTROL_BRIDGE_SESSION_DIR`,
`WorkbenchLiveSession` binds operations to the controller-owned paused scenario and verifies worker PID, bridge runtime id, and scenario id stability. Step Override source is compiled and loaded worker-side. Normal live calls do not invoke Maven/Gradle, resynchronize, or restart the worker.
-The lightweight non-Spring stdio MCP adapter and player-style Swing/WebView UI both delegate through `WorkbenchServices` / `WorkbenchController`. `LiveScenarioPlayer` is a Workbench-side headless presentation model for the editable session buffer, stable line identities, selection, click-to-seek playhead, and player state only; it does not parse or execute Pickleball steps, model ParsingMap/NodeMap semantics, or claim runtime rewind. While `RUNNING`, `WorkbenchController.executeStep` owns playhead follow once (`LivePlaybackCoordinator.followExecutedStep`); the Swing Play loop continues without remaking that mark, and leftover marks of an already-consumed step are no-ops. The controller also owns the watched-agent control lease (`HUMAN` / `AGENT`, banner `currentAction`, gated Save permission). UI mode writes a localhost attach endpoint to `.pickleball/workbench/attach.json` so an agent joins the same JVM/worker instead of starting a second Workbench. The left-rail picker reads project-owned feature files. The live editor is a Gherkin block WebView over that same player model. Global Play starts from the first executable step; Step Editor **Step** is isolated `executeStep`; **From Here** runs from the selected step; wait-at-end stays in play so Enter can append-and-continue. Mapping property types go through `mappingPut` / `mappingRestore`. Terminal tails existing worker log files. Diagnostic explorer reads retained `reports/diagnostic-runs` artifacts in the repository evidence order. The default buffer is a Workbench-owned browser demo against `URL.home`; **Save** is confirmation-gated and copies only a picker-loaded scenario back to its originating `.feature` file. OpenJFX is Workbench-only. MCP mode reserves stdout for newline-delimited MCP JSON-RPC and redirects ordinary output to stderr. The UI remains execution-oriented and does not recreate a project IDE, generic build/process UI, source navigator, or collaboration system.
+The lightweight non-Spring stdio MCP adapter and player-style Swing/WebView UI both delegate through `WorkbenchServices` / `WorkbenchController`. `LiveScenarioPlayer` is a Workbench-side headless presentation model for the editable session buffer, stable line identities, selection, click-to-seek playhead, and player state only; it does not parse or execute Pickleball steps, model ParsingMap/NodeMap semantics, or claim runtime rewind. `LiveEditorView` is the Text vs Blocks presentation choice for that same buffer; toggling it must not change document text, selection, or playhead id. While `RUNNING`, `WorkbenchController.executeStep` owns playhead follow once (`LivePlaybackCoordinator.followExecutedStep`); the Swing Play loop continues without remaking that mark, and leftover marks of an already-consumed step are no-ops. The controller also owns the watched-agent control lease (`HUMAN` / `AGENT`, banner `currentAction`, gated Save permission). UI mode writes a localhost attach endpoint to `.pickleball/workbench/attach.json` so an agent joins the same JVM/worker instead of starting a second Workbench. The left-rail picker filters project-owned scenarios by name (starts with / contains / ends with / full match; default contains; case-insensitive) and Cucumber tags (include AND, exclude NOT, Feature/Rule/outline/Examples inheritance parsed from catalog `.feature` files without calling Cucumber). Feature-file selection is an optional collapsed secondary filter; with none selected, name/tag apply to every catalog scenario. The live editor is Gherkin text or a block WebView over that same player model. Global Play starts from the first executable step; Step Editor **Step** is isolated `executeStep`; **From Here** runs from the selected step; wait-at-end stays in play so Enter can append-and-continue. Mapping property types go through `mappingPut` / `mappingRestore`. Terminal tails existing worker log files. Diagnostic explorer reads retained `reports/diagnostic-runs` artifacts in the repository evidence order. The default buffer is a Workbench-owned browser demo against `URL.home`; **Save** is confirmation-gated and copies only a picker-loaded scenario back to its originating `.feature` file. OpenJFX is Workbench-only. MCP mode reserves stdout for newline-delimited MCP JSON-RPC and redirects ordinary output to stderr. The UI remains execution-oriented and does not recreate a project IDE, generic build/process UI, source navigator, or collaboration system.
For this boundary, scenario validation is limited to the affected `@control-bridge` and/or `@step-override-bridge` tags with `pkb_parallel=80` where practical. Do not run `@all` for Workbench isolation changes.
diff --git a/docs/agent/repository-index.md b/docs/agent/repository-index.md
index 86a1c5f4..6d0e3fea 100644
--- a/docs/agent/repository-index.md
+++ b/docs/agent/repository-index.md
@@ -477,6 +477,7 @@ This inventory helps coding agents discover relevant files. It does not replace
- `pickleball-workbench/build.gradle`
- `pickleball-workbench/src/main/java/tools/dscode/workbench/bridge/ControlBridgeClient.java`
- `pickleball-workbench/src/main/java/tools/dscode/workbench/catalog/ConsumerFeatureCatalog.java`
+- `pickleball-workbench/src/main/java/tools/dscode/workbench/catalog/ScenarioFilter.java`
- `pickleball-workbench/src/main/java/tools/dscode/workbench/diagnostics/DiagnosticEvidenceNavigator.java`
- `pickleball-workbench/src/main/java/tools/dscode/workbench/lease/WorkbenchCallContext.java`
- `pickleball-workbench/src/main/java/tools/dscode/workbench/lease/WorkbenchControlLease.java`
@@ -492,6 +493,7 @@ This inventory helps coding agents discover relevant files. It does not replace
- `pickleball-workbench/src/main/java/tools/dscode/workbench/mcp/WorkbenchMcpServer.java`
- `pickleball-workbench/src/main/java/tools/dscode/workbench/mcp/WorkbenchMcpTools.java`
- `pickleball-workbench/src/main/java/tools/dscode/workbench/player/GherkinBlockDocument.java`
+- `pickleball-workbench/src/main/java/tools/dscode/workbench/player/LiveEditorView.java`
- `pickleball-workbench/src/main/java/tools/dscode/workbench/player/LiveFeatureSave.java`
- `pickleball-workbench/src/main/java/tools/dscode/workbench/player/LivePlaybackCoordinator.java`
- `pickleball-workbench/src/main/java/tools/dscode/workbench/player/LiveScenarioPlayer.java`
@@ -534,12 +536,14 @@ This inventory helps coding agents discover relevant files. It does not replace
- `pickleball-workbench/src/main/resources/tools/dscode/workbench/ui/web/mapping-editor.js`
- `pickleball-workbench/src/test/java/tools/dscode/workbench/bridge/ControlBridgeClientTest.java`
- `pickleball-workbench/src/test/java/tools/dscode/workbench/catalog/ConsumerFeatureCatalogTest.java`
+- `pickleball-workbench/src/test/java/tools/dscode/workbench/catalog/ScenarioFilterTest.java`
- `pickleball-workbench/src/test/java/tools/dscode/workbench/diagnostics/DiagnosticEvidenceNavigatorTest.java`
- `pickleball-workbench/src/test/java/tools/dscode/workbench/lease/WorkbenchControlLeaseTest.java`
- `pickleball-workbench/src/test/java/tools/dscode/workbench/mapping/MappingValueCodecTest.java`
- `pickleball-workbench/src/test/java/tools/dscode/workbench/mcp/WorkbenchAttachServerTest.java`
- `pickleball-workbench/src/test/java/tools/dscode/workbench/mcp/WorkbenchMcpServerTest.java`
- `pickleball-workbench/src/test/java/tools/dscode/workbench/player/GherkinBlockDocumentTest.java`
+- `pickleball-workbench/src/test/java/tools/dscode/workbench/player/LiveEditorViewTest.java`
- `pickleball-workbench/src/test/java/tools/dscode/workbench/player/LiveFeatureSaveTest.java`
- `pickleball-workbench/src/test/java/tools/dscode/workbench/player/LivePlaybackCoordinatorTest.java`
- `pickleball-workbench/src/test/java/tools/dscode/workbench/player/LiveScenarioPlayerTest.java`
diff --git a/docs/pickleball-workbench-player.md b/docs/pickleball-workbench-player.md
index 88afe3d8..96578c8b 100644
--- a/docs/pickleball-workbench-player.md
+++ b/docs/pickleball-workbench-player.md
@@ -25,15 +25,17 @@ The player, picker, Mapping editor, Terminal, and Diagnostic explorer do not cha
## Feature and scenario picker
-The expandable left rail lists `.feature` files from the synchronized consumer project: manifest source roots, conventional `src/test/resources/features`, live merged `features/` resources, and an explicit project-owned `pkb_features` value when present. It does not crawl an unrelated git worktree.
+The left rail lists scenarios from `.feature` files in the synchronized consumer project: manifest source roots, conventional `src/test/resources/features`, live merged `features/` resources, and an explicit project-owned `pkb_features` value when present. It does not crawl an unrelated git worktree.
-Toggle browse mode between Gherkin Feature name and file name + directory path. Click selects or deselects features. The scenario list then shows scenarios under the selection. With no feature selected, the search bar filters every scenario in that project catalog. Clicking a scenario loads it into the live editor. The default demo remains loaded until a scenario is chosen. **Save** is the only write-back path.
+Name and tag filtering is the primary UI. Type a scenario name and choose a match mode: starts with, contains (default), ends with, or full match. All four modes are case-insensitive and match only the Gherkin `Scenario` / `Scenario Outline` title. Include tags must all be present (AND). Exclude tags drop a scenario if it has any of them (NOT). Tag fields accept values with or without a leading `@` and split on commas and/or whitespace. Empty include/exclude means no tag constraint. Feature, Rule, scenario/outline, and Examples tags are inherited as Cucumber does; Workbench parses them from the same catalog files and does not call Cucumber from the controller JVM.
+
+Feature-file selection is secondary and collapsed behind **Filter by feature**. With no feature selected, name/tag filters apply to every catalog scenario. Opening that panel still toggles browse mode between Gherkin Feature name and file name + directory path, and click still selects or deselects features. Clicking a scenario loads it into the live editor. The default demo remains loaded until a scenario is chosen. **Save** is the only write-back path.
## Live Scenario Editor
-The center editor is an embedded HTML/JS block editor in JavaFX `WebView`. Blocks are Gherkin text, including `Given` / `When` / `Then`. Nested steps and `IF` / `ELSE` snap as parent/child using leading colons. Clicking a block instantly seeks the playhead, like clicking a waveform. The execution cursor is internal to an active run.
+The center editor is an embedded HTML/JS block editor in JavaFX `WebView`, or ordinary Gherkin text on the same `LiveScenarioPlayer` buffer. A prominent **Text | Blocks** toggle next to the editor heading switches those views without losing playhead, selection, or document text. Play, Step, and From Here keep using the same `LiveScenarioPlayer`. Blocks are Gherkin text, including `Given` / `When` / `Then`. Nested steps and `IF` / `ELSE` snap as parent/child using leading colons. Clicking a block or line instantly seeks the playhead, like clicking a waveform. The execution cursor is internal to an active run.
-Workbench chose OpenJFX `WebView` + `JFXPanel` over JCEF so the browser panel stays a Workbench-only Maven dependency that shades into the controller JAR. JDK 21 does not ship a modern browser component. If JavaFX cannot start, the same `LiveScenarioPlayer` buffer remains editable as plain Gherkin text.
+Workbench chose OpenJFX `WebView` + `JFXPanel` over JCEF so the browser panel stays a Workbench-only Maven dependency that shades into the controller JAR. JDK 21 does not ship a modern browser component. If JavaFX cannot start, the same `LiveScenarioPlayer` buffer remains editable as plain Gherkin text and Blocks is shown as unavailable.
The initial buffer is Workbench-owned sample content. It is not written back to consumer `.feature` files unless you use **Save** on a picker-loaded scenario and confirm the copy. The default demo is a small browser scenario against the Maven consumer local test site:
@@ -114,7 +116,7 @@ The live player is a collaborative testing space, not a second editor. Swing and
- A human can work alone: edit/play the live buffer, then **Save** asks before copying into the original scenario in the original `.feature` file.
- An agent attaches to the running UI through `.pickleball/workbench/attach.json` (localhost JSON tools over the same `WorkbenchServices`). It must not start a second Workbench JVM or worker.
-- After `workbench_request_control`, Swing play/edit/mapping/save/worker controls lock. A banner shows the agent name and `currentAction`. **Take control** always works and cancels in-flight Save permission waits.
+- After `workbench_request_control`, Swing play/edit/picker/filter/editor-view/mapping/save/worker controls lock. A banner shows the agent name and `currentAction`. **Take control** always works and cancels in-flight Save permission waits.
- `workbench_request_save` is the only original-feature write path for the agent. With the UI attached it blocks on Allow/Deny. Deny writes nothing. Headless stdio MCP may hold the lease without a banner; Save is still an explicit tool.
See [pickleball-workbench.md](pickleball-workbench.md) for attach discovery, tool names, and stdout rules.
@@ -127,7 +129,7 @@ The included consumer `@control-bridge` scenario verifies:
- the current ParsingMap catalog contains at least one NodeMap;
- a catalog reference resolves back to a live NodeMap.
-Workbench player/editor unit tests cover picker selection/search, block buffer ↔ player model, click-to-seek, global Play from start, the two Step Editor play actions, wait-at-end / Enter-to-append-and-run, in-place edit of previously executed text, leftover Play-loop playhead marks after `executeStep`, typed Mapping edits through `WorkbenchServices`, the non-empty browser demo seed, control-lease lock/Take control, permission grant/deny, and Save not writing without approval.
+Workbench player/editor unit tests cover picker name/tag/feature filtering (including Feature-level tag inheritance), block buffer ↔ player model, Text | Blocks view toggling without changing document text or playhead id, click-to-seek, global Play from start, the two Step Editor play actions, wait-at-end / Enter-to-append-and-run, in-place edit of previously executed text, leftover Play-loop playhead marks after `executeStep`, typed Mapping edits through `WorkbenchServices`, the non-empty browser demo seed, control-lease lock/Take control, permission grant/deny, and Save not writing without approval.
Workbench changes should continue to use the repository's focused validation policy:
diff --git a/docs/pickleball-workbench.md b/docs/pickleball-workbench.md
index ae463bec..8df90b18 100644
--- a/docs/pickleball-workbench.md
+++ b/docs/pickleball-workbench.md
@@ -119,23 +119,28 @@ The primary workspace is an interactive Gherkin player with a project feature pi
```text
+-------------------------------------------------------------------------+
-| Features | Project / readiness Play Pause Stop Player status |
-+----------+---------------------------+----------------------------------+
-| Feature /| LIVE GHERKIN EDITOR | Mapping | Terminal | Diagnostic |
-| file | (WebView block editor) | WebView tree / typed values |
-| picker | highlighted playhead | worker log / retained-run frames |
-| + search | | |
-+----------+---------------------------+ |
-| | Step Editor [Step] [From | |
-| | Here] [ live command ] | |
-+----------+---------------------------+----------------------------------+
+| Scenarios | Project / readiness Play Pause Stop Player status |
++-----------+--------------------------+----------------------------------+
+| Name + | LIVE GHERKIN EDITOR | Mapping | Terminal | Diagnostic |
+| match mode| [Text | Blocks] | WebView tree / typed values |
+| tags AND | playhead on same buffer | worker log / retained-run frames |
+| tags NOT | | |
+| scenario | | |
+| list | | |
+| (feature | | |
+| filter | | |
+| hidden) | | |
++-----------+--------------------------+ |
+| | Step Editor [Step] [From | |
+| | Here] [ live command ] | |
++-----------+--------------------------+----------------------------------+
| Footer activity |
+-------------------------------------------------------------------------+
```
-The expandable left rail lists feature files from the synchronized consumer project. Browse by Gherkin Feature name or by file name + directory path. Click toggles feature selection. The scenario list is limited to selected features, or to every project scenario when nothing is selected; the search bar filters that list. Clicking a scenario loads it into the live session buffer. Workbench does not write `.feature` files unless you use the explicit **Save** control.
+The left rail is a scenario filter, not a feature-file browser. Primary controls are scenario name (starts with / contains / ends with / full match; default contains; all four are case-insensitive against the Gherkin Scenario / Scenario Outline title), tags the scenario must have (AND), and tags it must not have (NOT). Include/exclude fields accept any number of tags, with or without a leading `@`, split on commas and/or whitespace. Empty include/exclude means no tag constraint. Feature-level tags, optional Rule tags, the scenario/outline's own tags, and Examples tags on an outline are inherited the same way Cucumber does; Workbench parses those tags from the catalog `.feature` files and does not call Cucumber. Feature-file selection is collapsed behind **Filter by feature** (Gherkin Feature name vs file path lives in that panel). Default: no feature filter, so name/tag apply to every catalog scenario in the synchronized project. Clicking a result still loads that scenario into the live session buffer. Workbench does not write `.feature` files unless you use the explicit **Save** control.
-The center editor is an embedded HTML/JS block editor hosted in JavaFX `WebView` (`JFXPanel`). Blocks are Gherkin text — including `Given` / `When` / `Then` — not a second language compiled to Gherkin. Nested steps and `IF` / `ELSE` blocks snap as parent/child using Pickleball's leading-colon grammar. The play header is unchanged: click-to-seek, global **Play** from the first step in a fresh worker context, **Step** = isolated `executeStep`, **From Here** = selected/playhead through the rest, stay in play at end, Enter append-and-run. JavaScript never executes Gherkin.
+The center editor shows the same `LiveScenarioPlayer` buffer as ordinary Gherkin text or as the embedded HTML/JS block editor hosted in JavaFX `WebView` (`JFXPanel`). A prominent **Text | Blocks** toggle next to the editor heading switches views without losing playhead, selection, or document text. Blocks are Gherkin text — including `Given` / `When` / `Then` — not a second language compiled to Gherkin. Nested steps and `IF` / `ELSE` blocks snap as parent/child using Pickleball's leading-colon grammar. The play header is unchanged: click-to-seek, global **Play** from the first step in a fresh worker context, **Step** = isolated `executeStep`, **From Here** = selected/playhead through the rest, stay in play at end, Enter append-and-run. JavaScript never executes Gherkin. If JavaFX cannot start, Text is already the fallback and Blocks stays disabled/unavailable.
The right side remains Mapping, Terminal, and Diagnostic Log Explorer. Low-level lifecycle controls stay under **Session**. Existing investigation tools stay under **Tools > Advanced Controls**.
@@ -202,7 +207,7 @@ NodeMap implementations that are not exact ordinary `NodeMap` instances remain i
### WebView packaging
-JDK 21 does not ship a modern browser panel. Workbench embeds OpenJFX `WebView` through `JFXPanel` for the Gherkin editor, Mapping tree, and Diagnostic explorer. That choice stays Workbench-only: Maven-central JavaFX modules are shaded into the controller executable, including platform natives under `javafx-natives//`. JCEF was not used because Chromium natives are harder to keep isolation-clean and do not package as ordinary Workbench dependencies. If JavaFX cannot start, the live editor falls back to the existing in-place text buffer on the same `LiveScenarioPlayer` model.
+JDK 21 does not ship a modern browser panel. Workbench embeds OpenJFX `WebView` through `JFXPanel` for the Gherkin editor, Mapping tree, and Diagnostic explorer. That choice stays Workbench-only: Maven-central JavaFX modules are shaded into the controller executable, including platform natives under `javafx-natives//`. JCEF was not used because Chromium natives are harder to keep isolation-clean and do not package as ordinary Workbench dependencies. If JavaFX cannot start, the live editor stays on the existing in-place text buffer on the same `LiveScenarioPlayer` model; the **Text | Blocks** toggle remains visible and Blocks is disabled so the fallback is honest.
### Terminal and Diagnostic Log Explorer
@@ -251,7 +256,7 @@ Workbench owns one control lease for the live session. Swing and MCP/HTTP adapte
While the human holds the lease, Swing play/edit/mapping/save/worker controls stay enabled. Agent mutating calls fail clearly until `workbench_request_control`.
-While an agent holds the lease, the human can watch the same window. Play, edit, Mapping writes, Save, and worker lifecycle controls lock. The WebView editors stay mounted and become read-only; they are not torn down. A banner names the agent and shows `currentAction`. **Take control** stays enabled. Take control returns the lease to `HUMAN`, unlocks Swing, and fails any in-flight agent permission wait so a blocked Save does not write.
+While an agent holds the lease, the human can watch the same window. Play, edit, picker/filter, editor view toggle, Mapping writes, Save, and worker lifecycle controls lock. The WebView editors stay mounted and become read-only; they are not torn down. A banner names the agent and shows `currentAction`. **Take control** stays enabled. Take control returns the lease to `HUMAN`, unlocks Swing, and fails any in-flight agent permission wait so a blocked Save does not write.
The agent should update `currentAction` as it works. Playhead, Mapping, Terminal, and screenshots follow because the agent uses the same `LiveScenarioPlayer` / worker as the UI. Testing the live scenario (`executeStep`, play, Mapping reads, evidence) is allowed on the agent lease. Copying the live scenario into the original `.feature` is not; that goes through `workbench_request_save` and waits for Allow/Deny in the Swing banner.
@@ -433,24 +438,28 @@ java -jar $workbenchJar ui ".\maven-consumer-project"
Use the UI-owned worker for runtime checks; do not run `worker-check` or `live-check` concurrently with the UI.
-1. Verify the top-level layout has an expandable Features picker, the Live Gherkin Editor and compact Step Editor in the center, and exactly Mapping / Terminal / Diagnostic Log Explorer on the right.
+1. Verify the top-level layout has a scenario name/tag filter rail (feature-file filter collapsed), the Live Gherkin Editor with a **Text | Blocks** toggle and compact Step Editor in the center, and exactly Mapping / Terminal / Diagnostic Log Explorer on the right.
2. Confirm the default buffer is the Workbench demo scenario and includes `navigate to: URL.home` plus a click on the local test site when no picker scenario is selected.
-3. Click different scenario blocks and verify the playhead highlight moves immediately to the clicked step.
-4. Edit previously typed or previously executed Gherkin directly in the Live Scenario Editor and verify the line text updates in place.
-5. Press global **Play** after seeking the playhead to a later step and verify execution still starts from the first executable step in a fresh worker context.
-6. Use **From Here** on a later executable step and verify playback starts there and continues through the rest of the buffer.
-7. Use **Step** in the Step Editor and verify isolated `executeStep` execution that leaves automatic playback paused.
-8. Let a run reach the end and verify the player stays in **Waiting for step**. Type a new step and press Enter; the step is appended and executed without dropping out of play.
-9. Treat **Pause** / **Stop** as presentation/control of automatic advancement only; they do not rewind browser or service side effects.
-10. Verify Mapping has no `Current Scope` control and no hard-coded NodeMap choices. Top-level properties come from the worker ParsingMap and accept typed in-place edits.
-11. Verify Terminal filters worker log files by level and continues as steps run, without writing to MCP stdout.
-12. Verify Diagnostic Log Explorer lists retained runs from `reports/diagnostic-runs` only, or shows an honest empty state.
-13. Verify **Tools > Advanced Controls** still exposes Status, Recent Events, Step Overrides, Evidence, and Breakpoints.
-14. Verify blocking runtime actions leave the Swing UI responsive.
-15. Load a picker scenario, click **Save**, and cancel the confirmation; the original `.feature` file must be unchanged. Confirming copies only that scenario back into the originating file.
-16. Attach an agent to `.pickleball/workbench/attach.json`, call `workbench_request_control`, and verify the banner plus locked play/edit/mapping/save/worker controls. **Take control** remains enabled.
-17. While the agent holds the lease, `workbench_request_save` shows Allow/Deny. Deny writes nothing. Take control cancels the wait without writing.
-18. The default demo remains unsavable for both human Save and agent `workbench_request_save`.
+3. Filter scenarios by name using contains (default) and the other match modes; confirm matching is case-insensitive and applies to the Scenario / Scenario Outline title.
+4. Filter with include tags (AND) and exclude tags (NOT), with and without `@`, and confirm Feature-level tags apply to scenarios in that feature.
+5. Confirm **Filter by feature** is collapsed by default and that name/tag filters then apply to every catalog scenario. Opening it still supports multi-select and Feature name vs file path.
+6. Click a filtered scenario and verify it loads into the live buffer. Switch **Text | Blocks** and verify playhead, selection, and document text are unchanged. If WebView is unavailable, Blocks is disabled and Text remains the editor.
+7. Click different scenario blocks/lines and verify the playhead highlight moves immediately to the clicked step.
+8. Edit previously typed or previously executed Gherkin directly in the Live Scenario Editor and verify the line text updates in place.
+9. Press global **Play** after seeking the playhead to a later step and verify execution still starts from the first executable step in a fresh worker context.
+10. Use **From Here** on a later executable step and verify playback starts there and continues through the rest of the buffer.
+11. Use **Step** in the Step Editor and verify isolated `executeStep` execution that leaves automatic playback paused.
+12. Let a run reach the end and verify the player stays in **Waiting for step**. Type a new step and press Enter; the step is appended and executed without dropping out of play.
+13. Treat **Pause** / **Stop** as presentation/control of automatic advancement only; they do not rewind browser or service side effects.
+14. Verify Mapping has no `Current Scope` control and no hard-coded NodeMap choices. Top-level properties come from the worker ParsingMap and accept typed in-place edits.
+15. Verify Terminal filters worker log files by level and continues as steps run, without writing to MCP stdout.
+16. Verify Diagnostic Log Explorer lists retained runs from `reports/diagnostic-runs` only, or shows an honest empty state.
+17. Verify **Tools > Advanced Controls** still exposes Status, Recent Events, Step Overrides, Evidence, and Breakpoints.
+18. Verify blocking runtime actions leave the Swing UI responsive.
+19. Load a picker scenario, click **Save**, and cancel the confirmation; the original `.feature` file must be unchanged. Confirming copies only that scenario back into the originating file.
+20. Attach an agent to `.pickleball/workbench/attach.json`, call `workbench_request_control`, and verify the banner plus locked play/edit/picker/filter/editor-view/mapping/save/worker controls. **Take control** remains enabled.
+21. While the agent holds the lease, `workbench_request_save` shows Allow/Deny. Deny writes nothing. Take control cancels the wait without writing.
+22. The default demo remains unsavable for both human Save and agent `workbench_request_save`.
## Regression
diff --git a/pickleball-workbench/AGENTS.md b/pickleball-workbench/AGENTS.md
index 5ceeedb4..201ad4bf 100644
--- a/pickleball-workbench/AGENTS.md
+++ b/pickleball-workbench/AGENTS.md
@@ -66,8 +66,8 @@ java -jar pickleball-workbench-.jar ui
The UI is player-style and execution-oriented. Its primary layout is:
```text
-left rail: project feature / scenario picker
-center: Live Gherkin block editor (JavaFX WebView) + compact Step Editor / Command
+left rail: scenario name/tag filters + results; optional feature-file filter
+center: Live Gherkin editor (Text | Blocks) + compact Step Editor / Command
right: Mapping | Terminal | Diagnostic Log Explorer
```
@@ -75,7 +75,7 @@ Low-level lifecycle controls live under the Session menu and existing investigat
`LiveScenarioPlayer` owns presentation/session-buffer state only: stable line IDs, the editable Gherkin document, selected line, playhead, and `STOPPED` / `PAUSED` / `RUNNING` / `WAITING_FOR_STEP`. It must remain headless-testable and must not parse/execute Pickleball steps, implement runtime rewind, model Mapping inheritance, or become Swing component state.
-The playhead is the user-visible needle. Clicking a scenario line instantly seeks it. Global Play always starts from the first executable step in a fresh worker context, not from the playhead. The Live Scenario Editor is an in-place Gherkin document presented as snap-together blocks whose text is Gherkin, including `Given` / `When` / `Then`. Users may edit any block, including previously executed text. The picker loads consumer scenarios into the live buffer. The default buffer is a Workbench-owned browser demo against `URL.home`. Workbench does not write `.feature` files unless **Save** is explicitly approved. Human Save asks before copying the live scenario into the original scenario in the original `.feature` file. An attached agent must use `workbench_request_save` and wait for Allow/Deny when the UI is present. Deny and Take control write nothing. WebView JavaScript must not execute Gherkin.
+The playhead is the user-visible needle. Clicking a scenario line instantly seeks it. Global Play always starts from the first executable step in a fresh worker context, not from the playhead. The Live Scenario Editor is an in-place Gherkin document presented as snap-together blocks whose text is Gherkin, including `Given` / `When` / `Then`. Users may edit any block, including previously executed text. The picker loads consumer scenarios into the live buffer after filtering by scenario name (starts with / contains / ends with / full match; default contains; all case-insensitive) and Cucumber tags (include AND, exclude NOT, with Feature/Rule/outline/Examples inheritance parsed from the `.feature` files). Feature-file selection is a collapsed secondary filter; with none selected, name/tag apply to every catalog scenario. The default buffer is a Workbench-owned browser demo against `URL.home`. Workbench does not write `.feature` files unless **Save** is explicitly approved. Human Save asks before copying the live scenario into the original scenario in the original `.feature` file. An attached agent must use `workbench_request_save` and wait for Allow/Deny when the UI is present. Deny and Take control write nothing. A prominent **Text | Blocks** toggle shows the same live buffer as ordinary Gherkin or as the WebView block editor without losing playhead, selection, or document text. If JavaFX/WebView is unavailable, Text is the fallback and Blocks stays honestly unavailable. WebView JavaScript must not execute Gherkin.
The Step Editor has two play actions: **Step** executes only the editor text through `WorkbenchServices.executeStep` and leaves automatic playback paused; **From Here** restarts into a fresh scenario context and runs from the selected/playhead step through the rest of the buffer. Enter while waiting at end appends the step and continues the live run. Do not strip Gherkin keywords or add a Swing-side step matcher. Worker-side `DynamicControl` / `GherkinControl` remain the only Gherkin interpreters.
@@ -93,7 +93,7 @@ Existing capabilities remain available: project/synchronization status, worker l
The Swing Mapping put control sends entered values as text; it does not create a second Mapping parser or state model. Step Override source is sent unchanged to worker-side compilation and must contain `{{CLASS_NAME}}`; the UI must never compile handlers in the controller JVM. Browser/service/screenshot controls only present bridge evidence already supplied by Pickleball. Breakpoint controls delegate the hook/filter/lease contract to the shared service and must not recreate coordinator semantics.
-Blocking synchronization, process, bridge, Mapping, event, screenshot, service-call, Step Override, and breakpoint actions must not run on the Swing Event Dispatch Thread. Live controls must target the controller-owned running/paused worker. Semantic-event cursors are worker-local and must reset when a fresh worker is started/restarted. Prefer headless-safe tests around player state and presentation/controller delegation rather than tests requiring a visible desktop.
+Blocking synchronization, process, bridge, Mapping, event, screenshot, service-call, Step Override, and breakpoint actions must not run on the Swing Event Dispatch Thread. Live controls must target the controller-owned running/paused worker. When an agent holds the control lease, lock picker/filter fields, scenario list, feature-filter disclosure, the Text | Blocks toggle, and the live editor the same way other play/edit controls lock. Semantic-event cursors are worker-local and must reset when a fresh worker is started/restarted. Prefer headless-safe tests around player state, catalog/filter models, editor-view toggling, and presentation/controller delegation rather than tests requiring a visible desktop.
## MCP stdio
diff --git a/pickleball-workbench/src/main/java/tools/dscode/workbench/catalog/ConsumerFeatureCatalog.java b/pickleball-workbench/src/main/java/tools/dscode/workbench/catalog/ConsumerFeatureCatalog.java
index c2621873..b05f6e26 100644
--- a/pickleball-workbench/src/main/java/tools/dscode/workbench/catalog/ConsumerFeatureCatalog.java
+++ b/pickleball-workbench/src/main/java/tools/dscode/workbench/catalog/ConsumerFeatureCatalog.java
@@ -10,7 +10,6 @@
import java.util.Comparator;
import java.util.LinkedHashSet;
import java.util.List;
-import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Stream;
@@ -24,8 +23,10 @@
* value when it is already present in project-owned configuration. It does
* not crawl a git worktree or invent a second project model.
*
- * Feature/Scenario titles are read as Gherkin structure labels for browsing
- * only. This class does not match or execute steps.
+ * Feature/Scenario titles and Gherkin tags are read as structure labels for
+ * browsing only. Feature, Rule, scenario/outline, and Examples tags are
+ * inherited the same way Cucumber does. This class does not match or execute
+ * steps and does not call Cucumber.
*/
public final class ConsumerFeatureCatalog {
public enum BrowseMode {
@@ -39,7 +40,8 @@ public record FeatureEntry(
String featureName,
String directoryPath,
String fileName,
- List scenarios
+ List scenarios,
+ List tags
) {
public FeatureEntry {
Objects.requireNonNull(file, "file");
@@ -48,6 +50,7 @@ public record FeatureEntry(
directoryPath = directoryPath == null ? "" : directoryPath;
fileName = fileName == null ? file.getFileName().toString() : fileName;
scenarios = List.copyOf(scenarios == null ? List.of() : scenarios);
+ tags = ScenarioFilter.copyTags(tags);
}
public String browseLabel(BrowseMode mode) {
@@ -65,7 +68,9 @@ public record ScenarioEntry(
String relativePath,
int startLine,
int endLine,
- List lines
+ List lines,
+ List tags,
+ List effectiveTags
) {
public ScenarioEntry {
name = name == null ? "" : name;
@@ -73,6 +78,8 @@ public record ScenarioEntry(
Objects.requireNonNull(file, "file");
relativePath = relativePath == null ? "" : relativePath;
lines = List.copyOf(lines == null ? List.of() : lines);
+ tags = ScenarioFilter.copyTags(tags);
+ effectiveTags = ScenarioFilter.copyTags(effectiveTags);
}
public String displayLabel() {
@@ -83,8 +90,8 @@ public String displayLabel() {
private final Path projectRoot;
private final List features;
private final Set selectedFeatures = new LinkedHashSet<>();
+ private final ScenarioFilter filter = new ScenarioFilter();
private BrowseMode browseMode = BrowseMode.FEATURE_NAME;
- private String scenarioQuery = "";
public ConsumerFeatureCatalog(Path projectRoot, WorkbenchManifest manifest) {
this(projectRoot, discover(projectRoot, manifest));
@@ -111,12 +118,16 @@ public void setBrowseMode(BrowseMode browseMode) {
this.browseMode = browseMode == null ? BrowseMode.FEATURE_NAME : browseMode;
}
+ public ScenarioFilter filter() {
+ return filter;
+ }
+
public String scenarioQuery() {
- return scenarioQuery;
+ return filter.nameQuery();
}
public void setScenarioQuery(String scenarioQuery) {
- this.scenarioQuery = scenarioQuery == null ? "" : scenarioQuery;
+ filter.setNameQuery(scenarioQuery);
}
public List features() {
@@ -163,20 +174,26 @@ public List selectedFeatureFiles() {
}
/**
- * Scenarios under the selected features, or every project scenario when
- * nothing is selected. The search bar then filters that list.
+ * Scenarios that pass the optional feature-file filter. With no feature
+ * selected, every catalog scenario is a candidate; name/tag filters then
+ * apply to that pool.
*/
- public List visibleScenarios() {
+ public List candidateScenarios() {
List pool = new ArrayList<>();
for (FeatureEntry feature : features) {
if (selectedFeatures.isEmpty() || selectedFeatures.contains(feature.file())) {
pool.addAll(feature.scenarios());
}
}
- String query = scenarioQuery.strip().toLowerCase(Locale.ROOT);
- if (!query.isEmpty()) {
- pool.removeIf(scenario -> !matches(scenario, query));
- }
+ return List.copyOf(pool);
+ }
+
+ /**
+ * Candidate scenarios after the primary name/tag filter. Feature-file
+ * selection is optional and does not restrict the pool when empty.
+ */
+ public List visibleScenarios() {
+ List pool = new ArrayList<>(filter.apply(candidateScenarios()));
pool.sort(Comparator
.comparing(ScenarioEntry::featureName, String.CASE_INSENSITIVE_ORDER)
.thenComparing(ScenarioEntry::name, String.CASE_INSENSITIVE_ORDER));
@@ -285,27 +302,68 @@ static Path resolveConfiguredFeatures(Path projectRoot, String value) {
static FeatureEntry readFeature(Path projectRoot, Path file) throws IOException {
List lines = Files.readAllLines(file, StandardCharsets.UTF_8);
String featureName = "";
+ List featureTags = List.of();
+ List ruleTags = List.of();
+ List pendingTags = new ArrayList<>();
String currentScenario = null;
int scenarioStart = -1;
+ boolean currentIsOutline = false;
+ List currentOwnTags = List.of();
+ LinkedHashSet currentExampleTags = new LinkedHashSet<>();
List header = new ArrayList<>();
List scenarios = new ArrayList<>();
for (int i = 0; i < lines.size(); i++) {
String trimmed = lines.get(i).strip();
+ if (ScenarioFilter.isGherkinTagLine(trimmed)) {
+ pendingTags.addAll(ScenarioFilter.parseGherkinTagLine(trimmed));
+ continue;
+ }
+ if (trimmed.isEmpty() || trimmed.startsWith("#")) {
+ continue;
+ }
if (startsWithKeyword(trimmed, "Feature:")) {
featureName = trimmed.substring("Feature:".length()).strip();
+ featureTags = List.copyOf(pendingTags);
+ ruleTags = List.of();
+ pendingTags.clear();
header.add(lines.get(i));
- } else if (startsWithKeyword(trimmed, "Scenario Outline:") || startsWithKeyword(trimmed, "Scenario:")) {
+ } else if (startsWithKeyword(trimmed, "Rule:")) {
+ ruleTags = List.copyOf(pendingTags);
+ pendingTags.clear();
+ } else if (startsWithKeyword(trimmed, "Scenario Outline:")
+ || startsWithKeyword(trimmed, "Scenario Template:")
+ || startsWithKeyword(trimmed, "Scenario:")) {
if (currentScenario != null) {
- scenarios.add(scenario(projectRoot, file, featureName, currentScenario, scenarioStart, i - 1, lines, header));
+ scenarios.add(scenario(
+ projectRoot, file, featureName, featureTags, ruleTags,
+ currentScenario, currentOwnTags, currentExampleTags,
+ scenarioStart, i - 1, lines, header
+ ));
}
+ currentIsOutline = startsWithKeyword(trimmed, "Scenario Outline:")
+ || startsWithKeyword(trimmed, "Scenario Template:");
currentScenario = trimmed.contains(":")
? trimmed.substring(trimmed.indexOf(':') + 1).strip()
: trimmed;
scenarioStart = i;
+ currentOwnTags = List.copyOf(pendingTags);
+ currentExampleTags.clear();
+ pendingTags.clear();
+ } else if (startsWithKeyword(trimmed, "Examples:") || startsWithKeyword(trimmed, "Example:")) {
+ if (currentIsOutline) {
+ currentExampleTags.addAll(pendingTags);
+ }
+ pendingTags.clear();
+ } else {
+ pendingTags.clear();
}
}
if (currentScenario != null) {
- scenarios.add(scenario(projectRoot, file, featureName, currentScenario, scenarioStart, lines.size() - 1, lines, header));
+ scenarios.add(scenario(
+ projectRoot, file, featureName, featureTags, ruleTags,
+ currentScenario, currentOwnTags, currentExampleTags,
+ scenarioStart, lines.size() - 1, lines, header
+ ));
}
Path relative = relativeTo(projectRoot, file);
String relativePath = relative.toString().replace('\\', '/');
@@ -316,7 +374,8 @@ static FeatureEntry readFeature(Path projectRoot, Path file) throws IOException
featureName,
parent == null ? "" : parent.toString().replace('\\', '/'),
file.getFileName().toString(),
- scenarios
+ scenarios,
+ featureTags
);
}
@@ -324,7 +383,11 @@ private static ScenarioEntry scenario(
Path projectRoot,
Path file,
String featureName,
+ List featureTags,
+ List ruleTags,
String name,
+ List ownTags,
+ Set exampleTags,
int start,
int end,
List lines,
@@ -341,6 +404,11 @@ private static ScenarioEntry scenario(
while (!body.isEmpty() && body.getLast().isBlank()) {
body.removeLast();
}
+ LinkedHashSet effective = new LinkedHashSet<>();
+ effective.addAll(featureTags);
+ effective.addAll(ruleTags);
+ effective.addAll(ownTags);
+ if (exampleTags != null) effective.addAll(exampleTags);
return new ScenarioEntry(
name,
featureName,
@@ -348,16 +416,12 @@ private static ScenarioEntry scenario(
relativeTo(projectRoot, file).toString().replace('\\', '/'),
start + 1,
end + 1,
- body
+ body,
+ ownTags,
+ List.copyOf(effective)
);
}
- private static boolean matches(ScenarioEntry scenario, String query) {
- return scenario.name().toLowerCase(Locale.ROOT).contains(query)
- || scenario.featureName().toLowerCase(Locale.ROOT).contains(query)
- || scenario.relativePath().toLowerCase(Locale.ROOT).contains(query);
- }
-
private static boolean startsWithKeyword(String trimmed, String keyword) {
return trimmed.startsWith(keyword);
}
diff --git a/pickleball-workbench/src/main/java/tools/dscode/workbench/catalog/ScenarioFilter.java b/pickleball-workbench/src/main/java/tools/dscode/workbench/catalog/ScenarioFilter.java
new file mode 100644
index 00000000..0dabdb79
--- /dev/null
+++ b/pickleball-workbench/src/main/java/tools/dscode/workbench/catalog/ScenarioFilter.java
@@ -0,0 +1,232 @@
+package tools.dscode.workbench.catalog;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Headless name/tag filter for catalog scenarios.
+ *
+ * Name matching is always case-insensitive and applies only to the Gherkin
+ * {@code Scenario} / {@code Scenario Outline} title. Tag matching follows
+ * Cucumber inheritance already materialized on {@link ConsumerFeatureCatalog.ScenarioEntry#effectiveTags()}:
+ * feature tags, optional Rule tags, the scenario/outline's own tags, and
+ * Examples tags on an outline. Include tags are AND; exclude tags are NOT
+ * (any listed exclude tag drops the scenario). Empty include/exclude means
+ * no tag constraint. Tag queries accept values with or without a leading
+ * {@code @} and split on commas and/or whitespace. Tag comparison is
+ * case-sensitive after {@code @} normalization, matching Cucumber.
+ */
+public final class ScenarioFilter {
+ public enum NameMatchMode {
+ STARTS_WITH("Starts with"),
+ CONTAINS("Contains"),
+ ENDS_WITH("Ends with"),
+ FULL_MATCH("Full match");
+
+ private final String label;
+
+ NameMatchMode(String label) {
+ this.label = label;
+ }
+
+ public String label() {
+ return label;
+ }
+
+ @Override
+ public String toString() {
+ return label;
+ }
+ }
+
+ public static final NameMatchMode DEFAULT_NAME_MATCH = NameMatchMode.CONTAINS;
+
+ private NameMatchMode nameMatchMode = DEFAULT_NAME_MATCH;
+ private String nameQuery = "";
+ private String includeTagsQuery = "";
+ private String excludeTagsQuery = "";
+
+ public NameMatchMode nameMatchMode() {
+ return nameMatchMode;
+ }
+
+ public void setNameMatchMode(NameMatchMode nameMatchMode) {
+ this.nameMatchMode = nameMatchMode == null ? DEFAULT_NAME_MATCH : nameMatchMode;
+ }
+
+ public String nameQuery() {
+ return nameQuery;
+ }
+
+ public void setNameQuery(String nameQuery) {
+ this.nameQuery = nameQuery == null ? "" : nameQuery;
+ }
+
+ public String includeTagsQuery() {
+ return includeTagsQuery;
+ }
+
+ public void setIncludeTagsQuery(String includeTagsQuery) {
+ this.includeTagsQuery = includeTagsQuery == null ? "" : includeTagsQuery;
+ }
+
+ public String excludeTagsQuery() {
+ return excludeTagsQuery;
+ }
+
+ public void setExcludeTagsQuery(String excludeTagsQuery) {
+ this.excludeTagsQuery = excludeTagsQuery == null ? "" : excludeTagsQuery;
+ }
+
+ public void copyFrom(ScenarioFilter other) {
+ if (other == null) return;
+ this.nameMatchMode = other.nameMatchMode;
+ this.nameQuery = other.nameQuery;
+ this.includeTagsQuery = other.includeTagsQuery;
+ this.excludeTagsQuery = other.excludeTagsQuery;
+ }
+
+ public List includeTags() {
+ return parseTagQuery(includeTagsQuery);
+ }
+
+ public List excludeTags() {
+ return parseTagQuery(excludeTagsQuery);
+ }
+
+ public List apply(
+ List scenarios
+ ) {
+ List source =
+ scenarios == null ? List.of() : scenarios;
+ List include = includeTags();
+ List exclude = excludeTags();
+ List matched = new ArrayList<>();
+ for (ConsumerFeatureCatalog.ScenarioEntry scenario : source) {
+ if (matches(scenario, include, exclude)) {
+ matched.add(scenario);
+ }
+ }
+ return List.copyOf(matched);
+ }
+
+ boolean matches(ConsumerFeatureCatalog.ScenarioEntry scenario) {
+ return matches(scenario, includeTags(), excludeTags());
+ }
+
+ private boolean matches(
+ ConsumerFeatureCatalog.ScenarioEntry scenario,
+ List include,
+ List exclude
+ ) {
+ if (scenario == null) return false;
+ if (!nameMatches(scenario.name())) return false;
+ Set effective = canonicalTagSet(scenario.effectiveTags());
+ if (!include.isEmpty() && !effective.containsAll(include)) return false;
+ if (!exclude.isEmpty()) {
+ for (String tag : exclude) {
+ if (effective.contains(tag)) return false;
+ }
+ }
+ return true;
+ }
+
+ private boolean nameMatches(String name) {
+ String query = nameQuery.strip();
+ if (query.isEmpty()) return true;
+ String haystack = name == null ? "" : name;
+ String a = haystack.toLowerCase(Locale.ROOT);
+ String b = query.toLowerCase(Locale.ROOT);
+ return switch (nameMatchMode) {
+ case STARTS_WITH -> a.startsWith(b);
+ case CONTAINS -> a.contains(b);
+ case ENDS_WITH -> a.endsWith(b);
+ case FULL_MATCH -> a.equals(b);
+ };
+ }
+
+ /**
+ * Splits a free-text tag field on commas and/or whitespace and strips a
+ * leading {@code @} from each token. Empty input is no constraint.
+ */
+ public static List parseTagQuery(String raw) {
+ if (raw == null || raw.isBlank()) return List.of();
+ LinkedHashSet tags = new LinkedHashSet<>();
+ for (String part : raw.split("[,\\s]+")) {
+ String canonical = canonicalTag(part);
+ if (!canonical.isEmpty()) tags.add(canonical);
+ }
+ return List.copyOf(tags);
+ }
+
+ public static String canonicalTag(String raw) {
+ if (raw == null) return "";
+ String tag = raw.strip();
+ while (tag.startsWith("@")) {
+ tag = tag.substring(1).strip();
+ }
+ return tag;
+ }
+
+ static Set canonicalTagSet(Collection tags) {
+ LinkedHashSet canonical = new LinkedHashSet<>();
+ if (tags == null) return canonical;
+ for (String tag : tags) {
+ String value = canonicalTag(tag);
+ if (!value.isEmpty()) canonical.add(value);
+ }
+ return canonical;
+ }
+
+ static List copyTags(Collection tags) {
+ return List.copyOf(canonicalTagSet(tags));
+ }
+
+ static List parseGherkinTagLine(String trimmed) {
+ if (trimmed == null || !isGherkinTagLine(trimmed)) return List.of();
+ LinkedHashSet tags = new LinkedHashSet<>();
+ for (String token : trimmed.split("\\s+")) {
+ String canonical = canonicalTag(token);
+ if (!canonical.isEmpty()) tags.add(canonical);
+ }
+ return List.copyOf(tags);
+ }
+
+ static boolean isGherkinTagLine(String trimmed) {
+ if (trimmed == null || trimmed.isEmpty() || !trimmed.startsWith("@")) return false;
+ String[] tokens = trimmed.split("\\s+");
+ if (tokens.length == 0) return false;
+ for (String token : tokens) {
+ if (!token.startsWith("@") || canonicalTag(token).isEmpty()) return false;
+ }
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return "ScenarioFilter{mode=" + nameMatchMode
+ + ", name='" + nameQuery
+ + "', include='" + includeTagsQuery
+ + "', exclude='" + excludeTagsQuery + "'}";
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) return true;
+ if (!(other instanceof ScenarioFilter that)) return false;
+ return nameMatchMode == that.nameMatchMode
+ && Objects.equals(nameQuery, that.nameQuery)
+ && Objects.equals(includeTagsQuery, that.includeTagsQuery)
+ && Objects.equals(excludeTagsQuery, that.excludeTagsQuery);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(nameMatchMode, nameQuery, includeTagsQuery, excludeTagsQuery);
+ }
+}
diff --git a/pickleball-workbench/src/main/java/tools/dscode/workbench/player/LiveEditorView.java b/pickleball-workbench/src/main/java/tools/dscode/workbench/player/LiveEditorView.java
new file mode 100644
index 00000000..d0b0f980
--- /dev/null
+++ b/pickleball-workbench/src/main/java/tools/dscode/workbench/player/LiveEditorView.java
@@ -0,0 +1,66 @@
+package tools.dscode.workbench.player;
+
+/**
+ * Headless Text vs Blocks presentation choice for the live scenario editor.
+ *
+ * The view mode is independent of {@link LiveScenarioPlayer}: switching
+ * does not change document text, line identities, selection, or playhead.
+ * When JavaFX {@code WebView} is unavailable, block view is honestly
+ * unavailable and the live buffer stays on the existing text fallback.
+ */
+public final class LiveEditorView {
+ public enum Mode {
+ TEXT,
+ BLOCKS
+ }
+
+ private final boolean blocksAvailable;
+ private Mode mode;
+
+ public LiveEditorView(boolean blocksAvailable) {
+ this.blocksAvailable = blocksAvailable;
+ this.mode = blocksAvailable ? Mode.BLOCKS : Mode.TEXT;
+ }
+
+ public static LiveEditorView blocksAvailable() {
+ return new LiveEditorView(true);
+ }
+
+ public static LiveEditorView blocksUnavailable() {
+ return new LiveEditorView(false);
+ }
+
+ public boolean canShowBlocks() {
+ return blocksAvailable;
+ }
+
+ public Mode mode() {
+ return mode;
+ }
+
+ public boolean showingBlocks() {
+ return mode == Mode.BLOCKS;
+ }
+
+ /**
+ * Selects Text or Blocks. Requests for Blocks when the WebView host is
+ * unavailable are ignored and return {@code false}; the view stays Text.
+ */
+ public boolean setMode(Mode requested) {
+ Mode next = requested == null ? mode : requested;
+ if (next == Mode.BLOCKS && !blocksAvailable) {
+ mode = Mode.TEXT;
+ return false;
+ }
+ mode = next;
+ return true;
+ }
+
+ public boolean showText() {
+ return setMode(Mode.TEXT);
+ }
+
+ public boolean showBlocks() {
+ return setMode(Mode.BLOCKS);
+ }
+}
diff --git a/pickleball-workbench/src/main/java/tools/dscode/workbench/ui/FeaturePickerPanel.java b/pickleball-workbench/src/main/java/tools/dscode/workbench/ui/FeaturePickerPanel.java
index 20706e5b..713197d1 100644
--- a/pickleball-workbench/src/main/java/tools/dscode/workbench/ui/FeaturePickerPanel.java
+++ b/pickleball-workbench/src/main/java/tools/dscode/workbench/ui/FeaturePickerPanel.java
@@ -1,10 +1,14 @@
package tools.dscode.workbench.ui;
import tools.dscode.workbench.catalog.ConsumerFeatureCatalog;
+import tools.dscode.workbench.catalog.ScenarioFilter;
import javax.swing.BorderFactory;
+import javax.swing.Box;
+import javax.swing.BoxLayout;
import javax.swing.DefaultListModel;
import javax.swing.JButton;
+import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
@@ -17,44 +21,48 @@
import javax.swing.event.DocumentListener;
import java.awt.BorderLayout;
import java.awt.Dimension;
-import java.awt.FlowLayout;
import java.awt.GridLayout;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
-import java.nio.file.Path;
-import java.util.LinkedHashMap;
-import java.util.Map;
import java.util.function.Consumer;
-/** Expandable left-rail feature/scenario picker over the project-owned catalog. */
+/**
+ * Left-rail scenario picker. Name and tag filters are the primary controls;
+ * feature-file selection is a collapsed secondary filter.
+ */
final class FeaturePickerPanel extends JPanel {
private final DefaultListModel featureModel = new DefaultListModel<>();
private final DefaultListModel scenarioModel = new DefaultListModel<>();
private final JList featureList = new JList<>(featureModel);
private final JList scenarioList = new JList<>(scenarioModel);
- private final JTextField search = new JTextField();
+ private final JTextField nameField = new JTextField();
+ private final JComboBox nameMatchMode =
+ new JComboBox<>(ScenarioFilter.NameMatchMode.values());
+ private final JTextField includeTags = new JTextField();
+ private final JTextField excludeTags = new JTextField();
private final JToggleButton featureNameMode = new JToggleButton("Feature name");
private final JToggleButton filePathMode = new JToggleButton("File path");
+ private final JToggleButton featureFilterToggle = new JToggleButton("Filter by feature");
+ private final JPanel featureFilterPanel = new JPanel(new BorderLayout(0, 4));
private final JButton saveButton = WorkbenchTheme.flatButton("Save", "Write the live buffer back to the loaded .feature file");
- private final JLabel status = WorkbenchTheme.muted("No features selected — search all project scenarios");
+ private final JLabel status = WorkbenchTheme.muted("Showing all project scenarios");
private ConsumerFeatureCatalog catalog;
- private final Map featuresByPath = new LinkedHashMap<>();
private Consumer onScenario;
private Runnable onSave;
- private boolean syncing;
+ private boolean saveEnabled;
private boolean locked;
FeaturePickerPanel() {
super(new BorderLayout(8, 8));
WorkbenchTheme.surface(this);
setBorder(WorkbenchTheme.cardBorder());
- setPreferredSize(new Dimension(280, 640));
- setMinimumSize(new Dimension(220, 320));
+ setPreferredSize(new Dimension(300, 640));
+ setMinimumSize(new Dimension(240, 320));
- add(header(), BorderLayout.NORTH);
- add(lists(), BorderLayout.CENTER);
- add(status, BorderLayout.SOUTH);
+ add(northChrome(), BorderLayout.NORTH);
+ add(labeled("Scenarios", scenarioList), BorderLayout.CENTER);
+ add(southChrome(), BorderLayout.SOUTH);
featureList.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
featureList.setCellRenderer((list, value, index, selected, focused) -> {
@@ -73,10 +81,14 @@ final class FeaturePickerPanel extends JPanel {
label.setBorder(new EmptyBorder(4, 6, 4, 6));
label.setBackground(selected ? WorkbenchTheme.PLAYHEAD : WorkbenchTheme.SURFACE);
label.setForeground(WorkbenchTheme.TEXT);
- label.setToolTipText(value == null ? null : value.featureName() + " — " + value.relativePath());
+ if (value != null) {
+ String tags = value.effectiveTags().isEmpty()
+ ? ""
+ : " @" + String.join(" @", value.effectiveTags());
+ label.setToolTipText(value.featureName() + " — " + value.relativePath() + tags);
+ }
return label;
});
- featureList.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
featureList.addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent event) {
@@ -97,27 +109,41 @@ public void mousePressed(MouseEvent event) {
ConsumerFeatureCatalog.ScenarioEntry selected = scenarioList.getSelectedValue();
if (selected != null && onScenario != null && !locked) onScenario.accept(selected);
});
- search.getDocument().addDocumentListener(new DocumentListener() {
- @Override public void insertUpdate(DocumentEvent event) { filterChanged(); }
- @Override public void removeUpdate(DocumentEvent event) { filterChanged(); }
- @Override public void changedUpdate(DocumentEvent event) { filterChanged(); }
- });
+ nameMatchMode.setSelectedItem(ScenarioFilter.DEFAULT_NAME_MATCH);
+ nameMatchMode.setToolTipText("Case-insensitive match against the Scenario / Scenario Outline title.");
+ nameMatchMode.addActionListener(event -> filterChanged());
+ listen(nameField);
+ listen(includeTags);
+ listen(excludeTags);
+ nameField.setToolTipText("Filter by scenario name. All four match modes are case-insensitive.");
+ includeTags.setToolTipText("Tags the scenario must have (AND). With or without @; split on commas or spaces. Empty means no include constraint.");
+ excludeTags.setToolTipText("Tags the scenario must not have (NOT). Any listed tag drops it. Empty means no exclude constraint.");
saveButton.setEnabled(false);
saveButton.addActionListener(event -> {
if (onSave != null && !locked) onSave.run();
});
+ featureFilterToggle.setFocusable(false);
+ featureFilterToggle.setToolTipText("Optional. Leave collapsed to apply name/tag filters to every catalog scenario.");
+ featureFilterToggle.addActionListener(event -> {
+ featureFilterPanel.setVisible(featureFilterToggle.isSelected());
+ revalidate();
+ repaint();
+ });
+ featureNameMode.setSelected(true);
+ featureNameMode.addActionListener(event -> setMode(ConsumerFeatureCatalog.BrowseMode.FEATURE_NAME));
+ filePathMode.addActionListener(event -> setMode(ConsumerFeatureCatalog.BrowseMode.FILE_PATH));
+ featureFilterPanel.setVisible(false);
}
void setCatalog(ConsumerFeatureCatalog catalog) {
this.catalog = catalog;
- featuresByPath.clear();
featureModel.clear();
if (catalog == null) {
refreshScenarios();
return;
}
+ applyFiltersFromUi();
for (ConsumerFeatureCatalog.FeatureEntry feature : catalog.featuresForBrowse()) {
- featuresByPath.put(feature.file(), feature);
featureModel.addElement(feature);
}
syncFeatureSelection();
@@ -126,16 +152,11 @@ void setCatalog(ConsumerFeatureCatalog catalog) {
private void syncFeatureSelection() {
if (catalog == null) return;
- syncing = true;
- try {
- featureList.clearSelection();
- for (int i = 0; i < featureModel.size(); i++) {
- if (catalog.selected(featureModel.get(i).file())) {
- featureList.addSelectionInterval(i, i);
- }
+ featureList.clearSelection();
+ for (int i = 0; i < featureModel.size(); i++) {
+ if (catalog.selected(featureModel.get(i).file())) {
+ featureList.addSelectionInterval(i, i);
}
- } finally {
- syncing = false;
}
}
@@ -147,8 +168,6 @@ void onSave(Runnable onSave) {
this.onSave = onSave;
}
- private boolean saveEnabled;
-
void setSaveEnabled(boolean enabled) {
saveEnabled = enabled;
saveButton.setEnabled(enabled && !locked);
@@ -159,43 +178,86 @@ void setLocked(boolean locked) {
saveButton.setEnabled(saveEnabled && !locked);
featureList.setEnabled(!locked);
scenarioList.setEnabled(!locked);
- search.setEnabled(!locked);
+ nameField.setEnabled(!locked);
+ nameMatchMode.setEnabled(!locked);
+ includeTags.setEnabled(!locked);
+ excludeTags.setEnabled(!locked);
+ featureFilterToggle.setEnabled(!locked);
featureNameMode.setEnabled(!locked);
filePathMode.setEnabled(!locked);
}
- private JPanel header() {
- JPanel header = new JPanel(new BorderLayout(6, 6));
+ private JPanel northChrome() {
+ JPanel north = new JPanel();
+ north.setOpaque(false);
+ north.setLayout(new BoxLayout(north, BoxLayout.Y_AXIS));
+
+ JPanel header = new JPanel(new BorderLayout(6, 0));
header.setOpaque(false);
- header.add(WorkbenchTheme.heading("Features"), BorderLayout.WEST);
+ header.add(WorkbenchTheme.heading("Scenarios"), BorderLayout.WEST);
+ header.add(saveButton, BorderLayout.EAST);
+ header.setAlignmentX(LEFT_ALIGNMENT);
+ header.setMaximumSize(new Dimension(Integer.MAX_VALUE, header.getPreferredSize().height + 8));
+ north.add(header);
+ north.add(Box.createVerticalStrut(8));
+
+ JPanel nameRow = new JPanel(new BorderLayout(6, 0));
+ nameRow.setOpaque(false);
+ nameRow.add(nameField, BorderLayout.CENTER);
+ nameMatchMode.setMaximumSize(nameMatchMode.getPreferredSize());
+ nameRow.add(nameMatchMode, BorderLayout.EAST);
+ north.add(labeledField("Scenario name", nameRow));
+ north.add(Box.createVerticalStrut(6));
+ north.add(labeledField("Must have all tags", includeTags));
+ north.add(Box.createVerticalStrut(6));
+ north.add(labeledField("Must not have tags", excludeTags));
+ north.add(Box.createVerticalStrut(8));
+ return north;
+ }
+
+ private JPanel southChrome() {
+ JPanel south = new JPanel();
+ south.setOpaque(false);
+ south.setLayout(new BoxLayout(south, BoxLayout.Y_AXIS));
+
+ featureFilterToggle.setAlignmentX(LEFT_ALIGNMENT);
+ featureFilterToggle.setMaximumSize(new Dimension(Integer.MAX_VALUE, featureFilterToggle.getPreferredSize().height));
+ south.add(featureFilterToggle);
+ south.add(Box.createVerticalStrut(4));
JPanel modes = new JPanel(new GridLayout(1, 2, 4, 0));
modes.setOpaque(false);
- featureNameMode.setSelected(true);
- featureNameMode.addActionListener(event -> setMode(ConsumerFeatureCatalog.BrowseMode.FEATURE_NAME));
- filePathMode.addActionListener(event -> setMode(ConsumerFeatureCatalog.BrowseMode.FILE_PATH));
modes.add(featureNameMode);
modes.add(filePathMode);
- header.add(modes, BorderLayout.CENTER);
+ featureFilterPanel.setOpaque(false);
+ featureFilterPanel.setAlignmentX(LEFT_ALIGNMENT);
+ featureFilterPanel.add(modes, BorderLayout.NORTH);
+ JScrollPane featureScroll = new JScrollPane(featureList);
+ featureScroll.setBorder(BorderFactory.createLineBorder(WorkbenchTheme.BORDER));
+ featureScroll.setPreferredSize(new Dimension(240, 140));
+ featureFilterPanel.add(featureScroll, BorderLayout.CENTER);
+ south.add(featureFilterPanel);
+ south.add(Box.createVerticalStrut(6));
+ status.setAlignmentX(LEFT_ALIGNMENT);
+ south.add(status);
+ return south;
+ }
- JPanel south = new JPanel(new BorderLayout(6, 6));
- south.setOpaque(false);
- search.setToolTipText("Filter scenarios. With no feature selected this searches the whole consumer project.");
- south.add(search, BorderLayout.CENTER);
- JPanel actions = new JPanel(new FlowLayout(FlowLayout.RIGHT, 0, 0));
- actions.setOpaque(false);
- actions.add(saveButton);
- south.add(actions, BorderLayout.EAST);
- header.add(south, BorderLayout.SOUTH);
- return header;
+ private JPanel labeledField(String title, JTextField field) {
+ JPanel wrap = new JPanel(new BorderLayout(0, 0));
+ wrap.setOpaque(false);
+ wrap.add(field, BorderLayout.CENTER);
+ return labeledField(title, wrap);
}
- private JPanel lists() {
- JPanel lists = new JPanel(new GridLayout(2, 1, 0, 8));
- lists.setOpaque(false);
- lists.add(labeled("Project features", featureList));
- lists.add(labeled("Scenarios", scenarioList));
- return lists;
+ private JPanel labeledField(String title, JPanel field) {
+ JPanel panel = new JPanel(new BorderLayout(0, 2));
+ panel.setOpaque(false);
+ panel.setAlignmentX(LEFT_ALIGNMENT);
+ panel.add(WorkbenchTheme.muted(title), BorderLayout.NORTH);
+ panel.add(field, BorderLayout.CENTER);
+ panel.setMaximumSize(new Dimension(Integer.MAX_VALUE, panel.getPreferredSize().height + 18));
+ return panel;
}
private JPanel labeled(String title, JList> list) {
@@ -217,27 +279,50 @@ private void setMode(ConsumerFeatureCatalog.BrowseMode mode) {
}
}
+ private void listen(JTextField field) {
+ field.getDocument().addDocumentListener(new DocumentListener() {
+ @Override public void insertUpdate(DocumentEvent event) { filterChanged(); }
+ @Override public void removeUpdate(DocumentEvent event) { filterChanged(); }
+ @Override public void changedUpdate(DocumentEvent event) { filterChanged(); }
+ });
+ }
+
private void filterChanged() {
if (catalog == null) return;
- catalog.setScenarioQuery(search.getText());
+ applyFiltersFromUi();
refreshScenarios();
}
+ private void applyFiltersFromUi() {
+ if (catalog == null) return;
+ ScenarioFilter filter = catalog.filter();
+ filter.setNameQuery(nameField.getText());
+ Object selected = nameMatchMode.getSelectedItem();
+ filter.setNameMatchMode(selected instanceof ScenarioFilter.NameMatchMode mode
+ ? mode
+ : ScenarioFilter.DEFAULT_NAME_MATCH);
+ filter.setIncludeTagsQuery(includeTags.getText());
+ filter.setExcludeTagsQuery(excludeTags.getText());
+ }
+
private void refreshScenarios() {
scenarioModel.clear();
if (catalog == null) {
status.setText("No synchronized consumer project.");
return;
}
+ int candidates = catalog.candidateScenarios().size();
for (ConsumerFeatureCatalog.ScenarioEntry scenario : catalog.visibleScenarios()) {
scenarioModel.addElement(scenario);
}
- if (catalog.selectedFeatureFiles().isEmpty()) {
- status.setText(catalog.scenarioQuery().isBlank()
- ? "No features selected — showing all project scenarios"
- : "Searching all project scenarios");
+ int visible = scenarioModel.size();
+ String featureNote = catalog.selectedFeatureFiles().isEmpty()
+ ? "all features"
+ : catalog.selectedFeatureFiles().size() + " feature(s)";
+ if (visible == candidates) {
+ status.setText(visible + " scenario(s) · " + featureNote);
} else {
- status.setText(catalog.selectedFeatureFiles().size() + " feature(s) selected");
+ status.setText(visible + " of " + candidates + " scenario(s) · " + featureNote);
}
}
}
diff --git a/pickleball-workbench/src/main/java/tools/dscode/workbench/ui/WorkbenchFrame.java b/pickleball-workbench/src/main/java/tools/dscode/workbench/ui/WorkbenchFrame.java
index 038e4ff8..234d4d21 100644
--- a/pickleball-workbench/src/main/java/tools/dscode/workbench/ui/WorkbenchFrame.java
+++ b/pickleball-workbench/src/main/java/tools/dscode/workbench/ui/WorkbenchFrame.java
@@ -10,6 +10,7 @@
import tools.dscode.workbench.mapping.MappingTreeModel;
import tools.dscode.workbench.mapping.MappingValueCodec;
import tools.dscode.workbench.mcp.WorkbenchAttachServer;
+import tools.dscode.workbench.player.LiveEditorView;
import tools.dscode.workbench.player.LivePlaybackCoordinator;
import tools.dscode.workbench.player.LiveScenarioPlayer;
import tools.dscode.workbench.player.WorkbenchSavePreview;
@@ -65,7 +66,11 @@ final class WorkbenchFrame extends JFrame {
private WebViewPanel mappingView;
private WebViewPanel diagnosticView;
private JComponent pickerSplit;
- private final JToggleButton pickerToggle = new JToggleButton("Features");
+ private JPanel editorHost;
+ private LiveEditorView editorView = LiveEditorView.blocksUnavailable();
+ private final JToggleButton textViewButton = new JToggleButton("Text");
+ private final JToggleButton blocksViewButton = new JToggleButton("Blocks");
+ private final JToggleButton pickerToggle = new JToggleButton("Scenarios");
private List mappingEntries = List.of();
private MappingTreeModel mappingModel;
private DiagnosticEvidenceNavigator diagnosticNavigator;
@@ -331,29 +336,82 @@ private JComponent scenarioPanel() {
JPanel panel = new JPanel(new BorderLayout(0, 4));
panel.setBackground(WorkbenchTheme.SURFACE);
panel.setBorder(WorkbenchTheme.cardBorder());
- panel.add(WorkbenchTheme.heading("Live Scenario Editor"), BorderLayout.NORTH);
+
+ JPanel header = new JPanel(new BorderLayout(8, 0));
+ header.setOpaque(false);
+ header.add(WorkbenchTheme.heading("Live Scenario Editor"), BorderLayout.WEST);
+ header.add(editorViewToggle(), BorderLayout.EAST);
+ panel.add(header, BorderLayout.NORTH);
scenarioEditor.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 14));
scenarioEditor.setLineWrap(false);
scenarioEditor.setTabSize(2);
- JPanel editorHost = new JPanel(new CardLayout());
+ editorHost = new JPanel(new CardLayout());
editorHost.setOpaque(false);
editorHost.add(new JScrollPane(scenarioEditor), "text");
if (gherkinView != null) {
editorHost.add(gherkinView, "web");
- ((CardLayout) editorHost.getLayout()).show(editorHost, "web");
}
panel.add(editorHost, BorderLayout.CENTER);
JPanel legend = new JPanel(new FlowLayout(FlowLayout.LEFT, 18, 2));
legend.setOpaque(false);
- legend.add(WorkbenchTheme.muted("Blocks are Gherkin text"));
+ legend.add(WorkbenchTheme.muted("Same live buffer in Text or Blocks"));
legend.add(WorkbenchTheme.muted("Play starts from the first step"));
- legend.add(WorkbenchTheme.muted("Click a block to move the playhead"));
+ legend.add(WorkbenchTheme.muted("Click to move the playhead"));
panel.add(legend, BorderLayout.SOUTH);
+ applyEditorView();
return panel;
}
+ private JPanel editorViewToggle() {
+ JPanel toggle = new JPanel(new GridLayout(1, 2, 0, 0));
+ toggle.setOpaque(false);
+ ButtonGroup group = new ButtonGroup();
+ textViewButton.setFocusable(false);
+ blocksViewButton.setFocusable(false);
+ textViewButton.setToolTipText("View and edit the live scenario as ordinary Gherkin text.");
+ blocksViewButton.setToolTipText(editorView.canShowBlocks()
+ ? "View and edit the same live buffer as Gherkin blocks."
+ : "Block view requires JavaFX WebView, which is not available in this process.");
+ textViewButton.addActionListener(event -> {
+ if (humanControlsLocked()) {
+ applyEditorView();
+ return;
+ }
+ editorView.showText();
+ applyEditorView();
+ });
+ blocksViewButton.addActionListener(event -> {
+ if (humanControlsLocked()) {
+ applyEditorView();
+ return;
+ }
+ editorView.showBlocks();
+ applyEditorView();
+ });
+ group.add(textViewButton);
+ group.add(blocksViewButton);
+ toggle.add(textViewButton);
+ toggle.add(blocksViewButton);
+ toggle.setBorder(BorderFactory.createLineBorder(WorkbenchTheme.BORDER));
+ return toggle;
+ }
+
+ private void applyEditorView() {
+ if (editorHost == null) return;
+ boolean blocks = editorView.showingBlocks() && gherkinView != null;
+ CardLayout cards = (CardLayout) editorHost.getLayout();
+ cards.show(editorHost, blocks ? "web" : "text");
+ textViewButton.setSelected(!blocks);
+ blocksViewButton.setSelected(blocks);
+ blocksViewButton.setEnabled(editorView.canShowBlocks() && !humanControlsLocked());
+ textViewButton.setEnabled(!humanControlsLocked());
+ if (blocks) {
+ pushGherkinView();
+ }
+ }
+
private JComponent stepPanel() {
JPanel panel = new JPanel(new BorderLayout(6, 5));
panel.setBorder(BorderFactory.createCompoundBorder(
@@ -490,8 +548,9 @@ public void actionPerformed(java.awt.event.ActionEvent event) {
private void configureWebViews() {
if (!JavaFxSupport.available()) {
+ editorView = LiveEditorView.blocksUnavailable();
webViewNote.setText("JavaFX WebView unavailable: " + JavaFxSupport.failure()
- + ". Using the text fallback. OpenJFX is Workbench-only.");
+ + ". Using the text fallback. OpenJFX is Workbench-only. Block view is unavailable.");
return;
}
try {
@@ -534,11 +593,14 @@ private void configureWebViews() {
"diagnosticHost",
diagnosticHost
);
+ editorView = LiveEditorView.blocksAvailable();
} catch (RuntimeException failure) {
gherkinView = null;
mappingView = null;
diagnosticView = null;
- webViewNote.setText("JavaFX WebView failed to start: " + failure.getMessage());
+ editorView = LiveEditorView.blocksUnavailable();
+ webViewNote.setText("JavaFX WebView failed to start: " + failure.getMessage()
+ + ". Using the text fallback. Block view is unavailable.");
}
}
@@ -1429,12 +1491,15 @@ private void applyLease(WorkbenchControlLeaseSnapshot snapshot) {
}
picker.setLocked(locked);
+ pickerToggle.setEnabled(!locked);
scenarioEditor.setEditable(!locked);
stepText.setEditable(!locked);
playButton.setEnabled(!locked);
pauseButton.setEnabled(!locked);
playerStopButton.setEnabled(!locked);
stepOnlyButton.setEnabled(!locked);
+ textViewButton.setEnabled(!locked);
+ blocksViewButton.setEnabled(editorView.canShowBlocks() && !locked);
takeControlButton.setEnabled(locked);
if (locked) {
fromHereButton.setEnabled(false);
diff --git a/pickleball-workbench/src/test/java/tools/dscode/workbench/catalog/ConsumerFeatureCatalogTest.java b/pickleball-workbench/src/test/java/tools/dscode/workbench/catalog/ConsumerFeatureCatalogTest.java
index 996485e0..72e6c52b 100644
--- a/pickleball-workbench/src/test/java/tools/dscode/workbench/catalog/ConsumerFeatureCatalogTest.java
+++ b/pickleball-workbench/src/test/java/tools/dscode/workbench/catalog/ConsumerFeatureCatalogTest.java
@@ -51,17 +51,149 @@ void discoversFeaturesFromProjectOwnedTestResourcesAndSupportsSelectionSearch()
assertEquals(2, catalog.visibleScenarios().size());
assertTrue(catalog.visibleScenarios().stream().allMatch(scenario -> scenario.featureName().equals("Sign in")));
- catalog.setScenarioQuery("locked");
+ catalog.filter().setNameQuery("locked");
assertEquals(1, catalog.visibleScenarios().size());
assertEquals("Locked account", catalog.visibleScenarios().getFirst().name());
catalog.clearFeatureSelection();
- catalog.setScenarioQuery("if else");
+ catalog.filter().setNameQuery("if else");
assertEquals(1, catalog.visibleScenarios().size());
assertEquals("IF ELSE branch", catalog.visibleScenarios().getFirst().name());
assertTrue(catalog.visibleScenarios().getFirst().lines().getFirst().startsWith("Feature:"));
}
+ @Test
+ void nameTagAndOptionalFeatureFiltersComposeAndInheritFeatureTags() throws Exception {
+ Path features = project.resolve("src/test/resources/features");
+ Files.createDirectories(features);
+ Files.writeString(features.resolve("auth.feature"), """
+ @feature-auth @shared
+ Feature: Sign in
+
+ @smoke @login
+ Scenario: Valid password
+ Given a user
+
+ @wip
+ Scenario: Locked account
+ Given a lock
+
+ @outline
+ Scenario Outline: Search term
+ When search
+ @fast
+ Examples:
+ | term |
+ | a |
+ @slow @db
+ Examples:
+ | term |
+ | b |
+ """);
+ Files.writeString(features.resolve("other.feature"), """
+ @other
+ Feature: Other
+
+ Scenario: Unrelated
+ Given stay
+ """);
+
+ ConsumerFeatureCatalog catalog = ConsumerFeatureCatalog.scan(project, null);
+ assertEquals(4, catalog.candidateScenarios().size());
+ assertEquals(4, catalog.visibleScenarios().size());
+
+ ConsumerFeatureCatalog.ScenarioEntry valid = named(catalog, "Valid password");
+ assertEquals(List.of("feature-auth", "shared", "smoke", "login"), valid.effectiveTags());
+ assertEquals(List.of("smoke", "login"), valid.tags());
+
+ ConsumerFeatureCatalog.ScenarioEntry locked = named(catalog, "Locked account");
+ assertTrue(locked.effectiveTags().contains("feature-auth"));
+ assertTrue(locked.effectiveTags().contains("wip"));
+
+ ConsumerFeatureCatalog.ScenarioEntry search = named(catalog, "Search term");
+ assertTrue(search.effectiveTags().containsAll(List.of("feature-auth", "shared", "outline", "fast", "slow", "db")));
+
+ catalog.filter().setNameMatchMode(ScenarioFilter.NameMatchMode.STARTS_WITH);
+ catalog.filter().setNameQuery("valid");
+ assertEquals(List.of("Valid password"), names(catalog));
+
+ catalog.filter().setNameMatchMode(ScenarioFilter.NameMatchMode.ENDS_WITH);
+ catalog.filter().setNameQuery("account");
+ assertEquals(List.of("Locked account"), names(catalog));
+
+ catalog.filter().setNameMatchMode(ScenarioFilter.NameMatchMode.FULL_MATCH);
+ catalog.filter().setNameQuery("search term");
+ assertEquals(List.of("Search term"), names(catalog));
+
+ catalog.filter().setNameMatchMode(ScenarioFilter.NameMatchMode.CONTAINS);
+ catalog.filter().setNameQuery("");
+ catalog.filter().setIncludeTagsQuery("@smoke @login");
+ assertEquals(List.of("Valid password"), names(catalog));
+
+ catalog.filter().setIncludeTagsQuery("");
+ catalog.filter().setExcludeTagsQuery("wip");
+ assertEquals(List.of("Unrelated", "Search term", "Valid password"), names(catalog));
+
+ catalog.filter().setIncludeTagsQuery("shared");
+ catalog.filter().setExcludeTagsQuery("@wip");
+ assertEquals(List.of("Search term", "Valid password"), names(catalog));
+
+ catalog.filter().setIncludeTagsQuery("@slow");
+ catalog.filter().setExcludeTagsQuery("");
+ assertEquals(List.of("Search term"), names(catalog));
+
+ catalog.filter().setIncludeTagsQuery("");
+ catalog.filter().setNameQuery("lock");
+ assertEquals(List.of("Locked account"), names(catalog));
+
+ catalog.clearFeatureSelection();
+ catalog.filter().setNameQuery("");
+ catalog.toggleFeature(features.resolve("other.feature"));
+ assertEquals(List.of("Unrelated"), names(catalog));
+ assertEquals(1, catalog.candidateScenarios().size());
+
+ catalog.clearFeatureSelection();
+ assertEquals(4, catalog.candidateScenarios().size());
+ assertEquals(4, catalog.visibleScenarios().size());
+ }
+
+ @Test
+ void ruleTagsAreInheritedWithFeatureTags() throws Exception {
+ Path features = project.resolve("src/test/resources/features");
+ Files.createDirectories(features);
+ Files.writeString(features.resolve("rules.feature"), """
+ @feature-tag
+ Feature: Rules
+
+ @rule-a
+ Rule: First
+
+ @own
+ Scenario: Inside rule
+ Given stay
+ """);
+
+ ConsumerFeatureCatalog catalog = ConsumerFeatureCatalog.scan(project, null);
+ ConsumerFeatureCatalog.ScenarioEntry inside = named(catalog, "Inside rule");
+ assertEquals(List.of("own"), inside.tags());
+ assertTrue(inside.effectiveTags().containsAll(List.of("feature-tag", "rule-a", "own")));
+ catalog.filter().setIncludeTagsQuery("feature-tag rule-a own");
+ assertEquals(List.of("Inside rule"), names(catalog));
+ }
+
+ private static ConsumerFeatureCatalog.ScenarioEntry named(ConsumerFeatureCatalog catalog, String name) {
+ return catalog.visibleScenarios().stream()
+ .filter(scenario -> scenario.name().equals(name))
+ .findFirst()
+ .orElseThrow();
+ }
+
+ private static List names(ConsumerFeatureCatalog catalog) {
+ return catalog.visibleScenarios().stream()
+ .map(ConsumerFeatureCatalog.ScenarioEntry::name)
+ .toList();
+ }
+
@Test
void prefersClasspathFeaturesConfigurationWithoutCrawlingUnrelatedDirectories() throws Exception {
Files.createDirectories(project.resolve("src/test/resources/features"));
diff --git a/pickleball-workbench/src/test/java/tools/dscode/workbench/catalog/ScenarioFilterTest.java b/pickleball-workbench/src/test/java/tools/dscode/workbench/catalog/ScenarioFilterTest.java
new file mode 100644
index 00000000..e21c5e1b
--- /dev/null
+++ b/pickleball-workbench/src/test/java/tools/dscode/workbench/catalog/ScenarioFilterTest.java
@@ -0,0 +1,125 @@
+package tools.dscode.workbench.catalog;
+
+import org.junit.jupiter.api.Test;
+
+import java.nio.file.Path;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class ScenarioFilterTest {
+ @Test
+ void nameMatchModesAreCaseInsensitiveAndDefaultToContains() {
+ ScenarioFilter filter = new ScenarioFilter();
+ assertEquals(ScenarioFilter.NameMatchMode.CONTAINS, filter.nameMatchMode());
+
+ ConsumerFeatureCatalog.ScenarioEntry login = scenario("Valid Login User");
+ ConsumerFeatureCatalog.ScenarioEntry logout = scenario("Logout path");
+ List pool = List.of(login, logout);
+
+ filter.setNameQuery("LOGIN");
+ assertEquals(List.of(login), filter.apply(pool));
+
+ filter.setNameMatchMode(ScenarioFilter.NameMatchMode.STARTS_WITH);
+ filter.setNameQuery("valid");
+ assertEquals(List.of(login), filter.apply(pool));
+ filter.setNameQuery("user");
+ assertTrue(filter.apply(pool).isEmpty());
+
+ filter.setNameMatchMode(ScenarioFilter.NameMatchMode.ENDS_WITH);
+ filter.setNameQuery("USER");
+ assertEquals(List.of(login), filter.apply(pool));
+ filter.setNameQuery("valid");
+ assertTrue(filter.apply(pool).isEmpty());
+
+ filter.setNameMatchMode(ScenarioFilter.NameMatchMode.FULL_MATCH);
+ filter.setNameQuery("valid login user");
+ assertEquals(List.of(login), filter.apply(pool));
+ filter.setNameQuery("valid login");
+ assertTrue(filter.apply(pool).isEmpty());
+ }
+
+ @Test
+ void includeTagsAreAndAndExcludeTagsAreNot() {
+ ConsumerFeatureCatalog.ScenarioEntry smokeLogin = scenario("A", "smoke", "login");
+ ConsumerFeatureCatalog.ScenarioEntry smokeOnly = scenario("B", "smoke");
+ ConsumerFeatureCatalog.ScenarioEntry wip = scenario("C", "wip", "login");
+ List pool = List.of(smokeLogin, smokeOnly, wip);
+
+ ScenarioFilter filter = new ScenarioFilter();
+ filter.setIncludeTagsQuery("@smoke @login");
+ assertEquals(List.of(smokeLogin), filter.apply(pool));
+
+ filter.setIncludeTagsQuery("smoke, login");
+ assertEquals(List.of(smokeLogin), filter.apply(pool));
+
+ filter.setIncludeTagsQuery("");
+ filter.setExcludeTagsQuery("@wip");
+ assertEquals(List.of(smokeLogin, smokeOnly), filter.apply(pool));
+
+ filter.setIncludeTagsQuery("login");
+ filter.setExcludeTagsQuery("wip");
+ assertEquals(List.of(smokeLogin), filter.apply(pool));
+ }
+
+ @Test
+ void emptyNameAndTagFieldsImposeNoConstraint() {
+ ConsumerFeatureCatalog.ScenarioEntry one = scenario("One", "alpha");
+ ConsumerFeatureCatalog.ScenarioEntry two = scenario("Two");
+ ScenarioFilter filter = new ScenarioFilter();
+ assertEquals(List.of(one, two), filter.apply(List.of(one, two)));
+ }
+
+ @Test
+ void nameAndTagFiltersCompose() {
+ ConsumerFeatureCatalog.ScenarioEntry keep = scenario("Locked account", "wip");
+ ConsumerFeatureCatalog.ScenarioEntry otherWip = scenario("Other wip", "wip");
+ ConsumerFeatureCatalog.ScenarioEntry lockedClean = scenario("Locked account clean");
+ ScenarioFilter filter = new ScenarioFilter();
+ filter.setNameQuery("locked");
+ filter.setIncludeTagsQuery("wip");
+ assertEquals(List.of(keep), filter.apply(List.of(keep, otherWip, lockedClean)));
+ }
+
+ @Test
+ void parseTagQueryAcceptsAtSignCommaAndWhitespace() {
+ assertEquals(List.of("smoke", "login", "wip"),
+ ScenarioFilter.parseTagQuery(" @smoke, login wip "));
+ assertTrue(ScenarioFilter.parseTagQuery("").isEmpty());
+ assertTrue(ScenarioFilter.parseTagQuery(" , ").isEmpty());
+ assertEquals("smoke", ScenarioFilter.canonicalTag("@@smoke"));
+ }
+
+ @Test
+ void tagComparisonIsCaseSensitiveAfterCanonicalizingAt() {
+ ConsumerFeatureCatalog.ScenarioEntry tagged = scenario("A", "Smoke");
+ ScenarioFilter filter = new ScenarioFilter();
+ filter.setIncludeTagsQuery("Smoke");
+ assertEquals(List.of(tagged), filter.apply(List.of(tagged)));
+ filter.setIncludeTagsQuery("smoke");
+ assertTrue(filter.apply(List.of(tagged)).isEmpty());
+ }
+
+ @Test
+ void gherkinTagLinesRequireAtPrefixedTokens() {
+ assertTrue(ScenarioFilter.isGherkinTagLine("@smoke @login"));
+ assertFalse(ScenarioFilter.isGherkinTagLine("Given @not-a-tag-line"));
+ assertEquals(List.of("smoke", "login"), ScenarioFilter.parseGherkinTagLine("@smoke @login"));
+ }
+
+ private static ConsumerFeatureCatalog.ScenarioEntry scenario(String name, String... effectiveTags) {
+ return new ConsumerFeatureCatalog.ScenarioEntry(
+ name,
+ "Demo",
+ Path.of("demo.feature"),
+ "demo.feature",
+ 1,
+ 3,
+ List.of("Scenario: " + name),
+ List.of(),
+ List.of(effectiveTags)
+ );
+ }
+}
diff --git a/pickleball-workbench/src/test/java/tools/dscode/workbench/player/LiveEditorViewTest.java b/pickleball-workbench/src/test/java/tools/dscode/workbench/player/LiveEditorViewTest.java
new file mode 100644
index 00000000..f2f2bbc2
--- /dev/null
+++ b/pickleball-workbench/src/test/java/tools/dscode/workbench/player/LiveEditorViewTest.java
@@ -0,0 +1,50 @@
+package tools.dscode.workbench.player;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class LiveEditorViewTest {
+ @Test
+ void togglingTextAndBlocksDoesNotChangeDocumentTextOrPlayhead() {
+ LiveScenarioPlayer player = new LiveScenarioPlayer(List.of(
+ "Given first",
+ "And second",
+ "Then third"
+ ));
+ long second = player.lines().get(1).id();
+ player.clickLine(second);
+ String document = player.documentText();
+ long selected = player.selectedId().orElseThrow();
+
+ LiveEditorView view = LiveEditorView.blocksAvailable();
+ assertEquals(LiveEditorView.Mode.BLOCKS, view.mode());
+ assertTrue(view.showText());
+ assertEquals(LiveEditorView.Mode.TEXT, view.mode());
+ assertEquals(document, player.documentText());
+ assertEquals(second, player.playheadId().orElseThrow());
+ assertEquals(selected, player.selectedId().orElseThrow());
+
+ assertTrue(view.showBlocks());
+ assertEquals(LiveEditorView.Mode.BLOCKS, view.mode());
+ assertEquals(document, player.documentText());
+ assertEquals(second, player.playheadId().orElseThrow());
+ assertEquals(selected, player.selectedId().orElseThrow());
+ assertEquals("And second", player.playheadLine().orElseThrow().text());
+ }
+
+ @Test
+ void unavailableBlocksStayOnTextAndRefuseBlockMode() {
+ LiveEditorView view = LiveEditorView.blocksUnavailable();
+ assertFalse(view.canShowBlocks());
+ assertEquals(LiveEditorView.Mode.TEXT, view.mode());
+ assertFalse(view.showBlocks());
+ assertEquals(LiveEditorView.Mode.TEXT, view.mode());
+ assertTrue(view.showText());
+ assertEquals(LiveEditorView.Mode.TEXT, view.mode());
+ }
+}
diff --git a/src/main/resources/META-INF/pickleball/guidance/docs/agent/feature-map.md b/src/main/resources/META-INF/pickleball/guidance/docs/agent/feature-map.md
index d8772d43..3a2226af 100644
--- a/src/main/resources/META-INF/pickleball/guidance/docs/agent/feature-map.md
+++ b/src/main/resources/META-INF/pickleball/guidance/docs/agent/feature-map.md
@@ -6,7 +6,7 @@ This file maps consumer-visible capabilities to implementation anchors, executab
|---|---|---|---|
| Build, publication, Java compatibility, nested controller distribution | `build.gradle`; `settings.gradle`; `pickleball-control-protocol/build.gradle`; `pickleball-workbench/build.gradle`; `src/main/aspectj`; search `verifyStrictControllerIsolation`, `verifyEmbeddedWorkbench`, `shadowJar`, `JavaLanguageVersion` | root tests; protocol `check`; `:pickleball-workbench:test`; artifact/dependency verification; Maven consumer build | `README.md`; `docs/getting-started.md`; `docs/cucumber-compatibility.md`; `docs/consumer-project.md`; `docs/pickleball-workbench.md` |
| Neutral controller/worker wire protocol | `pickleball-control-protocol/src/main/java/tools/dscode/control/protocol`; `ControlProtocol`; `ControlBridgeRequests`; `ControlBridgeResponses`; immutable `ControlBridge*` records | `verifyProtocolIsolation`; `ControlBridgeClientTest`; consumer `@control-bridge`; protocol/version/capability assertions | `docs/pickleball-workbench.md`; `pickleball-workbench/AGENTS.md` |
-| Pickleball Workbench synchronization / persistent live worker / MCP stdio / player-style Swing+WebView UI / watched-agent control lease | `pickleball-workbench`; `WorkbenchApplication`; `WorkbenchRuntimeBoundary`; `WorkbenchServices`; `WorkbenchController`; `tools.dscode.workbench.lease`; `WorkbenchAttachServer`; `tools.dscode.workbench.sync`; `WorkbenchWorkerManager`; `WorkbenchLiveSession`; `tools.dscode.workbench.bridge.ControlBridgeClient`; `tools.dscode.workbench.mcp`; `tools.dscode.workbench.player`; `tools.dscode.workbench.catalog`; `tools.dscode.workbench.mapping`; `tools.dscode.workbench.terminal`; `tools.dscode.workbench.diagnostics`; `tools.dscode.workbench.ui`; OpenJFX `WebView` / `JFXPanel`; protocol `ControlProtocol.WORKER_MAIN_CLASS` | `:pickleball-workbench:test`; `WorkbenchRuntimeBoundaryTest`; `ConsumerFeatureCatalogTest`; `GherkinBlockDocumentTest`; `LivePlaybackCoordinatorTest`; `LiveFeatureSaveTest`; `WorkbenchControlLeaseTest`; `WorkbenchControllerLeaseTest`; `WorkbenchAttachServerTest`; `MappingValueCodecTest`; `WorkerLogBufferTest`; `DiagnosticEvidenceNavigatorTest`; `WorkbenchUiControllerTest`; `LiveScenarioPlayerTest`; packaged UI/MCP probes | `docs/pickleball-workbench.md`; `docs/pickleball-workbench-player.md`; `pickleball-workbench/AGENTS.md` |
+| Pickleball Workbench synchronization / persistent live worker / MCP stdio / player-style Swing+WebView UI / watched-agent control lease / scenario name-tag filter / Text-Blocks editor toggle | `pickleball-workbench`; `WorkbenchApplication`; `WorkbenchRuntimeBoundary`; `WorkbenchServices`; `WorkbenchController`; `tools.dscode.workbench.lease`; `WorkbenchAttachServer`; `tools.dscode.workbench.sync`; `WorkbenchWorkerManager`; `WorkbenchLiveSession`; `tools.dscode.workbench.bridge.ControlBridgeClient`; `tools.dscode.workbench.mcp`; `tools.dscode.workbench.player`; `LiveEditorView`; `tools.dscode.workbench.catalog`; `ScenarioFilter`; `ConsumerFeatureCatalog`; `tools.dscode.workbench.mapping`; `tools.dscode.workbench.terminal`; `tools.dscode.workbench.diagnostics`; `tools.dscode.workbench.ui`; `FeaturePickerPanel`; OpenJFX `WebView` / `JFXPanel`; protocol `ControlProtocol.WORKER_MAIN_CLASS` | `:pickleball-workbench:test`; `WorkbenchRuntimeBoundaryTest`; `ConsumerFeatureCatalogTest`; `ScenarioFilterTest`; `GherkinBlockDocumentTest`; `LivePlaybackCoordinatorTest`; `LiveFeatureSaveTest`; `LiveEditorViewTest`; `WorkbenchControlLeaseTest`; `WorkbenchControllerLeaseTest`; `WorkbenchAttachServerTest`; `MappingValueCodecTest`; `WorkerLogBufferTest`; `DiagnosticEvidenceNavigatorTest`; `WorkbenchUiControllerTest`; `LiveScenarioPlayerTest`; packaged UI/MCP probes | `docs/pickleball-workbench.md`; `docs/pickleball-workbench-player.md`; `pickleball-workbench/AGENTS.md` |
| Consumer-side Control Bridge and live investigation | worker-side `pickleball-control-api/src/main/java/tools/dscode/control/bridge`; core `ControlRuntime`; protocol `tools.dscode.control.protocol`; controller client `tools.dscode.workbench.bridge.ControlBridgeClient`; endpoints `/v1/status`, `/v1/scenarios`, `/v1/events`, `/v1/pause`, `/v1/resume`, `/v1/steps/execute`, `/v1/mappings/*`, `/v1/browser/*`, `/v1/services/call`, `/v1/breakpoints*`, `/v1/step-overrides*` | controller-only fake-server `ControlBridgeClientTest`; `control-bridge.feature` tagged `@control-bridge`; `ControlBridgeTestSteps.java`; Workbench `worker-check` / `live-check` | `docs/dynamic-control-api.md`; `docs/pickleball-workbench.md`; `pickleball-workbench/AGENTS.md` |
| Consumer Workbench launcher/extractor | `tools.dscode.launcher.PickleballWorkbenchLauncher`; `ControlProtocol.EMBEDDED_WORKBENCH_RESOURCE`; root `shadowJar`; `verifyEmbeddedWorkbench`; content-addressed `.pickleball/workbench/controller//` extraction | `PickleballWorkbenchLauncherTest`; `verifyEmbeddedWorkbench`; `verifyWorkbenchEntrypoint` | `docs/getting-started.md`; `docs/consumer-project.md`; `docs/pickleball-workbench.md` |
| Step Override runtime and Workbench authoring | `src/main/java/tools/dscode/control/override`; `src/main/java/io/cucumber/core/runner/StepOverrideDispatcher.java`; `NPickleStepTestStepFactory`; `src/main/aspectj/tools/dscode/control/override/StepOverrideLifecycleAspect.aj`; bridge `/v1/step-overrides*`; `ControlBridgeClient`; `WorkbenchLiveSession`; MCP `workbench_step_override_*` | `StepOverrideCompilerTest`; `StepOverrideChecks`; `StepOverrideBridgeTestSteps`; `@step-override`; Workbench `live-check`; `WorkbenchMcpServerTest`; `WorkbenchUiControllerTest` | `docs/step-overrides.md`; `docs/pickleball-workbench.md`; `pickleball-workbench/AGENTS.md` |
@@ -47,6 +47,6 @@ The canonical worker bootstrap environment is `PKB_CONTROL_BRIDGE_SESSION_DIR`,
`WorkbenchLiveSession` binds operations to the controller-owned paused scenario and verifies worker PID, bridge runtime id, and scenario id stability. Step Override source is compiled and loaded worker-side. Normal live calls do not invoke Maven/Gradle, resynchronize, or restart the worker.
-The lightweight non-Spring stdio MCP adapter and player-style Swing/WebView UI both delegate through `WorkbenchServices` / `WorkbenchController`. `LiveScenarioPlayer` is a Workbench-side headless presentation model for the editable session buffer, stable line identities, selection, click-to-seek playhead, and player state only; it does not parse or execute Pickleball steps, model ParsingMap/NodeMap semantics, or claim runtime rewind. While `RUNNING`, `WorkbenchController.executeStep` owns playhead follow once (`LivePlaybackCoordinator.followExecutedStep`); the Swing Play loop continues without remaking that mark, and leftover marks of an already-consumed step are no-ops. The controller also owns the watched-agent control lease (`HUMAN` / `AGENT`, banner `currentAction`, gated Save permission). UI mode writes a localhost attach endpoint to `.pickleball/workbench/attach.json` so an agent joins the same JVM/worker instead of starting a second Workbench. The left-rail picker reads project-owned feature files. The live editor is a Gherkin block WebView over that same player model. Global Play starts from the first executable step; Step Editor **Step** is isolated `executeStep`; **From Here** runs from the selected step; wait-at-end stays in play so Enter can append-and-continue. Mapping property types go through `mappingPut` / `mappingRestore`. Terminal tails existing worker log files. Diagnostic explorer reads retained `reports/diagnostic-runs` artifacts in the repository evidence order. The default buffer is a Workbench-owned browser demo against `URL.home`; **Save** is confirmation-gated and copies only a picker-loaded scenario back to its originating `.feature` file. OpenJFX is Workbench-only. MCP mode reserves stdout for newline-delimited MCP JSON-RPC and redirects ordinary output to stderr. The UI remains execution-oriented and does not recreate a project IDE, generic build/process UI, source navigator, or collaboration system.
+The lightweight non-Spring stdio MCP adapter and player-style Swing/WebView UI both delegate through `WorkbenchServices` / `WorkbenchController`. `LiveScenarioPlayer` is a Workbench-side headless presentation model for the editable session buffer, stable line identities, selection, click-to-seek playhead, and player state only; it does not parse or execute Pickleball steps, model ParsingMap/NodeMap semantics, or claim runtime rewind. `LiveEditorView` is the Text vs Blocks presentation choice for that same buffer; toggling it must not change document text, selection, or playhead id. While `RUNNING`, `WorkbenchController.executeStep` owns playhead follow once (`LivePlaybackCoordinator.followExecutedStep`); the Swing Play loop continues without remaking that mark, and leftover marks of an already-consumed step are no-ops. The controller also owns the watched-agent control lease (`HUMAN` / `AGENT`, banner `currentAction`, gated Save permission). UI mode writes a localhost attach endpoint to `.pickleball/workbench/attach.json` so an agent joins the same JVM/worker instead of starting a second Workbench. The left-rail picker filters project-owned scenarios by name (starts with / contains / ends with / full match; default contains; case-insensitive) and Cucumber tags (include AND, exclude NOT, Feature/Rule/outline/Examples inheritance parsed from catalog `.feature` files without calling Cucumber). Feature-file selection is an optional collapsed secondary filter; with none selected, name/tag apply to every catalog scenario. The live editor is Gherkin text or a block WebView over that same player model. Global Play starts from the first executable step; Step Editor **Step** is isolated `executeStep`; **From Here** runs from the selected step; wait-at-end stays in play so Enter can append-and-continue. Mapping property types go through `mappingPut` / `mappingRestore`. Terminal tails existing worker log files. Diagnostic explorer reads retained `reports/diagnostic-runs` artifacts in the repository evidence order. The default buffer is a Workbench-owned browser demo against `URL.home`; **Save** is confirmation-gated and copies only a picker-loaded scenario back to its originating `.feature` file. OpenJFX is Workbench-only. MCP mode reserves stdout for newline-delimited MCP JSON-RPC and redirects ordinary output to stderr. The UI remains execution-oriented and does not recreate a project IDE, generic build/process UI, source navigator, or collaboration system.
For this boundary, scenario validation is limited to the affected `@control-bridge` and/or `@step-override-bridge` tags with `pkb_parallel=80` where practical. Do not run `@all` for Workbench isolation changes.
diff --git a/src/main/resources/META-INF/pickleball/guidance/docs/agent/repository-index.md b/src/main/resources/META-INF/pickleball/guidance/docs/agent/repository-index.md
index 86a1c5f4..6d0e3fea 100644
--- a/src/main/resources/META-INF/pickleball/guidance/docs/agent/repository-index.md
+++ b/src/main/resources/META-INF/pickleball/guidance/docs/agent/repository-index.md
@@ -477,6 +477,7 @@ This inventory helps coding agents discover relevant files. It does not replace
- `pickleball-workbench/build.gradle`
- `pickleball-workbench/src/main/java/tools/dscode/workbench/bridge/ControlBridgeClient.java`
- `pickleball-workbench/src/main/java/tools/dscode/workbench/catalog/ConsumerFeatureCatalog.java`
+- `pickleball-workbench/src/main/java/tools/dscode/workbench/catalog/ScenarioFilter.java`
- `pickleball-workbench/src/main/java/tools/dscode/workbench/diagnostics/DiagnosticEvidenceNavigator.java`
- `pickleball-workbench/src/main/java/tools/dscode/workbench/lease/WorkbenchCallContext.java`
- `pickleball-workbench/src/main/java/tools/dscode/workbench/lease/WorkbenchControlLease.java`
@@ -492,6 +493,7 @@ This inventory helps coding agents discover relevant files. It does not replace
- `pickleball-workbench/src/main/java/tools/dscode/workbench/mcp/WorkbenchMcpServer.java`
- `pickleball-workbench/src/main/java/tools/dscode/workbench/mcp/WorkbenchMcpTools.java`
- `pickleball-workbench/src/main/java/tools/dscode/workbench/player/GherkinBlockDocument.java`
+- `pickleball-workbench/src/main/java/tools/dscode/workbench/player/LiveEditorView.java`
- `pickleball-workbench/src/main/java/tools/dscode/workbench/player/LiveFeatureSave.java`
- `pickleball-workbench/src/main/java/tools/dscode/workbench/player/LivePlaybackCoordinator.java`
- `pickleball-workbench/src/main/java/tools/dscode/workbench/player/LiveScenarioPlayer.java`
@@ -534,12 +536,14 @@ This inventory helps coding agents discover relevant files. It does not replace
- `pickleball-workbench/src/main/resources/tools/dscode/workbench/ui/web/mapping-editor.js`
- `pickleball-workbench/src/test/java/tools/dscode/workbench/bridge/ControlBridgeClientTest.java`
- `pickleball-workbench/src/test/java/tools/dscode/workbench/catalog/ConsumerFeatureCatalogTest.java`
+- `pickleball-workbench/src/test/java/tools/dscode/workbench/catalog/ScenarioFilterTest.java`
- `pickleball-workbench/src/test/java/tools/dscode/workbench/diagnostics/DiagnosticEvidenceNavigatorTest.java`
- `pickleball-workbench/src/test/java/tools/dscode/workbench/lease/WorkbenchControlLeaseTest.java`
- `pickleball-workbench/src/test/java/tools/dscode/workbench/mapping/MappingValueCodecTest.java`
- `pickleball-workbench/src/test/java/tools/dscode/workbench/mcp/WorkbenchAttachServerTest.java`
- `pickleball-workbench/src/test/java/tools/dscode/workbench/mcp/WorkbenchMcpServerTest.java`
- `pickleball-workbench/src/test/java/tools/dscode/workbench/player/GherkinBlockDocumentTest.java`
+- `pickleball-workbench/src/test/java/tools/dscode/workbench/player/LiveEditorViewTest.java`
- `pickleball-workbench/src/test/java/tools/dscode/workbench/player/LiveFeatureSaveTest.java`
- `pickleball-workbench/src/test/java/tools/dscode/workbench/player/LivePlaybackCoordinatorTest.java`
- `pickleball-workbench/src/test/java/tools/dscode/workbench/player/LiveScenarioPlayerTest.java`
diff --git a/src/main/resources/META-INF/pickleball/guidance/docs/pickleball-workbench-player.md b/src/main/resources/META-INF/pickleball/guidance/docs/pickleball-workbench-player.md
index 88afe3d8..96578c8b 100644
--- a/src/main/resources/META-INF/pickleball/guidance/docs/pickleball-workbench-player.md
+++ b/src/main/resources/META-INF/pickleball/guidance/docs/pickleball-workbench-player.md
@@ -25,15 +25,17 @@ The player, picker, Mapping editor, Terminal, and Diagnostic explorer do not cha
## Feature and scenario picker
-The expandable left rail lists `.feature` files from the synchronized consumer project: manifest source roots, conventional `src/test/resources/features`, live merged `features/` resources, and an explicit project-owned `pkb_features` value when present. It does not crawl an unrelated git worktree.
+The left rail lists scenarios from `.feature` files in the synchronized consumer project: manifest source roots, conventional `src/test/resources/features`, live merged `features/` resources, and an explicit project-owned `pkb_features` value when present. It does not crawl an unrelated git worktree.
-Toggle browse mode between Gherkin Feature name and file name + directory path. Click selects or deselects features. The scenario list then shows scenarios under the selection. With no feature selected, the search bar filters every scenario in that project catalog. Clicking a scenario loads it into the live editor. The default demo remains loaded until a scenario is chosen. **Save** is the only write-back path.
+Name and tag filtering is the primary UI. Type a scenario name and choose a match mode: starts with, contains (default), ends with, or full match. All four modes are case-insensitive and match only the Gherkin `Scenario` / `Scenario Outline` title. Include tags must all be present (AND). Exclude tags drop a scenario if it has any of them (NOT). Tag fields accept values with or without a leading `@` and split on commas and/or whitespace. Empty include/exclude means no tag constraint. Feature, Rule, scenario/outline, and Examples tags are inherited as Cucumber does; Workbench parses them from the same catalog files and does not call Cucumber from the controller JVM.
+
+Feature-file selection is secondary and collapsed behind **Filter by feature**. With no feature selected, name/tag filters apply to every catalog scenario. Opening that panel still toggles browse mode between Gherkin Feature name and file name + directory path, and click still selects or deselects features. Clicking a scenario loads it into the live editor. The default demo remains loaded until a scenario is chosen. **Save** is the only write-back path.
## Live Scenario Editor
-The center editor is an embedded HTML/JS block editor in JavaFX `WebView`. Blocks are Gherkin text, including `Given` / `When` / `Then`. Nested steps and `IF` / `ELSE` snap as parent/child using leading colons. Clicking a block instantly seeks the playhead, like clicking a waveform. The execution cursor is internal to an active run.
+The center editor is an embedded HTML/JS block editor in JavaFX `WebView`, or ordinary Gherkin text on the same `LiveScenarioPlayer` buffer. A prominent **Text | Blocks** toggle next to the editor heading switches those views without losing playhead, selection, or document text. Play, Step, and From Here keep using the same `LiveScenarioPlayer`. Blocks are Gherkin text, including `Given` / `When` / `Then`. Nested steps and `IF` / `ELSE` snap as parent/child using leading colons. Clicking a block or line instantly seeks the playhead, like clicking a waveform. The execution cursor is internal to an active run.
-Workbench chose OpenJFX `WebView` + `JFXPanel` over JCEF so the browser panel stays a Workbench-only Maven dependency that shades into the controller JAR. JDK 21 does not ship a modern browser component. If JavaFX cannot start, the same `LiveScenarioPlayer` buffer remains editable as plain Gherkin text.
+Workbench chose OpenJFX `WebView` + `JFXPanel` over JCEF so the browser panel stays a Workbench-only Maven dependency that shades into the controller JAR. JDK 21 does not ship a modern browser component. If JavaFX cannot start, the same `LiveScenarioPlayer` buffer remains editable as plain Gherkin text and Blocks is shown as unavailable.
The initial buffer is Workbench-owned sample content. It is not written back to consumer `.feature` files unless you use **Save** on a picker-loaded scenario and confirm the copy. The default demo is a small browser scenario against the Maven consumer local test site:
@@ -114,7 +116,7 @@ The live player is a collaborative testing space, not a second editor. Swing and
- A human can work alone: edit/play the live buffer, then **Save** asks before copying into the original scenario in the original `.feature` file.
- An agent attaches to the running UI through `.pickleball/workbench/attach.json` (localhost JSON tools over the same `WorkbenchServices`). It must not start a second Workbench JVM or worker.
-- After `workbench_request_control`, Swing play/edit/mapping/save/worker controls lock. A banner shows the agent name and `currentAction`. **Take control** always works and cancels in-flight Save permission waits.
+- After `workbench_request_control`, Swing play/edit/picker/filter/editor-view/mapping/save/worker controls lock. A banner shows the agent name and `currentAction`. **Take control** always works and cancels in-flight Save permission waits.
- `workbench_request_save` is the only original-feature write path for the agent. With the UI attached it blocks on Allow/Deny. Deny writes nothing. Headless stdio MCP may hold the lease without a banner; Save is still an explicit tool.
See [pickleball-workbench.md](pickleball-workbench.md) for attach discovery, tool names, and stdout rules.
@@ -127,7 +129,7 @@ The included consumer `@control-bridge` scenario verifies:
- the current ParsingMap catalog contains at least one NodeMap;
- a catalog reference resolves back to a live NodeMap.
-Workbench player/editor unit tests cover picker selection/search, block buffer ↔ player model, click-to-seek, global Play from start, the two Step Editor play actions, wait-at-end / Enter-to-append-and-run, in-place edit of previously executed text, leftover Play-loop playhead marks after `executeStep`, typed Mapping edits through `WorkbenchServices`, the non-empty browser demo seed, control-lease lock/Take control, permission grant/deny, and Save not writing without approval.
+Workbench player/editor unit tests cover picker name/tag/feature filtering (including Feature-level tag inheritance), block buffer ↔ player model, Text | Blocks view toggling without changing document text or playhead id, click-to-seek, global Play from start, the two Step Editor play actions, wait-at-end / Enter-to-append-and-run, in-place edit of previously executed text, leftover Play-loop playhead marks after `executeStep`, typed Mapping edits through `WorkbenchServices`, the non-empty browser demo seed, control-lease lock/Take control, permission grant/deny, and Save not writing without approval.
Workbench changes should continue to use the repository's focused validation policy:
diff --git a/src/main/resources/META-INF/pickleball/guidance/docs/pickleball-workbench.md b/src/main/resources/META-INF/pickleball/guidance/docs/pickleball-workbench.md
index ae463bec..8df90b18 100644
--- a/src/main/resources/META-INF/pickleball/guidance/docs/pickleball-workbench.md
+++ b/src/main/resources/META-INF/pickleball/guidance/docs/pickleball-workbench.md
@@ -119,23 +119,28 @@ The primary workspace is an interactive Gherkin player with a project feature pi
```text
+-------------------------------------------------------------------------+
-| Features | Project / readiness Play Pause Stop Player status |
-+----------+---------------------------+----------------------------------+
-| Feature /| LIVE GHERKIN EDITOR | Mapping | Terminal | Diagnostic |
-| file | (WebView block editor) | WebView tree / typed values |
-| picker | highlighted playhead | worker log / retained-run frames |
-| + search | | |
-+----------+---------------------------+ |
-| | Step Editor [Step] [From | |
-| | Here] [ live command ] | |
-+----------+---------------------------+----------------------------------+
+| Scenarios | Project / readiness Play Pause Stop Player status |
++-----------+--------------------------+----------------------------------+
+| Name + | LIVE GHERKIN EDITOR | Mapping | Terminal | Diagnostic |
+| match mode| [Text | Blocks] | WebView tree / typed values |
+| tags AND | playhead on same buffer | worker log / retained-run frames |
+| tags NOT | | |
+| scenario | | |
+| list | | |
+| (feature | | |
+| filter | | |
+| hidden) | | |
++-----------+--------------------------+ |
+| | Step Editor [Step] [From | |
+| | Here] [ live command ] | |
++-----------+--------------------------+----------------------------------+
| Footer activity |
+-------------------------------------------------------------------------+
```
-The expandable left rail lists feature files from the synchronized consumer project. Browse by Gherkin Feature name or by file name + directory path. Click toggles feature selection. The scenario list is limited to selected features, or to every project scenario when nothing is selected; the search bar filters that list. Clicking a scenario loads it into the live session buffer. Workbench does not write `.feature` files unless you use the explicit **Save** control.
+The left rail is a scenario filter, not a feature-file browser. Primary controls are scenario name (starts with / contains / ends with / full match; default contains; all four are case-insensitive against the Gherkin Scenario / Scenario Outline title), tags the scenario must have (AND), and tags it must not have (NOT). Include/exclude fields accept any number of tags, with or without a leading `@`, split on commas and/or whitespace. Empty include/exclude means no tag constraint. Feature-level tags, optional Rule tags, the scenario/outline's own tags, and Examples tags on an outline are inherited the same way Cucumber does; Workbench parses those tags from the catalog `.feature` files and does not call Cucumber. Feature-file selection is collapsed behind **Filter by feature** (Gherkin Feature name vs file path lives in that panel). Default: no feature filter, so name/tag apply to every catalog scenario in the synchronized project. Clicking a result still loads that scenario into the live session buffer. Workbench does not write `.feature` files unless you use the explicit **Save** control.
-The center editor is an embedded HTML/JS block editor hosted in JavaFX `WebView` (`JFXPanel`). Blocks are Gherkin text — including `Given` / `When` / `Then` — not a second language compiled to Gherkin. Nested steps and `IF` / `ELSE` blocks snap as parent/child using Pickleball's leading-colon grammar. The play header is unchanged: click-to-seek, global **Play** from the first step in a fresh worker context, **Step** = isolated `executeStep`, **From Here** = selected/playhead through the rest, stay in play at end, Enter append-and-run. JavaScript never executes Gherkin.
+The center editor shows the same `LiveScenarioPlayer` buffer as ordinary Gherkin text or as the embedded HTML/JS block editor hosted in JavaFX `WebView` (`JFXPanel`). A prominent **Text | Blocks** toggle next to the editor heading switches views without losing playhead, selection, or document text. Blocks are Gherkin text — including `Given` / `When` / `Then` — not a second language compiled to Gherkin. Nested steps and `IF` / `ELSE` blocks snap as parent/child using Pickleball's leading-colon grammar. The play header is unchanged: click-to-seek, global **Play** from the first step in a fresh worker context, **Step** = isolated `executeStep`, **From Here** = selected/playhead through the rest, stay in play at end, Enter append-and-run. JavaScript never executes Gherkin. If JavaFX cannot start, Text is already the fallback and Blocks stays disabled/unavailable.
The right side remains Mapping, Terminal, and Diagnostic Log Explorer. Low-level lifecycle controls stay under **Session**. Existing investigation tools stay under **Tools > Advanced Controls**.
@@ -202,7 +207,7 @@ NodeMap implementations that are not exact ordinary `NodeMap` instances remain i
### WebView packaging
-JDK 21 does not ship a modern browser panel. Workbench embeds OpenJFX `WebView` through `JFXPanel` for the Gherkin editor, Mapping tree, and Diagnostic explorer. That choice stays Workbench-only: Maven-central JavaFX modules are shaded into the controller executable, including platform natives under `javafx-natives//`. JCEF was not used because Chromium natives are harder to keep isolation-clean and do not package as ordinary Workbench dependencies. If JavaFX cannot start, the live editor falls back to the existing in-place text buffer on the same `LiveScenarioPlayer` model.
+JDK 21 does not ship a modern browser panel. Workbench embeds OpenJFX `WebView` through `JFXPanel` for the Gherkin editor, Mapping tree, and Diagnostic explorer. That choice stays Workbench-only: Maven-central JavaFX modules are shaded into the controller executable, including platform natives under `javafx-natives//`. JCEF was not used because Chromium natives are harder to keep isolation-clean and do not package as ordinary Workbench dependencies. If JavaFX cannot start, the live editor stays on the existing in-place text buffer on the same `LiveScenarioPlayer` model; the **Text | Blocks** toggle remains visible and Blocks is disabled so the fallback is honest.
### Terminal and Diagnostic Log Explorer
@@ -251,7 +256,7 @@ Workbench owns one control lease for the live session. Swing and MCP/HTTP adapte
While the human holds the lease, Swing play/edit/mapping/save/worker controls stay enabled. Agent mutating calls fail clearly until `workbench_request_control`.
-While an agent holds the lease, the human can watch the same window. Play, edit, Mapping writes, Save, and worker lifecycle controls lock. The WebView editors stay mounted and become read-only; they are not torn down. A banner names the agent and shows `currentAction`. **Take control** stays enabled. Take control returns the lease to `HUMAN`, unlocks Swing, and fails any in-flight agent permission wait so a blocked Save does not write.
+While an agent holds the lease, the human can watch the same window. Play, edit, picker/filter, editor view toggle, Mapping writes, Save, and worker lifecycle controls lock. The WebView editors stay mounted and become read-only; they are not torn down. A banner names the agent and shows `currentAction`. **Take control** stays enabled. Take control returns the lease to `HUMAN`, unlocks Swing, and fails any in-flight agent permission wait so a blocked Save does not write.
The agent should update `currentAction` as it works. Playhead, Mapping, Terminal, and screenshots follow because the agent uses the same `LiveScenarioPlayer` / worker as the UI. Testing the live scenario (`executeStep`, play, Mapping reads, evidence) is allowed on the agent lease. Copying the live scenario into the original `.feature` is not; that goes through `workbench_request_save` and waits for Allow/Deny in the Swing banner.
@@ -433,24 +438,28 @@ java -jar $workbenchJar ui ".\maven-consumer-project"
Use the UI-owned worker for runtime checks; do not run `worker-check` or `live-check` concurrently with the UI.
-1. Verify the top-level layout has an expandable Features picker, the Live Gherkin Editor and compact Step Editor in the center, and exactly Mapping / Terminal / Diagnostic Log Explorer on the right.
+1. Verify the top-level layout has a scenario name/tag filter rail (feature-file filter collapsed), the Live Gherkin Editor with a **Text | Blocks** toggle and compact Step Editor in the center, and exactly Mapping / Terminal / Diagnostic Log Explorer on the right.
2. Confirm the default buffer is the Workbench demo scenario and includes `navigate to: URL.home` plus a click on the local test site when no picker scenario is selected.
-3. Click different scenario blocks and verify the playhead highlight moves immediately to the clicked step.
-4. Edit previously typed or previously executed Gherkin directly in the Live Scenario Editor and verify the line text updates in place.
-5. Press global **Play** after seeking the playhead to a later step and verify execution still starts from the first executable step in a fresh worker context.
-6. Use **From Here** on a later executable step and verify playback starts there and continues through the rest of the buffer.
-7. Use **Step** in the Step Editor and verify isolated `executeStep` execution that leaves automatic playback paused.
-8. Let a run reach the end and verify the player stays in **Waiting for step**. Type a new step and press Enter; the step is appended and executed without dropping out of play.
-9. Treat **Pause** / **Stop** as presentation/control of automatic advancement only; they do not rewind browser or service side effects.
-10. Verify Mapping has no `Current Scope` control and no hard-coded NodeMap choices. Top-level properties come from the worker ParsingMap and accept typed in-place edits.
-11. Verify Terminal filters worker log files by level and continues as steps run, without writing to MCP stdout.
-12. Verify Diagnostic Log Explorer lists retained runs from `reports/diagnostic-runs` only, or shows an honest empty state.
-13. Verify **Tools > Advanced Controls** still exposes Status, Recent Events, Step Overrides, Evidence, and Breakpoints.
-14. Verify blocking runtime actions leave the Swing UI responsive.
-15. Load a picker scenario, click **Save**, and cancel the confirmation; the original `.feature` file must be unchanged. Confirming copies only that scenario back into the originating file.
-16. Attach an agent to `.pickleball/workbench/attach.json`, call `workbench_request_control`, and verify the banner plus locked play/edit/mapping/save/worker controls. **Take control** remains enabled.
-17. While the agent holds the lease, `workbench_request_save` shows Allow/Deny. Deny writes nothing. Take control cancels the wait without writing.
-18. The default demo remains unsavable for both human Save and agent `workbench_request_save`.
+3. Filter scenarios by name using contains (default) and the other match modes; confirm matching is case-insensitive and applies to the Scenario / Scenario Outline title.
+4. Filter with include tags (AND) and exclude tags (NOT), with and without `@`, and confirm Feature-level tags apply to scenarios in that feature.
+5. Confirm **Filter by feature** is collapsed by default and that name/tag filters then apply to every catalog scenario. Opening it still supports multi-select and Feature name vs file path.
+6. Click a filtered scenario and verify it loads into the live buffer. Switch **Text | Blocks** and verify playhead, selection, and document text are unchanged. If WebView is unavailable, Blocks is disabled and Text remains the editor.
+7. Click different scenario blocks/lines and verify the playhead highlight moves immediately to the clicked step.
+8. Edit previously typed or previously executed Gherkin directly in the Live Scenario Editor and verify the line text updates in place.
+9. Press global **Play** after seeking the playhead to a later step and verify execution still starts from the first executable step in a fresh worker context.
+10. Use **From Here** on a later executable step and verify playback starts there and continues through the rest of the buffer.
+11. Use **Step** in the Step Editor and verify isolated `executeStep` execution that leaves automatic playback paused.
+12. Let a run reach the end and verify the player stays in **Waiting for step**. Type a new step and press Enter; the step is appended and executed without dropping out of play.
+13. Treat **Pause** / **Stop** as presentation/control of automatic advancement only; they do not rewind browser or service side effects.
+14. Verify Mapping has no `Current Scope` control and no hard-coded NodeMap choices. Top-level properties come from the worker ParsingMap and accept typed in-place edits.
+15. Verify Terminal filters worker log files by level and continues as steps run, without writing to MCP stdout.
+16. Verify Diagnostic Log Explorer lists retained runs from `reports/diagnostic-runs` only, or shows an honest empty state.
+17. Verify **Tools > Advanced Controls** still exposes Status, Recent Events, Step Overrides, Evidence, and Breakpoints.
+18. Verify blocking runtime actions leave the Swing UI responsive.
+19. Load a picker scenario, click **Save**, and cancel the confirmation; the original `.feature` file must be unchanged. Confirming copies only that scenario back into the originating file.
+20. Attach an agent to `.pickleball/workbench/attach.json`, call `workbench_request_control`, and verify the banner plus locked play/edit/picker/filter/editor-view/mapping/save/worker controls. **Take control** remains enabled.
+21. While the agent holds the lease, `workbench_request_save` shows Allow/Deny. Deny writes nothing. Take control cancels the wait without writing.
+22. The default demo remains unsavable for both human Save and agent `workbench_request_save`.
## Regression