diff --git a/app/src/androidTest/java/com/m57/hermescontrol/ui/chat/fullbleed/FullBleedChatAnchorTest.kt b/app/src/androidTest/java/com/m57/hermescontrol/ui/chat/fullbleed/FullBleedChatAnchorTest.kt index dfbf9241..58d09b5c 100644 --- a/app/src/androidTest/java/com/m57/hermescontrol/ui/chat/fullbleed/FullBleedChatAnchorTest.kt +++ b/app/src/androidTest/java/com/m57/hermescontrol/ui/chat/fullbleed/FullBleedChatAnchorTest.kt @@ -133,8 +133,6 @@ class FullBleedChatAnchorTest { isLoadingOlder = loading.value, listState = listState, scrollController = controller, - lastAnimatedMessageId = null, - onLastAnimatedMessageIdChange = {}, viewModel = viewModel, pagingSessionId = session.value, clarifyRequest = if (tailKey != null) ClarifyUi("Choose an option", listOf("Yes")) else null, diff --git a/app/src/androidTest/java/com/m57/hermescontrol/ui/chat/fullbleed/FullBleedChatListTest.kt b/app/src/androidTest/java/com/m57/hermescontrol/ui/chat/fullbleed/FullBleedChatListTest.kt index 9a4ef7f1..b55870f2 100644 --- a/app/src/androidTest/java/com/m57/hermescontrol/ui/chat/fullbleed/FullBleedChatListTest.kt +++ b/app/src/androidTest/java/com/m57/hermescontrol/ui/chat/fullbleed/FullBleedChatListTest.kt @@ -94,8 +94,6 @@ class FullBleedChatListTest { listState = listState, scope = CoroutineScope(Dispatchers.Main.immediate), ), - lastAnimatedMessageId = null, - onLastAnimatedMessageIdChange = {}, viewModel = mockk(relaxed = true), clarifyRequest = if (clarify) { diff --git a/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatMediaDelegate.kt b/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatMediaDelegate.kt index 6e2bab35..bb8f0afe 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatMediaDelegate.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatMediaDelegate.kt @@ -272,15 +272,3 @@ class ChatMediaDelegate( uiState.update { it.copy(openError = null) } } } - -internal fun sameMessages( - left: List, - right: List, -): Boolean = - left.size == right.size && - left.zip(right).all { (a, b) -> - a.id == b.id && - a.role == b.role && - a.content == b.content && - a.reasoningText == b.reasoningText - } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatScreen.kt index 1568e8dd..1288de73 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatScreen.kt @@ -375,7 +375,6 @@ fun ChatScreen( viewModel.consumeComposerTextRestore() } } - var lastAnimatedMessageId by rememberSaveable { mutableStateOf(null) } var showReloginDialog by rememberSaveable { mutableStateOf(false) } var showSubagentInspectionSheet by rememberSaveable { mutableStateOf(false) } LaunchedEffect(showSubagentInspectionSheet) { @@ -783,8 +782,6 @@ fun ChatScreen( }, listState = listState, scrollController = scrollController, - lastAnimatedMessageId = lastAnimatedMessageId, - onLastAnimatedMessageIdChange = { lastAnimatedMessageId = it }, viewModel = viewModel, clarifyRequest = state.clarifyRequest.takeUnless { timelineState.isHistorical }, onRespondClarify = viewModel::respondToClarify, diff --git a/app/src/main/java/com/m57/hermescontrol/ui/chat/MarkdownText.kt b/app/src/main/java/com/m57/hermescontrol/ui/chat/MarkdownText.kt index 836712b6..a3453697 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/chat/MarkdownText.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/chat/MarkdownText.kt @@ -74,7 +74,6 @@ import com.m57.hermescontrol.ui.chat.markdown.splitInlineMath import com.m57.hermescontrol.util.BidiUtils private val URL_PATTERN = Regex("""https?://[^\s)>\[\]"'‘’]+""") -private val TABLE_COL_WIDTH = 160.dp private fun bidiTextDirection(isRtl: Boolean): TextDirection = if (isRtl) TextDirection.Rtl else TextDirection.Ltr @@ -359,13 +358,13 @@ fun MarkdownText( .fillMaxWidth() .padding(vertical = 4.dp), ) { - com.m57.hermescontrol.ui.chat.components.InlineVideoPlayer( - videoUri = resolvedUri, + com.m57.hermescontrol.ui.chat.components.InlineMediaPlayer( + uri = resolvedUri, onFullScreenClick = { showVideoDialog = true }, ) if (showVideoDialog) { - com.m57.hermescontrol.ui.chat.components.VideoViewerDialog( - videoUri = resolvedUri, + com.m57.hermescontrol.ui.chat.components.MediaViewerDialog( + mediaUri = resolvedUri, onDismissRequest = { showVideoDialog = false }, ) } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/chat/ToolSchemaRegistry.kt b/app/src/main/java/com/m57/hermescontrol/ui/chat/ToolSchemaRegistry.kt index 4af0ace6..22afc91d 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/chat/ToolSchemaRegistry.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/chat/ToolSchemaRegistry.kt @@ -40,10 +40,6 @@ import androidx.compose.ui.graphics.vector.ImageVector */ data class ToolDisplayConfig( val name: String, - /** Key into the `args` dict for the one-line summary (e.g. "command" for terminal). */ - val summaryArgKey: String? = null, - /** Prefix for the summary line (e.g. "$ " for terminal). Emoji-free. */ - val summaryPrefix: String = "", /** Material icon for this tool type. */ val icon: ImageVector = Icons.Filled.Build, ) @@ -55,285 +51,238 @@ object ToolSchemaRegistry { "terminal" to ToolDisplayConfig( name = "terminal", - summaryArgKey = "command", - summaryPrefix = "$ ", icon = Icons.Filled.Terminal, ), "read_file" to ToolDisplayConfig( name = "read_file", - summaryArgKey = "path", icon = Icons.Filled.Description, ), "write_file" to ToolDisplayConfig( name = "write_file", - summaryArgKey = "path", icon = Icons.Filled.Edit, ), "patch" to ToolDisplayConfig( name = "patch", - summaryArgKey = "path", icon = Icons.Filled.Build, ), "search_files" to ToolDisplayConfig( name = "search_files", - summaryArgKey = "pattern", icon = Icons.Filled.Search, ), "web_search" to ToolDisplayConfig( name = "web_search", - summaryArgKey = "query", icon = Icons.Filled.Public, ), "browser_navigate" to ToolDisplayConfig( name = "browser_navigate", - summaryArgKey = "url", icon = Icons.Filled.Language, ), "browser_click" to ToolDisplayConfig( name = "browser_click", - summaryArgKey = "ref", icon = Icons.Filled.TouchApp, ), "browser_snapshot" to ToolDisplayConfig( name = "browser_snapshot", - summaryArgKey = null, icon = Icons.Filled.Photo, ), "clarify" to ToolDisplayConfig( name = "clarify", - summaryArgKey = "question", icon = Icons.Filled.ChatBubble, ), "delegate_task" to ToolDisplayConfig( name = "delegate_task", - summaryArgKey = "goal", icon = Icons.Filled.AccountTree, ), "execute_code" to ToolDisplayConfig( name = "execute_code", - summaryArgKey = "code", icon = Icons.Filled.PlayArrow, ), "todo" to ToolDisplayConfig( name = "todo", - summaryArgKey = null, icon = Icons.Filled.Checklist, ), "todo_list" to ToolDisplayConfig( name = "todo_list", - summaryArgKey = null, icon = Icons.Filled.Checklist, ), "fact_store" to ToolDisplayConfig( name = "fact_store", - summaryArgKey = null, icon = Icons.Filled.Psychology, ), "session_search" to ToolDisplayConfig( name = "session_search", - summaryArgKey = null, icon = Icons.Filled.Search, ), // ── Action-based tools ── "cronjob" to ToolDisplayConfig( name = "cronjob", - summaryArgKey = "action", icon = Icons.Filled.Schedule, ), "cronjob_manage" to ToolDisplayConfig( name = "cronjob_manage", - summaryArgKey = "action", icon = Icons.Filled.Schedule, ), "memory" to ToolDisplayConfig( name = "memory", - summaryArgKey = "action", icon = Icons.Filled.Memory, ), "fact_feedback" to ToolDisplayConfig( name = "fact_feedback", - summaryArgKey = "action", icon = Icons.Filled.ThumbUp, ), "process" to ToolDisplayConfig( name = "process", - summaryArgKey = "action", icon = Icons.Filled.Settings, ), "process_manage" to ToolDisplayConfig( name = "process_manage", - summaryArgKey = "action", icon = Icons.Filled.Settings, ), "skill_manage" to ToolDisplayConfig( name = "skill_manage", - summaryArgKey = "action", icon = Icons.Filled.Build, ), // ── Web / Browser tools ── "web_extract" to ToolDisplayConfig( name = "web_extract", - summaryArgKey = "urls", icon = Icons.Filled.Language, ), "browser_type" to ToolDisplayConfig( name = "browser_type", - summaryArgKey = null, icon = Icons.Filled.Keyboard, ), "browser_scroll" to ToolDisplayConfig( name = "browser_scroll", - summaryArgKey = "direction", icon = Icons.Filled.TouchApp, ), "browser_back" to ToolDisplayConfig( name = "browser_back", - summaryArgKey = null, icon = Icons.Filled.Language, ), "browser_press" to ToolDisplayConfig( name = "browser_press", - summaryArgKey = "key", icon = Icons.Filled.Keyboard, ), "browser_get_images" to ToolDisplayConfig( name = "browser_get_images", - summaryArgKey = null, icon = Icons.Filled.Image, ), "browser_vision" to ToolDisplayConfig( name = "browser_vision", - summaryArgKey = "question", icon = Icons.Filled.Visibility, ), "browser_console" to ToolDisplayConfig( name = "browser_console", - summaryArgKey = null, icon = Icons.Filled.Terminal, ), "browser_cdp" to ToolDisplayConfig( name = "browser_cdp", - summaryArgKey = "action", icon = Icons.Filled.Build, ), "browser_dialog" to ToolDisplayConfig( name = "browser_dialog", - summaryArgKey = "action", icon = Icons.Filled.ChatBubble, ), // ── Media / Vision tools ── "vision_analyze" to ToolDisplayConfig( name = "vision_analyze", - summaryArgKey = "image_url", icon = Icons.Filled.Visibility, ), "text_to_speech" to ToolDisplayConfig( name = "text_to_speech", - summaryArgKey = null, icon = Icons.AutoMirrored.Filled.VolumeUp, ), "video_generate" to ToolDisplayConfig( name = "video_generate", - summaryArgKey = null, icon = Icons.Filled.Movie, ), "image_generate" to ToolDisplayConfig( name = "image_generate", - summaryArgKey = null, icon = Icons.Filled.Image, ), // ── Social / Messaging tools ── "x_search" to ToolDisplayConfig( name = "x_search", - summaryArgKey = "query", icon = Icons.Filled.Forum, ), "send_message" to ToolDisplayConfig( name = "send_message", - summaryArgKey = null, icon = Icons.AutoMirrored.Filled.Send, ), // ── Skills tools ── "skills_list" to ToolDisplayConfig( name = "skills_list", - summaryArgKey = null, icon = Icons.AutoMirrored.Filled.MenuBook, ), "skill_view" to ToolDisplayConfig( name = "skill_view", - summaryArgKey = "name", icon = Icons.Filled.AutoStories, ), // ── On-demand tools ── "tool_search" to ToolDisplayConfig( name = "tool_search", - summaryArgKey = "query", icon = Icons.Filled.Search, ), "tool_describe" to ToolDisplayConfig( name = "tool_describe", - summaryArgKey = "name", icon = Icons.Filled.AutoStories, ), "tool_call" to ToolDisplayConfig( name = "tool_call", - summaryArgKey = "name", icon = Icons.Filled.Build, ), // ── Misc ── "read_terminal" to ToolDisplayConfig( name = "read_terminal", - summaryArgKey = "session_id", icon = Icons.Filled.Computer, ), "computer_use" to ToolDisplayConfig( name = "computer_use", - summaryArgKey = "action", icon = Icons.Filled.Computer, ), ) diff --git a/app/src/main/java/com/m57/hermescontrol/ui/chat/components/InlineMediaPlayer.kt b/app/src/main/java/com/m57/hermescontrol/ui/chat/components/InlineMediaPlayer.kt index 972f4399..702d490a 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/chat/components/InlineMediaPlayer.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/chat/components/InlineMediaPlayer.kt @@ -209,3 +209,18 @@ private fun InlineVideoPreview( } } } + +/** Formats milliseconds into `M:SS` or `H:MM:SS` */ +fun formatMediaDuration(durationMs: Long): String { + if (durationMs <= 0) return "0:00" + val totalSeconds = durationMs / 1000 + val seconds = totalSeconds % 60 + val minutes = (totalSeconds / 60) % 60 + val hours = totalSeconds / 3600 + + return if (hours > 0) { + "%d:%02d:%02d".format(hours, minutes, seconds) + } else { + "%d:%02d".format(minutes, seconds) + } +} diff --git a/app/src/main/java/com/m57/hermescontrol/ui/chat/components/InlineVideoPlayer.kt b/app/src/main/java/com/m57/hermescontrol/ui/chat/components/InlineVideoPlayer.kt deleted file mode 100644 index 15ed878c..00000000 --- a/app/src/main/java/com/m57/hermescontrol/ui/chat/components/InlineVideoPlayer.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.m57.hermescontrol.ui.chat.components - -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier - -/** - * Backwards-compatible wrapper delegating to [InlineMediaPlayer]. - */ -@Composable -fun InlineVideoPlayer( - videoUri: String, - modifier: Modifier = Modifier, - onFullScreenClick: () -> Unit = {}, -) { - InlineMediaPlayer( - uri = videoUri, - modifier = modifier, - onFullScreenClick = onFullScreenClick, - ) -} - -/** Formats milliseconds into `M:SS` or `H:MM:SS` */ -fun formatMediaDuration(durationMs: Long): String { - if (durationMs <= 0) return "0:00" - val totalSeconds = durationMs / 1000 - val seconds = totalSeconds % 60 - val minutes = (totalSeconds / 60) % 60 - val hours = totalSeconds / 3600 - - return if (hours > 0) { - "%d:%02d:%02d".format(hours, minutes, seconds) - } else { - "%d:%02d".format(minutes, seconds) - } -} diff --git a/app/src/main/java/com/m57/hermescontrol/ui/chat/components/MessageCards.kt b/app/src/main/java/com/m57/hermescontrol/ui/chat/components/MessageCards.kt index df4038d9..0c3ed069 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/chat/components/MessageCards.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/chat/components/MessageCards.kt @@ -90,7 +90,6 @@ import com.m57.hermescontrol.theme.CodeTerminalMuted import com.m57.hermescontrol.theme.CodeTerminalText import com.m57.hermescontrol.ui.chat.ClarifyUi import com.m57.hermescontrol.ui.chat.MarkdownText -import com.m57.hermescontrol.ui.chat.SubagentIndicator import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.withContext @@ -400,14 +399,6 @@ private data class TokenPattern( val color: Color, ) -private fun copyToClipboard( - context: Context, - text: String, -) { - val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager ?: return - clipboard.setPrimaryClip(ClipData.newPlainText(null, text)) -} - // ── ClarifyBubble ───────────────────────────────────────────────────────── /** @@ -670,58 +661,6 @@ fun ClarifyBubble( ) } -// ── SubagentCard ────────────────────────────────────────────────────────── - -/** - * Inline card showing subagent task progress. - * - * - While running: [CircularProgressIndicator] + goal text on [tertiaryContainer]. - * - On completion: ✅ checkmark + summary text. - */ -@Composable -fun SubagentCard( - indicator: SubagentIndicator, - modifier: Modifier = Modifier, -) { - val isComplete = indicator.type == "subagent.complete" - Surface( - modifier = modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 2.dp).testTag("subagent_card"), - shape = RoundedCornerShape(12.dp), - color = MaterialTheme.colorScheme.tertiaryContainer, - ) { - Row( - modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - if (isComplete) { - Text(text = "✅", fontSize = 14.sp) - } else { - CircularProgressIndicator( - modifier = Modifier.size(16.dp), - strokeWidth = 2.dp, - color = MaterialTheme.colorScheme.onTertiaryContainer, - ) - } - Spacer(Modifier.width(8.dp)) - val displayText = - if (isComplete && !indicator.summary.isNullOrBlank()) { - indicator.summary - } else if (!indicator.goal.isNullOrBlank()) { - indicator.goal - } else { - "Subagent task" - } - Text( - text = displayText, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onTertiaryContainer, - maxLines = 2, - overflow = TextOverflow.Ellipsis, - ) - } - } -} - // ── TypingIndicator ─────────────────────────────────────────────────────── /** diff --git a/app/src/main/java/com/m57/hermescontrol/ui/chat/components/VideoViewerDialog.kt b/app/src/main/java/com/m57/hermescontrol/ui/chat/components/VideoViewerDialog.kt deleted file mode 100644 index 429b5230..00000000 --- a/app/src/main/java/com/m57/hermescontrol/ui/chat/components/VideoViewerDialog.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.m57.hermescontrol.ui.chat.components - -import androidx.compose.runtime.Composable - -/** - * Backwards-compatible wrapper delegating to [MediaViewerDialog]. - */ -@Composable -fun VideoViewerDialog( - videoUri: String, - onDismissRequest: () -> Unit, -) { - MediaViewerDialog( - mediaUri = videoUri, - onDismissRequest = onDismissRequest, - ) -} diff --git a/app/src/main/java/com/m57/hermescontrol/ui/chat/fullbleed/FullBleedChatList.kt b/app/src/main/java/com/m57/hermescontrol/ui/chat/fullbleed/FullBleedChatList.kt index 0a72c056..3016ec59 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/chat/fullbleed/FullBleedChatList.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/chat/fullbleed/FullBleedChatList.kt @@ -93,8 +93,6 @@ fun FullBleedChatList( isLoadingOlder: Boolean, listState: androidx.compose.foundation.lazy.LazyListState, scrollController: ChatScrollController, - lastAnimatedMessageId: String?, - onLastAnimatedMessageIdChange: (String?) -> Unit, viewModel: ChatViewModel, clarifyRequest: ClarifyUi? = null, onRespondClarify: ((String) -> Unit)? = null,