feat: match Happy Engineering mobile screenshots
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user