diff --git a/LilAgents/CharacterContentView.swift b/LilAgents/CharacterContentView.swift index 01b78f5..f35d56d 100644 --- a/LilAgents/CharacterContentView.swift +++ b/LilAgents/CharacterContentView.swift @@ -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 } } diff --git a/LilAgents/ClaudeSession.swift b/LilAgents/ClaudeSession.swift index dc641d2..fc292d2 100644 --- a/LilAgents/ClaudeSession.swift +++ b/LilAgents/ClaudeSession.swift @@ -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]? @@ -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" @@ -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: diff --git a/LilAgents/WalkerCharacter.swift b/LilAgents/WalkerCharacter.swift index c789008..5f5e1aa 100644 --- a/LilAgents/WalkerCharacter.swift +++ b/LilAgents/WalkerCharacter.swift @@ -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() { @@ -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