Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 40 additions & 46 deletions apps/menubar/Sources/Windows/FloatingPetController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,14 @@ extension FloatingPetWindowControlling {
func updateIdleEscalationConfig(_ config: IdleEscalationConfig) {}
}

/// Unified interface for a floating-pet panel renderer, covering both the
/// full "Own"-mode panel (`FloatingPetPanelController`) and the compact
/// "Minimalist" strip (`MinimalistPanelController`) — one renderer
/// interface, two skins. Members outside a conformer's shape get a
/// default no-op via the extension below, so neither renderer is forced
/// to implement members that don't apply to it.
@MainActor
protocol FloatingPetPanelManaging: AnyObject {
protocol PanelManaging: AnyObject {
func show(frame: CGRect)
func hide()
func apply(state: ActivityState, visualMode: VisualMode)
Expand All @@ -85,38 +91,58 @@ protocol FloatingPetPanelManaging: AnyObject {
func setHUDPinned(_ pinned: Bool)
func setInteraction(_ interaction: FloatingInteraction?)
func setFrameChangeHandler(_ handler: @escaping (CGRect) -> Void)
/// Assigned session number for this panel (nil clears the session badge row).
/// Assigned session number for this window (nil clears the session badge row).
func applySessionNumber(_ number: Int?)
/// User-set rename label for this session, or `nil` to fall back to "Session N".
/// User-set rename label for this session (from `SessionLabelStore`), or
/// `nil` to fall back to "Session N". No-op for plain-origin/"combined"
/// windows, mirroring `applySessionNumber`.
func applySessionLabel(_ label: String?)
/// Last submitted prompt for this exact session, shown as a delayed hover tooltip.
/// Last submitted prompt for this exact session, shown as a delayed hover
/// tooltip on the session badge. `nil`/empty clears the tooltip.
func applySessionTooltip(_ summary: String?)
/// Reversible P15.08 conflict-bubble presentation; `nil` hides it.
func applyConflictBubble(_ payload: ConflictBubblePayload?)
/// Live-updates the idle-escalation thresholds for this panel's scene.
func updateIdleEscalationConfig(_ config: IdleEscalationConfig)
func applyActivity(_ state: ActivityState)
func applyPromptSummary(_ summary: String)
func applyBadgeScale(_ scale: Double)
}

extension FloatingPetPanelManaging {
func applyPromptTimerStatus(_ status: PromptTimerStatus?) {}
extension PanelManaging {
// Floating-only members: default no-ops so MinimalistPanelController
// (which has no sprite, RPG HUD, interaction, or idle-escalation concept)
// isn't forced to implement them.
func apply(state: ActivityState, visualMode: VisualMode) {}
func replacePets(codexPet: CodexPet, codogotchiPet: CodogotchiPet?) {}
func applyAttention(payload: AttentionPayload?, sourceEvent: SourceEvent?) {}
func applyGateBadge(content: GateBadgeContent?) {}
func applyPlatform(origin: String?) {}
func applyRPGState(halfHearts: Int, levelFraction: Double, level: Int, activeMinutes: Int, hudEnabled: Bool) {}
func setRPGHUDEnabled(_ enabled: Bool) {}
func setHUDDemoActive(_ active: Bool) {}
func setHUDPinned(_ pinned: Bool) {}
func setInteraction(_ interaction: FloatingInteraction?) {}
func updateIdleEscalationConfig(_ config: IdleEscalationConfig) {}

// Minimalist-only members: default no-ops so FloatingPetPanelController
// (which renders a sprite, not a compact strip) isn't forced to implement them.
func applyActivity(_ state: ActivityState) {}
func applyPromptSummary(_ summary: String) {}
func applyBadgeScale(_ scale: Double) {}

// Shared members with default no-ops (kept for convenience; both
// conformers implement these directly).
func applyPromptTimerStatus(_ status: PromptTimerStatus?) {}
func applyAttention(payload: AttentionPayload?, sourceEvent: SourceEvent?) {}
func applyGateBadge(content: GateBadgeContent?) {}
func applyPlatform(origin: String?) {}
func applySessionNumber(_ number: Int?) {}
func applySessionLabel(_ label: String?) {}
func applySessionTooltip(_ summary: String?) {}
func applyConflictBubble(_ payload: ConflictBubblePayload?) {}
func updateIdleEscalationConfig(_ config: IdleEscalationConfig) {}
}

@MainActor
final class FloatingPetController: NSObject, FloatingPetVisibilityControlling, FloatingPetWindowControlling {
private let panel: FloatingPetPanelManaging
private let panel: PanelManaging
private let visibleFrameProvider: () -> CGRect
private let saveState: (FloatingAppState) throws -> Void
private var state: FloatingAppState
Expand All @@ -128,7 +154,7 @@ final class FloatingPetController: NSObject, FloatingPetVisibilityControlling, F
var onVisibilityChanged: ((Bool) -> Void)?

init(
panel: FloatingPetPanelManaging,
panel: PanelManaging,
visibleFrameProvider: @escaping () -> CGRect,
saveState: @escaping (FloatingAppState) throws -> Void = AppStateStore.save,
notificationCenter: NotificationCenter = .default,
Expand Down Expand Up @@ -299,42 +325,10 @@ final class FloatingPetController: NSObject, FloatingPetVisibilityControlling, F
}
}

@MainActor
protocol MinimalistPanelManaging: AnyObject {
func show(frame: CGRect)
func hide()
func applyPlatform(origin: String?)
func applyActivity(_ state: ActivityState)
/// Pool-computed prompt-timer status for display (see `PromptTimerTracker`).
func applyPromptTimerStatus(_ status: PromptTimerStatus?)
func applyAttention(payload: AttentionPayload?, sourceEvent: SourceEvent?)
func applyPromptSummary(_ summary: String)
func applyBadgeScale(_ scale: Double)
func applyGateBadge(content: GateBadgeContent?)
func setFrameChangeHandler(_ handler: @escaping (CGRect) -> Void)
/// Assigned session number for this strip (nil clears the session badge row).
func applySessionNumber(_ number: Int?)
/// User-set rename label for this session (from `SessionLabelStore`), or
/// `nil` to fall back to "Session N". No-op for plain-origin/"combined"
/// windows, mirroring `applySessionNumber`.
func applySessionLabel(_ label: String?)
/// Last submitted prompt for this exact session, shown as a delayed hover
/// tooltip on the session badge. `nil`/empty clears the tooltip.
func applySessionTooltip(_ summary: String?)
/// Reversible P15.08 conflict-bubble presentation; `nil` hides it.
func applyConflictBubble(_ payload: ConflictBubblePayload?)
}

extension MinimalistPanelManaging {
func applyPromptTimerStatus(_ status: PromptTimerStatus?) {}
func applySessionLabel(_ label: String?) {}
func applySessionTooltip(_ summary: String?) {}
}

@MainActor
final class MinimalistWindowController: NSObject, FloatingPetWindowControlling {
private let origin: WindowKey
private let panel: MinimalistPanelManaging
private let panel: PanelManaging
private let visibleFrameProvider: () -> CGRect
private let saveState: (FloatingAppState) throws -> Void
private let notificationCenter: NotificationCenter
Expand All @@ -352,7 +346,7 @@ final class MinimalistWindowController: NSObject, FloatingPetWindowControlling {

init(
origin: WindowKey,
panel: MinimalistPanelManaging,
panel: PanelManaging,
visibleFrameProvider: @escaping () -> CGRect,
saveState: @escaping (FloatingAppState) throws -> Void = AppStateStore.save,
notificationCenter: NotificationCenter = .default,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import AppKit

@MainActor
final class FloatingPetPanelController: FloatingPetPanelManaging {
final class FloatingPetPanelController: PanelManaging {
private var codexPet: CodexPet
private var codogotchiPet: CodogotchiPet?
private let demoFrameInterval: TimeInterval?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import AppKit
/// clipped repaints when a dismiss and a combined-window chip swap resized the
/// same panel in one tick. With two panels neither failure mode is reachable.
@MainActor
final class MinimalistPanelController: MinimalistPanelManaging {
final class MinimalistPanelController: PanelManaging {
private enum Layout {
static let height: CGFloat = 58
/// Extra vertical room for the `PlatformSessionBadge` row, only added
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import XCTest

@MainActor
final class FloatingPetControllerTests: XCTestCase {
final class FloatingPetPanelSpy: FloatingPetPanelManaging {
final class FloatingPetPanelSpy: PanelManaging {
var shownFrames: [CGRect] = []
var hideCount = 0
var appliedStates: [(ActivityState, VisualMode)] = []
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ private final class StubWindowController: FloatingPetWindowControlling {
}

@MainActor
private final class StubMinimalistPanel: MinimalistPanelManaging {
private final class StubMinimalistPanel: PanelManaging {
var visible = false
var platformOrigin: String?
var activityLabel = ""
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"schemaVersion": 1,
"recordedAt": "2026-07-12T07:48:56.116Z",
"outcome": "skipped",
"note": "PR review disabled by policy",
"prBodyRefresh": {
"attemptedAt": "2026-07-12T07:48:59.138Z",
"status": "updated"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"ticket": "P17.04",
"invocations": [
{
"runnerKind": "codex-cli",
"reviewedHeadSha": "ff35ca6c9ed2263f3455e8e67b2e5f669516059c",
"outcome": "clean",
"completedAt": "2026-07-12T07:48:08.231Z",
"terminatedReason": "completed",
"rawOutput": "docs/product/delivery/phase-17/reviews/P17.04-subagent-review.report.md",
"filledPrompt": "docs/product/delivery/phase-17/reviews/P17.04-subagent-review.prompt.md",
"fallbackLevel": "preferred",
"schemaVersion": 1,
"primaryAgent": "claude",
"runnerSelfReport": "completed",
"fallbackFrom": null,
"findings": [],
"probedSurfaces": [],
"patches": []
}
]
}
Loading