@tduncan / Cortana / commits / 8780404

feat: add support for streaming reasoning content and auto-expand reasoning widget during generation

abdulmominsakib committed Apr 2, 2026 at 14:31 UTC 878040434ef4da546ad2219774e688040a63d754
3 files changed +50 -14
lib/features/chat/data/chat_service.dart
+34 -12
@@ -430,12 +430,23 @@ class OpenAICompatibleChatService implements ChatService {
430 }
431 try {
432 final json = jsonDecode(data) as Map<String, dynamic>;
433 - final content = json['choices']?[0]?['delta']?['content'];
434 - if (content != null && content is String) {
435 - yield ChatResponse(
436 - type: ChatResponseType.message,
437 - content: content,
438 - );
433 + final delta = json['choices']?[0]?['delta'] as Map<String, dynamic>?;
434 + if (delta != null) {
435 + final content = delta['content'] as String?;
436 + final reasoning = (delta['reasoning'] ?? delta['reasoning_content']) as String?;
437 +
438 + if (content != null && content.isNotEmpty) {
439 + yield ChatResponse(
440 + type: ChatResponseType.message,
441 + content: content,
442 + );
443 + }
444 + if (reasoning != null && reasoning.isNotEmpty) {
445 + yield ChatResponse(
446 + type: ChatResponseType.reasoning,
447 + reasoningContent: reasoning,
448 + );
449 + }
450 }
451 } catch (e) {
452 // ignore: empty_catches
@@ -585,12 +596,23 @@ class OpenRouterChatService implements ChatService {
596 }
597 try {
598 final json = jsonDecode(data) as Map<String, dynamic>;
588 - final content = json['choices']?[0]?['delta']?['content'];
589 - if (content != null && content is String) {
590 - yield ChatResponse(
591 - type: ChatResponseType.message,
592 - content: content,
593 - );
599 + final delta = json['choices']?[0]?['delta'] as Map<String, dynamic>?;
600 + if (delta != null) {
601 + final content = delta['content'] as String?;
602 + final reasoning = (delta['reasoning'] ?? delta['reasoning_content']) as String?;
603 +
604 + if (content != null && content.isNotEmpty) {
605 + yield ChatResponse(
606 + type: ChatResponseType.message,
607 + content: content,
608 + );
609 + }
610 + if (reasoning != null && reasoning.isNotEmpty) {
611 + yield ChatResponse(
612 + type: ChatResponseType.reasoning,
613 + reasoningContent: reasoning,
614 + );
615 + }
616 }
617 } catch (e) {
618 // ignore: empty_catches
lib/features/chat/views/components/reasoning_widget.dart
+15 -1
@@ -15,7 +15,21 @@ class ReasoningWidget extends StatefulWidget {
15 }
16
17 class _ReasoningWidgetState extends State<ReasoningWidget> {
18 - bool _isExpanded = false;
18 + late bool _isExpanded;
19 +
20 + @override
21 + void initState() {
22 + super.initState();
23 + _isExpanded = widget.isStreaming;
24 + }
25 +
26 + @override
27 + void didUpdateWidget(ReasoningWidget oldWidget) {
28 + super.didUpdateWidget(oldWidget);
29 + if (widget.isStreaming && !oldWidget.isStreaming) {
30 + _isExpanded = true;
31 + }
32 + }
33
34 String _getLastLines(String content, int maxLines) {
35 final lines = content
pubspec.yaml
+1 -1
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
16 # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
17 # In Windows, build-name is used as the major, minor, and patch parts
18 # of the product and file versions while build-number is used as the build suffix.
19 -version: 1.0.0+2
19 +version: 1.0.0+3
20
21 environment:
22 sdk: ^3.11.0