event bus start: code, PRs e specs

This commit is contained in:
bQUARKz 2026-03-12 09:20:17 +00:00
parent a16d63cfb5
commit e7670b5474
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
16 changed files with 731 additions and 42 deletions

View File

@ -0,0 +1,124 @@
# PR-07a Assets Event Topology and Lifecycle Foundation
Domain owner: `docs/studio`
## Briefing
Refactor the `Assets` workspace foundation away from a monolithic `AssetWorkspace` that redraws major regions after local changes.
This slice establishes the corrective architectural direction for the Studio as a whole.
`Assets` is the first consumer and proving ground, but the target is not an `Assets`-only pattern.
The Studio-standard direction is:
- lifecycle-aware UI components;
- typed workspace events as the update mechanism;
- component-scoped subscriptions;
- explicit separation between structural workspace sync and local UI projection updates;
- reusable workspace framework pieces that other Studio workspaces must consume instead of inventing their own refresh-heavy flow.
## Objective
Create the event-driven workspace foundation required for all later `Assets` refactor slices and establish it as the correct Studio-wide pattern for workspace implementation.
After this PR:
- the workspace root coordinates composition instead of owning every region render path;
- navigator, row, details, and details-internal controls can subscribe independently;
- the workspace event bus becomes the primary update path for UI change propagation;
- `refresh()` is no longer the default answer for local state changes.
- the extracted lifecycle/event-driven primitives are designed for reuse by non-`Assets` workspaces.
## Dependencies
- [`../specs/4. Assets Workspace Specification.md`](../specs/4.%20Assets%20Workspace%20Specification.md)
- [`./PR-05a-assets-workspace-foundation-and-service-state.md`](./PR-05a-assets-workspace-foundation-and-service-state.md)
- existing `StudioWorkspaceEventBus`
- existing `StudioControlLifecycle` support
## Scope
- define the target event topology for the `Assets` workspace
- define the target event topology as the canonical Studio workspace model
- split workspace responsibilities between composition, state coordination, and component rendering
- extract reusable workspace framework primitives where the abstraction is already justified by the `Assets` refactor
- introduce lifecycle-aware component boundaries for:
- navigator host
- asset-list host
- asset-row item
- details host
- details-local sections/forms
- introduce reusable patterns or base components for:
- workspace composition roots
- lifecycle-managed event subscribers
- projection host controls
- structural-sync versus local-patch orchestration
- define typed events for:
- structural snapshot changes
- projection changes
- selection changes
- selected-asset details lifecycle
- local patch propagation
- demote global redraw events to transitional or removable status
## Non-Goals
- no final navigator visuals redesign in this slice
- no final details-panel redesign in this slice
- no mutation-semantics redesign beyond event routing needs
- no broad shell-level event-system rewrite outside `Assets`
- no fake generalization disconnected from concrete `Assets` usage
## Execution Method
1. Define the component tree and ownership model.
The workspace root should compose controls and services, not render every region inline.
2. Introduce event contracts for the asset workspace projection model.
The baseline contract should distinguish:
- structural workspace sync events
- navigator projection events
- selected-asset details events
- per-asset patch events
3. Convert `AssetWorkspace` into a composition root plus orchestration layer.
It may still coordinate service calls, but rendering responsibilities should move into dedicated controls.
4. Extract reusable workspace-framework pieces while doing the refactor.
`Assets` should consume the same primitives that future Studio workspaces are expected to consume.
5. Require lifecycle installation for event-consuming controls.
Every component that subscribes to workspace events must implement `StudioControlLifecycle` and subscribe only while attached to the scene.
6. Mark the existing redraw-request pattern as transitional.
`StudioAssetsNavigatorRedrawRequestedEvent` and `StudioAssetsDetailsRedrawRequestedEvent` should no longer be the target architecture.
7. Propagate the rule to Studio documentation.
The resulting plans/spec updates should make clear that future workspaces are expected to build on this framework instead of introducing workspace-local refresh architecture.
## Acceptance Criteria
- the refactor has a clear component topology instead of one render-heavy workspace class
- lifecycle-aware controls own their own subscriptions
- the workspace event bus carries typed update events that components can consume independently
- local UI changes can be expressed without a full workspace refresh path
- the old redraw-request events are either removed or isolated behind temporary compatibility adapters
- the extracted primitives are reusable by other Studio workspaces
- the plan explicitly establishes this direction as the Studio-standard workspace architecture
## Validation
- unit tests for event routing between composition root and child controls
- unit tests for lifecycle subscribe/unsubscribe behavior on workspace controls
- smoke validation that mounting and unmounting the workspace does not leak subscriptions
## Affected Artifacts
- `prometeu-studio/src/main/java/p/studio/workspaces/assets/AssetWorkspace.java`
- new reusable workspace-framework classes under `prometeu-studio/src/main/java/p/studio/...`
- new `Assets` workspace controls under `prometeu-studio/src/main/java/p/studio/workspaces/assets/...`
- `prometeu-studio/src/main/java/p/studio/events/...`
- `docs/studio/specs/1. Studio Shell and Workspace Layout Specification.md`
- `docs/studio/specs/3. Studio Components Module Specification.md`
- tests for workspace event topology and lifecycle behavior

View File

@ -0,0 +1,75 @@
# PR-07b Asset Navigator and Row Subscriptions
Domain owner: `docs/studio`
## Briefing
Refactor the navigator side of the `Assets` workspace so the list and each asset row update through event subscriptions instead of workspace-wide rerendering.
## Objective
Make navigator behavior event-directed and component-local.
After this PR:
- the navigator list subscribes to projection changes it actually needs;
- each asset row subscribes to row-scoped summary/selection updates;
- search and filters change the navigator projection without rebuilding the whole workspace;
- selection styling and row patching happen without forcing a full list refresh from the workspace root.
- the navigator and row patterns are implemented in a way that other Studio workspaces can reuse for list/detail navigation surfaces.
## Dependencies
- [`./PR-07a-assets-event-topology-and-lifecycle-foundation.md`](./PR-07a-assets-event-topology-and-lifecycle-foundation.md)
- [`../specs/4. Assets Workspace Specification.md`](../specs/4.%20Assets%20Workspace%20Specification.md)
## Scope
- extract the navigator into dedicated controls
- separate navigator projection calculation from visual control ownership
- introduce row-scoped subscriptions and row identity handling
- keep reusable list/projection primitives outside `Assets`-only naming where they are genuinely cross-workspace
- publish projection updates for:
- search changes
- filter changes
- structural asset collection changes
- per-asset summary patches
- selection changes
- remove direct row bookkeeping from the workspace root where possible
## Non-Goals
- no details-panel refactor in this slice
- no final mutation confirmation flow refactor in this slice
- no broad service-layer redesign beyond what navigator subscriptions require
## Execution Method
1. Introduce an `AssetNavigatorControl` or equivalent host with lifecycle-managed subscriptions.
2. Extract row rendering into an `AssetRowControl` or equivalent lifecycle-managed component.
3. Move search/filter handling to event publication plus projection recalculation.
4. Publish selection updates as typed events that row controls can consume directly.
5. Replace root-owned row maps and manual selection restyling with row-scoped update flow.
## Acceptance Criteria
- asset rows are no longer rebuilt by default on every local navigator change
- search and filter changes update the navigator projection only
- selecting an asset updates only the controls that depend on selection
- patching one asset summary updates the affected row without requiring a full workspace reload
- navigator controls subscribe and unsubscribe through the lifecycle support
- reusable navigator/list subscription patterns are left available for future Studio workspaces
## Validation
- unit tests for navigator projection event flow
- unit tests for row identity stability across patches
- unit tests for selection update behavior without full projection rebuild
- UI smoke validation for search, filters, and selection transitions
## Affected Artifacts
- `prometeu-studio/src/main/java/p/studio/workspaces/assets/AssetWorkspace.java`
- new navigator and row controls under `prometeu-studio/src/main/java/p/studio/workspaces/assets/...`
- `prometeu-studio/src/main/java/p/studio/events/...`
- tests for navigator projection and row update behavior

