import "package:flutter/foundation.dart"; import "dart:convert"; import "../../src/chat/tool_loop_service.dart"; import "../../src/api/openrouter_client.dart"; import "../../src/session/conversation_history.dart"; import "../../src/session/session_store.dart"; import "../../src/session/session_types.dart"; import "../../src/services/cost_tracker.dart" as cost_tracker; import "settings_provider.dart"; class ChatProvider extends ChangeNotifier { ChatProvider(this._settingsProvider); final SettingsProvider _settingsProvider; final ToolLoopService _toolLoopService = ToolLoopService(); ConversationHistory? _conversationHistory; OpenRouterClient? _client; bool _stopRequested = false; List _messages = []; List> _apiMessages = >[]; bool isLoading = false; List get messages => _messages; int get messageCount => _messages.length; bool get hasConversation => _conversationHistory != null; bool get isStopping => _stopRequested; void setConversation(ConversationHistory history) { _conversationHistory = history; _messages = history.getMessages(); _apiMessages = _buildApiMessages(_messages); notifyListeners(); } void clearConversation() { _conversationHistory = null; _messages = []; _apiMessages = >[]; isLoading = false; notifyListeners(); } Future sendMessage(String text) async { if (text.isEmpty || _conversationHistory == null) return; final apiKey = _settingsProvider.settings.openRouterApiKey; if (apiKey == null || apiKey.isEmpty) { throw Exception( "OpenRouter API key not set. Please configure it in settings.", ); } final savedModel = _settingsProvider.settings.model; final model = _settingsProvider.normalizeModelId(savedModel); if (savedModel != model) { print("Normalizing legacy model ID from $savedModel to $model"); await _settingsProvider.updateModel(model); } try { _stopRequested = false; _client = await OpenRouterClientFactory.create(apiKey: apiKey); final session = _conversationHistory!.session; final workingDirectory = session?.workingDirectory; if (session != null) { session.model = model; if (session.name == "New Chat") { session.name = _buildSessionName(text); } } // add user message to conversation _conversationHistory!.addMessage("user", text); _messages = _conversationHistory!.getMessages(); _apiMessages.add({"role": "user", "content": text}); isLoading = true; notifyListeners(); final toolLoopResult = await _toolLoopService.runTurn( client: _client!, model: model, apiMessages: _apiMessages.take(_apiMessages.length - 1).toList(), userText: text, workingDirectory: workingDirectory, onToolCall: (toolName, input) { _conversationHistory!.addMessage( "tool", _formatToolCall(toolName, input), ); _messages = _conversationHistory!.getMessages(); notifyListeners(); }, onToolResult: (toolName, result) { _conversationHistory!.addMessage( "tool", _formatToolResult(toolName, result), ); _messages = _conversationHistory!.getMessages(); notifyListeners(); }, ); _apiMessages = toolLoopResult.apiMessages; // add assistant message to visible conversation _conversationHistory!.addMessage( "assistant", toolLoopResult.responseText, tokens: toolLoopResult.response.outputTokens, ); _messages = _conversationHistory!.getMessages(); // track cost (set to 0 for now — OpenRouter pricing varies by model) final inputTokens = toolLoopResult.response.inputTokens ?? 0; final outputTokens = toolLoopResult.response.outputTokens ?? 0; cost_tracker.addToTotalSessionCost( cost: 0.0, inputTokens: inputTokens, outputTokens: outputTokens, cacheReadTokens: 0, cacheCreationTokens: 0, model: toolLoopResult.response.model, ); // save session if (session != null) { await SessionStore.instance.saveSession(session); } notifyListeners(); } catch (error, stackTrace) { print("Failed to send message: $error"); print(stackTrace); if (error is RequestCancelledException) { _conversationHistory!.addMessage("assistant", "Generation stopped."); final session = _conversationHistory!.session; _messages = _conversationHistory!.getMessages(); if (session != null) { await SessionStore.instance.saveSession(session); } return; } if (error is ToolLoopException) { _apiMessages = List>.from(error.apiMessages); } _conversationHistory!.addMessage( "assistant", _buildTurnFailureMessage(error), ); final session = _conversationHistory!.session; _messages = _conversationHistory!.getMessages(); if (session != null) { await SessionStore.instance.saveSession(session); } rethrow; } finally { _client?.close(); _client = null; _stopRequested = false; isLoading = false; notifyListeners(); } } void stopGenerating() { if (!isLoading) { return; } _stopRequested = true; print("Stopping active turn"); _client?.cancelActiveRequest(); notifyListeners(); } @override void dispose() { _client?.close(); super.dispose(); } String _buildSessionName(String text) { final sanitized = text.replaceAll(RegExp(r"\s+"), " ").trim(); if (sanitized.isEmpty) { return "New Chat"; } const maxLength = 48; if (sanitized.length <= maxLength) { return sanitized; } return "${sanitized.substring(0, maxLength - 1).trimRight()}…"; } List> _buildApiMessages(List messages) { return messages .where( (message) => message.role == "user" || message.role == "assistant", ) .map( (message) => { "role": message.role, "content": message.content, }, ) .toList(growable: true); } String _formatToolCall(String toolName, Map input) { const encoder = JsonEncoder.withIndent(" "); return "$toolName call\n${encoder.convert(input)}"; } String _formatToolResult(String toolName, String result) { return "$toolName result\n$result"; } String _buildTurnFailureMessage(Object error) { return "This turn failed before the assistant could finish: $error"; } }