Files
captain-mobile/lib/services/terminal_service.dart
ARCHITECT 3663e4c622 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>
2026-01-16 18:34:02 +00:00

116 lines
2.9 KiB
Dart

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