View File

@ -0,0 +1,82 @@
# PR-07c Asset Details and Form Lifecycle
Domain owner: `docs/studio`
## Briefing
Refactor the selected-asset details side so the details host and its internal sections/forms subscribe only to the state they need.
This slice explicitly covers the problem called out in the current direction:
- the selected-asset details host must not own all redraws;
- each internal section/form must update from events instead of full details rebuilds.
## Objective
Make the details area componentized, lifecycle-aware, and event-driven.
After this PR:
- summary/actions, runtime contract, inputs/preview, diagnostics, and mutation-preview sections can update independently;
- details load state, ready state, and error state are event-driven;
- local form interactions such as preload toggles or preview selection do not rebuild unrelated details content;
- internal controls subscribe only while mounted.
- the details-section and form-subscription patterns become reusable Studio workspace primitives where appropriate.
## Dependencies
- [`./PR-07a-assets-event-topology-and-lifecycle-foundation.md`](./PR-07a-assets-event-topology-and-lifecycle-foundation.md)
- [`./PR-05c-selected-asset-details-contract-and-preview.md`](./PR-05c-selected-asset-details-contract-and-preview.md)
- [`./PR-05e-assets-staged-mutations-preview-and-apply.md`](./PR-05e-assets-staged-mutations-preview-and-apply.md)
## Scope
- extract the details host into dedicated controls
- split details content into lifecycle-aware sections
- extract reusable section-host and form-event patterns when they are not asset-specific
- define details events for:
- details loading started
- details ready
- details failed
- local summary patch applied
- preview input changed
- preview zoom changed
- mutation preview state changed
- route details-local form actions through the workspace bus instead of root-owned imperative redraw
## Non-Goals
- no navigator refactor in this slice
- no shell activity redesign in this slice
- no cross-workspace form framework extraction unless needed by the `Assets` details controls
## Execution Method
1. Introduce a dedicated details host control with lifecycle-managed subscriptions.
2. Extract stable details sections into separate controls.
3. Move details load and error transitions to typed events.
4. Route details-local interactions through narrow events and local state holders.
5. Remove root-level details reconstruction for interactions that affect only one section.
## Acceptance Criteria
- the selected-asset details view is no longer rebuilt as one large region for section-local changes
- forms and preview controls update independently
- details-ready and details-error transitions are observable through typed events
- details-local subscriptions are owned by the mounted controls, not the workspace root
- changing one local control does not force unrelated details sections to rerender
- reusable details/form lifecycle patterns are available for future Studio workspaces
## Validation
- unit tests for details lifecycle event flow
- unit tests for section-local updates
- unit tests for preload-toggle or equivalent form patch behavior
- UI smoke validation for selection, details loading, preview interaction, and diagnostics visibility
## Affected Artifacts
- `prometeu-studio/src/main/java/p/studio/workspaces/assets/AssetWorkspace.java`
- new details controls under `prometeu-studio/src/main/java/p/studio/workspaces/assets/...`
- `prometeu-studio/src/main/java/p/studio/events/...`
- tests for details load state and section-local updates

View File

@ -0,0 +1,83 @@
# PR-07d Asset Mutation and Structural Sync Orchestration
Domain owner: `docs/studio`
## Briefing
Replace the current mutation flow that falls back to `refresh()` after many operations with an event-directed orchestration model.
The key rule is:
- local patches stay local;
- only structural workspace changes trigger structural sync;
- structural sync is explicit and typed, not an incidental rerender path.
## Objective
Make asset operations compatible with the event-driven component model.
After this PR:
- direct mutations publish patch or structural-sync events according to their actual impact;
- row and details controls react to targeted operation results when possible;
- create/remove/relocate or other structural operations request a workspace sync explicitly;
- the workspace no longer treats every successful operation as a reason to reload everything.
- the local-patch versus structural-sync rule is available as a reusable Studio workspace orchestration rule.
## Dependencies
- [`./PR-07a-assets-event-topology-and-lifecycle-foundation.md`](./PR-07a-assets-event-topology-and-lifecycle-foundation.md)
- [`./PR-07b-asset-navigator-and-row-subscriptions.md`](./PR-07b-asset-navigator-and-row-subscriptions.md)
- [`./PR-07c-asset-details-and-form-lifecycle.md`](./PR-07c-asset-details-and-form-lifecycle.md)
- [`./PR-05d-assets-activity-progress-and-logs-integration.md`](./PR-05d-assets-activity-progress-and-logs-integration.md)
- [`./PR-05e-assets-staged-mutations-preview-and-apply.md`](./PR-05e-assets-staged-mutations-preview-and-apply.md)
## Scope
- classify operations by update impact:
- local summary patch
- details patch
- structural workspace sync
- failure retention/reporting
- extract reusable orchestration helpers or contracts where the distinction is cross-workspace rather than asset-specific
- route mutation preview/apply results through typed events
- replace generic `refresh()` fallbacks for non-structural success paths
- keep progress/log/activity integration aligned with the new orchestration path
- make structural sync requests explicit and testable
## Non-Goals
- no redesign of packer service semantics
- no generic Studio-wide command bus abstraction in this slice
- no persistence work unrelated to asset-workspace orchestration
## Execution Method
1. Define operation-result events and structural-sync request events.
2. Map each mutation action to its update strategy.
3. Update mutation flows to publish events instead of directly forcing global workspace refresh.
4. Keep structural reload only for actions that truly change collection shape or asset identity.
5. Wire progress, logs, and activity to the new operation flow without reintroducing redraw coupling.
## Acceptance Criteria
- include/exclude, preload, and similar local operations can update via targeted events
- create/register/remove/relocate use explicit structural-sync flow only when required
- mutation preview and apply flows do not directly rebuild navigator and details by default
- activity/log/progress signals remain correct under the new orchestration
- the remaining structural sync path is narrow, explicit, and justified by actual data-shape changes
- the orchestration rule is documented as reusable Studio behavior, not an `Assets` exception
## Validation
- unit tests for operation-to-update-strategy mapping
- unit tests for structural-sync event publication
- unit tests for targeted patch propagation after successful operations
- UI smoke validation for register/include/exclude/relocate/remove flows
## Affected Artifacts
- `prometeu-studio/src/main/java/p/studio/workspaces/assets/AssetWorkspace.java`
- mutation and orchestration classes under `prometeu-studio/src/main/java/p/studio/workspaces/assets/...`
- `prometeu-studio/src/main/java/p/studio/events/...`
- tests for mutation orchestration and structural sync behavior

