Widen gameplay coach surface orchestration

This commit is contained in:
axiomlogicnexus 2026-05-07 22:19:12 +02:00
parent 7b32abdb9f
commit 023536695e
6 changed files with 505 additions and 6 deletions

View file

@ -2,6 +2,89 @@
#include "HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.h"
namespace HyperTwistTrainingPanelWidgetInternal
{
FString ResolveCoachHandoffStartActionSourceLabel(
const FHyperTwistTrainingCoachPanelState& PanelState,
const EHyperTwistTrainingCoachHandoffKind HandoffKind,
const FString& FallbackSourceLabel
)
{
if (HandoffKind == EHyperTwistTrainingCoachHandoffKind::Queue
&& !PanelState.PreferredQueueRecoveryActionSourceLabel.IsEmpty())
{
return PanelState.PreferredQueueRecoveryActionSourceLabel;
}
if (PanelState.PreferredCoachHandoffKind == HandoffKind
&& !PanelState.PreferredCoachHandoffSourceLabel.IsEmpty())
{
return PanelState.PreferredCoachHandoffSourceLabel;
}
if ((HandoffKind == EHyperTwistTrainingCoachHandoffKind::FollowUp
|| HandoffKind == EHyperTwistTrainingCoachHandoffKind::Recommended)
&& !PanelState.PreferredGuidanceSourceLabel.IsEmpty())
{
return PanelState.PreferredGuidanceSourceLabel;
}
if (!PanelState.PreferredCoachHandoffSourceLabel.IsEmpty())
{
return PanelState.PreferredCoachHandoffSourceLabel;
}
if (HandoffKind == EHyperTwistTrainingCoachHandoffKind::Queue
&& !PanelState.NextQueueSourceLabel.IsEmpty())
{
return PanelState.NextQueueSourceLabel;
}
if (!PanelState.PreferredLaunchBudgetSourceLabel.IsEmpty())
{
return PanelState.PreferredLaunchBudgetSourceLabel;
}
return FallbackSourceLabel;
}
FString ResolvePrimaryCoachActionStartActionSourceLabel(
const FHyperTwistTrainingCoachPanelState& PanelState,
const FString& FallbackSourceLabel
)
{
if (PanelState.PreferredQueueRecoveryAction != EHyperTwistTrainingQueueRecoveryAction::None)
{
return ResolveCoachHandoffStartActionSourceLabel(
PanelState,
EHyperTwistTrainingCoachHandoffKind::Queue,
FallbackSourceLabel
);
}
if (PanelState.PreferredCoachHandoffKind != EHyperTwistTrainingCoachHandoffKind::None)
{
return ResolveCoachHandoffStartActionSourceLabel(
PanelState,
PanelState.PreferredCoachHandoffKind,
FallbackSourceLabel
);
}
if (!PanelState.PreferredGuidanceSourceLabel.IsEmpty())
{
return PanelState.PreferredGuidanceSourceLabel;
}
if (!PanelState.PreferredLaunchBudgetSourceLabel.IsEmpty())
{
return PanelState.PreferredLaunchBudgetSourceLabel;
}
return FallbackSourceLabel;
}
}
void UHyperTwistTrainingPanelWidget::NativeConstruct()
{
Super::NativeConstruct();
@ -315,6 +398,34 @@ FHyperTwistTrainingDeck UHyperTwistTrainingPanelWidget::BuildActiveMethodDrillFo
return CachedMethodDrillFollowUpDeck;
}
FString UHyperTwistTrainingPanelWidget::ResolveCoachHandoffStartActionSourceLabel(
const EHyperTwistTrainingCoachHandoffKind HandoffKind
) const
{
return HyperTwistTrainingPanelWidgetInternal::ResolveCoachHandoffStartActionSourceLabel(
CachedCoachPanelState,
HandoffKind,
FString::Printf(
TEXT("coach-panel-%s"),
HandoffKind == EHyperTwistTrainingCoachHandoffKind::Queue
? TEXT("queue")
: (HandoffKind == EHyperTwistTrainingCoachHandoffKind::FollowUp
? TEXT("follow-up")
: (HandoffKind == EHyperTwistTrainingCoachHandoffKind::Recommended
? TEXT("recommended")
: TEXT("handoff")))
)
);
}
FString UHyperTwistTrainingPanelWidget::ResolvePrimaryCoachActionStartActionSourceLabel() const
{
return HyperTwistTrainingPanelWidgetInternal::ResolvePrimaryCoachActionStartActionSourceLabel(
CachedCoachPanelState,
TEXT("coach-panel-primary-action")
);
}
FHyperTwistTrainingRunState UHyperTwistTrainingPanelWidget::StartCoachRecommendedRun(
const int32 MaxCases,
const FString& StartActionSourceLabel
@ -395,6 +506,55 @@ FHyperTwistTrainingRunState UHyperTwistTrainingPanelWidget::StartCoachQueueEntry
return CachedRunState;
}
FHyperTwistTrainingRunState UHyperTwistTrainingPanelWidget::StartDisplayedCoachRecommendedRun(
const int32 MaxCases
)
{
return StartCoachRecommendedRun(
MaxCases,
ResolveCoachHandoffStartActionSourceLabel(EHyperTwistTrainingCoachHandoffKind::Recommended)
);
}
FHyperTwistTrainingRunState UHyperTwistTrainingPanelWidget::StartDisplayedCoachFollowUpRun(
const int32 MaxCases
)
{
return StartCoachFollowUpRun(
MaxCases,
ResolveCoachHandoffStartActionSourceLabel(EHyperTwistTrainingCoachHandoffKind::FollowUp)
);
}
FHyperTwistTrainingRunState UHyperTwistTrainingPanelWidget::StartDisplayedPrimaryCoachAction(
const int32 MaxCases
)
{
return StartPrimaryCoachAction(MaxCases, ResolvePrimaryCoachActionStartActionSourceLabel());
}
FHyperTwistTrainingRunState UHyperTwistTrainingPanelWidget::StartDisplayedNextQueuedCoachRun(
const int32 MaxCases
)
{
return StartNextQueuedCoachRun(
MaxCases,
ResolveCoachHandoffStartActionSourceLabel(EHyperTwistTrainingCoachHandoffKind::Queue)
);
}
FHyperTwistTrainingRunState UHyperTwistTrainingPanelWidget::StartDisplayedCoachQueueEntry(
const int32 MaxCases
)
{
if (CachedCoachPanelState.ActiveQueueEntryId.IsEmpty())
{
return FHyperTwistTrainingRunState();
}
return StartCoachQueueEntry(CachedCoachPanelState.ActiveQueueEntryId, MaxCases);
}
bool UHyperTwistTrainingPanelWidget::DeferCoachQueueEntry(
const FString& EntryId,
const FString& DeferredUntilUtc,
@ -446,6 +606,19 @@ bool UHyperTwistTrainingPanelWidget::PromoteCoachQueueEntry(
return bSucceeded;
}
bool UHyperTwistTrainingPanelWidget::PromoteDisplayedCoachQueueEntry()
{
if (CachedCoachPanelState.ActiveQueueEntryId.IsEmpty())
{
return false;
}
return PromoteCoachQueueEntry(
CachedCoachPanelState.ActiveQueueEntryId,
ResolveCoachHandoffStartActionSourceLabel(EHyperTwistTrainingCoachHandoffKind::Queue)
);
}
FHyperTwistTrainingRunState UHyperTwistTrainingPanelWidget::StartActiveMethodDrillFollowUpRun(
const FString& SessionId
)

View file

@ -2,6 +2,89 @@
#include "HyperTwistTraining/HyperTwistTrainingRuntimeLibrary.h"
namespace HyperTwistTrainingSessionActorInternal
{
FString ResolveCoachHandoffStartActionSourceLabel(
const FHyperTwistTrainingCoachPanelState& PanelState,
const EHyperTwistTrainingCoachHandoffKind HandoffKind,
const FString& FallbackSourceLabel
)
{
if (HandoffKind == EHyperTwistTrainingCoachHandoffKind::Queue
&& !PanelState.PreferredQueueRecoveryActionSourceLabel.IsEmpty())
{
return PanelState.PreferredQueueRecoveryActionSourceLabel;
}
if (PanelState.PreferredCoachHandoffKind == HandoffKind
&& !PanelState.PreferredCoachHandoffSourceLabel.IsEmpty())
{
return PanelState.PreferredCoachHandoffSourceLabel;
}
if ((HandoffKind == EHyperTwistTrainingCoachHandoffKind::FollowUp
|| HandoffKind == EHyperTwistTrainingCoachHandoffKind::Recommended)
&& !PanelState.PreferredGuidanceSourceLabel.IsEmpty())
{
return PanelState.PreferredGuidanceSourceLabel;
}
if (!PanelState.PreferredCoachHandoffSourceLabel.IsEmpty())
{
return PanelState.PreferredCoachHandoffSourceLabel;
}
if (HandoffKind == EHyperTwistTrainingCoachHandoffKind::Queue
&& !PanelState.NextQueueSourceLabel.IsEmpty())
{
return PanelState.NextQueueSourceLabel;
}
if (!PanelState.PreferredLaunchBudgetSourceLabel.IsEmpty())
{
return PanelState.PreferredLaunchBudgetSourceLabel;
}
return FallbackSourceLabel;
}
FString ResolvePrimaryCoachActionStartActionSourceLabel(
const FHyperTwistTrainingCoachPanelState& PanelState,
const FString& FallbackSourceLabel
)
{
if (PanelState.PreferredQueueRecoveryAction != EHyperTwistTrainingQueueRecoveryAction::None)
{
return ResolveCoachHandoffStartActionSourceLabel(
PanelState,
EHyperTwistTrainingCoachHandoffKind::Queue,
FallbackSourceLabel
);
}
if (PanelState.PreferredCoachHandoffKind != EHyperTwistTrainingCoachHandoffKind::None)
{
return ResolveCoachHandoffStartActionSourceLabel(
PanelState,
PanelState.PreferredCoachHandoffKind,
FallbackSourceLabel
);
}
if (!PanelState.PreferredGuidanceSourceLabel.IsEmpty())
{
return PanelState.PreferredGuidanceSourceLabel;
}
if (!PanelState.PreferredLaunchBudgetSourceLabel.IsEmpty())
{
return PanelState.PreferredLaunchBudgetSourceLabel;
}
return FallbackSourceLabel;
}
}
AHyperTwistTrainingSessionActor::AHyperTwistTrainingSessionActor()
{
PrimaryActorTick.bCanEverTick = false;
@ -296,6 +379,34 @@ FHyperTwistTrainingRunState AHyperTwistTrainingSessionActor::StartRunFromTemplat
return CachedRunState;
}
FString AHyperTwistTrainingSessionActor::ResolveCoachHandoffStartActionSourceLabel(
const EHyperTwistTrainingCoachHandoffKind HandoffKind
) const
{
return HyperTwistTrainingSessionActorInternal::ResolveCoachHandoffStartActionSourceLabel(
CachedCoachPanelState,
HandoffKind,
FString::Printf(
TEXT("coach-session-actor-%s"),
HandoffKind == EHyperTwistTrainingCoachHandoffKind::Queue
? TEXT("queue")
: (HandoffKind == EHyperTwistTrainingCoachHandoffKind::FollowUp
? TEXT("follow-up")
: (HandoffKind == EHyperTwistTrainingCoachHandoffKind::Recommended
? TEXT("recommended")
: TEXT("handoff")))
)
);
}
FString AHyperTwistTrainingSessionActor::ResolvePrimaryCoachActionStartActionSourceLabel() const
{
return HyperTwistTrainingSessionActorInternal::ResolvePrimaryCoachActionStartActionSourceLabel(
CachedCoachPanelState,
TEXT("coach-session-actor-primary-action")
);
}
FHyperTwistTrainingRunState AHyperTwistTrainingSessionActor::StartCoachRecommendedTrainingRun(
const int32 MaxCases
)
@ -304,7 +415,7 @@ FHyperTwistTrainingRunState AHyperTwistTrainingSessionActor::StartCoachRecommend
this,
AutoStartSessionId + TEXT("_coach"),
MaxCases > 0 ? MaxCases : DefaultCoachMaxCases,
FString()
ResolveCoachHandoffStartActionSourceLabel(EHyperTwistTrainingCoachHandoffKind::Recommended)
);
RefreshCachedTrainingState();
return CachedRunState;
@ -318,7 +429,7 @@ FHyperTwistTrainingRunState AHyperTwistTrainingSessionActor::StartCoachFollowUpT
this,
AutoStartSessionId + TEXT("_follow_up"),
MaxCases > 0 ? MaxCases : DefaultCoachMaxCases,
FString()
ResolveCoachHandoffStartActionSourceLabel(EHyperTwistTrainingCoachHandoffKind::FollowUp)
);
RefreshCachedTrainingState();
return CachedRunState;
@ -332,7 +443,7 @@ FHyperTwistTrainingRunState AHyperTwistTrainingSessionActor::StartPrimaryCoachTr
this,
AutoStartSessionId + TEXT("_primary"),
MaxCases > 0 ? MaxCases : DefaultCoachMaxCases,
FString()
ResolvePrimaryCoachActionStartActionSourceLabel()
);
RefreshCachedTrainingState();
return CachedRunState;
@ -346,7 +457,7 @@ FHyperTwistTrainingRunState AHyperTwistTrainingSessionActor::StartNextQueuedCoac
this,
AutoStartSessionId + TEXT("_queue"),
MaxCases > 0 ? MaxCases : DefaultCoachMaxCases,
FString()
ResolveCoachHandoffStartActionSourceLabel(EHyperTwistTrainingCoachHandoffKind::Queue)
);
RefreshCachedTrainingState();
return CachedRunState;
@ -367,6 +478,46 @@ FHyperTwistTrainingRunState AHyperTwistTrainingSessionActor::StartCoachQueueEntr
return CachedRunState;
}
FHyperTwistTrainingRunState AHyperTwistTrainingSessionActor::StartDisplayedCoachRecommendedTrainingRun(
const int32 MaxCases
)
{
return StartCoachRecommendedTrainingRun(MaxCases);
}
FHyperTwistTrainingRunState AHyperTwistTrainingSessionActor::StartDisplayedCoachFollowUpTrainingRun(
const int32 MaxCases
)
{
return StartCoachFollowUpTrainingRun(MaxCases);
}
FHyperTwistTrainingRunState AHyperTwistTrainingSessionActor::StartDisplayedPrimaryCoachTrainingRun(
const int32 MaxCases
)
{
return StartPrimaryCoachTrainingRun(MaxCases);
}
FHyperTwistTrainingRunState AHyperTwistTrainingSessionActor::StartDisplayedNextQueuedCoachTrainingRun(
const int32 MaxCases
)
{
return StartNextQueuedCoachTrainingRun(MaxCases);
}
FHyperTwistTrainingRunState AHyperTwistTrainingSessionActor::StartDisplayedCoachQueueEntryTrainingRun(
const int32 MaxCases
)
{
if (CachedCoachPanelState.ActiveQueueEntryId.IsEmpty())
{
return FHyperTwistTrainingRunState();
}
return StartCoachQueueEntryTrainingRun(CachedCoachPanelState.ActiveQueueEntryId, MaxCases);
}
bool AHyperTwistTrainingSessionActor::DeferCoachQueueEntry(
const FString& EntryId,
const FString& DeferredUntilUtc,
@ -409,12 +560,22 @@ bool AHyperTwistTrainingSessionActor::PromoteCoachQueueEntry(const FString& Entr
const bool bSucceeded = UHyperTwistTrainingRuntimeLibrary::PromoteCoachSessionQueueEntry(
this,
EntryId,
FString()
ResolveCoachHandoffStartActionSourceLabel(EHyperTwistTrainingCoachHandoffKind::Queue)
);
RefreshCachedTrainingState();
return bSucceeded;
}
bool AHyperTwistTrainingSessionActor::PromoteDisplayedCoachQueueEntry()
{
if (CachedCoachPanelState.ActiveQueueEntryId.IsEmpty())
{
return false;
}
return PromoteCoachQueueEntry(CachedCoachPanelState.ActiveQueueEntryId);
}
FHyperTwistTrainingRepositoryRoundTripVerification
AHyperTwistTrainingSessionActor::VerifyRepositoryRoundTrip()
{

View file

@ -206,6 +206,14 @@ public:
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Drill")
FHyperTwistTrainingDeck BuildActiveMethodDrillFollowUpDeck();
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach")
FString ResolveCoachHandoffStartActionSourceLabel(
EHyperTwistTrainingCoachHandoffKind HandoffKind
) const;
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach")
FString ResolvePrimaryCoachActionStartActionSourceLabel() const;
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach")
FHyperTwistTrainingRunState StartCoachRecommendedRun(
int32 MaxCases,
@ -236,6 +244,21 @@ public:
int32 MaxCases
);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach")
FHyperTwistTrainingRunState StartDisplayedCoachRecommendedRun(int32 MaxCases);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach")
FHyperTwistTrainingRunState StartDisplayedCoachFollowUpRun(int32 MaxCases);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach")
FHyperTwistTrainingRunState StartDisplayedPrimaryCoachAction(int32 MaxCases);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach")
FHyperTwistTrainingRunState StartDisplayedNextQueuedCoachRun(int32 MaxCases);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach")
FHyperTwistTrainingRunState StartDisplayedCoachQueueEntry(int32 MaxCases);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach")
bool DeferCoachQueueEntry(
const FString& EntryId,
@ -257,6 +280,9 @@ public:
const FString& StartActionSourceLabel
);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach")
bool PromoteDisplayedCoachQueueEntry();
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Drill")
FHyperTwistTrainingRunState StartActiveMethodDrillFollowUpRun(const FString& SessionId);

View file

@ -187,6 +187,14 @@ public:
const FString& SessionId
);
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach")
FString ResolveCoachHandoffStartActionSourceLabel(
EHyperTwistTrainingCoachHandoffKind HandoffKind
) const;
UFUNCTION(BlueprintPure, Category = "HyperTwist|Training|Coach")
FString ResolvePrimaryCoachActionStartActionSourceLabel() const;
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach")
FHyperTwistTrainingRunState StartCoachRecommendedTrainingRun(int32 MaxCases);
@ -205,6 +213,21 @@ public:
int32 MaxCases
);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach")
FHyperTwistTrainingRunState StartDisplayedCoachRecommendedTrainingRun(int32 MaxCases);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach")
FHyperTwistTrainingRunState StartDisplayedCoachFollowUpTrainingRun(int32 MaxCases);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach")
FHyperTwistTrainingRunState StartDisplayedPrimaryCoachTrainingRun(int32 MaxCases);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach")
FHyperTwistTrainingRunState StartDisplayedNextQueuedCoachTrainingRun(int32 MaxCases);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach")
FHyperTwistTrainingRunState StartDisplayedCoachQueueEntryTrainingRun(int32 MaxCases);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach")
bool DeferCoachQueueEntry(
const FString& EntryId,
@ -223,6 +246,9 @@ public:
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach")
bool PromoteCoachQueueEntry(const FString& EntryId);
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training|Coach")
bool PromoteDisplayedCoachQueueEntry();
UFUNCTION(BlueprintCallable, Category = "HyperTwist|Training")
FHyperTwistTrainingRepositoryRoundTripVerification VerifyRepositoryRoundTrip();
};

