Skip to content
Open
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
29 changes: 29 additions & 0 deletions LilAgents/DockMagnificationSettings.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import Foundation

/// User defaults for dock-like idle shrink (menu bar: "Shrink when idle" + delay).
enum DockMagnificationSettings {
private static let enabledKey = "dockShrinkWhenIdleEnabled"
private static let idleSecondsKey = "dockShrinkIdleSeconds"

static let idlePresetsSeconds: [Int] = [10, 15, 20, 30, 60]

static var isEnabled: Bool {
get {
if UserDefaults.standard.object(forKey: enabledKey) == nil { return true }
return UserDefaults.standard.bool(forKey: enabledKey)
}
set { UserDefaults.standard.set(newValue, forKey: enabledKey) }
}

/// Delay after popover closes before shrinking (seconds).
static var idleSeconds: TimeInterval {
get {
let v = UserDefaults.standard.integer(forKey: idleSecondsKey)
if v <= 0 { return 20 }
return TimeInterval(v)
}
set {
UserDefaults.standard.set(Int(newValue), forKey: idleSecondsKey)
}
}
}
46 changes: 46 additions & 0 deletions LilAgents/LilAgentsApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,20 @@ class AppDelegate: NSObject, NSApplicationDelegate {
soundItem.state = .on
menu.addItem(soundItem)

let shrinkWhenIdleItem = NSMenuItem(title: "Shrink when idle", action: #selector(toggleDockShrinkWhenIdle(_:)), keyEquivalent: "")
shrinkWhenIdleItem.state = DockMagnificationSettings.isEnabled ? .on : .off
menu.addItem(shrinkWhenIdleItem)

let shrinkAfterItem = NSMenuItem(title: "Shrink after", action: nil, keyEquivalent: "")
let shrinkAfterMenu = NSMenu()
for sec in DockMagnificationSettings.idlePresetsSeconds {
let it = NSMenuItem(title: "\(sec) seconds", action: #selector(setDockShrinkIdleDelay(_:)), keyEquivalent: "")
it.tag = sec
shrinkAfterMenu.addItem(it)
}
shrinkAfterItem.submenu = shrinkAfterMenu
menu.addItem(shrinkAfterItem)

// Provider submenu
let providerItem = NSMenuItem(title: "Provider", action: nil, keyEquivalent: "")
let providerMenu = NSMenu()
Expand Down Expand Up @@ -106,6 +120,19 @@ class AppDelegate: NSObject, NSApplicationDelegate {
menu.addItem(quitItem)

statusItem?.menu = menu
syncDockShrinkMenuItems()
}

private func syncDockShrinkMenuItems() {
guard let menu = statusItem?.menu else { return }
menu.item(withTitle: "Shrink when idle")?.state = DockMagnificationSettings.isEnabled ? .on : .off
guard let sm = menu.item(withTitle: "Shrink after")?.submenu else { return }
let cur = Int(DockMagnificationSettings.idleSeconds)
let enabled = DockMagnificationSettings.isEnabled
for item in sm.items {
item.state = item.tag == cur ? .on : .off
item.isEnabled = enabled
}
}

// MARK: - Menu Actions
Expand Down Expand Up @@ -221,6 +248,25 @@ class AppDelegate: NSObject, NSApplicationDelegate {
sender.state = WalkerCharacter.soundsEnabled ? .on : .off
}

@objc func toggleDockShrinkWhenIdle(_ sender: NSMenuItem) {
DockMagnificationSettings.isEnabled.toggle()
sender.state = DockMagnificationSettings.isEnabled ? .on : .off
syncDockShrinkMenuItems()
controller?.characters.forEach { $0.applyDockMagnificationSettingsChanged() }
}

@objc func setDockShrinkIdleDelay(_ sender: NSMenuItem) {
let sec = sender.tag
guard sec > 0 else { return }
DockMagnificationSettings.idleSeconds = TimeInterval(sec)
if let sm = sender.menu {
for item in sm.items {
item.state = item.tag == sec ? .on : .off
}
}
syncDockShrinkMenuItems()
}

@objc func quitApp() {
NSApp.terminate(nil)
}
Expand Down
105 changes: 90 additions & 15 deletions LilAgents/WalkerCharacter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,15 @@ class WalkerCharacter {
private var wasPopoverVisibleBeforeEnvironmentHide = false
private var wasBubbleVisibleBeforeEnvironmentHide = false

// Dock-like idle shrink: smaller when unused; click snaps back to full size (like dock magnification).
private static let dockCompactScale: CGFloat = 0.56
private var dockMagnification: CGFloat = 1.0
private var dockMagnificationTarget: CGFloat = 1.0
private var popoverDismissedAt: CFTimeInterval?

private var effectiveDisplayHeight: CGFloat { displayHeight * dockMagnification }
private var effectiveDisplayWidth: CGFloat { displayWidth * dockMagnification }

init(videoName: String) {
self.videoName = videoName
}
Expand All @@ -78,7 +87,7 @@ class WalkerCharacter {
playerLayer = AVPlayerLayer(player: queuePlayer)
playerLayer.videoGravity = .resizeAspect
playerLayer.backgroundColor = NSColor.clear.cgColor
playerLayer.frame = CGRect(x: 0, y: 0, width: displayWidth, height: displayHeight)
playerLayer.frame = CGRect(x: 0, y: 0, width: effectiveDisplayWidth, height: effectiveDisplayHeight)

let screen = NSScreen.main!
let dockTopY = screen.visibleFrame.origin.y
Expand All @@ -104,8 +113,10 @@ class WalkerCharacter {
hostView.wantsLayer = true
hostView.layer?.backgroundColor = NSColor.clear.cgColor
hostView.layer?.addSublayer(playerLayer)
hostView.autoresizingMask = [.width, .height]

window.contentView = hostView
popoverDismissedAt = CACurrentMediaTime()
window.orderFrontRegardless()
}

Expand Down Expand Up @@ -169,9 +180,78 @@ class WalkerCharacter {
}
}

// MARK: - Dock magnification

private func refreshDockMagnificationTarget(now: CFTimeInterval) {
if isOnboarding || isIdleForPopover {
dockMagnificationTarget = 1.0
return
}
if !DockMagnificationSettings.isEnabled {
dockMagnificationTarget = 1.0
return
}
if let t0 = popoverDismissedAt, now - t0 >= DockMagnificationSettings.idleSeconds {
dockMagnificationTarget = Self.dockCompactScale
} else {
dockMagnificationTarget = 1.0
}
}

private func tickDockMagnification() {
let now = CACurrentMediaTime()
refreshDockMagnificationTarget(now: now)
let delta = dockMagnificationTarget - dockMagnification
if abs(delta) < 0.003 {
dockMagnification = dockMagnificationTarget
return
}
let k: CGFloat = dockMagnificationTarget < dockMagnification ? 0.12 : 0.28
dockMagnification += delta * k
}

/// Horizontal center stays aligned with the full-size walk path; bottom stays anchored above the dock.
private func layoutWindowForDock(dockX: CGFloat, dockWidth: CGFloat, dockTopY: CGFloat) {
let w = effectiveDisplayWidth
let h = effectiveDisplayHeight
let travelFull = max(dockWidth - displayWidth, 0)
let leftFull = dockX + travelFull * positionProgress + currentFlipCompensation
let centerX = leftFull + displayWidth / 2
let x = centerX - w / 2
let fullPad = displayHeight * 0.15
let y = dockTopY - fullPad + yOffset

var frame = window.frame
frame.origin = NSPoint(x: x, y: y)
frame.size = NSSize(width: w, height: h)
window.setFrame(frame, display: true)

if let host = window.contentView {
host.frame = NSRect(x: 0, y: 0, width: w, height: h)
}
playerLayer.frame = CGRect(x: 0, y: 0, width: w, height: h)
}

private func snapDockMagnificationToFull() {
dockMagnification = 1.0
dockMagnificationTarget = 1.0
popoverDismissedAt = nil
}

/// Call when menu toggles idle shrink off so windows return to full size immediately.
func applyDockMagnificationSettingsChanged() {
if !DockMagnificationSettings.isEnabled {
snapDockMagnificationToFull()
}
}


// MARK: - Click Handling & Popover

func handleClick() {
if DockMagnificationSettings.isEnabled, dockMagnification < 0.98 {
snapDockMagnificationToFull()
}
if isOnboarding {
openOnboardingPopover()
return
Expand Down Expand Up @@ -236,10 +316,12 @@ class WalkerCharacter {
isPaused = true
pauseEndTime = CACurrentMediaTime() + Double.random(in: 1.0...3.0)
queuePlayer.seek(to: .zero)
popoverDismissedAt = CACurrentMediaTime()
controller?.completeOnboarding()
}

func openPopover() {
popoverDismissedAt = nil
// Close any other open popover
if let siblings = controller?.characters {
for sibling in siblings where sibling !== self && sibling.isIdleForPopover {
Expand Down Expand Up @@ -304,6 +386,8 @@ class WalkerCharacter {
func closePopover() {
guard isIdleForPopover else { return }

popoverDismissedAt = CACurrentMediaTime()

popoverWindow?.orderOut(nil)
removeEventMonitors()

Expand Down Expand Up @@ -773,13 +857,11 @@ class WalkerCharacter {
// MARK: - Frame Update

func update(dockX: CGFloat, dockWidth: CGFloat, dockTopY: CGFloat) {
tickDockMagnification()
currentTravelDistance = max(dockWidth - displayWidth, 0)

if isIdleForPopover {
let travelDistance = currentTravelDistance
let x = dockX + travelDistance * positionProgress + currentFlipCompensation
let bottomPadding = displayHeight * 0.15
let y = dockTopY - bottomPadding + yOffset
window.setFrameOrigin(NSPoint(x: x, y: y))
layoutWindowForDock(dockX: dockX, dockWidth: dockWidth, dockTopY: dockTopY)
updatePopoverPosition()
updateThinkingBubble()
return
Expand All @@ -791,11 +873,7 @@ class WalkerCharacter {
if now >= pauseEndTime {
startWalk()
} else {
let travelDistance = max(dockWidth - displayWidth, 0)
let x = dockX + travelDistance * positionProgress + currentFlipCompensation
let bottomPadding = displayHeight * 0.15
let y = dockTopY - bottomPadding + yOffset
window.setFrameOrigin(NSPoint(x: x, y: y))
layoutWindowForDock(dockX: dockX, dockWidth: dockWidth, dockTopY: dockTopY)
return
}
}
Expand All @@ -820,10 +898,7 @@ class WalkerCharacter {
return
}

let x = dockX + travelDistance * positionProgress + currentFlipCompensation
let bottomPadding = displayHeight * 0.15
let y = dockTopY - bottomPadding + yOffset
window.setFrameOrigin(NSPoint(x: x, y: y))
layoutWindowForDock(dockX: dockX, dockWidth: dockWidth, dockTopY: dockTopY)
}

updateThinkingBubble()
Expand Down
4 changes: 4 additions & 0 deletions lil-agents.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
A10000010000000000000032 /* ShellEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10000020000000000000031 /* ShellEnvironment.swift */; };
A10000010000000000000033 /* CodexSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10000020000000000000032 /* CodexSession.swift */; };
A10000010000000000000034 /* CopilotSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10000020000000000000033 /* CopilotSession.swift */; };
A10000010000000000000036 /* DockMagnificationSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10000020000000000000034 /* DockMagnificationSettings.swift */; };
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
Expand All @@ -43,6 +44,7 @@
A10000020000000000000031 /* ShellEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShellEnvironment.swift; sourceTree = "<group>"; };
A10000020000000000000032 /* CodexSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodexSession.swift; sourceTree = "<group>"; };
A10000020000000000000033 /* CopilotSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopilotSession.swift; sourceTree = "<group>"; };
A10000020000000000000034 /* DockMagnificationSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DockMagnificationSettings.swift; sourceTree = "<group>"; };
A10000030000000000000001 /* lil agents.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "lil agents.app"; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */

Expand Down Expand Up @@ -79,6 +81,7 @@
A10000020000000000000033 /* CopilotSession.swift */,
A10000020000000000000023 /* TerminalView.swift */,
A10000020000000000000024 /* WalkerCharacter.swift */,
A10000020000000000000034 /* DockMagnificationSettings.swift */,
A10000020000000000000025 /* LilAgentsController.swift */,
A10000020000000000000002 /* walk-bruce-01.mov */,
A10000020000000000000005 /* walk-jazz-01.mov */,
Expand Down Expand Up @@ -186,6 +189,7 @@
A10000010000000000000034 /* CopilotSession.swift in Sources */,
A10000010000000000000023 /* TerminalView.swift in Sources */,
A10000010000000000000024 /* WalkerCharacter.swift in Sources */,
A10000010000000000000036 /* DockMagnificationSettings.swift in Sources */,
A10000010000000000000025 /* LilAgentsController.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down