View File

@ -0,0 +1,74 @@
# PR-07e Assets Refactor Cleanup and Regression Coverage
Domain owner: `docs/studio`
## Briefing
Consolidate the refactor by removing obsolete redraw-heavy code paths, tightening naming, and locking the event-driven behavior with tests.
## Objective
Finish the corrective refactor so the codebase does not drift back toward monolithic render ownership.
After this PR:
- obsolete redraw-request and monolithic render helpers are removed;
- remaining component boundaries are clearer and easier to maintain;
- tests guard lifecycle, event routing, and selective-update behavior;
- the workspace code is organized around durable responsibilities rather than incremental leftovers.
- the resulting framework is left in a shape that other Studio workspaces can adopt directly.
## Dependencies
- [`./PR-07a-assets-event-topology-and-lifecycle-foundation.md`](./PR-07a-assets-event-topology-and-lifecycle-foundation.md)
- [`./PR-07b-asset-navigator-and-row-subscriptions.md`](./PR-07b-asset-navigator-and-row-subscriptions.md)
- [`./PR-07c-asset-details-and-form-lifecycle.md`](./PR-07c-asset-details-and-form-lifecycle.md)
- [`./PR-07d-asset-mutation-and-structural-sync-orchestration.md`](./PR-07d-asset-mutation-and-structural-sync-orchestration.md)
## Scope
- remove dead or transitional redraw-oriented code
- rename classes/events where the old naming reflects the wrong direction
- tighten package structure for navigator, details, and orchestration code
- make reusable framework pieces visible and discoverable to future workspace implementations
- add regression coverage for:
- lifecycle subscription hygiene
- selection propagation
- row patch propagation
- details-local updates
- structural sync boundaries
- update Studio learn/spec material if the refactor exposes terminology drift
## Non-Goals
- no new user-facing asset features in this slice
- no speculative refactor outside the `Assets` workspace and its direct event contracts
## Execution Method
1. Remove compatibility layers that existed only to bridge from the old refresh-heavy implementation.
2. Normalize class and event naming around lifecycle and event-driven ownership.
3. Reorganize tests to reflect component boundaries instead of one giant workspace class.
4. Update documentation to reflect the stabilized architecture.
## Acceptance Criteria
- the refactored `Assets` workspace no longer depends on redraw-request events as a primary mechanism
- code ownership is split along navigator, details, and orchestration boundaries
- lifecycle and event-driven behavior are covered by focused tests
- the remaining `AssetWorkspace` root is materially smaller and primarily compositional
- the reusable framework surface is clear enough for other workspaces to consume instead of cloning `Assets` internals
## Validation
- full unit-test pass for asset-workspace packages
- targeted regression tests for mount/unmount lifecycle safety
- targeted regression tests proving that local updates stay local unless structural sync is requested
## Affected Artifacts
- `prometeu-studio/src/main/java/p/studio/workspaces/assets/...`
- `prometeu-studio/src/main/java/p/studio/events/...`
- `prometeu-studio/src/test/java/p/studio/workspaces/assets/...`
- `docs/studio/specs/4. Assets Workspace Specification.md`
- `docs/studio/learn/mental-model-assets-workspace.md`

View File

@ -43,8 +43,17 @@ The current Studio execution queue is:
3. [`PR-05c-selected-asset-details-contract-and-preview.md`](./PR-05c-selected-asset-details-contract-and-preview.md) 3. [`PR-05c-selected-asset-details-contract-and-preview.md`](./PR-05c-selected-asset-details-contract-and-preview.md)
4. [`PR-05d-assets-activity-progress-and-logs-integration.md`](./PR-05d-assets-activity-progress-and-logs-integration.md) 4. [`PR-05d-assets-activity-progress-and-logs-integration.md`](./PR-05d-assets-activity-progress-and-logs-integration.md)
5. [`PR-05e-assets-staged-mutations-preview-and-apply.md`](./PR-05e-assets-staged-mutations-preview-and-apply.md) 5. [`PR-05e-assets-staged-mutations-preview-and-apply.md`](./PR-05e-assets-staged-mutations-preview-and-apply.md)
6. [`PR-06-project-scoped-studio-state-and-activity-persistence.md`](./PR-06-project-scoped-studio-state-and-activity-persistence.md) 6. [`PR-07a-assets-event-topology-and-lifecycle-foundation.md`](./PR-07a-assets-event-topology-and-lifecycle-foundation.md)
7. [`PR-07b-asset-navigator-and-row-subscriptions.md`](./PR-07b-asset-navigator-and-row-subscriptions.md)
8. [`PR-07c-asset-details-and-form-lifecycle.md`](./PR-07c-asset-details-and-form-lifecycle.md)
9. [`PR-07d-asset-mutation-and-structural-sync-orchestration.md`](./PR-07d-asset-mutation-and-structural-sync-orchestration.md)
10. [`PR-07e-assets-refactor-cleanup-and-regression-coverage.md`](./PR-07e-assets-refactor-cleanup-and-regression-coverage.md)
11. [`PR-06-project-scoped-studio-state-and-activity-persistence.md`](./PR-06-project-scoped-studio-state-and-activity-persistence.md)
The `PR-07` family is a corrective refactor pass for the current `Assets` implementation.
It exists to replace the refresh-heavy direction with lifecycle-managed, event-driven ownership.
It also establishes the intended Studio-wide workspace framework, with `Assets` as the first consumer and proof point.
Recommended execution order: Recommended execution order:
`PR-05a -> PR-05b -> PR-05c -> PR-05d -> PR-05e -> PR-06` `PR-05a -> PR-05b -> PR-05c -> PR-05d -> PR-05e -> PR-07a -> PR-07b -> PR-07c -> PR-07d -> PR-07e -> PR-06`

View File

