11 files changed
+19
-190
No files matched your search
-2
@@ -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,
|
||||
|
||||
-2
@@ -94,8 +94,6 @@ class FullBleedChatListTest {
|
||||
listState = listState,
|
||||
scope = CoroutineScope(Dispatchers.Main.immediate),
|
||||
),
|
||||
lastAnimatedMessageId = null,
|
||||
onLastAnimatedMessageIdChange = {},
|
||||
viewModel = mockk<ChatViewModel>(relaxed = true),
|
||||
clarifyRequest =
|
||||
if (clarify) {
|
||||
|
||||
@@ -272,15 +272,3 @@ class ChatMediaDelegate(
|
||||
uiState.update { it.copy(openError = null) }
|
||||
}
|
||||
}
|
||||
|
||||
internal fun sameMessages(
|
||||
left: List<ChatMessage>,
|
||||
right: List<ChatMessage>,
|
||||
): 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
|
||||
}
|
||||
@@ -375,7 +375,6 @@ fun ChatScreen(
|
||||
viewModel.consumeComposerTextRestore()
|
||||
}
|
||||
}
|
||||
var lastAnimatedMessageId by rememberSaveable { mutableStateOf<String?>(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,
|
||||
|
||||
@@ -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 },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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 ───────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user