From a443ab28993c561d9cdf3e7b6a088c427887acb9 Mon Sep 17 00:00:00 2001 From: akdenizemirhan Date: Wed, 25 Mar 2026 23:33:37 +0300 Subject: [PATCH 1/2] fix: preserve assistant responses in chat history Streaming text from Claude's responses was not being saved to the session history. When the popover closed and reopened, only user messages appeared - assistant responses were lost. The root cause: Claude CLI returns streaming text via "assistant" type messages, but the "result" type message has an empty "result" field. The history was only populated from the "result" field, so responses were never saved. Fix: accumulate streaming text during the assistant response and save it to history when the turn completes. Co-Authored-By: Claude Opus 4.6 (1M context) --- LilAgents/ClaudeSession.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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: From 5c63fa7d3e6d549af577f8aa236f518fb4550769 Mon Sep 17 00:00:00 2001 From: akdenizemirhan Date: Thu, 26 Mar 2026 00:34:30 +0300 Subject: [PATCH 2/2] feat: drag and fling characters to reposition them Characters sometimes block UI elements underneath. This adds the ability to grab and drag them out of the way. - Click and drag to move a character (5px threshold prevents accidental drags) - Short click still opens the chat popover as before - Fling a character and it slides on ice with friction, bouncing off screen edges - Character resumes walking from the new position after 2 seconds Co-Authored-By: Claude Opus 4.6 (1M context) --- LilAgents/CharacterContentView.swift | 36 +++++++++++- LilAgents/WalkerCharacter.swift | 83 ++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+), 1 deletion(-) 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/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