@ -23,6 +23,8 @@ This specification consolidates the accepted Studio shell decision into normativ
4. The main shell must expose a center workspace host. 4. The main shell must expose a center workspace host.
5. The main shell must expose a right global utility panel. 5. The main shell must expose a right global utility panel.
6. The main shell must expose an always-visible run surface in the top-right area. 6. The main shell must expose an always-visible run surface in the top-right area.
7. Workspaces mounted in the shell must follow the Studio event-driven workspace framework.
8. The shell must preserve a clear path for reusable workspace framework primitives shared across workspaces.
## Project Entry ## Project Entry
@ -48,6 +50,20 @@ The baseline workspace set includes:
`BuilderWorkspace` is transitional and must not be treated as a long-term architectural reference for the final shipping surface. `BuilderWorkspace` is transitional and must not be treated as a long-term architectural reference for the final shipping surface.
## Workspace Architecture Model
The shell-level workspace host exists to mount independently authored workspaces under one Studio contract.
Baseline workspace architecture rules are:
- each workspace should have a composition root that mounts the workspace into the shell;
- a workspace composition root should coordinate workspace services, state orchestration, and structural sync, but it should not own every local render path directly;
- event-observing workspace controls must be lifecycle-managed;
- workspace-local updates should flow through the Studio event system rather than ad hoc imperative redraw chains;
- future workspaces should consume shared Studio workspace framework primitives where those primitives already cover the needed behavior.
The shell must not encourage a workspace model where every interaction falls back to whole-workspace refresh.
## Right Utility Panel ## Right Utility Panel
The right-side global utility surface is tab-based. The right-side global utility surface is tab-based.
@ -95,6 +111,7 @@ Rules:
- defining the full internal layout of every workspace - defining the full internal layout of every workspace
- defining the final future tab set of the right utility panel - defining the final future tab set of the right utility panel
- defining the detailed activity rendering model - defining the detailed activity rendering model
- defining every reusable workspace primitive in this document
## Exit Criteria ## Exit Criteria
@ -102,4 +119,5 @@ This specification is complete enough when:
- project entry and shell regions are unambiguous; - project entry and shell regions are unambiguous;
- workspace and shell responsibilities are clearly separated; - workspace and shell responsibilities are clearly separated;
- the shell-level expectations for workspace architecture are explicit;
- the baseline Studio frame is stable enough for implementation planning. - the baseline Studio frame is stable enough for implementation planning.

View File

@ -23,6 +23,9 @@ This specification consolidates the accepted Studio UI foundations decision into
3. The Studio event system must include one global bus and one bus per workspace. 3. The Studio event system must include one global bus and one bus per workspace.
4. Every event published on a workspace bus must also be published on the global bus automatically. 4. Every event published on a workspace bus must also be published on the global bus automatically.
5. Studio UI must preserve theme and i18n compatibility by construction. 5. Studio UI must preserve theme and i18n compatibility by construction.
6. Lifecycle-managed event subscription is the canonical way Studio controls observe external state.
7. The Studio must provide a reusable workspace framework for event-driven workspace composition.
8. Studio workspaces must consume that framework rather than invent refresh-heavy workspace-local patterns.
## Naming by Role ## Naming by Role
@ -118,6 +121,7 @@ Shared Studio foundations include:
- shell-level UI conventions; - shell-level UI conventions;
- event publication and observation infrastructure; - event publication and observation infrastructure;
- reusable workspace framework primitives;
- reusable Studio control conventions. - reusable Studio control conventions.
## Subscription Lifecycle ## Subscription Lifecycle
@ -135,6 +139,27 @@ This exists to make event wiring visible, testable, and safe under UI lifecycle
The exact base class or interface shape is an implementation concern, but the lifecycle itself is normative. The exact base class or interface shape is an implementation concern, but the lifecycle itself is normative.
## Canonical Workspace Framework
The Studio workspace model is not just a collection of unrelated screens.
Baseline workspace-framework rules are:
- a workspace should be built as a composition root plus lifecycle-managed child controls;
- child controls should subscribe only to the workspace events or external state they actually consume;
- local UI changes should be propagated through typed events and targeted patches where identity is preserved;
- structural synchronization should be explicit and separate from local patch propagation;
- whole-workspace refresh is a structural recovery path, not the default routine interaction path.
The baseline reusable workspace framework should cover at least:
- composition-root conventions;
- lifecycle-managed event-subscriber controls;
- projection-host patterns for list/tree/detail surfaces;
- local-patch versus structural-sync orchestration rules.
The first concrete proving ground for this framework may be a single workspace, but the resulting framework is normative for future Studio workspace implementation.
Shared Studio foundations do not include: Shared Studio foundations do not include:
- domain-specific workspace business logic; - domain-specific workspace business logic;
@ -146,6 +171,7 @@ Shared Studio foundations do not include:
- defining the full event catalog in advance - defining the full event catalog in advance
- defining every future workspace interaction - defining every future workspace interaction
- defining the first concrete control implementation wave in detail - defining the first concrete control implementation wave in detail
- allowing each workspace to define its own incompatible event/lifecycle model
## Exit Criteria ## Exit Criteria
@ -153,4 +179,5 @@ This specification is complete enough when:
- naming rules are stable enough to guide new Studio code; - naming rules are stable enough to guide new Studio code;
- the event topology is explicit; - the event topology is explicit;
- the lifecycle and workspace-framework rules are explicit;
- theme and i18n constraints are unambiguous for shared Studio UI work. - theme and i18n constraints are unambiguous for shared Studio UI work.

View File

@ -21,6 +21,7 @@ This specification consolidates the accepted Studio components module decision i
3. A control enters the module when it is needed by the current Studio UI wave. 3. A control enters the module when it is needed by the current Studio UI wave.
4. Controls in the module wrap and shape the JavaFX controls the Studio actively uses, but do not clone the full JavaFX API. 4. Controls in the module wrap and shape the JavaFX controls the Studio actively uses, but do not clone the full JavaFX API.
5. Every admitted control must preserve theme and i18n compatibility. 5. Every admitted control must preserve theme and i18n compatibility.
6. Reusable event-driven workspace controls should be admitted when they are part of the active Studio workspace framework.
## Module Role ## Module Role
@ -28,7 +29,8 @@ This specification consolidates the accepted Studio components module decision i
- the authoritative Studio control layer; - the authoritative Studio control layer;
- the home of the Studio visual dialect; - the home of the Studio visual dialect;
- the place where Studio-facing controls are shaped for Studio use. - the place where Studio-facing controls are shaped for Studio use;
- the preferred home for reusable lifecycle-aware workspace controls when those controls are part of the visible Studio UI dialect.
`prometeu-studio-components` is not: `prometeu-studio-components` is not:
@ -45,6 +47,7 @@ Rules:
- immediate current Studio use is sufficient justification; - immediate current Studio use is sufficient justification;
- a second use is not required when the control is clearly part of the shell or the current workspace surface; - a second use is not required when the control is clearly part of the shell or the current workspace surface;
- no control should be added without an immediate Studio consumer. - no control should be added without an immediate Studio consumer.
- a control needed first by one workspace may still be admitted when it is clearly intended as a reusable Studio workspace primitive.
## Studio Usage Rule ## Studio Usage Rule
@ -81,6 +84,25 @@ Rules:
These hooks exist so event-driven controls remain predictable and safe to embed in shell and workspace UI. These hooks exist so event-driven controls remain predictable and safe to embed in shell and workspace UI.
## Reusable Workspace Primitives
Some reusable controls are not shell-only and are not domain-specific either.
They may still belong in `prometeu-studio-components` when they are part of the visible Studio workspace framework.
Illustrative admissible examples include:
- projection hosts for list/tree/detail surfaces;
- lifecycle-managed row or item controls;
- reusable section or inspector surfaces;
- visible controls that encode local-patch versus structural-sync interaction patterns.
What does not belong here:
- workspace-specific orchestration logic;
- domain-specific service calls;
- hidden coordination code with no visible control responsibility.
## Typical Control Scope ## Typical Control Scope
Typical controls that belong in the module include: Typical controls that belong in the module include:
@ -135,6 +157,7 @@ The module must remain:
- defining the first concrete component set in full detail - defining the first concrete component set in full detail
- defining final package layout exhaustively - defining final package layout exhaustively
- mirroring the entire JavaFX control hierarchy - mirroring the entire JavaFX control hierarchy
- storing domain-specific workspace orchestration code in the components module
## Exit Criteria ## Exit Criteria
@ -142,4 +165,5 @@ This specification is complete enough when:
- the module role is unambiguous; - the module role is unambiguous;
- admission rules are stable enough to guide implementation; - admission rules are stable enough to guide implementation;
- reusable workspace-control admission is clear enough to prevent local reimplementation drift;
- the JavaFX wrapping boundary is clear enough to prevent drift. - the JavaFX wrapping boundary is clear enough to prevent drift.

