@@ -176,8 +176,8 @@ final class ConversationController: ObservableObject {
176176 }
177177
178178 func send( ) {
179- let text = draft. trimmingCharacters ( in : . whitespacesAndNewlines )
180- guard acceptsInput, !text. isEmpty || !attachments. isEmpty else { return }
179+ let text = draft
180+ guard acceptsInput, !text. trimmingCharacters ( in : . whitespacesAndNewlines ) . isEmpty || !attachments. isEmpty else { return }
181181 let files = attachments
182182 let turn = Turn . foreground ( UUID ( ) )
183183 composerTransaction = ComposerTransaction ( turn: turn, text: text, originalDraft: draft, attachments: files)
@@ -222,15 +222,24 @@ final class ConversationController: ObservableObject {
222222 }
223223
224224 func choose( _ option: ConfigOption , value: String ) {
225- let wireValue : ACPSessionConfigValue = option. valueType == " boolean " ? . boolean( value == " true " ) : . select( value)
225+ let wireValue : ACPSessionConfigValue
226+ switch option. valueType {
227+ case " select " : wireValue = . select( value)
228+ case " boolean " :
229+ guard value == " true " || value == " false " else { return }
230+ wireValue = . boolean( value == " true " )
231+ default : return
232+ }
226233 client. setConfig ( id: option. id, value: wireValue) { [ weak self] result in
227234 guard let self else { return }
228235 switch result {
229236 case . failure( let error) : self . fail ( error)
230237 case . success( let payload) :
231- let refreshed = Self . parseConfigOptions ( payload [ " configOptions " ] )
232- if !refreshed. isEmpty { self . configOptions = refreshed }
233- else if let index = self . configOptions. firstIndex ( where: { $0. id == option. id } ) { self . configOptions [ index] . currentValue = value }
238+ if let refreshed = payload [ " configOptions " ] {
239+ self . configOptions = Self . parseConfigOptions ( refreshed)
240+ } else if let index = self . configOptions. firstIndex ( where: { $0. id == option. id } ) {
241+ self . configOptions [ index] . currentValue = value
242+ }
234243 self . publishCurrentConfig ( userSelected: true )
235244 }
236245 }
@@ -297,7 +306,8 @@ final class ConversationController: ObservableObject {
297306 case . agentThought( let message) : applyMessage ( message, role: . thought)
298307 case . toolCall( let update) , . toolCallUpdate( let update) : updateTool ( update)
299308 case . toolCallContent( let id, let content) : appendToolContent ( id: id, content: content)
300- case . plan( let id, let entries) : applyPlan ( id: id, entries: entries)
309+ case . plan( let plan) : applyPlan ( plan)
310+ case . planRemoved( let id) : removePlan ( id: id)
301311 case . usage( let usage) : contextUsed = usage. used; contextSize = usage. size
302312 case . tokenUsage( let usage) : tokenUsage = usage
303313 case . configOptions( let options) : configOptions = Self . parseConfigOptions ( options. anyValue) ; publishCurrentConfig ( )
@@ -319,18 +329,33 @@ final class ConversationController: ObservableObject {
319329 }
320330
321331 private func applyMessage( _ message: DesktopMessageUpdate , role: TranscriptRole ) {
322- if role == . user, composerTransaction != nil , message. content. allSatisfy ( { consumeComposerEcho ( $0) } ) {
323- if let optimistic = composerTransaction? . transcriptEntryID { messageEntryIDs [ message. messageId] = optimistic }
332+ if message. replace, !message. hasContent {
333+ if role == . user, let optimistic = composerTransaction? . transcriptEntryID {
334+ messageEntryIDs [ message. messageId] = optimistic
335+ }
324336 return
325337 }
326- if message. replace, !message. hasContent { return }
338+ if role == . user, composerTransaction != nil {
339+ if !message. content. isEmpty, consumeComposerEcho ( message. content, requireComplete: message. replace) {
340+ if let optimistic = composerTransaction? . transcriptEntryID { messageEntryIDs [ message. messageId] = optimistic }
341+ return
342+ }
343+ if message. replace, let optimistic = composerTransaction? . transcriptEntryID {
344+ messageEntryIDs [ message. messageId] = optimistic
345+ }
346+ }
327347 let blocks = message. content
328348 let text = blocks. map ( Self . contentText) . joined ( )
329349 if role == . assistant { latestAssistantSource = message. replace ? text : latestAssistantSource + text }
330350 if let entryID = messageEntryIDs [ message. messageId] , let index = entries. firstIndex ( where: { $0. id == entryID } ) {
331351 if message. replace {
332352 entries [ index] . contentBlocks = blocks
333353 entries [ index] . text = String ( text. suffix ( 256 * 1024 ) )
354+ if role == . user {
355+ entries [ index] . presentation = . user( UserMessagePresentation (
356+ text: entries [ index] . text, media: blocks. flatMap { Self . userContent ( $0) . media }
357+ ) )
358+ }
334359 } else {
335360 entries [ index] . contentBlocks. append ( contentsOf: blocks)
336361 entries [ index] . text = String ( ( entries [ index] . text + text) . suffix ( 256 * 1024 ) )
@@ -360,22 +385,29 @@ final class ConversationController: ObservableObject {
360385 }
361386 }
362387
363- private func applyPlan( id: String , entries plan: [ DesktopPlanEntry ] ? ) {
364- if plan == nil {
365- if let entryID = planEntryIDs. removeValue ( forKey: id) { entries. removeAll { $0. id == entryID } }
366- transcriptRevision += 1
367- return
368- }
369- let rows = plan!. map { " [ " + ( $0. status ?? " pending " ) + " ] " + $0. content } . joined ( separator: " \n " )
370- if let entryID = planEntryIDs [ id] , let index = entries. firstIndex ( where: { $0. id == entryID } ) {
371- entries [ index] . text = rows; entries [ index] . formatted = Self . markdown ( rows)
388+ private func applyPlan( _ plan: DesktopPlanContent ) {
389+ let text : String
390+ switch plan {
391+ case . items( _, let entries) :
392+ text = entries. map { " [ " + ( $0. status ?? " pending " ) + " ] " + $0. content } . joined ( separator: " \n " )
393+ case . file( _, let uri) : text = " [Plan file]( " + uri + " ) "
394+ case . markdown( _, let content) : text = content
395+ case . unknown: return
396+ }
397+ if let entryID = planEntryIDs [ plan. id] , let index = entries. firstIndex ( where: { $0. id == entryID } ) {
398+ entries [ index] . text = text; entries [ index] . formatted = Self . markdown ( text)
372399 } else {
373- let entry = TranscriptEntry ( role: . plan, title: " Plan " , text: rows , formatted: Self . markdown ( rows ) )
374- appendEntry ( entry) ; planEntryIDs [ id] = entry. id
400+ let entry = TranscriptEntry ( role: . plan, title: " Plan " , text: text , formatted: Self . markdown ( text ) )
401+ appendEntry ( entry) ; planEntryIDs [ plan . id] = entry. id
375402 }
376403 transcriptRevision += 1
377404 }
378405
406+ private func removePlan( id: String ) {
407+ if let entryID = planEntryIDs. removeValue ( forKey: id) { entries. removeAll { $0. id == entryID } }
408+ transcriptRevision += 1
409+ }
410+
379411 private func applyTurnState( _ state: DesktopTurnState ) {
380412 let turn = Turn . autonomous ( state. turnId)
381413 if state. active { reduceSettlement ( . started( turn, prompt: " " ) ) }
@@ -482,21 +514,17 @@ final class ConversationController: ObservableObject {
482514
483515 private func updateTool( _ patch: DesktopToolUpdate ) {
484516 guard let id = patch. toolCallId else { return }
485- var update = toolStates [ id] ?? DesktopToolUpdate ( toolCallId: id)
486- func supplied( _ field: String ) -> Bool { patch. present. isEmpty || patch. present. contains ( field) }
487- if supplied ( " title " ) { update. title = patch. cleared. contains ( " title " ) ? nil : patch. title }
488- if supplied ( " kind " ) { update. kind = patch. cleared. contains ( " kind " ) ? nil : patch. kind }
489- if supplied ( " status " ) { update. status = patch. cleared. contains ( " status " ) ? nil : patch. status }
490- if supplied ( " content " ) { update. content = patch. cleared. contains ( " content " ) ? nil : patch. content }
491- if supplied ( " rawInput " ) { update. rawInput = patch. cleared. contains ( " rawInput " ) ? nil : patch. rawInput }
492- if supplied ( " rawOutput " ) { update. rawOutput = patch. cleared. contains ( " rawOutput " ) ? nil : patch. rawOutput }
517+ let update = ( toolStates [ id] ?? DesktopToolUpdate ( toolCallId: id) ) . merging ( patch)
493518 toolStates [ id] = update
494519 let dictionary = Self . toolDictionary ( update)
495520 if let index = entries. lastIndex ( where: { $0. role == . tool && $0. toolCallID == id } ) {
496521 let tool = Self . toolPresentation ( dictionary)
497522 entries [ index] . title = tool. title
498523 entries [ index] . text = tool. detail
499524 entries [ index] . presentation = . tool( tool)
525+ if patch. present. contains ( " rawInput " ) {
526+ entries [ index] . backgrounded = ( update. rawInput? . anyValue as? [ String : Any ] ) ? [ " background " ] as? Bool == true
527+ }
500528 entries [ index] . isStreaming = tool. status == . inProgress || tool. status == . pending
501529 if entries [ index] . isStreaming { streamingEntryIDs. insert ( entries [ index] . id) }
502530 else { streamingEntryIDs. remove ( entries [ index] . id) }
@@ -561,22 +589,33 @@ final class ConversationController: ObservableObject {
561589 transcriptRevision += 1
562590 }
563591
564- private func consumeComposerEcho( _ content : DesktopContentBlock ) -> Bool {
592+ private func consumeComposerEcho( _ contents : [ DesktopContentBlock ] , requireComplete : Bool ) -> Bool {
565593 guard var transaction = composerTransaction else { return false }
566- switch content {
567- case . text( let text) :
568- guard !transaction. echoedTextComplete else { return false }
569- let candidate = transaction. echoedText + text
570- guard !transaction. text. isEmpty, transaction. text. hasPrefix ( candidate) else { return false }
571- transaction. echoedText = candidate
572- transaction. echoedTextComplete = candidate == transaction. text
573- case . image:
574- guard transaction. echoedAttachmentIndex < transaction. attachments. count, transaction. attachments [ transaction. echoedAttachmentIndex] . kind == . image else { return false }
575- transaction. echoedAttachmentIndex += 1
576- case . audio:
577- guard transaction. echoedAttachmentIndex < transaction. attachments. count, transaction. attachments [ transaction. echoedAttachmentIndex] . kind == . audio else { return false }
578- transaction. echoedAttachmentIndex += 1
579- default : return false
594+ for content in contents {
595+ switch content {
596+ case . text( let text) :
597+ if transaction. text. isEmpty {
598+ guard text. isEmpty else { return false }
599+ transaction. echoedTextComplete = true
600+ } else {
601+ guard !transaction. echoedTextComplete else { return false }
602+ let candidate = transaction. echoedText + text
603+ guard transaction. text. hasPrefix ( candidate) else { return false }
604+ transaction. echoedText = candidate
605+ transaction. echoedTextComplete = candidate == transaction. text
606+ }
607+ case . image:
608+ guard transaction. echoedAttachmentIndex < transaction. attachments. count, transaction. attachments [ transaction. echoedAttachmentIndex] . kind == . image else { return false }
609+ transaction. echoedAttachmentIndex += 1
610+ case . audio:
611+ guard transaction. echoedAttachmentIndex < transaction. attachments. count, transaction. attachments [ transaction. echoedAttachmentIndex] . kind == . audio else { return false }
612+ transaction. echoedAttachmentIndex += 1
613+ default : return false
614+ }
615+ }
616+ if requireComplete {
617+ guard ( transaction. text. isEmpty || transaction. echoedTextComplete) ,
618+ transaction. echoedAttachmentIndex == transaction. attachments. count else { return false }
580619 }
581620 composerTransaction = transaction
582621 return true
@@ -716,8 +755,9 @@ final class ConversationController: ObservableObject {
716755
717756 static func parseConfigOptions( _ value: Any ? ) -> [ ConfigOption ] {
718757 ( value as? [ [ String : Any ] ] ?? [ ] ) . compactMap { item in
719- guard let id = ( item [ " configId " ] ?? item [ " id " ] ) as? String else { return nil }
720- let type = item [ " type " ] as? String ?? " select "
758+ guard let id = item [ " configId " ] as? String ,
759+ let type = item [ " type " ] as? String ,
760+ type == " select " || type == " boolean " else { return nil }
721761 let raw = item [ " options " ] as? [ [ String : Any ] ] ?? [ ]
722762 var groups : [ ConfigGroup ] = [ ]
723763 var ungrouped : [ ConfigChoice ] = [ ]
0 commit comments