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
36 changes: 35 additions & 1 deletion LilAgents/CharacterContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,41 @@ class CharacterContentView: NSView {
return hitRect.contains(localPoint) ? self : nil
}

private var dragStartPoint: NSPoint?
private var windowStartOrigin: NSPoint?
private var isDragging = false

override func mouseDown(with event: NSEvent) {
character?.handleClick()
dragStartPoint = NSEvent.mouseLocation
windowStartOrigin = window?.frame.origin
isDragging = false
}

override func mouseDragged(with event: NSEvent) {
guard let startPoint = dragStartPoint, let startOrigin = windowStartOrigin else { return }
let current = NSEvent.mouseLocation
let dx = current.x - startPoint.x
let dy = current.y - startPoint.y

if !isDragging && (abs(dx) > 5 || abs(dy) > 5) {
isDragging = true
character?.startDrag()
}

if isDragging {
window?.setFrameOrigin(NSPoint(x: startOrigin.x + dx, y: startOrigin.y + dy))
character?.trackDragVelocity()
}
}

override func mouseUp(with event: NSEvent) {
if !isDragging {
character?.handleClick()
} else {
character?.endDrag()
}
isDragging = false
dragStartPoint = nil
windowStartOrigin = nil
}
}
8 changes: 7 additions & 1 deletion LilAgents/ClaudeSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ class ClaudeSession {
private var lineBuffer = ""
private(set) var isRunning = false
private(set) var isBusy = false // true between send() and result
private var currentStreamingResponse = ""
private static var claudePath: String?
private static var shellEnvironment: [String: String]?

Expand Down Expand Up @@ -262,6 +263,7 @@ class ClaudeSession {
for block in content {
let blockType = block["type"] as? String ?? ""
if blockType == "text", let text = block["text"] as? String {
currentStreamingResponse += text
onText?(text)
} else if blockType == "tool_use" {
let toolName = block["name"] as? String ?? "Tool"
Expand Down Expand Up @@ -304,9 +306,13 @@ class ClaudeSession {

case "result":
isBusy = false
if let result = json["result"] as? String, !result.isEmpty {
let responseText = currentStreamingResponse.trimmingCharacters(in: .whitespacesAndNewlines)
if !responseText.isEmpty {
history.append(Message(role: .assistant, text: responseText))
} else if let result = json["result"] as? String, !result.isEmpty {
history.append(Message(role: .assistant, text: result))
}
currentStreamingResponse = ""
onTurnComplete?()

default:
Expand Down
83 changes: 83 additions & 0 deletions LilAgents/WalkerCharacter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,65 @@ class WalkerCharacter {
window.orderFrontRegardless()
}

// MARK: - Drag Support

var isBeingDragged = false
private var dragVelocityX: CGFloat = 0
private var lastDragX: CGFloat = 0
private var lastDragTime: CFTimeInterval = 0
private var isSliding = false
private var slideVelocity: CGFloat = 0
private var slideY: CGFloat = 0
private var lastDockX: CGFloat = 0
private var lastDockTopY: CGFloat = 0

func startDrag() {
isBeingDragged = true
isSliding = false
isWalking = false
isPaused = true
lastDragX = NSEvent.mouseLocation.x
lastDragTime = CACurrentMediaTime()
dragVelocityX = 0
}

func trackDragVelocity() {
let now = CACurrentMediaTime()
let currentX = NSEvent.mouseLocation.x
let dt = now - lastDragTime
if dt > 0.001 {
dragVelocityX = (currentX - lastDragX) / CGFloat(dt)
}
lastDragX = currentX
lastDragTime = now
}

func endDrag() {
isBeingDragged = false
// Start ice-slide with fling velocity
let clampedVelocity = max(-2000, min(2000, dragVelocityX))
if abs(clampedVelocity) > 50 {
isSliding = true
slideVelocity = clampedVelocity
slideY = window.frame.origin.y
} else {
isSliding = false
syncPositionFromWindow()
pauseEndTime = CACurrentMediaTime() + 2.0
}
}

private func syncPositionFromWindow() {
if currentTravelDistance > 0 {
let rawProgress = (window.frame.origin.x - lastDockX - currentFlipCompensation) / currentTravelDistance
positionProgress = max(0, min(1, rawProgress))
walkStartPixel = currentTravelDistance * positionProgress
walkEndPixel = walkStartPixel
walkStartPos = positionProgress
walkEndPos = positionProgress
}
}

// MARK: - Click Handling & Popover

func handleClick() {
Expand Down Expand Up @@ -710,6 +769,30 @@ class WalkerCharacter {

func update(dockX: CGFloat, dockWidth: CGFloat, dockTopY: CGFloat) {
currentTravelDistance = max(dockWidth - displayWidth, 0)
lastDockX = dockX
lastDockTopY = dockTopY
// Don't override window position while user is dragging
if isBeingDragged { return }
// Ice-slide after fling
if isSliding {
let friction: CGFloat = 0.92
slideVelocity *= friction
let dx = slideVelocity * (1.0 / 60.0)
var newX = window.frame.origin.x + dx
// Bounce off screen edges
let screen = window.screen ?? NSScreen.main!
let minX = screen.frame.origin.x
let maxX = screen.frame.origin.x + screen.frame.width - displayWidth
if newX < minX { newX = minX; slideVelocity = -slideVelocity * 0.5 }
if newX > maxX { newX = maxX; slideVelocity = -slideVelocity * 0.5 }
window.setFrameOrigin(NSPoint(x: newX, y: slideY))
if abs(slideVelocity) < 10 {
isSliding = false
syncPositionFromWindow()
pauseEndTime = CACurrentMediaTime() + 2.0
}
return
}
if isIdleForPopover {
let travelDistance = currentTravelDistance
let x = dockX + travelDistance * positionProgress + currentFlipCompensation
Expand Down