View File

@ -68,6 +68,14 @@ The workspace must help the user understand:
- what the asset declares toward the runtime-facing contract; - what the asset declares toward the runtime-facing contract;
- which operations are safe, staged, blocked, or destructive. - which operations are safe, staged, blocked, or destructive.
The `Assets` workspace is also the first concrete consumer of the Studio event-driven workspace framework.
That means:
- `Assets` must prove the canonical Studio workspace architecture in real use;
- reusable lifecycle and projection patterns extracted here should be consumable by future Studio workspaces;
- `Assets` must not rely on a monolithic workspace root that redraws most of the UI after local interactions.
## Baseline Layout ## Baseline Layout
The baseline workspace layout is: The baseline workspace layout is:
@ -80,6 +88,18 @@ The baseline workspace layout is:
Filesystem structure may be visible and actionable as supporting context, but it is not the primary identity model of the workspace. Filesystem structure may be visible and actionable as supporting context, but it is not the primary identity model of the workspace.
## Component Ownership Rules
The `Assets` workspace must be implemented as a composition root plus lifecycle-managed child controls.
Rules:
- the workspace root should coordinate service calls, workspace state, and structural synchronization;
- the workspace root should not own the render and update path of every navigator row, details section, and form control directly;
- the navigator host, asset rows, selected-asset details host, and details-internal sections/forms should subscribe only to the events and state they consume;
- event-consuming controls must subscribe only while mounted in the UI tree;
- reusable framework primitives should be consumed where the Studio already provides them instead of duplicating local refresh-oriented mechanisms.
## Asset Navigator Rules ## Asset Navigator Rules
### Primary Navigation Unit ### Primary Navigation Unit
@ -133,6 +153,17 @@ Filesystem structure may be visible and actionable as supporting context, but it
- If the selected asset is removed from the navigator, selection must clear explicitly. - If the selected asset is removed from the navigator, selection must clear explicitly.
- Selection must never silently drift to another asset due to refresh ordering. - Selection must never silently drift to another asset due to refresh ordering.
### Local Rendering Rules
- The workspace must use event-directed local redraws for routine interactions.
- The navigator host must react to projection-level events rather than whole-workspace rerender requests.
- Asset row items should react to row-scoped patch and selection events when identity is preserved.
- Selecting an asset must update navigator selection styling and the selected-asset details surface without rebuilding the entire workspace shell.
- Search and filter changes must redraw the navigator projection only.
- Details-local interactions such as preview input selection, preview zoom, or runtime-contract edits must redraw the selected-asset details surface only.
- Structural operations such as asset creation, removal, relocation, or full workspace reload may rebuild both navigator and details projections.
- Workspace-scoped progress and logging updates must not force navigator or selected-asset details redraw unless the underlying projection actually changed.
### State Rules ### State Rules
- The navigator must define explicit `loading assets` state. - The navigator must define explicit `loading assets` state.
@ -158,6 +189,14 @@ The selected asset view must use this stable composition:
This composition is stable across asset families. This composition is stable across asset families.
The selected-asset details area must follow component-local ownership.
Rules:
- the details host should react to details lifecycle events such as loading, ready, and error;
- details sections should update independently when only one section's projection changed;
- forms and preview-local controls should publish and consume narrow typed events rather than trigger full details rebuild by default.
### Summary ### Summary
- `Summary` must always be present for a selected asset. - `Summary` must always be present for a selected asset.
@ -253,6 +292,14 @@ Inline staged panels are allowed for non-destructive blocked or inspect-only flo
Modal review and confirmation is required for `Exclude From Build`, `Remove`, and relocation commits. Modal review and confirmation is required for `Exclude From Build`, `Remove`, and relocation commits.
Routine state patches such as `preload`, `Include In Build`, and `Exclude From Build` must patch the affected asset projection locally when identity is preserved. Full workspace refresh is reserved for operations that change workspace structure or identity resolution.
Operation orchestration must follow this rule:
- local patch when stable identity and local projection are preserved;
- explicit structural sync when collection shape, identity, or projection membership changes;
- no routine mutation path should fall back to whole-workspace refresh merely for implementation convenience.
### Mutations Requiring Preview ### Mutations Requiring Preview
- `Exclude From Build` - `Exclude From Build`
@ -295,7 +342,6 @@ This specification does not define:
- the exact JavaFX component tree or control class structure of the `Assets` workspace; - the exact JavaFX component tree or control class structure of the `Assets` workspace;
- final reusable component boundaries for all preview surfaces; - final reusable component boundaries for all preview surfaces;
- cross-workspace reuse beyond the `Assets` workspace;
- future multi-select or bulk-edit UX beyond the currently defined staged batch summary rules. - future multi-select or bulk-edit UX beyond the currently defined staged batch summary rules.
## Exit Criteria ## Exit Criteria
@ -307,4 +353,5 @@ This specification is satisfied when the Studio `Assets` workspace:
- explains the selected asset through summary, contract, inputs, diagnostics, and actions; - explains the selected asset through summary, contract, inputs, diagnostics, and actions;
- keeps activity, progress, and logs clearly separated; - keeps activity, progress, and logs clearly separated;
- stages sensitive mutations through preview-first flows; - stages sensitive mutations through preview-first flows;
- proves the Studio event-driven workspace framework through lifecycle-managed local ownership;
- and behaves as a didactic helper for the packer model. - and behaves as a didactic helper for the packer model.

View File

@ -0,0 +1,15 @@
package p.studio.events;
import p.studio.projects.ProjectReference;
import p.studio.workspaces.assets.AssetWorkspaceAssetSummary;
import java.util.Objects;
public record StudioAssetsAssetSummaryPatchedEvent(
ProjectReference project,
AssetWorkspaceAssetSummary summary) implements StudioEvent {
public StudioAssetsAssetSummaryPatchedEvent {
Objects.requireNonNull(project, "project");
Objects.requireNonNull(summary, "summary");
}
}

View File

@ -0,0 +1,11 @@
package p.studio.events;
import p.studio.projects.ProjectReference;
import java.util.Objects;
public record StudioAssetsDetailsRedrawRequestedEvent(ProjectReference project) implements StudioEvent {
public StudioAssetsDetailsRedrawRequestedEvent {
Objects.requireNonNull(project, "project");
}
}

View File

@ -0,0 +1,11 @@
package p.studio.events;
import p.studio.projects.ProjectReference;
import java.util.Objects;
public record StudioAssetsNavigatorRedrawRequestedEvent(ProjectReference project) implements StudioEvent {
public StudioAssetsNavigatorRedrawRequestedEvent {
Objects.requireNonNull(project, "project");
}
}

