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

111
lib/widgets/code_block.dart Normal file
View File

@@ -0,0 +1,111 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:markdown/markdown.dart' as md;
class CodeBlockBuilder extends MarkdownElementBuilder {
@override
Widget? visitElementAfter(md.Element element, TextStyle? preferredStyle) {
final code = element.textContent;
final language = element.attributes['class']?.replaceFirst('language-', '') ?? '';
return CodeBlockWidget(
code: code,
language: language,
);
}
}
class CodeBlockWidget extends StatelessWidget {
final String code;
final String language;
const CodeBlockWidget({
super.key,
required this.code,
this.language = '',
});
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.symmetric(vertical: 8),
decoration: BoxDecoration(
color: const Color(0xFF0D1117),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey.shade800),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Header with language and copy button
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: Colors.grey.shade900,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(8),
topRight: Radius.circular(8),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
language.isNotEmpty ? language : 'code',
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade400,
fontFamily: 'JetBrainsMono',
),
),
InkWell(
onTap: () {
Clipboard.setData(ClipboardData(text: code));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Code copied'),
duration: Duration(seconds: 1),
),
);
},
child: Padding(
padding: const EdgeInsets.all(4),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.copy, size: 14, color: Colors.grey.shade400),
const SizedBox(width: 4),
Text(
'Copy',
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade400,
),
),
],
),
),
),
],
),
),
// Code content
SingleChildScrollView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.all(12),
child: SelectableText(
code.trimRight(),
style: const TextStyle(
fontFamily: 'JetBrainsMono',
fontSize: 13,
color: Color(0xFFE6EDF3),
height: 1.5,
),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,68 @@
import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'code_block.dart';
class MarkdownViewer extends StatelessWidget {
final String data;
final bool selectable;
const MarkdownViewer({
super.key,
required this.data,
this.selectable = true,
});
@override
Widget build(BuildContext context) {
return MarkdownBody(
data: data,
selectable: selectable,
styleSheet: MarkdownStyleSheet(
p: const TextStyle(fontSize: 15, height: 1.5),
h1: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
h2: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
h3: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
h4: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
code: TextStyle(
fontFamily: 'JetBrainsMono',
backgroundColor: Colors.black.withOpacity(0.3),
fontSize: 13,
),
codeblockDecoration: BoxDecoration(
color: const Color(0xFF0D1117),
borderRadius: BorderRadius.circular(8),
),
blockquotePadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
blockquoteDecoration: BoxDecoration(
border: Border(
left: BorderSide(
color: Theme.of(context).colorScheme.primary,
width: 3,
),
),
),
listBullet: TextStyle(
color: Theme.of(context).colorScheme.primary,
),
tableHead: const TextStyle(fontWeight: FontWeight.bold),
tableBody: const TextStyle(),
tableBorder: TableBorder.all(
color: Colors.grey.shade700,
width: 1,
),
tableCellsPadding: const EdgeInsets.all(8),
horizontalRuleDecoration: BoxDecoration(
border: Border(
top: BorderSide(color: Colors.grey.shade700, width: 1),
),
),
),
builders: {
'code': CodeBlockBuilder(),
},
);
}
}

View File

@@ -0,0 +1,139 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import '../models/message.dart';
import 'code_block.dart';
class MessageBubble extends StatelessWidget {
final Message message;
const MessageBubble({super.key, required this.message});
@override
Widget build(BuildContext context) {
final isUser = message.isUser;
return Padding(
padding: EdgeInsets.only(
top: 8,
bottom: 8,
left: isUser ? 40 : 0,
right: isUser ? 0 : 40,
),
child: Column(
crossAxisAlignment:
isUser ? CrossAxisAlignment.end : CrossAxisAlignment.start,
children: [
// Role label
Padding(
padding: const EdgeInsets.only(bottom: 4, left: 4, right: 4),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
isUser ? Icons.person : Icons.smart_toy,
size: 14,
color: Colors.grey.shade500,
),
const SizedBox(width: 4),
Text(
isUser ? 'You' : 'Captain',
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade500,
fontWeight: FontWeight.w500,
),
),
if (message.isStreaming) ...[
const SizedBox(width: 8),
SizedBox(
width: 12,
height: 12,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Theme.of(context).colorScheme.primary,
),
),
],
],
),
),
// Message content
GestureDetector(
onLongPress: () {
Clipboard.setData(ClipboardData(text: message.content));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Copied to clipboard'),
duration: Duration(seconds: 1),
),
);
},
child: Container(
decoration: BoxDecoration(
color: isUser
? Theme.of(context).colorScheme.primary.withOpacity(0.2)
: const Color(0xFF2D2D2D),
borderRadius: BorderRadius.circular(16),
),
padding: const EdgeInsets.all(12),
child: isUser
? SelectableText(
message.content,
style: const TextStyle(fontSize: 15),
)
: MarkdownBody(
data: message.content,
selectable: true,
styleSheet: MarkdownStyleSheet(
p: const TextStyle(fontSize: 15),
code: TextStyle(
fontFamily: 'JetBrainsMono',
backgroundColor: Colors.black.withOpacity(0.3),
fontSize: 13,
),
codeblockDecoration: BoxDecoration(
color: Colors.black.withOpacity(0.3),
borderRadius: BorderRadius.circular(8),
),
blockquotePadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
blockquoteDecoration: BoxDecoration(
border: Border(
left: BorderSide(
color: Theme.of(context).colorScheme.primary,
width: 3,
),
),
),
),
builders: {
'code': CodeBlockBuilder(),
},
),
),
),
// Timestamp
Padding(
padding: const EdgeInsets.only(top: 4, left: 4, right: 4),
child: Text(
_formatTime(message.timestamp),
style: TextStyle(
fontSize: 10,
color: Colors.grey.shade600,
),
),
),
],
),
);
}
String _formatTime(DateTime time) {
final hour = time.hour.toString().padLeft(2, '0');
final minute = time.minute.toString().padLeft(2, '0');
return '$hour:$minute';
}
}

View File

@@ -0,0 +1,4 @@
// Re-export xterm's TerminalView for convenience
// This file exists for future customization if needed
export 'package:xterm/xterm.dart' show Terminal, TerminalView, TerminalController, TerminalStyle, TerminalTheme;