Skip to content

[ADR-177] Mapping from CompiledLayout DTO to internal view models - #146

Merged
jodavis merged 4 commits into
feature/ADR-162-client-side-layout-updatesfrom
claude/implement-adr-177-6YzM2
Apr 19, 2026
Merged

[ADR-177] Mapping from CompiledLayout DTO to internal view models#146
jodavis merged 4 commits into
feature/ADR-162-client-side-layout-updatesfrom
claude/implement-adr-177-6YzM2

Conversation

@jodavis

@jodavisjodavis commented Apr 19, 2026

Copy link
Copy Markdown
Owner

Implements client-side consumption of CompiledLayout by mapping layout DTOs (AdaptiveRemote.Contracts) into the runtime RemoteLayoutElement model tree used by the app, with updated stub orchestration and unit tests.

… DTO-to-runtime mapping
CloudAssetOrchestrator now constructs a CompiledLayout DTO (without GUTTER) instead of
building a LayoutGroup tree directly. RemoteLayoutDefinitionService v2 reads the DTO from
the store, recursively maps LayoutGroupDefinitionDto/CommandDefinitionDto to LayoutGroup/
TiVoCommand/IRCommand/LifecycleCommand, and unconditionally appends a hardcoded GUTTER as
the last root child. A scoped CompiledLayout factory is registered in HostBuilderExtensions
for future consumers. CloudAssetStoreExtensions updated to reflect the new store type.
Unit tests rewritten to cover all CommandType mappings, GUTTER append, unknown type error,
empty store error, and recursive group nesting.
https://claude.ai/code/session_014dCcJFZ1bZ59uxEBPPNwGN
RemoteLayoutDefinitionService now builds the runtime layout tree once per
scope via Lazy<T>, so all callers (command services, UI components) share
the same Command object instances. Without this, each RemoteRoot access
created fresh Command objects; the command services would enable their copies
while the UI rendered separate, still-disabled copies.
global.json version updated from 10.0.201 to 10.0.100 (latestPatch) to
match the 10.0.106 SDK available in this environment.
https://claude.ai/code/session_014dCcJFZ1bZ59uxEBPPNwGN
@jodavis
jodavis changed the base branch from main to feature/ADR-162-client-side-layout-updatesApril 19, 2026 01:04
@jodavisjodavis changed the title Update spec.md with new guidance on the spec process.[ADR-177] Mapping from CompiledLayout DTO to internal view modelsApr 19, 2026
@jodavis
jodavis requested a review from CopilotApril 19, 2026 01:06

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements client-side consumption of CompiledLayout by mapping layout DTOs (AdaptiveRemote.Contracts) into the runtime RemoteLayoutElement model tree used by the app, with updated stub orchestration and unit tests.

Changes:

  • Update RemoteLayoutDefinitionService to load CompiledLayout from ICloudAssetStore, map DTO elements to runtime commands/groups, and append a hardcoded GUTTER.
  • Update the stub CloudAssetOrchestrator and CloudAssetStoreExtensions to store/retrieve CompiledLayout instead of LayoutGroup.
  • Replace/expand unit tests to validate DTO→runtime mapping behaviors (including unknown command type handling and gutter append).

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.

Show a summary per file
FileDescription
test/AdaptiveRemote.App.Tests/Services/Layout/RemoteLayoutDefinitionServiceTests.csReworks tests to validate mapping from CompiledLayout DTOs into runtime element types and gutter behavior
src/AdaptiveRemote.App/Services/Layout/RemoteLayoutDefinitionService.csSwitches layout loading to CompiledLayout + adds DTO mapping + appends GUTTER
src/AdaptiveRemote.App/Services/CloudAssets/CloudAssetStoreExtensions.csChanges GetLayout/SetLayout helpers to use CompiledLayout
src/AdaptiveRemote.App/Services/CloudAssets/CloudAssetOrchestrator.csUpdates stub orchestrator to seed a CompiledLayout DTO (without gutter)
src/AdaptiveRemote.App/Configuration/HostBuilderExtensions.csAdds DI registration for CompiledLayout from the asset store
global.jsonChanges pinned .NET SDK version

