tvOS T1 Device-Iteration 2 (Phase 1) Implementation Plan¶
For agentic workers: REQUIRED SUB-SKILL: superpowers:subagent-driven-development or superpowers:executing-plans. Checkbox (
- [ ]) steps.
Goal: Make tvOS preset transitions seamless (no re-seed/restart), add select-click cycle toggle, fix the overlay cycle toggle (rename/center/reachable), and add a tvOS app icon + static Top Shelf.
Architecture: App-layer only. RenderCoordinator.switchPreset re-seeds only on a structural change; the cycler completes its morph via a structure-holding AppState.adoptTransition so no structural change occurs → no re-seed. Engine/WGSL untouched.
Spec: docs/superpowers/specs/2026-06-16-tvos-t1-device-iteration-2-design.md
Task 1: Structural-change predicate + seamless switchPreset¶
Files: Modify Sources/Player/RenderCoordinator.swift; Test Tests/RenderCoordinatorTests.swift.
- [ ] Step 1: Failing test (append to
RenderCoordinatorTests):
func testIsStructuralChange() {
let p = anyPreset().params
var look = p; look.bloomIntensity += 0.5 // a non-structural (look) param
XCTAssertFalse(RenderCoordinator.isStructuralChange(look, p))
var count = p; count.agentCount = p.agentCount / 2 + 1
XCTAssertTrue(RenderCoordinator.isStructuralChange(count, p))
var pop = p; pop.species[0].populationShare += 0.1
XCTAssertTrue(RenderCoordinator.isStructuralChange(pop, p))
}
-
[ ] Step 2: Run → fails (
isStructuralChangeundefined).xcodebuild test … -only-testing:PhysarumAppTests/RenderCoordinatorTests -destination 'platform=macOS'. -
[ ] Step 3: Add the helper + reuse it. In
RenderCoordinator:
/// A change that requires re-seeding the sim (vs a live look-only edit).
static func isStructuralChange(_ a: PresetParams, _ b: PresetParams) -> Bool {
a.agentCount != b.agentCount
|| a.spawnPattern != b.spawnPattern
|| zip(a.species, b.species).contains { $0.populationShare != $1.populationShare }
}
Rewrite setParams's needsRespawn to let needsRespawn = Self.isStructuralChange(p, params).
Rewrite switchPreset:
func switchPreset(_ preset: Preset, live: PresetParams) {
guard preset.id != currentPresetID else { return }
currentPresetID = preset.id
let structural = Self.isStructuralChange(live, params)
params = live
guard structural else { sim.updateLiveParams(live); return }
respawnWork?.cancel()
if let s = try? Self.makeSim(device: device, lib: lib, field: currentField, preset: live, tier: tier, queue: queue) {
sim = s
renderer.resetFeedback()
}
}
- [ ] Step 4: Run → passes. Same command.
- [ ] Step 5: Commit
feat(tvos): switchPreset re-seeds only on structural change (seamless look adoption).
Task 2: Structure-holding transition completion¶
Files: Modify Sources/Model/AppState.swift, Sources/Ambient/AmbientController.swift; Test Tests/AmbientControllerTests.swift.
- [ ] Step 1: Failing test (append to
AmbientControllerTests):
func testTransitionHoldsStructureNoReseed() {
let (state, presets, c) = make() // selected presets[0], morphDuration 1.0
let from = presets[0].params
let target = presets[1]
c.apply(.next)
c.tick(1.1) // cross t=1
XCTAssertEqual(state.selectedID, target.id)
XCTAssertEqual(state.baseline, target.params)
// edited keeps the running sim's (from) structure → no structural change → no re-seed
XCTAssertEqual(state.edited.agentCount, from.agentCount)
XCTAssertEqual(state.edited.spawnPattern, from.spawnPattern)
for i in 0..<3 { XCTAssertEqual(state.edited.species[i].populationShare, from.species[i].populationShare) }
XCTAssertFalse(RenderCoordinator.isStructuralChange(state.edited, from))
}
(Requires PresetParams: Equatable for baseline ==; if it isn't, assert field-wise instead — check and adjust.)
-
[ ] Step 2: Run → fails (
adoptTransitionundefined / still callsselect). -
[ ] Step 3: Add
AppState.adoptTransition:
/// Adopt a preset as the new selection during a seamless cycler transition: update
/// selection + baseline but DON'T touch `edited` — the morph already left it at the
/// target's look while holding the running structure, so no re-seed is triggered.
func adoptTransition(id: String) {
guard let p = presets.first(where: { $0.id == id }) else { return }
selectedID = id
baseline = p.params
undoStack.removeAll(); redoStack.removeAll()
}
- [ ] Step 4: In
AmbientController.tick, replaceappState.select(id: m.toID)withappState.adoptTransition(id: m.toID)(keep the surroundingmorph = nil; dwellElapsed = 0). - [ ] Step 5: Run → passes. If
baseline ==fails to compile, compare a representative field (e.g.state.baseline.bloomIntensity) or add field-wise asserts. - [ ] Step 6: Commit
feat(tvos): cycler transition holds structure (no re-seed) via adoptTransition.
Task 3: Select-click toggles the cycle¶
Files: Modify Sources/TV/TVPlayerView.swift. (View — no unit test; G2 logic already covered by AmbientController toggle tests.)
- [ ] Step 1: Add to the
MetalViewmodifier chain (next to.onPlayPauseCommand):
.onTapGesture { if !controller.overlayVisible { withAnimation { controller.apply(.togglePlaylist) } } }
- [ ] Step 2: tvOS-sim build green (Task 6).
- [ ] Step 3: Commit
feat(tvos): touchpad select-click toggles the cycle.
Task 4: Overlay cycle toggle — rename, center, reachable¶
Files: Modify Sources/TV/TVOverlayView.swift.
- [ ] Step 1: Replace the header
HStack { name; Spacer; Button("Pause/Resume Cycle") }with a centered name + a centered, focusable cycle toggle above the carousel:
VStack(spacing: 24) {
Text(appState.selectedPreset.name)
.font(.largeTitle.bold())
.foregroundStyle(.white)
.frame(maxWidth: .infinity)
Button {
controller.apply(.togglePlaylist)
} label: {
Label(controller.isCycling ? "Cycle On" : "Cycle Off",
systemImage: controller.isCycling ? "arrow.triangle.2.circlepath" : "pause.fill")
}
.frame(maxWidth: .infinity)
ScrollView(.horizontal, showsIndicators: false) {
LazyHStack(spacing: 32) { /* unchanged cards */ }
.padding(.horizontal, 80).padding(.vertical, 40)
}
.focusSection()
}
(Keep alignment neutral now that content is centered; keep the existing .padding(60).background(.black.opacity(0.55)) on the inner VStack and the root .onMoveCommand { .down → dismiss } + .onAppear { focusedCard = appState.selectedID }.)
- [ ] Step 2: tvOS-sim build green.
- [ ] Step 3: Commit
feat(tvos): overlay cycle toggle renamed Cycle On/Off, centered + reachable.
Task 5: tvOS app icon + static Top Shelf (Brand Assets)¶
Files: Create scripts/gen-tvos-icons.sh, Resources/Assets.xcassets/TVBrandAssets.brandassets/**; Modify project.yml.
- [ ] Step 1: Write
scripts/gen-tvos-icons.sh— render fromassets/icon.svg(+ KSP background) the canvases: 400×240, 1280×768 (app-icon layers, square logo centered on bg, fit to 5:3); 1920×720 & 3840×1440 (Top Shelf); 2320×720 & 4640×1440 (Top Shelf Wide). Usesips/qlmanage+ a compositing step (background fill + centered logo). Reproducible, likegen-icons.sh. - [ ] Step 2: Build
TVBrandAssets.brandassetswith correctContents.jsonfor:App Icon.imagestack(single.imagestacklayer→Content.imageset400×240),App Icon - App Store.imagestack(1280×768),Top Shelf Image.imageset(1920×720 @1x, 3840×1440 @2x),Top Shelf Image Wide.imageset(2320×720 @1x, 4640×1440 @2x). Brand-assetsContents.jsondeclaresprimary-app-icon+ the two top-shelf roles. - [ ] Step 3:
project.yml— add under targetsettings.base:"ASSETCATALOG_COMPILER_APPICON_NAME[sdk=appletvos*]": "TVBrandAssets"(macOS/iOS keepAppIcon). Regenerate. - [ ] Step 4: tvOS-sim build green (asset catalog compiles, no missing-icon error). Boot sim, install, confirm the icon appears in the sim Home screen.
- [ ] Step 5: Commit
feat(tvos): add tvOS app icon + static Top Shelf (brand assets).
Task 6: Gates + device deploy¶
- [ ] Full macOS host suite green (
xcodebuild test … -destination 'platform=macOS'). - [ ] tvOS-sim build green; screenshot.
- [ ] Adversarial multi-lens review (workflow) of the diff; fix confirmed findings.
- [ ] Release device build (
platform=tvOS,id=c2f1eba2a1841970a35e5e333fb1ba5dd1985bd9,-allowProvisioningUpdates) +devicectl install/launchto ATV70720F26-…. - [ ] Hand to Johannes: seamless transition (no restart), select-click toggle + HUD, overlay "Cycle On/Off" reachable + centered, app icon on Home screen, static top shelf.
Self-Review¶
Spec coverage: G1 → Tasks 1+2 (+tests); G2 → Task 3; G3 → Task 4; G4 → Task 5. Placeholders: none (Task 5 asset PNGs are generated by the scripted step). Type consistency: isStructuralChange(_:_:) defined in Task 1 and reused in Tasks 1 (setParams) + 2 (test); adoptTransition(id:) defined in Task 2 and called by AmbientController; togglePlaylist matches existing ControlIntent. Risk: PresetParams: Equatable for the baseline == assert — verify in Task 2 Step 5, fall back to field-wise; overlay focus + brand-asset structure are empirical/fiddly → gated by tvOS-sim build + device sign-off.