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,63 @@
import 'package:flutter/foundation.dart';
import '../models/user.dart';
import '../services/api_service.dart';
import '../services/auth_service.dart';
class AuthProvider with ChangeNotifier {
final ApiService _apiService = ApiService();
late final AuthService _authService;
User? _user;
bool _isLoading = true;
String? _error;
AuthProvider() {
_authService = AuthService(_apiService);
_loadStoredUser();
}
User? get user => _user;
bool get isLoading => _isLoading;
bool get isAuthenticated => _user != null && !_user!.isExpired;
String? get error => _error;
String? get token => _user?.token;
ApiService get apiService => _apiService;
Future<void> _loadStoredUser() async {
_isLoading = true;
notifyListeners();
_user = await _authService.getStoredUser();
_isLoading = false;
notifyListeners();
}
Future<bool> login(String username, String password) async {
_isLoading = true;
_error = null;
notifyListeners();
try {
_user = await _authService.login(username, password);
_isLoading = false;
if (_user == null) {
_error = 'Invalid credentials';
}
notifyListeners();
return _user != null;
} catch (e) {
_error = 'Connection error';
_isLoading = false;
notifyListeners();
return false;
}
}
Future<void> logout() async {
await _authService.logout();
_user = null;
notifyListeners();
}
}

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();
}
}