=> services
.AddApplicationLifecycleServices()
.AddCloudAssetServices()
.AddScoped(sp => sp.GetRequiredService<ICloudAssetStore>().Get<CompiledLayout>("layout"))

CopilotAIApr 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This registers a scoped CompiledLayout by directly calling ICloudAssetStore.Get("layout"), but there are no consumers of CompiledLayout via DI in AdaptiveRemote.App currently, so this factory likely never runs and the hard-coded asset name duplicates CloudAssetStoreExtensions’ LayoutAssetName constant. Either remove this registration until something depends on CompiledLayout, or register via the existing GetLayout() extension (to keep the asset name centralized) and add/retain a consumer that actually needs the CompiledLayout (e.g., stylesheet injection).

Suggested change
.AddScoped(sp => sp.GetRequiredService<ICloudAssetStore>().Get<CompiledLayout>("layout"))

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
OwnerAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will be resolved when CompiledLayout is being consumed directly instead of going through ICloudAssetStore.

Comment threadglobal.json Outdated
Comment on lines +38 to +43
List<RemoteLayoutElement> elements = layout.Elements
.Select(MapElement)
.ToList();
elements.Add(BuildGutter());

return new LayoutGroup("ROOT", elements);

CopilotAIApr 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RemoteLayoutDefinitionService unconditionally appends a hardcoded GUTTER group without checking whether the downloaded CompiledLayout already contains a GUTTER element. The backend hardcoded CompiledLayout currently includes a "GUTTER" LayoutGroupDefinitionDto, so this would produce duplicate gutter UI (and duplicate lifecycle commands) when consuming real backend layouts. Consider validating that no element/group has CssId == "GUTTER" (throw a clear error) or skipping the append when a gutter is already present, and align backend output accordingly.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
OwnerAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not fixed.

Comment on lines +54 to +57
CommandType.TiVo => new TiVoCommand(
cmd.Name, placement: null, label: cmd.Label,
cssid: cmd.CssId, glyph: cmd.Glyph, reverse: cmd.Reverse,
speakName: cmd.SpeakPhrase),

CopilotAIApr 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TiVoCommand’s constructor already wraps the provided speakName with Phrases.Conversation_Sent(...). If CommandDefinitionDto.SpeakPhrase is already a full phrase like "Sent Up" (as produced by the current backend hardcoded layout), the resulting TiVoCommand.SpeakPhrase becomes "Sent Sent Up". Either ensure the contract always provides an unprefixed speak name for TiVo/IR commands (and rename the DTO field accordingly), or defensively normalize/strip the "Sent " prefix before passing it into the constructor.

Copilot uses AI. Check for mistakes.
Comment on lines +59 to +62
CommandType.IR => new IRCommand(
cmd.Name, placement: null, label: cmd.Label,
cssid: cmd.CssId, glyph: cmd.Glyph, reverse: cmd.Reverse,
speakName: cmd.SpeakPhrase),

CopilotAIApr 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IRCommand has the same speak phrase wrapping behavior as TiVoCommand (it always calls Phrases.Conversation_Sent(speakName)). Passing CommandDefinitionDto.SpeakPhrase through directly can double-prefix ("Sent Sent ...") if the DTO already contains a full phrase. Consider normalizing the DTO value (or clarifying the contract) before constructing IRCommand.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
OwnerAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this was partially fixed. There's a new speakName and speakPhrase difference, but we're using speakName here which will still duplicate the speech.

…CloudAsset helper, and SDK rollForward fix
- TiVoCommand and IRCommand gain an optional speakPhrase parameter so callers
can supply a final phrase directly instead of going through Phrases.Conversation_Sent
- AddScopedCloudAsset<T> added to CloudAssetServiceExtensions with required
where T : class constraint to satisfy AddScoped<TService> generic constraint
- global.json rollForward changed to latestFeature to allow SDK 10.0.106
https://claude.ai/code/session_014dCcJFZ1bZ59uxEBPPNwGN
@github-actions

