Initial commit: Captain Claude Mobile App

- Flutter app with chat and terminal screens
- WebSocket integration for real-time chat
- xterm integration for screen sessions
- Markdown rendering with code blocks
- JWT authentication

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
ARCHITECT
2026-01-16 18:34:02 +00:00
commit 3663e4c622
31 changed files with 2343 additions and 0 deletions

View File

@@ -0,0 +1,145 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import '../models/message.dart';
import '../services/chat_service.dart';
import '../providers/auth_provider.dart';
class ChatProvider with ChangeNotifier {
final ChatService _chatService = ChatService();
final List<Message> _messages = [];
AuthProvider? _authProvider;
StreamSubscription? _messageSubscription;
StreamSubscription? _stateSubscription;
String? _currentConversationId;
bool _isTyping = false;
String _streamingContent = '';
Message? _streamingMessage;
List<Message> get messages => List.unmodifiable(_messages);
bool get isTyping => _isTyping;
bool get isConnected => _chatService.currentState == ChatConnectionState.connected;
ChatConnectionState get connectionState => _chatService.currentState;
String? get currentConversationId => _currentConversationId;
void updateAuth(AuthProvider auth) {
_authProvider = auth;
_chatService.setToken(auth.token);
if (auth.isAuthenticated && !isConnected) {
connect();
} else if (!auth.isAuthenticated && isConnected) {
disconnect();
}
}
Future<void> connect() async {
if (_authProvider?.token == null) return;
_chatService.setToken(_authProvider!.token);
_stateSubscription?.cancel();
_stateSubscription = _chatService.connectionState.listen((state) {
notifyListeners();
});
_messageSubscription?.cancel();
_messageSubscription = _chatService.messages.listen(_handleMessage);
await _chatService.connect();
}
void _handleMessage(Map<String, dynamic> data) {
final type = data['type'];
switch (type) {
case 'connected':
notifyListeners();
break;
case 'start':
_currentConversationId = data['conversation_id'];
_isTyping = true;
_streamingContent = '';
_streamingMessage = Message(
role: 'assistant',
content: '',
isStreaming: true,
);
_messages.add(_streamingMessage!);
notifyListeners();
break;
case 'delta':
_streamingContent += data['content'] ?? '';
if (_streamingMessage != null) {
final index = _messages.indexOf(_streamingMessage!);
if (index >= 0) {
_messages[index] = _streamingMessage!.copyWith(
content: _streamingContent,
);
}
}
notifyListeners();
break;
case 'done':
_isTyping = false;
if (_streamingMessage != null) {
final index = _messages.indexOf(_streamingMessage!);
if (index >= 0) {
_messages[index] = _streamingMessage!.copyWith(
content: data['content'] ?? _streamingContent,
isStreaming: false,
);
}
}
_streamingMessage = null;
_streamingContent = '';
notifyListeners();
break;
case 'error':
_isTyping = false;
_messages.add(Message(
role: 'assistant',
content: 'Error: ${data['message']}',
));
notifyListeners();
break;
}
}
void sendMessage(String content, {List<String>? files}) {
if (content.trim().isEmpty) return;
_messages.add(Message(
role: 'user',
content: content,
attachments: files,
));
notifyListeners();
_chatService.sendMessage(content, files: files);
}
void clearMessages() {
_messages.clear();
_currentConversationId = null;
notifyListeners();
}
void disconnect() {
_chatService.disconnect();
_messageSubscription?.cancel();
_stateSubscription?.cancel();
}
@override
void dispose() {
disconnect();
_chatService.dispose();
super.dispose();
}
}