@@ -1442,8 +1442,51 @@ export function parseSpecialTags(content: string, isStreaming: boolean): ParsedS
14421442 segments . push ( { type : 'text' , content } )
14431443 }
14441444
1445+ if ( ! isStreaming ) {
1446+ recoverTrailingBareOptions ( segments )
1447+ }
1448+
14451449 return { segments, hasPendingTag }
14461450}
1451+ /**
1452+ * Recovers a trailing bare-JSON options payload the model emitted WITHOUT the
1453+ * `<options>` wrapper (observed when an automation prompt asks the model to
1454+ * "(re)send suggested actions" and it answers with the JSON as content). The
1455+ * shape check is strict — a non-empty object whose every value is
1456+ * { title, description } with numeric-string keys — so ordinary JSON in prose
1457+ * cannot false-positive. Only a message's FINAL text segment is considered,
1458+ * mirroring the tag contract (options go last), and only when no options tag
1459+ * already parsed. Never applied mid-stream: a partial JSON tail must not
1460+ * flicker between prose and a card.
1461+ */
1462+ function recoverTrailingBareOptions ( segments : ContentSegment [ ] ) : void {
1463+ const last = segments [ segments . length - 1 ]
1464+ if ( ! last || last . type !== 'text' ) return
1465+ if ( segments . some ( ( segment ) => segment . type === 'options' ) ) return
1466+ const text = last . content
1467+ if ( ! text . trimEnd ( ) . endsWith ( '}' ) ) return
1468+ // The payload nests objects, so the START brace is the first one from which
1469+ // the remainder parses — probe brace positions left to right (bounded).
1470+ let start = - 1
1471+ let parsed : unknown
1472+ let probe = text . indexOf ( '{' )
1473+ for ( let attempts = 0 ; probe !== - 1 && attempts < 20 ; attempts ++ ) {
1474+ try {
1475+ parsed = JSON . parse ( text . slice ( probe ) . trim ( ) )
1476+ start = probe
1477+ break
1478+ } catch {
1479+ probe = text . indexOf ( '{' , probe + 1 )
1480+ }
1481+ }
1482+ if ( start === - 1 ) return
1483+ if ( ! isOptionsTagData ( parsed ) || Object . keys ( parsed as object ) . length === 0 ) return
1484+ if ( ! Object . keys ( parsed as object ) . every ( ( key ) => / ^ \d + $ / . test ( key ) ) ) return
1485+ const prefix = text . slice ( 0 , start ) . replace ( / \s + $ / , '' )
1486+ segments . pop ( )
1487+ if ( prefix ) segments . push ( { type : 'text' , content : prefix } )
1488+ segments . push ( { type : 'options' , data : parsed } )
1489+ }
14471490
14481491interface SpecialTagsProps {
14491492 segment : Exclude < ContentSegment , { type : 'text' } >
0 commit comments