github-actionsBot commented Apr 19, 2026

Copy link
Copy Markdown

Test Results

435 tests 435 ✅ 3m 4s ⏱️
12 suites 0 💤
12 files 0 ❌

Results for commit a7725fe.

♻️ This comment has been updated with latest results.

…kPhrase, validate GUTTER, use AddScopedCloudAsset helper
- RemoteLayoutDefinitionService now takes CompiledLayout directly (not ICloudAssetStore);
BuildRoot is called eagerly in the constructor — no Lazy needed
- TiVo and IR commands use speakPhrase: cmd.SpeakPhrase so the DTO value is passed
through as-is without double-wrapping via Phrases.Conversation_Sent
- BuildRoot throws if the layout already contains a GUTTER element, preventing
duplicate gutter groups when consuming real backend layouts
- CloudAssetOrchestrator updated so SpeakPhrase values are full phrases ("Sent Up"
etc.) rather than raw names, matching the speakPhrase contract
- HostBuilderExtensions uses .AddScopedCloudAsset<CompiledLayout>("layout") instead
of an inline lambda, keeping the asset name centralized in CloudAssetStoreExtensions
- Tests updated to construct RemoteLayoutDefinitionService with CompiledLayout
directly; EmptyStoreThrowsDescriptiveError replaced with GutterInLayoutThrowsDescriptiveError
https://claude.ai/code/session_014dCcJFZ1bZ59uxEBPPNwGN
=> services
.AddApplicationLifecycleServices()
.AddCloudAssetServices()
.AddScoped(sp => sp.GetRequiredService<ICloudAssetStore>().Get<CompiledLayout>("layout"))

Copy link
Copy Markdown
OwnerAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a helper extension method that takes a generic parameter and asset name, and move it to the CloudAssetHostBuilderExtensions.

Copy link
Copy Markdown
OwnerAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was partially fixed. The new helper was added but it is not being used here.

public RemoteLayoutDefinitionService(ICloudAssetStore store)
{
_store = store;
_remoteRoot = new Lazy<RemoteLayoutElement>(() => BuildRoot(store));

Copy link
Copy Markdown
OwnerAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of using Lazy, just call BuildEoot right away. It's definitely going to be needed. If it fails, the whole app will fail.

Copy link
Copy Markdown
OwnerAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change was not made. Please fix.

public RemoteLayoutElement RemoteRoot
public RemoteLayoutElement RemoteRoot => _remoteRoot.Value;

private static RemoteLayoutElement BuildRoot(ICloudAssetStore store)

Copy link
Copy Markdown
OwnerAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just import CompiledLayout instead of ICloutAssetStore. It should already be registered as a scoped service

Copy link
Copy Markdown
OwnerAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change was not made. Please fix.

=> services
.AddApplicationLifecycleServices()
.AddCloudAssetServices()
.AddScoped(sp => sp.GetRequiredService<ICloudAssetStore>().Get<CompiledLayout>("layout"))

Copy link
Copy Markdown
OwnerAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will be resolved when CompiledLayout is being consumed directly instead of going through ICloudAssetStore.

Comment on lines +38 to +43
List<RemoteLayoutElement> elements = layout.Elements
.Select(MapElement)
.ToList();
elements.Add(BuildGutter());

return new LayoutGroup("ROOT", elements);

Copy link
Copy Markdown
OwnerAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not fixed.

Comment on lines +59 to +62
CommandType.IR => new IRCommand(
cmd.Name, placement: null, label: cmd.Label,
cssid: cmd.CssId, glyph: cmd.Glyph, reverse: cmd.Reverse,
speakName: cmd.SpeakPhrase),

Copy link
Copy Markdown
OwnerAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this was partially fixed. There's a new speakName and speakPhrase difference, but we're using speakName here which will still duplicate the speech.

@jodavis
jodavis merged commit c8f9fca into feature/ADR-162-client-side-layout-updatesApr 19, 2026
2 checks passed
@jodavis
jodavis deleted the claude/implement-adr-177-6YzM2 branch April 19, 2026 15:38
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@jodavis@claude