View file

@ -68,7 +68,8 @@ Current correction call from the source-exposed audit and current execution refr
- the imported generated-mode gap is closed; request/config/selection plumbing and the bounded clean-room executor are already landed in first-party code
- the latest landed bounded `Phase 4` packet closes the retained-idle comparison/outcome actionability gap at the end of the recognition-assisted coach-to-review continuity lane, and the final retained-surface confirmation pass now leaves that closure lane functionally complete
- the retained idle widget surface now keeps retained comparison and decision history inspectable without continuing to advertise live launch or guidance-outcome actions once the coach-to-review loop has already collapsed to truthful closed-loop idle
- the current next bounded packet is to widen into the first thin gameplay-facing consumers of coach scheduling and recommendations, starting from the already-exposed panel-widget and session-actor coach state/action surfaces without reopening backend continuity or analytics policy work
- the latest landed bounded `Phase 4` packet widens the first gameplay-facing coach consumer layer by giving the panel-widget and session-actor surfaces displayed coach-action helpers plus default source-label routing, so gameplay/Blueprint callers no longer have to fabricate coach provenance or manually chase active queue-entry ids just to launch the current action
- the current next bounded packet is to stay on those same surfaces and expose one thin, truthful primary-action descriptor / availability layer for gameplay callers so they can bind a single coach CTA without reconstructing queue-vs-follow-up-vs-recommended state from the raw panel booleans
- the current execution-discipline rule that broad refactor / monolith-splitting work should not interrupt the active bounded roadmap packet unless structure is actually blocking it
## Current execution-reality references

