Paste and drop images into every box that talks to an agent

This commit is contained in:
Ian Gulliver
2026-10-20 17:32:32 -07:00
parent 49f57b73fc
commit 15482473f7
9 changed files with 133 additions and 83 deletions
+39 -13
View File
@@ -93,6 +93,10 @@ final class ImageDrafts {
byKey[key] ?? []
}
func has(_ key: String) -> Bool {
byKey[key] != nil
}
func remove(_ key: String, _ id: UUID) {
let rest = images(key).filter { $0.id != id }
byKey[key] = rest.isEmpty ? nil : rest
@@ -218,6 +222,38 @@ struct PasteboardCommands: Commands {
}
}
private struct ImageInput: ViewModifier {
let key: String
@Environment(Store.self) private var store
@Environment(Overlays.self) private var overlays
@ViewState private var targeted = false
func body(content: Content) -> some View {
content
.focusedValue(\.composer, ComposerTarget(paste: { pb in store.images.add(key, pasteboard: pb, overlays) }))
.overlay {
if targeted {
RoundedRectangle(cornerRadius: 6).stroke(Palette.accent)
}
}
.overlay {
Color.clear
.contentShape(Rectangle())
.allowsHitTesting(false)
.onDrop(of: [.image, .fileURL], isTargeted: $targeted) { providers in
store.images.add(key, providers: providers, overlays)
return true
}
}
}
}
extension View {
func imageInput(_ key: String) -> some View {
modifier(ImageInput(key: key))
}
}
struct Thumbnails: View {
let key: String
@Environment(Store.self) private var store
@@ -282,7 +318,6 @@ struct ComposerView: View {
@Environment(\.inRightPane) private var inRightPane
@ViewState private var text = ""
@ViewState private var pick: String?
@ViewState private var targeted = false
@ViewState private var shownOverride: Bool?
@FocusState private var focused: Bool
@@ -467,7 +502,6 @@ struct ComposerView: View {
.scrollContentBackground(.hidden)
.writingToolsBehavior(.disabled)
.focused($focused)
.focusedValue(\.composer, ComposerTarget(paste: { pb in store.images.add(draftKey, pasteboard: pb, overlays) }))
.onKeyPress(.return, phases: .down) { press in
let marked = (NSApp.keyWindow?.firstResponder as? NSTextView)?.hasMarkedText() ?? false
if press.modifiers.contains(.shift) || marked {
@@ -489,16 +523,8 @@ struct ComposerView: View {
.allowsHitTesting(false)
}
}
.overlay(RoundedRectangle(cornerRadius: 6).stroke(targeted ? Palette.accent : inRightPane ? accent : focused ? Palette.accent : Palette.border, lineWidth: inRightPane && focused ? 2 : 1))
.overlay {
Color.clear
.contentShape(Rectangle())
.allowsHitTesting(false)
.onDrop(of: [.image, .fileURL], isTargeted: $targeted) { providers in
store.images.add(draftKey, providers: providers, overlays)
return true
}
}
.overlay(RoundedRectangle(cornerRadius: 6).stroke(inRightPane ? accent : focused ? Palette.accent : Palette.border, lineWidth: inRightPane && focused ? 2 : 1))
.imageInput(draftKey)
}
private func reportDraft() {
@@ -510,7 +536,7 @@ struct ComposerView: View {
private func submit(_ m: ModelState) {
let value = text
let images = store.images
if trimmed(value).isEmpty && images.images(draftKey).isEmpty {
if trimmed(value).isEmpty && !images.has(draftKey) {
return
}
guard let drafts = store.drafts else {