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
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"),
Files("Files", Icons.Filled.Folder, "Workspace", "Project files"),
Terminal("Terminal", Icons.Filled.Terminal, "Terminal", "Run commands"),
Status("Status", Icons.Outlined.Bolt, "Sessions", "Companion status"),
Settings("Settings", Icons.Filled.Settings, "Settings", "Pair companion")
Inbox("Inbox", Icons.Filled.ChatBubble, "Inbox", "connected"),
Terminals("Terminals", Icons.Filled.Terminal, "Terminals", "connected"),
Settings("Settings", Icons.Filled.Settings, "Settings", "connected")
}
data class FileEntry(val name: String, val path: String, val type: String, val size: Long)
@@ -231,7 +229,7 @@ class MainActivity : ComponentActivity() {
fun HermesNativeApp() {
val context = LocalContext.current
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) }
LaunchedEffect(screen) {
@@ -240,8 +238,8 @@ fun HermesNativeApp() {
MaterialTheme(
colorScheme = darkColorScheme(
background = Cream,
surface = Paper,
background = AppBackground,
surface = RowSurface,
primary = Teal,
onPrimary = Color.Black,
onBackground = Ink,
@@ -250,7 +248,7 @@ fun HermesNativeApp() {
) {
Scaffold(
modifier = Modifier.fillMaxSize(),
containerColor = Cream,
containerColor = AppBackground,
topBar = { NativeTopBar(screen, online) },
bottomBar = {
NativeBottomBar(
@@ -272,10 +270,8 @@ fun HermesNativeApp() {
label = "screen-slide"
) { active ->
when (active) {
Screen.Quest -> QuestScreen(api)
Screen.Files -> FilesScreen(api)
Screen.Terminal -> TerminalScreen(api)
Screen.Status -> StatusScreen(api)
Screen.Inbox -> InboxScreen(api)
Screen.Terminals -> TerminalsScreen(api)
Screen.Settings -> SettingsScreen(api)
}
}
@@ -312,37 +308,26 @@ fun NativeTopBar(screen: Screen, online: Boolean) {
.padding(horizontal = 16.dp, vertical = 10.dp),
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) {
RaisedCircle(color = Purple, shadow = PurpleDark, content = "H")
Column(Modifier.weight(1f)) {
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(10.dp)) {
Text(screen.title, Modifier.weight(1f), fontSize = 24.sp, fontWeight = FontWeight.SemiBold, color = Ink, lineHeight = 28.sp)
if (screen == Screen.Terminals) Text("+", color = Ink, fontSize = 26.sp, fontWeight = FontWeight.Light)
}
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
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(
modifier = Modifier
.fillMaxWidth()
.navigationBarsPadding()
.height(82.dp),
containerColor = Paper,
.height(76.dp),
containerColor = AppBackground,
tonalElevation = 0.dp
) {
Screen.entries.forEach { item ->
@@ -352,9 +337,9 @@ fun NativeBottomBar(current: Screen, onSelect: (Screen) -> Unit) {
icon = { Icon(item.icon, contentDescription = item.label) },
label = { Text(item.label, fontWeight = FontWeight.Black, fontSize = 11.sp) },
colors = NavigationBarItemDefaults.colors(
selectedIconColor = Teal,
selectedTextColor = Teal,
indicatorColor = TealPanel,
selectedIconColor = Ink,
selectedTextColor = Ink,
indicatorColor = Color.Transparent,
unselectedIconColor = Muted,
unselectedTextColor = Muted
)
@@ -365,7 +350,7 @@ fun NativeBottomBar(current: Screen, onSelect: (Screen) -> Unit) {
}
@Composable
fun QuestScreen(api: CompanionApi) {
fun InboxScreen(api: CompanionApi) {
val scope = remember { CoroutineScope(Dispatchers.Main) }
var prompt by remember { mutableStateOf("") }
var busy by remember { mutableStateOf(false) }
@@ -373,12 +358,7 @@ fun QuestScreen(api: CompanionApi) {
val messages = remember { mutableStateListOf<ChatMessage>() }
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.")
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)
}
Text("Hello!", color = Ink, fontSize = 32.sp, fontWeight = FontWeight.Normal, modifier = Modifier.padding(top = 18.dp, bottom = 24.dp))
if (messages.isEmpty()) EmptyLesson()
messages.forEach { Bubble(it) }
if (error.isNotBlank()) ErrorCard(error)
@@ -453,7 +433,7 @@ fun FilesScreen(api: CompanionApi) {
}
@Composable
fun TerminalScreen(api: CompanionApi) {
fun TerminalsScreen(api: CompanionApi) {
val scope = remember { CoroutineScope(Dispatchers.Main) }
var cwd by remember { mutableStateOf(".") }
var command by remember { mutableStateOf("pwd && ls") }
@@ -461,7 +441,11 @@ fun TerminalScreen(api: CompanionApi) {
var busy by remember { mutableStateOf(false) }
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()) {
Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
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.") }
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()) {
Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
FieldLabel("Companion server URL")
@@ -547,11 +536,29 @@ fun ScreenList(content: @Composable ColumnScope.() -> Unit) {
}
@Composable
fun RaisedCard(modifier: Modifier = Modifier, color: Color = Paper, shadow: Color = NeutralShadow, border: Color = NeutralBorder, content: @Composable () -> Unit) {
Box(modifier.padding(bottom = 5.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() }
fun SectionLabel(text: String) {
Text(text, color = Muted, fontSize = 12.sp, fontWeight = FontWeight.SemiBold, letterSpacing = 0.6.sp, modifier = Modifier.padding(top = 10.dp, start = 2.dp))
}
@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
@@ -611,9 +618,9 @@ val Red = Color(0xFFF87171)
val PaleRed = Color(0xFF311719)
val RedBorder = Color(0xFF7A363A)
val RedShadow = Color(0xFF0A0304)
val Cream = Color(0xFF000000)
val Paper = Color(0xFF0B0F12)
val Surface = Color(0xFF12181D)
val AppBackground = Color(0xFF212121)
val RowSurface = Color(0xFF303030)
val Surface = Color(0xFF2B2B2B)
val Ink = Color(0xFFF4F4F5)
val Muted = Color(0xFF9CA3AF)
val NeutralBorder = Color(0xFF2A3138)