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
6 changes: 5 additions & 1 deletion LilAgents/AgentSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Foundation
// MARK: - Provider

enum AgentProvider: String, CaseIterable {
case claude, codex, copilot, gemini
case claude, codex, copilot, gemini, qoder

private static let defaultsKey = "selectedProvider"

Expand All @@ -23,6 +23,7 @@ enum AgentProvider: String, CaseIterable {
case .codex: return "Codex"
case .copilot: return "Copilot"
case .gemini: return "Gemini"
case .qoder: return "Qoder"
}
}

Expand All @@ -49,6 +50,8 @@ enum AgentProvider: String, CaseIterable {
return "To install, run this in Terminal:\n brew install copilot-cli\n\nOr: npm install -g @github/copilot-cli"
case .gemini:
return "To install, run this in Terminal:\n npm install -g @google/gemini-cli\n\nThen authenticate:\n gemini auth"
case .qoder:
return "To install, run this in Terminal:\n curl -fsSL https://qoder.ai/install.sh | sh\n\nOr download from https://qoder.ai/download"
}
}

Expand All @@ -58,6 +61,7 @@ enum AgentProvider: String, CaseIterable {
case .codex: return CodexSession()
case .copilot: return CopilotSession()
case .gemini: return GeminiSession()
case .qoder: return QoderSession()
}
}
}
Expand Down
64 changes: 52 additions & 12 deletions LilAgents/LilAgentsController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,18 @@ class LilAgentsController {
private static let onboardingKey = "hasCompletedOnboarding"
private var isHiddenForEnvironment = false

// Energy optimization: cached dock geometry
private struct DockGeometry {
var x: CGFloat
var width: CGFloat
var topY: CGFloat
var screenWidth: CGFloat
var screenOriginY: CGFloat
}
private var cachedDock: DockGeometry?
private var lastDockCheckTime: CFTimeInterval = 0
private static let dockCacheInterval: CFTimeInterval = 2.0 // Re-check every 2s

func start() {
let char1 = WalkerCharacter(videoName: "walk-bruce-01")
char1.accelStart = 3.0
Expand All @@ -30,8 +42,8 @@ class LilAgentsController {
char1.flipXOffset = 0
char2.flipXOffset = -9

char1.positionProgress = 0.3
char2.positionProgress = 0.7
char1.positionProgress = 0.2
char2.positionProgress = 0.8

char1.pauseEndTime = CACurrentMediaTime() + Double.random(in: 0.5...2.0)
char2.pauseEndTime = CACurrentMediaTime() + Double.random(in: 8.0...14.0)
Expand Down Expand Up @@ -135,12 +147,26 @@ class LilAgentsController {

// MARK: - Display Link

// Energy optimization: throttle tick rate.
// Characters walk at ~10fps visually; 60fps is wasteful for window positioning.
private static let tickInterval: CFTimeInterval = 1.0 / 15.0 // 15fps
private var lastTickTime: CFTimeInterval = 0

private func startDisplayLink() {
CVDisplayLinkCreateWithActiveCGDisplays(&displayLink)
guard let displayLink = displayLink else { return }

let callback: CVDisplayLinkOutputCallback = { _, _, _, _, _, userInfo -> CVReturn in
let controller = Unmanaged<LilAgentsController>.fromOpaque(userInfo!).takeUnretainedValue()
let now = CVGetCurrentHostTime()
let nowSeconds = Double(now) / Double(CVGetHostClockFrequency())

// Throttle: skip frames faster than 1/15s
if nowSeconds - controller.lastTickTime < 1.0 / 15.0 {
return kCVReturnSuccess
}
controller.lastTickTime = nowSeconds

DispatchQueue.main.async {
controller.tick()
}
Expand Down Expand Up @@ -198,20 +224,30 @@ class LilAgentsController {
guard let screen = activeScreen else { return }
guard updateEnvironmentVisibility(for: screen) else { return }

let screenWidth = screen.frame.width
let dockX: CGFloat
let dockWidth: CGFloat
let dockTopY: CGFloat
let now = CACurrentMediaTime()

// Dock is on this screen — constrain to dock area
(dockX, dockWidth) = getDockIconArea(screenWidth: screenWidth)
dockTopY = screen.visibleFrame.origin.y
// Energy optimization: cache dock geometry, re-read periodically
if cachedDock == nil ||
now - lastDockCheckTime > Self.dockCacheInterval ||
cachedDock?.screenWidth != screen.frame.width ||
cachedDock?.screenOriginY != screen.frame.origin.y {
let (dx, dw) = getDockIconArea(screenWidth: screen.frame.width)
cachedDock = DockGeometry(
x: dx, width: dw,
topY: screen.visibleFrame.origin.y,
screenWidth: screen.frame.width,
screenOriginY: screen.frame.origin.y
)
lastDockCheckTime = now
}
let dockX = cachedDock!.x
let dockWidth = cachedDock!.width
let dockTopY = cachedDock!.topY

updateDebugLine(dockX: dockX, dockWidth: dockWidth, dockTopY: dockTopY)

let activeChars = characters.filter { $0.window.isVisible && $0.isManuallyVisible }

let now = CACurrentMediaTime()
let anyWalking = activeChars.contains { $0.isWalking }
for char in activeChars {
if char.isIdleForPopover { continue }
Expand All @@ -220,12 +256,16 @@ class LilAgentsController {
}
}
for char in activeChars {
char.update(dockX: dockX, dockWidth: dockWidth, dockTopY: dockTopY)
char.update(dockX: dockX, dockWidth: dockWidth, dockTopY: dockTopY, now: now)
}

// Energy optimization: only reassign window level when z-order changes
let sorted = activeChars.sorted { $0.positionProgress < $1.positionProgress }
for (i, char) in sorted.enumerated() {
char.window.level = NSWindow.Level(rawValue: NSWindow.Level.statusBar.rawValue + i)
let desiredLevel = NSWindow.Level(rawValue: NSWindow.Level.statusBar.rawValue + i)
if char.window.level != desiredLevel {
char.window.level = desiredLevel
}
}
}

Expand Down
Loading