View File

@ -0,0 +1,15 @@
package p.studio.events;
import p.studio.projects.ProjectReference;
import p.studio.workspaces.assets.AssetWorkspaceSelectionKey;
import java.util.Objects;
public record StudioAssetsWorkspaceSelectionRequestedEvent(
ProjectReference project,
AssetWorkspaceSelectionKey selectionKey) implements StudioEvent {
public StudioAssetsWorkspaceSelectionRequestedEvent {
Objects.requireNonNull(project, "project");
Objects.requireNonNull(selectionKey, "selectionKey");
}
}

View File

@ -86,13 +86,6 @@ public final class AssetWorkspace implements Workspace {
null); null);
} }
public AssetWorkspace(
ProjectReference projectReference,
AssetWorkspaceService assetWorkspaceService,
AssetWorkspaceMutationService mutationService) {
this(projectReference, assetWorkspaceService, defaultWorkspaceBus(), mutationService, null);
}
private AssetWorkspace( private AssetWorkspace(
ProjectReference projectReference, ProjectReference projectReference,
AssetWorkspaceService assetWorkspaceService, AssetWorkspaceService assetWorkspaceService,
@ -123,6 +116,7 @@ public final class AssetWorkspace implements Workspace {
packerEventAdapter); packerEventAdapter);
this.packService = new FileSystemPackerBuildService(new p.packer.building.PackerBuildPlanner(), packerEventAdapter); this.packService = new FileSystemPackerBuildService(new p.packer.building.PackerBuildPlanner(), packerEventAdapter);
subscribeLocalEvents();
root.getStyleClass().add("assets-workspace"); root.getStyleClass().add("assets-workspace");
root.setCenter(buildLayout()); root.setCenter(buildLayout());
root.setBottom(buildLogsPane()); root.setBottom(buildLogsPane());
@ -157,6 +151,33 @@ public final class AssetWorkspace implements Workspace {
return new StudioWorkspaceEventBus(WorkspaceId.ASSETS, Container.events()); return new StudioWorkspaceEventBus(WorkspaceId.ASSETS, Container.events());
} }
private void subscribeLocalEvents() {
workspaceBus.subscribe(StudioAssetsWorkspaceSelectionRequestedEvent.class, event -> {
if (projectMatches(event.project())) {
applySelectionRequest(event.selectionKey());
}
});
workspaceBus.subscribe(StudioAssetsNavigatorRedrawRequestedEvent.class, event -> {
if (projectMatches(event.project())) {
renderNavigator();
}
});
workspaceBus.subscribe(StudioAssetsDetailsRedrawRequestedEvent.class, event -> {
if (projectMatches(event.project())) {
renderDetails();
}
});
workspaceBus.subscribe(StudioAssetsAssetSummaryPatchedEvent.class, event -> {
if (projectMatches(event.project())) {
applyAssetSummaryPatch(event.summary());
}
});
}
private boolean projectMatches(ProjectReference project) {
return projectReference.equals(project);
}
private VBox buildLayout() { private VBox buildLayout() {
inlineProgressLabel.getStyleClass().add("assets-workspace-inline-progress-label"); inlineProgressLabel.getStyleClass().add("assets-workspace-inline-progress-label");
inlineProgressLabel.setText(Container.i18n().text(I18n.ASSETS_PROGRESS_IDLE)); inlineProgressLabel.setText(Container.i18n().text(I18n.ASSETS_PROGRESS_IDLE));
@ -179,7 +200,7 @@ public final class AssetWorkspace implements Workspace {
final String current = newValue == null ? "" : newValue; final String current = newValue == null ? "" : newValue;
if (!previous.equals(current)) { if (!previous.equals(current)) {
searchQuery = current; searchQuery = current;
renderState(); requestNavigatorRedraw();
} }
}); });
@ -269,7 +290,7 @@ public final class AssetWorkspace implements Workspace {
} else { } else {
activeFilters.remove(filter); activeFilters.remove(filter);
} }
renderState(); requestNavigatorRedraw();
}); });
filterButtons.put(filter, button); filterButtons.put(filter, button);
filterBar.getChildren().add(button); filterBar.getChildren().add(button);
@ -285,7 +306,7 @@ public final class AssetWorkspace implements Workspace {
stagedMutationPreview = null; stagedMutationPreview = null;
selectedPreviewInput = null; selectedPreviewInput = null;
selectedPreviewZoom = 1; selectedPreviewZoom = 1;
renderState(); requestRedraw();
} }
setInlineProgress(Container.i18n().text(I18n.ASSETS_PROGRESS_REFRESHING), ProgressBar.INDETERMINATE_PROGRESS, true); setInlineProgress(Container.i18n().text(I18n.ASSETS_PROGRESS_REFRESHING), ProgressBar.INDETERMINATE_PROGRESS, true);
appendLog("Assets refresh started."); appendLog("Assets refresh started.");
@ -303,7 +324,7 @@ public final class AssetWorkspace implements Workspace {
} }
setInlineProgress(Container.i18n().text(I18n.ASSETS_PROGRESS_IDLE), 0, false); setInlineProgress(Container.i18n().text(I18n.ASSETS_PROGRESS_IDLE), 0, false);
appendLog("Assets refresh failed: " + rootCauseMessage(throwable)); appendLog("Assets refresh failed: " + rootCauseMessage(throwable));
renderState(); requestRedraw();
workspaceBus.publish(new StudioAssetsWorkspaceRefreshFailedEvent(projectReference, rootCauseMessage(throwable))); workspaceBus.publish(new StudioAssetsWorkspaceRefreshFailedEvent(projectReference, rootCauseMessage(throwable)));
return; return;
} }
@ -315,7 +336,7 @@ public final class AssetWorkspace implements Workspace {
pendingSelectionKey = null; pendingSelectionKey = null;
setInlineProgress(Container.i18n().text(I18n.ASSETS_PROGRESS_IDLE), 0, false); setInlineProgress(Container.i18n().text(I18n.ASSETS_PROGRESS_IDLE), 0, false);
appendLog("Assets refresh completed: " + state.assets().size() + " assets."); appendLog("Assets refresh completed: " + state.assets().size() + " assets.");
renderState(); requestRedraw();
workspaceBus.publish(new StudioAssetsWorkspaceRefreshedEvent(projectReference, state.assets().size())); workspaceBus.publish(new StudioAssetsWorkspaceRefreshedEvent(projectReference, state.assets().size()));
state.selectedAsset().ifPresent(asset -> { state.selectedAsset().ifPresent(asset -> {
workspaceBus.publish(new StudioAssetsWorkspaceSelectionChangedEvent(projectReference, asset.selectionKey())); workspaceBus.publish(new StudioAssetsWorkspaceSelectionChangedEvent(projectReference, asset.selectionKey()));
@ -339,7 +360,7 @@ public final class AssetWorkspace implements Workspace {
stagedMutationPreview = null; stagedMutationPreview = null;
selectedPreviewInput = null; selectedPreviewInput = null;
selectedPreviewZoom = 1; selectedPreviewZoom = 1;
renderDetails(); requestDetailsRedraw();
} }
setInlineProgress(Container.i18n().text(I18n.ASSETS_PROGRESS_LOADING_DETAILS), ProgressBar.INDETERMINATE_PROGRESS, true); setInlineProgress(Container.i18n().text(I18n.ASSETS_PROGRESS_LOADING_DETAILS), ProgressBar.INDETERMINATE_PROGRESS, true);
appendLog("Loading details for " + selectionKey.stableKey() + "."); appendLog("Loading details for " + selectionKey.stableKey() + ".");
@ -359,7 +380,7 @@ public final class AssetWorkspace implements Workspace {
} }
setInlineProgress(Container.i18n().text(I18n.ASSETS_PROGRESS_IDLE), 0, false); setInlineProgress(Container.i18n().text(I18n.ASSETS_PROGRESS_IDLE), 0, false);
appendLog("Asset details failed: " + message); appendLog("Asset details failed: " + message);
renderDetails(); requestDetailsRedraw();
return; return;
} }
@ -369,7 +390,7 @@ public final class AssetWorkspace implements Workspace {
selectedPreviewZoom = 1; selectedPreviewZoom = 1;
setInlineProgress(Container.i18n().text(I18n.ASSETS_PROGRESS_IDLE), 0, false); setInlineProgress(Container.i18n().text(I18n.ASSETS_PROGRESS_IDLE), 0, false);
appendLog("Asset details ready for " + details.summary().assetName() + "."); appendLog("Asset details ready for " + details.summary().assetName() + ".");
renderDetails(); requestDetailsRedraw();
})); }));
} }
@ -394,6 +415,19 @@ public final class AssetWorkspace implements Workspace {
renderDetails(); renderDetails();
} }
private void requestRedraw() {
requestNavigatorRedraw();
requestDetailsRedraw();
}
private void requestNavigatorRedraw() {
workspaceBus.publish(new StudioAssetsNavigatorRedrawRequestedEvent(projectReference));
}
private void requestDetailsRedraw() {
workspaceBus.publish(new StudioAssetsDetailsRedrawRequestedEvent(projectReference));
}
private void renderNavigator() { private void renderNavigator() {
assetRowsBySelectionKey.clear(); assetRowsBySelectionKey.clear();
switch (state.status()) { switch (state.status()) {
@ -556,7 +590,7 @@ public final class AssetWorkspace implements Workspace {
inputButton.setOnAction(event -> { inputButton.setOnAction(event -> {
selectedPreviewInput = input; selectedPreviewInput = input;
selectedPreviewZoom = 1; selectedPreviewZoom = 1;
renderState(); requestDetailsRedraw();
}); });
roleBox.getChildren().add(inputButton); roleBox.getChildren().add(inputButton);
} }
@ -718,7 +752,7 @@ public final class AssetWorkspace implements Workspace {
button.setDisable(zoom > maxZoom); button.setDisable(zoom > maxZoom);
button.setOnAction(event -> { button.setOnAction(event -> {
selectedPreviewZoom = zoom; selectedPreviewZoom = zoom;
renderState(); requestDetailsRedraw();
}); });
zoomBar.getChildren().add(button); zoomBar.getChildren().add(button);
} }
@ -824,17 +858,9 @@ public final class AssetWorkspace implements Workspace {
} }
final AssetWorkspaceAssetSummary updatedSummary = withPreload(details.summary(), preloadEnabled); final AssetWorkspaceAssetSummary updatedSummary = withPreload(details.summary(), preloadEnabled);
selectedAssetDetails = new AssetWorkspaceAssetDetails(
updatedSummary,
details.outputFormat(),
details.outputCodec(),
details.inputsByRole(),
details.diagnostics());
state = AssetWorkspaceState.ready(replaceAssetSummary(updatedSummary), updatedSummary.selectionKey());
setInlineProgress(Container.i18n().text(I18n.ASSETS_PROGRESS_IDLE), 0, false); setInlineProgress(Container.i18n().text(I18n.ASSETS_PROGRESS_IDLE), 0, false);
appendLog("Preload updated for " + updatedSummary.assetName() + "."); appendLog("Preload updated for " + updatedSummary.assetName() + ".");
renderNavigator(); workspaceBus.publish(new StudioAssetsAssetSummaryPatchedEvent(projectReference, updatedSummary));
renderDetails();
})); }));
} }
@ -869,12 +895,40 @@ public final class AssetWorkspace implements Workspace {
summary.hasDiagnostics()); summary.hasDiagnostics());
} }
private AssetWorkspaceAssetSummary withBuildParticipation(
AssetWorkspaceAssetSummary summary,
AssetWorkspaceBuildParticipation buildParticipation) {
return new AssetWorkspaceAssetSummary(
summary.selectionKey(),
summary.assetName(),
summary.state(),
buildParticipation,
summary.assetId(),
summary.assetFamily(),
summary.assetRoot(),
summary.preload(),
summary.hasDiagnostics());
}
private List<AssetWorkspaceAssetSummary> replaceAssetSummary(AssetWorkspaceAssetSummary updatedSummary) { private List<AssetWorkspaceAssetSummary> replaceAssetSummary(AssetWorkspaceAssetSummary updatedSummary) {
return state.assets().stream() return state.assets().stream()
.map(asset -> asset.selectionKey().equals(updatedSummary.selectionKey()) ? updatedSummary : asset) .map(asset -> asset.selectionKey().equals(updatedSummary.selectionKey()) ? updatedSummary : asset)
.toList(); .toList();
} }
private void applyAssetSummaryPatch(AssetWorkspaceAssetSummary updatedSummary) {
state = AssetWorkspaceState.ready(replaceAssetSummary(updatedSummary), state.selectedKey());
if (selectedAssetDetails != null && selectedAssetDetails.summary().selectionKey().equals(updatedSummary.selectionKey())) {
selectedAssetDetails = new AssetWorkspaceAssetDetails(
updatedSummary,
selectedAssetDetails.outputFormat(),
selectedAssetDetails.outputCodec(),
selectedAssetDetails.inputsByRole(),
selectedAssetDetails.diagnostics());
}
requestRedraw();
}
private void renderNavigatorProjection(AssetNavigatorProjection projection) { private void renderNavigatorProjection(AssetNavigatorProjection projection) {
navigatorContent.getChildren().clear(); navigatorContent.getChildren().clear();
for (AssetNavigatorGroup group : projection.groups()) { for (AssetNavigatorGroup group : projection.groups()) {
@ -983,11 +1037,15 @@ public final class AssetWorkspace implements Workspace {
} }
private void selectAsset(AssetWorkspaceSelectionKey selectionKey) { private void selectAsset(AssetWorkspaceSelectionKey selectionKey) {
workspaceBus.publish(new StudioAssetsWorkspaceSelectionRequestedEvent(projectReference, selectionKey));
}
private void applySelectionRequest(AssetWorkspaceSelectionKey selectionKey) {
state = state.withSelection(selectionKey); state = state.withSelection(selectionKey);
stagedMutationPreview = null; stagedMutationPreview = null;
appendLog("Selected asset " + selectionKey.stableKey() + "."); appendLog("Selected asset " + selectionKey.stableKey() + ".");
updateNavigatorSelection(); updateNavigatorSelection();
renderDetails(); requestDetailsRedraw();
workspaceBus.publish(new StudioAssetsWorkspaceSelectionChangedEvent(projectReference, selectionKey)); workspaceBus.publish(new StudioAssetsWorkspaceSelectionChangedEvent(projectReference, selectionKey));
loadSelectedAssetDetails(selectionKey); loadSelectedAssetDetails(selectionKey);
} }
@ -1043,7 +1101,6 @@ public final class AssetWorkspace implements Workspace {
if (!result.diagnostics().isEmpty()) { if (!result.diagnostics().isEmpty()) {
appendLog("Doctor diagnostics: " + result.diagnostics().size() + "."); appendLog("Doctor diagnostics: " + result.diagnostics().size() + ".");
} }
refresh();
} }
private void handlePackResult(PackerBuildResult result) { private void handlePackResult(PackerBuildResult result) {
@ -1089,14 +1146,14 @@ public final class AssetWorkspace implements Workspace {
try { try {
stagedMutationPreview = mutationService.preview(projectReference, selectedAsset, action, null); stagedMutationPreview = mutationService.preview(projectReference, selectedAsset, action, null);
appendLog("Preview ready for " + actionLabel(action) + "."); appendLog("Preview ready for " + actionLabel(action) + ".");
renderState(); requestDetailsRedraw();
Platform.runLater(() -> detailsScroll.setVvalue(0.0d)); Platform.runLater(() -> detailsScroll.setVvalue(0.0d));
} catch (RuntimeException runtimeException) { } catch (RuntimeException runtimeException) {
final String message = rootCauseMessage(runtimeException); final String message = rootCauseMessage(runtimeException);
appendLog("Preview failed: " + message); appendLog("Preview failed: " + message);
workspaceBus.publish(new StudioAssetsMutationFailedEvent(projectReference, action, message)); workspaceBus.publish(new StudioAssetsMutationFailedEvent(projectReference, action, message));
stagedMutationPreview = null; stagedMutationPreview = null;
renderState(); requestDetailsRedraw();
} }
} }
@ -1111,19 +1168,21 @@ public final class AssetWorkspace implements Workspace {
if (!preview.canApply()) { if (!preview.canApply()) {
stagedMutationPreview = preview; stagedMutationPreview = preview;
appendLog(actionLabel(action) + " blocked."); appendLog(actionLabel(action) + " blocked.");
renderState(); requestDetailsRedraw();
return; return;
} }
mutationService.apply(projectReference, preview); mutationService.apply(projectReference, preview);
appendLog("Applied " + actionLabel(preview.action()) + "."); appendLog("Applied " + actionLabel(preview.action()) + ".");
stagedMutationPreview = null; stagedMutationPreview = null;
refresh(); if (!applyMutationSummaryPatch(preview)) {
refresh();
}
} catch (RuntimeException runtimeException) { } catch (RuntimeException runtimeException) {
final String message = rootCauseMessage(runtimeException); final String message = rootCauseMessage(runtimeException);
appendLog("Mutation failed: " + message); appendLog("Mutation failed: " + message);
workspaceBus.publish(new StudioAssetsMutationFailedEvent(projectReference, action, message)); workspaceBus.publish(new StudioAssetsMutationFailedEvent(projectReference, action, message));
stagedMutationPreview = null; stagedMutationPreview = null;
renderState(); requestDetailsRedraw();
} }
} }
@ -1148,16 +1207,31 @@ public final class AssetWorkspace implements Workspace {
mutationService.apply(projectReference, preview); mutationService.apply(projectReference, preview);
appendLog("Applied " + actionLabel(preview.action()) + "."); appendLog("Applied " + actionLabel(preview.action()) + ".");
stagedMutationPreview = null; stagedMutationPreview = null;
refresh(); if (!applyMutationSummaryPatch(preview)) {
refresh();
}
} catch (RuntimeException runtimeException) { } catch (RuntimeException runtimeException) {
final String message = rootCauseMessage(runtimeException); final String message = rootCauseMessage(runtimeException);
appendLog("Mutation failed: " + message); appendLog("Mutation failed: " + message);
workspaceBus.publish(new StudioAssetsMutationFailedEvent(projectReference, action, message)); workspaceBus.publish(new StudioAssetsMutationFailedEvent(projectReference, action, message));
stagedMutationPreview = null; stagedMutationPreview = null;
renderState(); requestDetailsRedraw();
} }
} }
private boolean applyMutationSummaryPatch(AssetWorkspaceMutationPreview preview) {
final AssetWorkspaceAssetSummary updatedSummary = switch (preview.action()) {
case INCLUDE_IN_BUILD -> withBuildParticipation(preview.asset(), AssetWorkspaceBuildParticipation.INCLUDED);
case EXCLUDE_FROM_BUILD -> withBuildParticipation(preview.asset(), AssetWorkspaceBuildParticipation.EXCLUDED);
default -> null;
};
if (updatedSummary == null) {
return false;
}
workspaceBus.publish(new StudioAssetsAssetSummaryPatchedEvent(projectReference, updatedSummary));
return true;
}
private Node createStagedMutationPanel(AssetWorkspaceMutationPreview preview) { private Node createStagedMutationPanel(AssetWorkspaceMutationPreview preview) {
final VBox panel = new VBox(10); final VBox panel = new VBox(10);
panel.getStyleClass().add("assets-mutation-panel"); panel.getStyleClass().add("assets-mutation-panel");
@ -1191,7 +1265,7 @@ public final class AssetWorkspace implements Workspace {
cancel.getStyleClass().addAll("studio-button", "studio-button-cancel"); cancel.getStyleClass().addAll("studio-button", "studio-button-cancel");
cancel.setOnAction(event -> { cancel.setOnAction(event -> {
stagedMutationPreview = null; stagedMutationPreview = null;
renderState(); requestDetailsRedraw();
}); });
final Button apply = new Button(Container.i18n().text(I18n.ASSETS_MUTATION_APPLY)); final Button apply = new Button(Container.i18n().text(I18n.ASSETS_MUTATION_APPLY));
apply.getStyleClass().addAll("studio-button", "studio-button-primary"); apply.getStyleClass().addAll("studio-button", "studio-button-primary");
@ -1310,7 +1384,7 @@ public final class AssetWorkspace implements Workspace {
final String message = rootCauseMessage(runtimeException); final String message = rootCauseMessage(runtimeException);
appendLog("Mutation failed: " + message); appendLog("Mutation failed: " + message);
stagedMutationPreview = preview; stagedMutationPreview = preview;
renderState(); requestDetailsRedraw();
} }
} }

View File

@ -15,6 +15,6 @@
"asset_id" : 8, "asset_id" : 8,
"asset_uuid" : "9a7386e7-6f0e-4e4c-9919-0de71e0b7031", "asset_uuid" : "9a7386e7-6f0e-4e4c-9919-0de71e0b7031",
"root" : "sound", "root" : "sound",
"included_in_build" : true "included_in_build" : false
} ] } ]
} }