View file

@ -0,0 +1,112 @@
# HyperTwist Phase 4 gameplay coach surface orchestration packet
Created on `2026-05-07`
Status:
- first-party HyperTwist packet
- bounded Phase `4` gameplay-facing consumer slice
## Purpose
This packet starts the first thin gameplay-facing consumer widening above the now-stable coach scheduling and recommendation backend by making the panel-widget and session-actor surfaces executable without extra caller-side provenance glue.
The open tasks were:
- stop forcing gameplay / Blueprint callers to invent their own coach start-action source labels before they can launch the currently displayed recommended, follow-up, queue, or primary coach action
- stop dropping coach provenance entirely on the session-actor path when those actions are launched or deferred work is promoted
- expose one thin displayed-action helper layer so callers can act on the retained panel state directly instead of manually wiring active queue-entry ids every time
It is not:
- a new backend continuity packet
- a new analytics-selection packet
- a dashboard rewrite
- a broader gameplay UI integration pass
## Scope
Bounded lane:
- add panel-widget helpers that resolve default coach handoff / primary-action source labels from the cached coach panel state
- add panel-widget helpers that launch the currently displayed recommended, follow-up, primary, next-queued, and active queue-entry actions without extra caller-side routing
- add panel-widget helper coverage for promoting the currently displayed queue entry
- update the session-actor launch and promotion path to stop recording empty source labels
- mirror the same displayed-action helper layer on the session actor for gameplay-facing Blueprint consumers
Out of scope:
- changing coach recommendation policy
- changing queue execution policy
- changing retained dashboard continuity behavior
- widening into a richer gameplay HUD or CTA presentation pass in the same packet
## Why this was the right next packet
Before this slice:
- the recognition-assisted coach-to-review closure lane was already documented as complete
- the repo already exposed rich cached coach state on `HyperTwistTrainingPanelWidget` and `HyperTwistTrainingSessionActor`
- those surfaces already had the raw launch verbs for recommended, follow-up, queue, and queue-entry work
But the first gameplay-facing consumer seam was still obvious:
- the widget surface still required callers to supply coach start-action source labels themselves
- the actor surface launched and promoted coach work with empty source labels, which threw away provenance and downstream analytics context
- callers still had to manually thread the active queue-entry id out of cached panel state for common displayed-action cases
That meant:
- the backend was ready
- but the first gameplay-facing consumer layer still imposed avoidable orchestration work and provenance loss on the caller
So the next bounded move was:
- keep the backend untouched
- and make the existing panel/actor surfaces honest thin consumers of the current coach panel state
## What landed
Primary code changes:
- `UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingPanelWidget.h`
- `UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingPanelWidget.cpp`
- added coach handoff and primary-action source-label resolvers driven by `CachedCoachPanelState`
- added displayed-action helpers for recommended, follow-up, primary, next-queued, and active queue-entry coach launches
- added a displayed queue-entry promotion helper
- `UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistTraining/HyperTwistTrainingSessionActor.h`
- `UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistTraining/HyperTwistTrainingSessionActor.cpp`
- added the same source-label resolvers and displayed-action helpers for gameplay-facing actor callers
- updated existing actor launch and queue-promotion methods so they no longer pass empty start-action source labels into the runtime path
## Product effect
The first gameplay-facing coach consumers are now thinner and more truthful:
- gameplay / Blueprint callers can execute the displayed coach action surface without manufacturing source labels first
- session-actor launches and queue promotions now preserve coach provenance instead of silently dropping it
- callers no longer need to manually copy `ActiveQueueEntryId` out of cached panel state for the common displayed queue-entry path
## Acceptance criteria
- panel-widget callers can resolve default coach source labels from the cached coach panel state
- panel-widget callers can launch the displayed recommended, follow-up, primary, next-queued, and active queue-entry actions through dedicated helpers
- session-actor launches and queue promotions no longer record empty source labels
- session-actor callers get the same displayed-action helper layer
- full product build succeeds
## Validation checklist
1. build `UnrealHyperTwist.sln` / `UnrealHyperTwistEditor`
2. confirm the panel widget compiles with the new displayed-action and source-label helper surface
3. confirm the session actor compiles with the new displayed-action helper surface
4. confirm actor-side coach launches and queue promotions now route non-empty source labels
## Validation result
- full `Development Editor|Win64` build succeeded on `2026-05-07`
- build completed clean with `0` warnings and `0` errors
## Next bounded follow-on slice
- stay on the same gameplay-facing consumer lane and expose one thin primary coach CTA descriptor / availability surface on the panel-widget and session-actor layer so gameplay callers can bind a single truthful coach action without reconstructing queue-vs-follow-up-vs-recommended state from raw booleans