Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
1 parent
908255f5ad
commit
11dd1eff92
8 files changed
+414
-20
No files matched your search
@@ -114,8 +114,10 @@ data class SubagentIndicator(
|
|||||||
) {
|
) {
|
||||||
val isComplete: Boolean get() = type == "subagent.complete" || status == "completed" || status == "done"
|
val isComplete: Boolean get() = type == "subagent.complete" || status == "completed" || status == "done"
|
||||||
val isFailed: Boolean get() = status == "failed" || status == "interrupted"
|
val isFailed: Boolean get() = status == "failed" || status == "interrupted"
|
||||||
|
val isCancelled: Boolean get() = status == "cancelled" || status == "stopped" || status == "canceled"
|
||||||
|
val isSteered: Boolean get() = status == "steered"
|
||||||
val isQueued: Boolean get() = status == "queued"
|
val isQueued: Boolean get() = status == "queued"
|
||||||
val isRunning: Boolean get() = !isComplete && !isFailed && !isQueued
|
val isRunning: Boolean get() = !isComplete && !isFailed && !isCancelled && !isQueued
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class MessageRole {
|
enum class MessageRole {
|
||||||
|
|||||||
@@ -930,6 +930,8 @@ fun ChatScreen(
|
|||||||
SubagentInspectionSheet(
|
SubagentInspectionSheet(
|
||||||
indicators = state.subagentIndicators,
|
indicators = state.subagentIndicators,
|
||||||
todos = state.todos,
|
todos = state.todos,
|
||||||
|
onSteerSubagent = { indicator, msg -> viewModel.steerSubagent(indicator, msg) },
|
||||||
|
onStopSubagent = { indicator -> viewModel.stopSubagent(indicator) },
|
||||||
onDismiss = { showSubagentInspectionSheet = false },
|
onDismiss = { showSubagentInspectionSheet = false },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1960,6 +1960,85 @@ class ChatViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send course-correction guidance to a live subagent child session (issue #1030).
|
||||||
|
*/
|
||||||
|
fun steerSubagent(
|
||||||
|
indicator: SubagentIndicator,
|
||||||
|
message: String,
|
||||||
|
) {
|
||||||
|
val trimmed = message.trim()
|
||||||
|
if (trimmed.isBlank()) return
|
||||||
|
val sessionId = runtimeSessionId ?: return
|
||||||
|
val subagentId = indicator.subagentId
|
||||||
|
val steerCommand =
|
||||||
|
if (!subagentId.isNullOrBlank()) {
|
||||||
|
"/steer $subagentId $trimmed"
|
||||||
|
} else {
|
||||||
|
"/steer $trimmed"
|
||||||
|
}
|
||||||
|
viewModelScope.launch(ioDispatcher) {
|
||||||
|
wsClient.sendRedirect(
|
||||||
|
sessionId,
|
||||||
|
steerCommand,
|
||||||
|
onSent = { id -> trackRequest(id, WsMethods.SESSION_REDIRECT) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
_uiState.update { current ->
|
||||||
|
val updated =
|
||||||
|
current.subagentIndicators.map { ind ->
|
||||||
|
if (ind.subagentId == indicator.subagentId &&
|
||||||
|
(ind.goal == indicator.goal || indicator.subagentId != null)
|
||||||
|
) {
|
||||||
|
val newLogs =
|
||||||
|
(ind.logs + SubagentLogLine(text = "Course correction: \"$trimmed\"", isSummary = true))
|
||||||
|
.takeLast(30)
|
||||||
|
ind.copy(status = "steered", logs = newLogs)
|
||||||
|
} else {
|
||||||
|
ind
|
||||||
|
}
|
||||||
|
}
|
||||||
|
current.copy(subagentIndicators = updated)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop / interrupt a single live subagent early (issue #1030).
|
||||||
|
*/
|
||||||
|
fun stopSubagent(indicator: SubagentIndicator) {
|
||||||
|
val sessionId = runtimeSessionId ?: return
|
||||||
|
val subagentId = indicator.subagentId
|
||||||
|
val stopCommand =
|
||||||
|
if (!subagentId.isNullOrBlank()) {
|
||||||
|
"/stop $subagentId"
|
||||||
|
} else {
|
||||||
|
"/stop"
|
||||||
|
}
|
||||||
|
viewModelScope.launch(ioDispatcher) {
|
||||||
|
wsClient.sendRedirect(
|
||||||
|
sessionId,
|
||||||
|
stopCommand,
|
||||||
|
onSent = { id -> trackRequest(id, WsMethods.SESSION_REDIRECT) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
_uiState.update { current ->
|
||||||
|
val updated =
|
||||||
|
current.subagentIndicators.map { ind ->
|
||||||
|
if (ind.subagentId == indicator.subagentId &&
|
||||||
|
(ind.goal == indicator.goal || indicator.subagentId != null)
|
||||||
|
) {
|
||||||
|
val newLogs =
|
||||||
|
(ind.logs + SubagentLogLine(text = "Stopped subagent", isError = true))
|
||||||
|
.takeLast(30)
|
||||||
|
ind.copy(status = "cancelled", logs = newLogs)
|
||||||
|
} else {
|
||||||
|
ind
|
||||||
|
}
|
||||||
|
}
|
||||||
|
current.copy(subagentIndicators = updated)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun createNewSession(setLoading: Boolean = true) {
|
fun createNewSession(setLoading: Boolean = true) {
|
||||||
// A fresh create has no persisted row until the first prompt.
|
// A fresh create has no persisted row until the first prompt.
|
||||||
sessionHasServerPresence = false
|
sessionHasServerPresence = false
|
||||||
|
|||||||
+234
-19
@@ -1,5 +1,6 @@
|
|||||||
package com.m57.hermescontrol.ui.chat.components
|
package com.m57.hermescontrol.ui.chat.components
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
@@ -15,8 +16,12 @@ import androidx.compose.foundation.lazy.LazyColumn
|
|||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.AltRoute
|
||||||
import androidx.compose.material.icons.automirrored.filled.FormatListBulleted
|
import androidx.compose.material.icons.automirrored.filled.FormatListBulleted
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.Send
|
||||||
import androidx.compose.material.icons.filled.Autorenew
|
import androidx.compose.material.icons.filled.Autorenew
|
||||||
import androidx.compose.material.icons.filled.Cancel
|
import androidx.compose.material.icons.filled.Cancel
|
||||||
import androidx.compose.material.icons.filled.CheckCircle
|
import androidx.compose.material.icons.filled.CheckCircle
|
||||||
@@ -24,12 +29,18 @@ import androidx.compose.material.icons.filled.Close
|
|||||||
import androidx.compose.material.icons.filled.ExpandLess
|
import androidx.compose.material.icons.filled.ExpandLess
|
||||||
import androidx.compose.material.icons.filled.ExpandMore
|
import androidx.compose.material.icons.filled.ExpandMore
|
||||||
import androidx.compose.material.icons.filled.Groups
|
import androidx.compose.material.icons.filled.Groups
|
||||||
|
import androidx.compose.material.icons.filled.HourglassEmpty
|
||||||
import androidx.compose.material.icons.filled.RadioButtonUnchecked
|
import androidx.compose.material.icons.filled.RadioButtonUnchecked
|
||||||
|
import androidx.compose.material.icons.filled.Stop
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.FilledTonalButton
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
|
import androidx.compose.material3.OutlinedButton
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
@@ -45,6 +56,7 @@ import androidx.compose.ui.platform.testTag
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
import androidx.compose.ui.text.style.TextDecoration
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
@@ -54,13 +66,15 @@ import com.m57.hermescontrol.ui.chat.SubagentIndicator
|
|||||||
import com.m57.hermescontrol.ui.chat.TodoItem
|
import com.m57.hermescontrol.ui.chat.TodoItem
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inspection sheet displaying active & completed subagent tasks and agent todos.
|
* Inspection sheet displaying active & completed subagent tasks and agent todos (issue #1030).
|
||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun SubagentInspectionSheet(
|
fun SubagentInspectionSheet(
|
||||||
indicators: List<SubagentIndicator> = emptyList(),
|
indicators: List<SubagentIndicator> = emptyList(),
|
||||||
todos: List<TodoItem> = emptyList(),
|
todos: List<TodoItem> = emptyList(),
|
||||||
|
onSteerSubagent: ((SubagentIndicator, String) -> Unit)? = null,
|
||||||
|
onStopSubagent: ((SubagentIndicator) -> Unit)? = null,
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
@@ -197,7 +211,21 @@ fun SubagentInspectionSheet(
|
|||||||
"subagent-${indicator.subagentId ?: indicator.goal ?: "${indicator.type}_$index"}"
|
"subagent-${indicator.subagentId ?: indicator.goal ?: "${indicator.type}_$index"}"
|
||||||
},
|
},
|
||||||
) { _, indicator ->
|
) { _, indicator ->
|
||||||
InspectionItemCard(indicator = indicator)
|
InspectionItemCard(
|
||||||
|
indicator = indicator,
|
||||||
|
onSteer =
|
||||||
|
if (onSteerSubagent != null) {
|
||||||
|
{ message -> onSteerSubagent(indicator, message) }
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
},
|
||||||
|
onStop =
|
||||||
|
if (onStopSubagent != null) {
|
||||||
|
{ onStopSubagent(indicator) }
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -418,29 +446,77 @@ private fun TodoInspectionCard(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun InspectionItemCard(indicator: SubagentIndicator) {
|
private fun InspectionItemCard(
|
||||||
|
indicator: SubagentIndicator,
|
||||||
|
onSteer: ((String) -> Unit)? = null,
|
||||||
|
onStop: (() -> Unit)? = null,
|
||||||
|
) {
|
||||||
|
var showSteerInput by remember { mutableStateOf(false) }
|
||||||
|
var steerText by remember { mutableStateOf("") }
|
||||||
|
val statusColors = LocalHermesStatusColors.current
|
||||||
|
|
||||||
|
val (statusIcon, statusTint, statusLabel) =
|
||||||
|
when {
|
||||||
|
indicator.isComplete -> {
|
||||||
|
Triple(
|
||||||
|
Icons.Filled.CheckCircle,
|
||||||
|
statusColors.success,
|
||||||
|
stringResource(R.string.subagent_status_completed),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
indicator.isFailed -> {
|
||||||
|
Triple(
|
||||||
|
Icons.Filled.Cancel,
|
||||||
|
statusColors.error,
|
||||||
|
stringResource(R.string.subagent_status_failed),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
indicator.isCancelled -> {
|
||||||
|
Triple(
|
||||||
|
Icons.Filled.Cancel,
|
||||||
|
statusColors.warning,
|
||||||
|
stringResource(R.string.subagent_status_cancelled),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
indicator.isSteered -> {
|
||||||
|
Triple(
|
||||||
|
Icons.AutoMirrored.Filled.AltRoute,
|
||||||
|
MaterialTheme.colorScheme.primary,
|
||||||
|
stringResource(R.string.subagent_status_steered),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
indicator.isQueued -> {
|
||||||
|
Triple(
|
||||||
|
Icons.Filled.HourglassEmpty,
|
||||||
|
MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
stringResource(R.string.subagent_status_running),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
Triple(
|
||||||
|
Icons.Filled.Autorenew,
|
||||||
|
MaterialTheme.colorScheme.primary,
|
||||||
|
stringResource(R.string.subagent_status_running),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
shape = RoundedCornerShape(12.dp),
|
shape = RoundedCornerShape(12.dp),
|
||||||
color = MaterialTheme.colorScheme.surfaceContainerHigh,
|
color = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.padding(12.dp)) {
|
Column(modifier = Modifier.padding(12.dp)) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
// Header Row: Status Icon, Goal, and Status Badge
|
||||||
val statusColors = LocalHermesStatusColors.current
|
Row(
|
||||||
val (statusIcon, statusTint) =
|
modifier = Modifier.fillMaxWidth(),
|
||||||
when {
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
indicator.isComplete -> {
|
) {
|
||||||
Icons.Filled.CheckCircle to statusColors.success
|
|
||||||
}
|
|
||||||
|
|
||||||
indicator.isFailed -> {
|
|
||||||
Icons.Filled.Cancel to statusColors.error
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
Icons.Filled.Autorenew to MaterialTheme.colorScheme.primary
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = statusIcon,
|
imageVector = statusIcon,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
@@ -456,6 +532,44 @@ private fun InspectionItemCard(indicator: SubagentIndicator) {
|
|||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Surface(
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
color = statusTint.copy(alpha = 0.15f),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = statusLabel,
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
color = statusTint,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Duration & Model info
|
||||||
|
if (indicator.durationSeconds != null || !indicator.model.isNullOrBlank()) {
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
if (indicator.durationSeconds != null) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.subagent_duration, indicator.durationSeconds),
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (!indicator.model.isNullOrBlank()) {
|
||||||
|
Text(
|
||||||
|
text = indicator.model,
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.8f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!indicator.summary.isNullOrBlank()) {
|
if (!indicator.summary.isNullOrBlank()) {
|
||||||
@@ -467,6 +581,7 @@ private fun InspectionItemCard(indicator: SubagentIndicator) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Live Transcript Logs
|
||||||
if (indicator.logs.isNotEmpty()) {
|
if (indicator.logs.isNotEmpty()) {
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
Column(
|
Column(
|
||||||
@@ -503,6 +618,106 @@ private fun InspectionItemCard(indicator: SubagentIndicator) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Interactive Controls for active subagent (issue #1030)
|
||||||
|
if (indicator.isRunning || indicator.isSteered) {
|
||||||
|
Spacer(modifier = Modifier.height(10.dp))
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
if (onSteer != null) {
|
||||||
|
FilledTonalButton(
|
||||||
|
onClick = { showSteerInput = !showSteerInput },
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Filled.AltRoute,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(16.dp),
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(6.dp))
|
||||||
|
Text(stringResource(R.string.subagent_action_steer))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onStop != null) {
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = onStop,
|
||||||
|
colors =
|
||||||
|
ButtonDefaults.outlinedButtonColors(
|
||||||
|
contentColor = statusColors.error,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Stop,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(16.dp),
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
Text(stringResource(R.string.subagent_action_stop))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inline Steering Input Field
|
||||||
|
AnimatedVisibility(visible = showSteerInput) {
|
||||||
|
Column(modifier = Modifier.padding(top = 8.dp)) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = steerText,
|
||||||
|
onValueChange = { steerText = it },
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.subagent_steer_placeholder),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
singleLine = true,
|
||||||
|
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Send),
|
||||||
|
keyboardActions =
|
||||||
|
KeyboardActions(
|
||||||
|
onSend = {
|
||||||
|
if (steerText.isNotBlank()) {
|
||||||
|
onSteer?.invoke(steerText)
|
||||||
|
steerText = ""
|
||||||
|
showSteerInput = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
textStyle = MaterialTheme.typography.bodySmall,
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(6.dp))
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
if (steerText.isNotBlank()) {
|
||||||
|
onSteer?.invoke(steerText)
|
||||||
|
steerText = ""
|
||||||
|
showSteerInput = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
enabled = steerText.isNotBlank(),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Filled.Send,
|
||||||
|
contentDescription = stringResource(R.string.subagent_action_steer),
|
||||||
|
tint =
|
||||||
|
if (steerText.isNotBlank()) {
|
||||||
|
MaterialTheme.colorScheme.primary
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -846,4 +846,18 @@
|
|||||||
<string name="group_chat_settings_system_prompt_hint">이 그룹을 위한 사용자 지정 규칙, 어조 또는 컨텍스트 (선택사항)…</string>
|
<string name="group_chat_settings_system_prompt_hint">이 그룹을 위한 사용자 지정 규칙, 어조 또는 컨텍스트 (선택사항)…</string>
|
||||||
<string name="group_chat_settings_system_prompt_desc">이 방의 모든 봇에 적용되는 사용자 지정 가이드라인</string>
|
<string name="group_chat_settings_system_prompt_desc">이 방의 모든 봇에 적용되는 사용자 지정 가이드라인</string>
|
||||||
<string name="group_chat_action_settings">설정</string>
|
<string name="group_chat_action_settings">설정</string>
|
||||||
|
<string name="subagent_agent_plan">에이전트 계획</string>
|
||||||
|
<string name="subagent_live_transcript">실시간 기록</string>
|
||||||
|
<string name="subagent_no_active">활성 작업 또는 계획 항목이 없습니다.</string>
|
||||||
|
<string name="subagent_summary">요약: %1$s</string>
|
||||||
|
<string name="subagent_task_plan_inspection">작업 및 계획 검사</string>
|
||||||
|
<string name="subagent_action_steer">안내</string>
|
||||||
|
<string name="subagent_action_stop">중지</string>
|
||||||
|
<string name="subagent_steer_placeholder">하위 에이전트 안내 입력…</string>
|
||||||
|
<string name="subagent_status_running">실행 중</string>
|
||||||
|
<string name="subagent_status_completed">완료됨</string>
|
||||||
|
<string name="subagent_status_steered">안내됨</string>
|
||||||
|
<string name="subagent_status_cancelled">취소됨</string>
|
||||||
|
<string name="subagent_status_failed">실패</string>
|
||||||
|
<string name="subagent_duration">%1$.1f초</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1089,6 +1089,15 @@
|
|||||||
<string name="subagent_no_active">没有活动任务或计划项。</string>
|
<string name="subagent_no_active">没有活动任务或计划项。</string>
|
||||||
<string name="subagent_summary">摘要:%1$s</string>
|
<string name="subagent_summary">摘要:%1$s</string>
|
||||||
<string name="subagent_task_plan_inspection">任务与计划检查</string>
|
<string name="subagent_task_plan_inspection">任务与计划检查</string>
|
||||||
|
<string name="subagent_action_steer">引导</string>
|
||||||
|
<string name="subagent_action_stop">停止</string>
|
||||||
|
<string name="subagent_steer_placeholder">向子代理发送调整引导…</string>
|
||||||
|
<string name="subagent_status_running">运行中</string>
|
||||||
|
<string name="subagent_status_completed">已完成</string>
|
||||||
|
<string name="subagent_status_steered">已引导</string>
|
||||||
|
<string name="subagent_status_cancelled">已取消</string>
|
||||||
|
<string name="subagent_status_failed">失败</string>
|
||||||
|
<string name="subagent_duration">%1$.1f秒</string>
|
||||||
<string name="system_auth_required">需要认证</string>
|
<string name="system_auth_required">需要认证</string>
|
||||||
<string name="system_checkpoint_prune">清理检查点</string>
|
<string name="system_checkpoint_prune">清理检查点</string>
|
||||||
<string name="system_config_migrate">迁移配置</string>
|
<string name="system_config_migrate">迁移配置</string>
|
||||||
|
|||||||
@@ -1218,6 +1218,15 @@
|
|||||||
<string name="subagent_no_active">No active tasks or plan items.</string>
|
<string name="subagent_no_active">No active tasks or plan items.</string>
|
||||||
<string name="subagent_summary">Summary: %1$s</string>
|
<string name="subagent_summary">Summary: %1$s</string>
|
||||||
<string name="subagent_task_plan_inspection">Task & Plan Inspection</string>
|
<string name="subagent_task_plan_inspection">Task & Plan Inspection</string>
|
||||||
|
<string name="subagent_action_steer">Steer</string>
|
||||||
|
<string name="subagent_action_stop">Stop</string>
|
||||||
|
<string name="subagent_steer_placeholder">Course-correction guidance for subagent…</string>
|
||||||
|
<string name="subagent_status_running">Running</string>
|
||||||
|
<string name="subagent_status_completed">Completed</string>
|
||||||
|
<string name="subagent_status_steered">Steered</string>
|
||||||
|
<string name="subagent_status_cancelled">Cancelled</string>
|
||||||
|
<string name="subagent_status_failed">Failed</string>
|
||||||
|
<string name="subagent_duration">%1$.1fs</string>
|
||||||
<string name="system_auth_required">Auth required</string>
|
<string name="system_auth_required">Auth required</string>
|
||||||
<string name="system_checkpoint_prune">Checkpoint prune</string>
|
<string name="system_checkpoint_prune">Checkpoint prune</string>
|
||||||
<string name="system_config_migrate">Config migrate</string>
|
<string name="system_config_migrate">Config migrate</string>
|
||||||
|
|||||||
@@ -3939,4 +3939,68 @@ class ChatViewModelTest {
|
|||||||
.contains("Backend exploded"),
|
.contains("Backend exploded"),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Subagent steering & cancellation (issue #1030) ───────────────────
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testSteerSubagent_sendsRedirectAndUpdatesStatus() =
|
||||||
|
runTest {
|
||||||
|
val (viewModel, sessionId) = createViewModelWithSession()
|
||||||
|
val indicator =
|
||||||
|
SubagentIndicator(
|
||||||
|
type = "subagent.start",
|
||||||
|
subagentId = "sub-123",
|
||||||
|
goal = "Search database",
|
||||||
|
status = "running",
|
||||||
|
)
|
||||||
|
mockEventsFlow.emit(
|
||||||
|
WsEvent.SubagentEvent(
|
||||||
|
type = "subagent.start",
|
||||||
|
payload = mapOf("subagent_id" to "sub-123", "goal" to "Search database"),
|
||||||
|
sessionId = sessionId,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
viewModel.steerSubagent(indicator, "Use index scan instead")
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
val updated =
|
||||||
|
viewModel.uiState.value.subagentIndicators
|
||||||
|
.first { it.subagentId == "sub-123" }
|
||||||
|
assertEquals("steered", updated.status)
|
||||||
|
assertTrue(updated.logs.any { it.text.contains("Use index scan instead") })
|
||||||
|
io.mockk.verify { HermesWsClient.sendRedirect(sessionId, "/steer sub-123 Use index scan instead", any()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testStopSubagent_sendsRedirectAndMarksCancelled() =
|
||||||
|
runTest {
|
||||||
|
val (viewModel, sessionId) = createViewModelWithSession()
|
||||||
|
val indicator =
|
||||||
|
SubagentIndicator(
|
||||||
|
type = "subagent.start",
|
||||||
|
subagentId = "sub-123",
|
||||||
|
goal = "Run deep scan",
|
||||||
|
status = "running",
|
||||||
|
)
|
||||||
|
mockEventsFlow.emit(
|
||||||
|
WsEvent.SubagentEvent(
|
||||||
|
type = "subagent.start",
|
||||||
|
payload = mapOf("subagent_id" to "sub-123", "goal" to "Run deep scan"),
|
||||||
|
sessionId = sessionId,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
viewModel.stopSubagent(indicator)
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
val updated =
|
||||||
|
viewModel.uiState.value.subagentIndicators
|
||||||
|
.first { it.subagentId == "sub-123" }
|
||||||
|
assertEquals("cancelled", updated.status)
|
||||||
|
assertTrue(updated.logs.any { it.text.contains("Stopped subagent") })
|
||||||
|
io.mockk.verify { HermesWsClient.sendRedirect(sessionId, "/stop sub-123", any()) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user