feat: match Happy Engineering mobile screenshots

This commit is contained in:
Hermes Agent
2026-07-09 11:54:30 +00:00
parent 68a1884ea5
commit 73e17676b5
@@ -105,11 +105,9 @@ import java.net.URLEncoder
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
enum class Screen(val label: String, val icon: ImageVector, val title: String, val kicker: String) { enum class Screen(val label: String, val icon: ImageVector, val title: String, val kicker: String) {
Quest("Chat", Icons.Filled.ChatBubble, "Agent", "Claude Code mobile client"), Inbox("Inbox", Icons.Filled.ChatBubble, "Inbox", "connected"),
Files("Files", Icons.Filled.Folder, "Workspace", "Project files"), Terminals("Terminals", Icons.Filled.Terminal, "Terminals", "connected"),
Terminal("Terminal", Icons.Filled.Terminal, "Terminal", "Run commands"), Settings("Settings", Icons.Filled.Settings, "Settings", "connected")
Status("Status", Icons.Outlined.Bolt, "Sessions", "Companion status"),
Settings("Settings", Icons.Filled.Settings, "Settings", "Pair companion")
} }
data class FileEntry(val name: String, val path: String, val type: String, val size: Long) data class FileEntry(val name: String, val path: String, val type: String, val size: Long)
@@ -231,7 +229,7 @@ class MainActivity : ComponentActivity() {
fun HermesNativeApp() { fun HermesNativeApp() {
val context = LocalContext.current val context = LocalContext.current
val api = remember { CompanionApi(context.applicationContext) } val api = remember { CompanionApi(context.applicationContext) }
var screen by remember { mutableStateOf(Screen.Quest) } var screen by remember { mutableStateOf(Screen.Inbox) }
var online by remember { mutableStateOf(false) } var online by remember { mutableStateOf(false) }
LaunchedEffect(screen) { LaunchedEffect(screen) {
@@ -240,8 +238,8 @@ fun HermesNativeApp() {
MaterialTheme( MaterialTheme(
colorScheme = darkColorScheme( colorScheme = darkColorScheme(
background = Cream, background = AppBackground,
surface = Paper, surface = RowSurface,
primary = Teal, primary = Teal,
onPrimary = Color.Black, onPrimary = Color.Black,
onBackground = Ink, onBackground = Ink,
@@ -250,7 +248,7 @@ fun HermesNativeApp() {
) { ) {
Scaffold( Scaffold(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
containerColor = Cream, containerColor = AppBackground,
topBar = { NativeTopBar(screen, online) }, topBar = { NativeTopBar(screen, online) },
bottomBar = { bottomBar = {
NativeBottomBar( NativeBottomBar(
@@ -272,10 +270,8 @@ fun HermesNativeApp() {
label = "screen-slide" label = "screen-slide"
) { active -> ) { active ->
when (active) { when (active) {
Screen.Quest -> QuestScreen(api) Screen.Inbox -> InboxScreen(api)
Screen.Files -> FilesScreen(api) Screen.Terminals -> TerminalsScreen(api)
Screen.Terminal -> TerminalScreen(api)
Screen.Status -> StatusScreen(api)
Screen.Settings -> SettingsScreen(api) Screen.Settings -> SettingsScreen(api)
} }
} }
@@ -312,37 +308,26 @@ fun NativeTopBar(screen: Screen, online: Boolean) {
.padding(horizontal = 16.dp, vertical = 10.dp), .padding(horizontal = 16.dp, vertical = 10.dp),
verticalArrangement = Arrangement.spacedBy(10.dp) verticalArrangement = Arrangement.spacedBy(10.dp)
) { ) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) { Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp)) {
RaisedCircle(color = Purple, shadow = PurpleDark, content = "H") Text(screen.title, Modifier.weight(1f), fontSize = 24.sp, fontWeight = FontWeight.SemiBold, color = Ink, lineHeight = 28.sp)
Column(Modifier.weight(1f)) { if (screen == Screen.Terminals) Text("+", color = Ink, fontSize = 26.sp, fontWeight = FontWeight.Light)
TinyLabel(screen.kicker)
Text(screen.title, fontSize = 30.sp, fontWeight = FontWeight.Black, color = Ink, lineHeight = 31.sp)
}
StatusDot(online)
}
RaisedCard(
color = if (online) TealPanel else WarningPanel,
shadow = PanelShadow,
border = if (online) TealBorder else WarningBorder,
modifier = Modifier.fillMaxWidth()
) {
Row(Modifier.padding(12.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Icon(if (online) Icons.Filled.Wifi else Icons.Filled.WifiOff, contentDescription = null, tint = if (online) Teal else Orange)
Text(if (online) "Companion connected" else "Set companion URL and access key", color = if (online) Teal else Orange, fontWeight = FontWeight.Black)
} }
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp)) {
Box(Modifier.size(8.dp).clip(CircleShape).background(if (online) Teal else Muted))
Text(if (online) "connected" else "offline", color = if (online) Teal else Muted, fontWeight = FontWeight.SemiBold, fontSize = 13.sp)
} }
} }
} }
@Composable @Composable
fun NativeBottomBar(current: Screen, onSelect: (Screen) -> Unit) { fun NativeBottomBar(current: Screen, onSelect: (Screen) -> Unit) {
Surface(shadowElevation = 14.dp, color = Paper, tonalElevation = 0.dp) { Surface(shadowElevation = 0.dp, color = AppBackground, tonalElevation = 0.dp) {
NavigationBar( NavigationBar(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.navigationBarsPadding() .navigationBarsPadding()
.height(82.dp), .height(76.dp),
containerColor = Paper, containerColor = AppBackground,
tonalElevation = 0.dp tonalElevation = 0.dp
) { ) {
Screen.entries.forEach { item -> Screen.entries.forEach { item ->
@@ -352,9 +337,9 @@ fun NativeBottomBar(current: Screen, onSelect: (Screen) -> Unit) {
icon = { Icon(item.icon, contentDescription = item.label) }, icon = { Icon(item.icon, contentDescription = item.label) },
label = { Text(item.label, fontWeight = FontWeight.Black, fontSize = 11.sp) }, label = { Text(item.label, fontWeight = FontWeight.Black, fontSize = 11.sp) },
colors = NavigationBarItemDefaults.colors( colors = NavigationBarItemDefaults.colors(
selectedIconColor = Teal, selectedIconColor = Ink,
selectedTextColor = Teal, selectedTextColor = Ink,
indicatorColor = TealPanel, indicatorColor = Color.Transparent,
unselectedIconColor = Muted, unselectedIconColor = Muted,
unselectedTextColor = Muted unselectedTextColor = Muted
) )
@@ -365,7 +350,7 @@ fun NativeBottomBar(current: Screen, onSelect: (Screen) -> Unit) {
} }
@Composable @Composable
fun QuestScreen(api: CompanionApi) { fun InboxScreen(api: CompanionApi) {
val scope = remember { CoroutineScope(Dispatchers.Main) } val scope = remember { CoroutineScope(Dispatchers.Main) }
var prompt by remember { mutableStateOf("") } var prompt by remember { mutableStateOf("") }
var busy by remember { mutableStateOf(false) } var busy by remember { mutableStateOf(false) }
@@ -373,12 +358,7 @@ fun QuestScreen(api: CompanionApi) {
val messages = remember { mutableStateListOf<ChatMessage>() } val messages = remember { mutableStateListOf<ChatMessage>() }
ScreenList { ScreenList {
HeroCard(icon = Icons.Filled.ChatBubble, title = "Claude Code from anywhere.", label = "Agent control", text = "Send prompts to your local Hermes companion and continue work from your phone.") Text("Hello!", color = Ink, fontSize = 32.sp, fontWeight = FontWeight.Normal, modifier = Modifier.padding(top = 18.dp, bottom = 24.dp))
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
PathPill(Icons.Filled.CheckCircle, "Connect", true)
PathPill(Icons.Outlined.Bolt, "Prompt", true, current = true)
PathPill(Icons.Filled.ChatBubble, "Review", false)
}
if (messages.isEmpty()) EmptyLesson() if (messages.isEmpty()) EmptyLesson()
messages.forEach { Bubble(it) } messages.forEach { Bubble(it) }
if (error.isNotBlank()) ErrorCard(error) if (error.isNotBlank()) ErrorCard(error)
@@ -453,7 +433,7 @@ fun FilesScreen(api: CompanionApi) {
} }
@Composable @Composable
fun TerminalScreen(api: CompanionApi) { fun TerminalsScreen(api: CompanionApi) {
val scope = remember { CoroutineScope(Dispatchers.Main) } val scope = remember { CoroutineScope(Dispatchers.Main) }
var cwd by remember { mutableStateOf(".") } var cwd by remember { mutableStateOf(".") }
var command by remember { mutableStateOf("pwd && ls") } var command by remember { mutableStateOf("pwd && ls") }
@@ -461,7 +441,11 @@ fun TerminalScreen(api: CompanionApi) {
var busy by remember { mutableStateOf(false) } var busy by remember { mutableStateOf(false) }
ScreenList { ScreenList {
HeroCard(icon = Icons.Filled.Terminal, title = "Run workspace commands.", label = "Terminal", text = "Execute commands on the paired machine without leaving the app.", orbColor = Purple, orbShadow = PurpleDark) SectionLabel("MACHINES")
ListRow(icon = Icons.Filled.Terminal, title = "Hermes", subtitle = "hermes-mobile • online") {}
TextButton(onClick = {}, modifier = Modifier.fillMaxWidth()) { Text("Show archived", color = Muted, fontWeight = FontWeight.SemiBold) }
SectionLabel("NEW CHAT")
ListRow(icon = Icons.Filled.ChatBubble, title = "New chat", subtitle = "Start a session on the paired machine") {}
RaisedCard(Modifier.fillMaxWidth()) { RaisedCard(Modifier.fillMaxWidth()) {
Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) { Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
FieldLabel("Working directory") FieldLabel("Working directory")
@@ -515,7 +499,12 @@ fun SettingsScreen(api: CompanionApi) {
var status by remember { mutableStateOf("Paste the companion access key, then test the connection.") } var status by remember { mutableStateOf("Paste the companion access key, then test the connection.") }
ScreenList { ScreenList {
CompactQuest(title = "Pair companion", label = "Settings", text = status, icon = Icons.Filled.Settings, orbColor = Purple, orbShadow = PurpleDark) {} SectionLabel("MACHINES")
ListRow(icon = Icons.Filled.Terminal, title = "Hermes", subtitle = "linux • online") {}
SectionLabel("FEATURES")
ListRow(icon = Icons.Filled.Settings, title = "Appearance", subtitle = "Customize how the app looks") {}
ListRow(icon = Icons.Outlined.Bolt, title = "Agent Defaults", subtitle = "Default model, effort, and permissions") {}
SectionLabel("COMPANION")
RaisedCard(Modifier.fillMaxWidth()) { RaisedCard(Modifier.fillMaxWidth()) {
Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) { Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
FieldLabel("Companion server URL") FieldLabel("Companion server URL")
@@ -547,11 +536,29 @@ fun ScreenList(content: @Composable ColumnScope.() -> Unit) {
} }
@Composable @Composable
fun RaisedCard(modifier: Modifier = Modifier, color: Color = Paper, shadow: Color = NeutralShadow, border: Color = NeutralBorder, content: @Composable () -> Unit) { fun SectionLabel(text: String) {
Box(modifier.padding(bottom = 5.dp)) { Text(text, color = Muted, fontSize = 12.sp, fontWeight = FontWeight.SemiBold, letterSpacing = 0.6.sp, modifier = Modifier.padding(top = 10.dp, start = 2.dp))
Box(Modifier.matchParentSize().padding(top = 5.dp).clip(RoundedCornerShape(22.dp)).background(shadow)) }
Card(modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(22.dp), colors = CardDefaults.cardColors(containerColor = color), border = BorderStroke(2.dp, border), elevation = CardDefaults.cardElevation(0.dp)) { content() }
@Composable
fun ListRow(icon: ImageVector, title: String, subtitle: String, onClick: () -> Unit) {
RaisedCard(Modifier.fillMaxWidth()) {
Row(Modifier.clickable(onClick = onClick).padding(horizontal = 14.dp, vertical = 13.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) {
Box(Modifier.size(32.dp).clip(RoundedCornerShape(8.dp)).background(Surface), contentAlignment = Alignment.Center) {
Icon(icon, null, tint = Ink, modifier = Modifier.size(19.dp))
} }
Column(Modifier.weight(1f)) {
Text(title, color = Ink, fontWeight = FontWeight.SemiBold, fontSize = 16.sp, maxLines = 1, overflow = TextOverflow.Ellipsis)
Text(subtitle, color = Muted, fontWeight = FontWeight.Normal, fontSize = 13.sp, maxLines = 1, overflow = TextOverflow.Ellipsis)
}
Text("", color = Muted, fontSize = 22.sp, fontWeight = FontWeight.Light)
}
}
}
@Composable
fun RaisedCard(modifier: Modifier = Modifier, color: Color = RowSurface, shadow: Color = Color.Transparent, border: Color = Color.Transparent, content: @Composable () -> Unit) {
Card(modifier = modifier.fillMaxWidth(), shape = RoundedCornerShape(14.dp), colors = CardDefaults.cardColors(containerColor = color), border = BorderStroke(0.dp, border), elevation = CardDefaults.cardElevation(0.dp)) { content() }
} }
@Composable @Composable
@@ -611,9 +618,9 @@ val Red = Color(0xFFF87171)
val PaleRed = Color(0xFF311719) val PaleRed = Color(0xFF311719)
val RedBorder = Color(0xFF7A363A) val RedBorder = Color(0xFF7A363A)
val RedShadow = Color(0xFF0A0304) val RedShadow = Color(0xFF0A0304)
val Cream = Color(0xFF000000) val AppBackground = Color(0xFF212121)
val Paper = Color(0xFF0B0F12) val RowSurface = Color(0xFF303030)
val Surface = Color(0xFF12181D) val Surface = Color(0xFF2B2B2B)
val Ink = Color(0xFFF4F4F5) val Ink = Color(0xFFF4F4F5)
val Muted = Color(0xFF9CA3AF) val Muted = Color(0xFF9CA3AF)
val NeutralBorder = Color(0xFF2A3138) val NeutralBorder = Color(0xFF2A3138)