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
+31 -7
View File
@@ -261,7 +261,7 @@ private struct ChangesHead: View {
}
Spacer(minLength: 0)
if let taskId = store.selectedTaskId, let drafts = store.drafts {
let n = drafts.lineComments(taskId).filter { !trimmed($0.text).isEmpty }.count
let n = drafts.lineComments(taskId).filter(lineCommentReady(store.images, taskId)).count
if n > 0 {
Button("Send \(n) \(n == 1 ? "comment" : "comments")") {
sendComments(taskId, drafts)
@@ -277,12 +277,18 @@ private struct ChangesHead: View {
}
private func sendComments(_ taskId: String, _ drafts: Drafts) {
let sent = drafts.takeLineComments(taskId)
let sent = drafts.takeLineComments(taskId, ready: lineCommentReady(store.images, taskId))
let keys = sent.map { lineCommentImageKey(taskId, $0.anchor) }
let imgs = keys.map { store.images.take($0) }
Task {
do {
try await store.messageTask(taskId, commentsMessage(sent))
let ids = try await uploadImages(store.api, imgs.flatMap { $0 })
try await store.messageTask(taskId, commentsMessage(sent, imgs.map(\.count)), images: ids)
} catch {
drafts.restoreLineComments(taskId, sent)
for (key, i) in zip(keys, imgs) {
store.images.restore(key, i)
}
overlays.showAlert(errorText(error))
}
}
@@ -530,9 +536,21 @@ private func patchRows(_ lines: ArraySlice<String>, _ path: String) -> [DiffRow]
}
}
func commentsMessage(_ comments: [LineComment]) -> String {
comments.map { c in
"`\(c.anchor.path):\(c.anchor.line)`\(c.anchor.old ? " (removed line)" : "")\n> \(c.anchor.quote)\n\n\(trimmed(c.text))"
func lineCommentImageKey(_ taskId: String, _ anchor: LineAnchor) -> String {
"comment:\(taskId):\(anchor.path):\(anchor.line):\(anchor.old):\(anchor.quote)"
}
@MainActor
func lineCommentReady(_ images: ImageDrafts, _ taskId: String) -> (LineComment) -> Bool {
{ c in !trimmed(c.text).isEmpty || images.has(lineCommentImageKey(taskId, c.anchor)) }
}
func commentsMessage(_ comments: [LineComment], _ imageCounts: [Int]) -> String {
var first = 1
return zip(comments, imageCounts).map { c, n in
let attached = n == 0 ? "" : n == 1 ? "(image \(first) attached)" : "(images \(first)–\(first + n - 1) attached)"
first += n
return ["`\(c.anchor.path):\(c.anchor.line)`\(c.anchor.old ? " (removed line)" : "")\n> \(c.anchor.quote)", trimmed(c.text), attached].filter { !$0.isEmpty }.joined(separator: "\n\n")
}.joined(separator: "\n\n")
}
@@ -576,7 +594,7 @@ struct DiffLines: View {
let comment = drafts.lineComment(taskId, anchor)
DiffLineRow(row: row, mark: comment != nil, hoverable: true)
.simultaneousGesture(TapGesture().onEnded {
drafts.toggleLineComment(taskId, anchor)
drafts.toggleLineComment(taskId, anchor, ready: lineCommentReady(store.images, taskId))
})
if comment != nil {
LineCommentBox(drafts: drafts, taskId: taskId, anchor: anchor)
@@ -642,11 +660,15 @@ private struct LineCommentBox: View {
let drafts: Drafts
let taskId: String
let anchor: LineAnchor
@Environment(Store.self) private var store
@FocusState private var focused: Bool
var body: some View {
let text = Binding(get: { drafts.lineComment(taskId, anchor)?.text ?? "" }, set: { drafts.setLineComment(taskId, anchor, $0) })
let key = lineCommentImageKey(taskId, anchor)
VStack(alignment: .trailing, spacing: 4) {
Thumbnails(key: key)
.frame(maxWidth: .infinity, alignment: .leading)
TextEditor(text: text)
.font(Fonts.font(12))
.scrollContentBackground(.hidden)
@@ -666,7 +688,9 @@ private struct LineCommentBox: View {
}
}
.overlay(RoundedRectangle(cornerRadius: 6).stroke(focused ? Palette.accent : Palette.border))
.imageInput(key)
Button("remove") {
_ = store.images.take(key)
drafts.dropLineComment(taskId, anchor)
}
.buttonStyle(SmallLinkButtonStyle())