From 51f3453cfdf658da4666a1a5e8b33439fe930a5c Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 7 Apr 2026 11:40:27 +0000 Subject: [PATCH 1/2] fix: resolve multiple bugs found during code review - Implement _readPrivateKey to read SSH key files from device storage - Fix duplicate disconnect status emission in SSHClientImpl - Fix user message being dropped from chat state in ChatBloc - Refactor _onLoadSession to use if/else instead of async fold callbacks - Fix Gemini system prompt: use systemInstruction field instead of injecting as user message - Add dangerous command blocking in BashTool for safety - Replace print() calls with debugPrint() in AppLogger Co-Authored-By: Mohamed --- lib/core/services/app_logger.dart | 7 +- .../datasources/api/gemini_api_client.dart | 15 ++- lib/data/datasources/ssh/ssh_client_impl.dart | 24 +++- lib/data/tools/bash_tool.dart | 17 +++ lib/presentation/blocs/chat/chat_bloc.dart | 116 +++++++++--------- 5 files changed, 109 insertions(+), 70 deletions(-) diff --git a/lib/core/services/app_logger.dart b/lib/core/services/app_logger.dart index 2b5faf2..0d37603 100644 --- a/lib/core/services/app_logger.dart +++ b/lib/core/services/app_logger.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'package:flutter/foundation.dart'; import 'package:logger/logger.dart'; import 'package:path_provider/path_provider.dart'; import 'package:intl/intl.dart'; @@ -38,7 +39,7 @@ class AppLogger { final dateStr = DateFormat('yyyy-MM-dd').format(DateTime.now()); _logFile = File('${logsDir.path}/app_log_$dateStr.txt'); } catch (e) { - print('Failed to initialize log file: $e'); + debugPrint('Failed to initialize log file: $e'); } } @@ -63,7 +64,7 @@ class AppLogger { ); } } catch (e) { - print('Failed to write to log file: $e'); + debugPrint('Failed to write to log file: $e'); } } @@ -114,7 +115,7 @@ class _CustomOutput extends LogOutput { void output(OutputEvent event) { for (var line in event.lines) { appLogger._addToBuffer(line); - print(line); + debugPrint(line); } } } diff --git a/lib/data/datasources/api/gemini_api_client.dart b/lib/data/datasources/api/gemini_api_client.dart index 3804414..d4786de 100644 --- a/lib/data/datasources/api/gemini_api_client.dart +++ b/lib/data/datasources/api/gemini_api_client.dart @@ -42,6 +42,10 @@ class GeminiApiClient extends BaseApiClient { ), data: { 'contents': _convertMessages(messages, systemPrompt), + if (systemPrompt != null && systemPrompt.isNotEmpty) + 'systemInstruction': { + 'parts': [{'text': systemPrompt}], + }, if (tools.isNotEmpty) 'tools': [{'functionDeclarations': _convertTools(tools)}], 'generationConfig': { if (maxTokens != null) 'maxOutputTokens': maxTokens, @@ -115,14 +119,9 @@ class GeminiApiClient extends BaseApiClient { List> _convertMessages(List messages, String? systemPrompt) { final contents = >[]; - - // Add system prompt as first user message if provided - if (systemPrompt != null && systemPrompt.isNotEmpty) { - contents.add({ - 'role': 'user', - 'parts': [{'text': systemPrompt}], - }); - } + + // System prompt is passed separately via systemInstruction in the request body, + // so we do not inject it as a user message here. for (final msg in messages) { contents.add({ diff --git a/lib/data/datasources/ssh/ssh_client_impl.dart b/lib/data/datasources/ssh/ssh_client_impl.dart index a526447..d65ce0f 100644 --- a/lib/data/datasources/ssh/ssh_client_impl.dart +++ b/lib/data/datasources/ssh/ssh_client_impl.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:io'; import 'package:dartssh2/dartssh2.dart'; import '../../../core/errors/exceptions.dart'; import '../../../core/constants/app_constants.dart'; @@ -66,7 +67,6 @@ class SSHClientImpl { Future disconnect() async { try { _logger.info('Disconnecting from SSH server', tag: 'SSH'); - _updateStatus(SSHConnectionStatus.disconnected); _sftpClient?.close(); _session?.close(); @@ -195,7 +195,27 @@ class SSHClientImpl { } Future _readPrivateKey(String path) async { - throw UnimplementedError('Private key reading not implemented'); + try { + _logger.debug('Reading private key from: $path', tag: 'SSH'); + // Read the private key file via SSH command on local device + // For mobile, the path is expected to be a local file path + final file = File(path); + if (!await file.exists()) { + throw SSHException( + message: 'Private key file not found at: $path', + ); + } + final content = await file.readAsString(); + _logger.debug('Private key read successfully', tag: 'SSH'); + return content; + } catch (e) { + if (e is SSHException) rethrow; + _logger.error('Failed to read private key: ${e.toString()}', error: e, tag: 'SSH'); + throw SSHException( + message: 'Failed to read private key file: ${e.toString()}', + details: e, + ); + } } Future dispose() async { diff --git a/lib/data/tools/bash_tool.dart b/lib/data/tools/bash_tool.dart index 1178a84..9848d69 100644 --- a/lib/data/tools/bash_tool.dart +++ b/lib/data/tools/bash_tool.dart @@ -28,10 +28,27 @@ class BashTool extends BaseTool { @override bool get isReadOnly => false; + /// Dangerous command patterns that should be blocked for safety. + static final _dangerousPatterns = [ + RegExp(r'rm\s+(-[a-zA-Z]*f[a-zA-Z]*\s+)?/\s*$'), + RegExp(r'rm\s+-[a-zA-Z]*r[a-zA-Z]*f[a-zA-Z]*\s+/\s*$'), + RegExp(r'mkfs\.'), + RegExp(r'dd\s+.*of=/dev/'), + RegExp(r':(\s*)\{\s*:\|:\s*&\s*\}\s*;\s*:'), + ]; + @override Future execute(Map arguments) async { final command = arguments['command'] as String; + // Check for dangerous commands + for (final pattern in _dangerousPatterns) { + if (pattern.hasMatch(command)) { + return 'Error: This command has been blocked for safety. ' + 'It matches a dangerous pattern that could cause data loss.'; + } + } + final result = await sshRepository.executeCommand(command); return result.fold( diff --git a/lib/presentation/blocs/chat/chat_bloc.dart b/lib/presentation/blocs/chat/chat_bloc.dart index 29d1a70..72d6bf6 100644 --- a/lib/presentation/blocs/chat/chat_bloc.dart +++ b/lib/presentation/blocs/chat/chat_bloc.dart @@ -102,8 +102,9 @@ class ChatBloc extends Bloc { _logger.debug('Created user message: ${userMessage.id}', tag: 'ChatBloc'); // Add user message to UI + final messagesWithUser = [...currentState.messages, userMessage]; emit(currentState.copyWith( - messages: [...currentState.messages, userMessage], + messages: messagesWithUser, )); // Save user message @@ -122,7 +123,7 @@ class ChatBloc extends Bloc { ); emit(currentState.copyWith( - messages: [...currentState.messages, assistantMessage], + messages: [...messagesWithUser, assistantMessage], isStreaming: true, )); @@ -256,62 +257,63 @@ class ChatBloc extends Bloc { final result = await loadSession(event.sessionId); - result.fold( - (failure) async { - _logger.warning('Session not found: ${event.sessionId}', tag: 'ChatBloc'); - // If session not found, create a new one - if (failure.toString().contains('Session not found')) { - // Get connection info - final providersResult = await getProviders(); - providersResult.fold( - (error) { - _logger.error('No provider configured', tag: 'ChatBloc'); - emit(ChatError(message: 'No provider configured. Please add a provider in settings.')); - }, - (providers) { - if (providers.isEmpty) { - _logger.error('No providers available', tag: 'ChatBloc'); - emit(ChatError(message: 'No provider configured. Please add a provider in settings.')); - return; - } - - final defaultProvider = providers.firstWhere( - (p) => p.isDefault, - orElse: () => providers.first, - ); - - _logger.info('Creating new session with provider: ${defaultProvider.name}', tag: 'ChatBloc'); - // Create new session - final newSession = Session( - id: event.sessionId, - name: 'New Chat', - sshConfigId: 'default', - providerId: defaultProvider.id, - workingDirectory: '/home', - createdAt: DateTime.now(), - updatedAt: DateTime.now(), - messages: [], - ); - - emit(ChatLoaded( - session: newSession, - messages: [], - )); - }, - ); - } else { - _logger.error('Failed to load session: ${failure.toString()}', tag: 'ChatBloc'); - emit(ChatError(message: failure.toString())); - } - }, - (session) { - _logger.info('Session loaded successfully with ${session.messages.length} messages', tag: 'ChatBloc'); - emit(ChatLoaded( - session: session, - messages: session.messages, - )); - }, + if (result.isRight()) { + final session = result.getOrElse((_) => throw StateError('unreachable')); + _logger.info('Session loaded successfully with ${session.messages.length} messages', tag: 'ChatBloc'); + emit(ChatLoaded( + session: session, + messages: session.messages, + )); + return; + } + + // Handle failure + final failure = result.fold((f) => f, (_) => throw StateError('unreachable')); + _logger.warning('Session not found: ${event.sessionId}', tag: 'ChatBloc'); + + if (!failure.toString().contains('Session not found')) { + _logger.error('Failed to load session: ${failure.toString()}', tag: 'ChatBloc'); + emit(ChatError(message: failure.toString())); + return; + } + + // Session not found — create a new one + final providersResult = await getProviders(); + + if (providersResult.isLeft()) { + _logger.error('No provider configured', tag: 'ChatBloc'); + emit(ChatError(message: 'No provider configured. Please add a provider in settings.')); + return; + } + + final providers = providersResult.getOrElse((_) => []); + if (providers.isEmpty) { + _logger.error('No providers available', tag: 'ChatBloc'); + emit(ChatError(message: 'No provider configured. Please add a provider in settings.')); + return; + } + + final defaultProvider = providers.firstWhere( + (p) => p.isDefault, + orElse: () => providers.first, ); + + _logger.info('Creating new session with provider: ${defaultProvider.name}', tag: 'ChatBloc'); + final newSession = Session( + id: event.sessionId, + name: 'New Chat', + sshConfigId: 'default', + providerId: defaultProvider.id, + workingDirectory: '/home', + createdAt: DateTime.now(), + updatedAt: DateTime.now(), + messages: [], + ); + + emit(ChatLoaded( + session: newSession, + messages: [], + )); } List _getAvailableTools() { From 56ade8ad78b7252954f9e7262da4d37ae1d20766 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 7 Apr 2026 11:45:23 +0000 Subject: [PATCH 2/2] fix: correct getOrElse signature to use no-argument callback Co-Authored-By: Mohamed --- lib/presentation/blocs/chat/chat_bloc.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/presentation/blocs/chat/chat_bloc.dart b/lib/presentation/blocs/chat/chat_bloc.dart index 72d6bf6..a3c4fff 100644 --- a/lib/presentation/blocs/chat/chat_bloc.dart +++ b/lib/presentation/blocs/chat/chat_bloc.dart @@ -258,7 +258,7 @@ class ChatBloc extends Bloc { final result = await loadSession(event.sessionId); if (result.isRight()) { - final session = result.getOrElse((_) => throw StateError('unreachable')); + final session = result.getOrElse(() => throw StateError('unreachable')); _logger.info('Session loaded successfully with ${session.messages.length} messages', tag: 'ChatBloc'); emit(ChatLoaded( session: session, @@ -286,7 +286,7 @@ class ChatBloc extends Bloc { return; } - final providers = providersResult.getOrElse((_) => []); + final providers = providersResult.getOrElse(() => []); if (providers.isEmpty) { _logger.error('No providers available', tag: 'ChatBloc'); emit(ChatError(message: 'No provider configured. Please add a provider in settings.'));