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:
115
lib/services/terminal_service.dart
Normal file
115
lib/services/terminal_service.dart
Normal file
@@ -0,0 +1,115 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||
import '../config/api_config.dart';
|
||||
|
||||
enum TerminalConnectionState {
|
||||
disconnected,
|
||||
connecting,
|
||||
connected,
|
||||
error,
|
||||
}
|
||||
|
||||
class TerminalService {
|
||||
WebSocketChannel? _channel;
|
||||
String? _token;
|
||||
String? _sessionName;
|
||||
|
||||
final _outputController = StreamController<String>.broadcast();
|
||||
final _stateController = StreamController<TerminalConnectionState>.broadcast();
|
||||
|
||||
Stream<String> get output => _outputController.stream;
|
||||
Stream<TerminalConnectionState> get connectionState => _stateController.stream;
|
||||
|
||||
TerminalConnectionState _currentState = TerminalConnectionState.disconnected;
|
||||
TerminalConnectionState get currentState => _currentState;
|
||||
|
||||
void setToken(String? token) {
|
||||
_token = token;
|
||||
}
|
||||
|
||||
Future<void> connect(String sessionName) async {
|
||||
if (_token == null) {
|
||||
_updateState(TerminalConnectionState.error);
|
||||
return;
|
||||
}
|
||||
|
||||
_sessionName = sessionName;
|
||||
_updateState(TerminalConnectionState.connecting);
|
||||
|
||||
try {
|
||||
final uri = Uri.parse('${ApiConfig.wsUrl}${ApiConfig.wsTerminal(sessionName)}');
|
||||
_channel = WebSocketChannel.connect(uri);
|
||||
|
||||
await _channel!.ready;
|
||||
|
||||
// Send auth token
|
||||
_channel!.sink.add(jsonEncode({'token': _token}));
|
||||
|
||||
_channel!.stream.listen(
|
||||
(data) {
|
||||
try {
|
||||
final message = jsonDecode(data);
|
||||
if (message['type'] == 'connected') {
|
||||
_updateState(TerminalConnectionState.connected);
|
||||
} else if (message['type'] == 'output') {
|
||||
_outputController.add(message['data'] ?? '');
|
||||
}
|
||||
} catch (e) {
|
||||
// Raw data
|
||||
_outputController.add(data.toString());
|
||||
}
|
||||
},
|
||||
onError: (error) {
|
||||
_updateState(TerminalConnectionState.error);
|
||||
},
|
||||
onDone: () {
|
||||
_updateState(TerminalConnectionState.disconnected);
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
_updateState(TerminalConnectionState.error);
|
||||
}
|
||||
}
|
||||
|
||||
void sendInput(String input) {
|
||||
if (_channel == null || _currentState != TerminalConnectionState.connected) {
|
||||
return;
|
||||
}
|
||||
|
||||
_channel!.sink.add(jsonEncode({
|
||||
'type': 'input',
|
||||
'data': input,
|
||||
}));
|
||||
}
|
||||
|
||||
void resize(int rows, int cols) {
|
||||
if (_channel == null || _currentState != TerminalConnectionState.connected) {
|
||||
return;
|
||||
}
|
||||
|
||||
_channel!.sink.add(jsonEncode({
|
||||
'type': 'resize',
|
||||
'rows': rows,
|
||||
'cols': cols,
|
||||
}));
|
||||
}
|
||||
|
||||
void _updateState(TerminalConnectionState state) {
|
||||
_currentState = state;
|
||||
_stateController.add(state);
|
||||
}
|
||||
|
||||
void disconnect() {
|
||||
_channel?.sink.close();
|
||||
_channel = null;
|
||||
_sessionName = null;
|
||||
_updateState(TerminalConnectionState.disconnected);
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
disconnect();
|
||||
_outputController.close();
|
||||
_stateController.close();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user