* feat(chat): compact sticky task progress chip (#942) Re-add a glanceable progress strip above the chat timeline while work is active. Distinct from the inline TodoTaskCard (permanent timeline record): this chip is the temporary "what is happening right now" surface. - Shows only while todos are incomplete or a subagent is running. - Bounds to the same hydrated state.todos / subagentIndicators the card uses -- NOT the old broken state.todos gate removed in #811. - Label: "Tasks 2/5 · <in-progress task> · N agent" (agent count when relevant). - Tap opens SubagentInspectionSheet + resumes bottom-follow. - Auto-hides when all work completes/cancels; rebuilds on session resume. - i18n strings added (en/zh/ko); ktlint clean; compileDebugKotlin + unit tests green. * refactor(chat): drop inline TodoTaskCard, keep single sticky progress chip Per follow-up to #942: the inline todo card in the chat view is removed. The sticky TaskProgressChip above the timeline is now the only glanceable surface; the tool message itself remains the permanent record. - Remove inline TodoTaskCard early-return in ToolBubble.kt (todo tool output now renders like any other tool bubble). - Delete TodoTaskCard.kt + TodoTaskCardTest.kt (SecurityRiskChip lives in ToolBubble.kt, so risk display is unaffected). - Redesign chip: replace the ElectricBolt icon with a real CircularProgressIndicator (no emoji), keep ChevronRight affordance. - Label handles the no-todos case (subagents only): 'N agent running'. - Drop dead todo_task_count string; add task_progress_agent_running (en/zh/ko). ktlint clean; compileDebugKotlin + chat unit tests green. * chore: clear pre-existing compile warnings - HermesWsClient.kt: drop unnecessary safe call on non-null ResponseBody (resp.body.string()) and the now-dead 'body != null' guard it implied. - SessionsScreen.kt: replace deprecated Icons.Filled.CallSplit with the AutoMirrored variant (match existing Send usage in this file). ktlint clean; compileDebugKotlin SUCCESSFUL with no warnings. * chore(chat): replace emoji status markers with Material icons in inspection sheet The sticky progress chip opens SubagentInspectionSheet on tap; that sheet used emoji Text glyphs for status. Replaced with real Material icons: - Todo rows: completed -> CheckCircle (success), in-progress -> Autorenew (primary), cancelled -> Cancel (warning), pending -> RadioButtonUnchecked (muted). - Subagent rows: complete -> CheckCircle (success), failed -> Cancel (error), running -> Autorenew (primary). - Section headers: ElectricBolt -> FormatListBulleted (agent plan) + Groups (subagents). Icons tinted via LocalHermesStatusColors. No emoji remain in the chip's tap surface. ktlint clean; compileDebugKotlin SUCCESSFUL. * fix(lint): drop orphaned todo_task_count translation in ko Removing the string from the default (en) locale left a stale Korean translation, which tripped Lint's ExtraTranslation error and aborted lintDebug. Removing the orphan so default + translations stay in sync. android-lint now BUILD SUCCESSFUL (0 errors). * fix(chat): address #943 review - progress count, resume hydration, chip tests, plurals - TaskProgressChip: current-task number is now the first unfinished, non-cancelled todo's 1-based position (active index + 1), so a completed task 1 + in-progress task 2 reads as 2/5 instead of 1/5 (review point 2). - Deterministic resume hydration: hydrate todos directly from cached Room messages and from the merged REST transcript inside ChatViewModel load paths, in addition to the WS reducer, so a resumed session rebuilds the chip without waiting for a follow-up event (review point 3). - Extracted computeChipDisplay / shouldShowProgressChip pure functions and added TaskProgressChipTest covering active-task progress, completed/cancelled hide, subagent-only state, and transcript-resume hydration (review point 4). - agent count strings are now Android plurals (task_progress_agents / task_progress_agent_running) in en/zh/ko to avoid '2 agent' (review point 5). Co-Authored-By trailer intentionally omitted per repo convention. * fix(chat): derive chip number and content from the same selected task computeChipDisplay selected the task number from the first unfinished/ non-cancelled todo but the content independently from the first in_progress todo. With a pending task 1 + in_progress task 2 this produced 'Tasks 1/N · <task2 content>'. Now both values come from one selected item (prefer in_progress, fall back to first pending/non-terminal). Adds two focused tests: same-item selection, and pending-only fallback.
This commit is contained in:
13 files changed
+417
-324
No files matched your search
@@ -1,133 +0,0 @@
|
||||
package com.m57.hermescontrol.ui.chat
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.junit4.createComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||
import androidx.compose.ui.test.onNodeWithTag
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.filters.MediumTest
|
||||
import com.m57.hermescontrol.R
|
||||
import com.m57.hermescontrol.theme.HermesControlTheme
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@MediumTest
|
||||
class TodoTaskCardTest {
|
||||
@get:Rule
|
||||
val composeTestRule = createComposeRule()
|
||||
|
||||
@Test
|
||||
fun todoTool_displaysExpandedTaskCard() {
|
||||
val message =
|
||||
ChatMessage(
|
||||
role = MessageRole.TOOL,
|
||||
toolName = null,
|
||||
toolStatus = ToolStatus.COMPLETED,
|
||||
toolOutputRiskData = ToolOutputRiskData(risk = "high", findings = emptyList(), redacted = false),
|
||||
content =
|
||||
"""{
|
||||
"name": "todo",
|
||||
"result": {
|
||||
"todos": [
|
||||
{"id": "one", "content": "Finished", "status": "completed"},
|
||||
{"id": "two", "content": "Working", "status": "in_progress"},
|
||||
{"id": "three", "content": "Later", "status": "pending"}
|
||||
],
|
||||
"summary": {"total": 3, "pending": 1, "in_progress": 1, "completed": 1, "cancelled": 0}
|
||||
}
|
||||
}""",
|
||||
)
|
||||
|
||||
composeTestRule.setContent {
|
||||
HermesControlTheme {
|
||||
ToolBubble(message = message)
|
||||
}
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithTag("todo_task_card").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithText(taskCount(1, 3)).assertIsDisplayed()
|
||||
composeTestRule.onNodeWithText("Finished").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithText("Working").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithText("Later").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithText("Risky output").assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun emptyTodoOutput_usesGenericToolBubble() {
|
||||
val message =
|
||||
ChatMessage(
|
||||
role = MessageRole.TOOL,
|
||||
toolName = "todo",
|
||||
toolStatus = ToolStatus.COMPLETED,
|
||||
content = """{"name":"todo","result":{"todos":[]}}""",
|
||||
)
|
||||
|
||||
composeTestRule.setContent {
|
||||
HermesControlTheme {
|
||||
ToolBubble(message = message)
|
||||
}
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithTag("todo_task_card").assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun gatewayAliases_renderStructuredStatuses() {
|
||||
val message =
|
||||
ChatMessage(
|
||||
role = MessageRole.TOOL,
|
||||
toolStatus = ToolStatus.COMPLETED,
|
||||
content =
|
||||
"""{"tool_name":"todo_write","result":{"todos":[
|
||||
{"id":"one","content":"Done alias","status":"done"},
|
||||
{"id":"two","content":"Running alias","status":"running"},
|
||||
{"id":"three","content":"Failed alias","status":"failed"},
|
||||
{"id":"four","content":"Queued alias","status":"queued"}
|
||||
]}}""",
|
||||
)
|
||||
|
||||
composeTestRule.setContent {
|
||||
HermesControlTheme {
|
||||
ToolBubble(message = message)
|
||||
}
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText(taskCount(1, 4)).assertIsDisplayed()
|
||||
composeTestRule.onNodeWithContentDescription("Completed").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithContentDescription("Cancelled").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithText("Running alias").assertIsDisplayed()
|
||||
composeTestRule.onNodeWithText("Queued alias").assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun malformedTodoOutput_usesGenericToolBubble() {
|
||||
val message =
|
||||
ChatMessage(
|
||||
role = MessageRole.TOOL,
|
||||
toolName = "todo",
|
||||
toolStatus = ToolStatus.COMPLETED,
|
||||
content = "not json",
|
||||
)
|
||||
|
||||
composeTestRule.setContent {
|
||||
HermesControlTheme {
|
||||
ToolBubble(message = message)
|
||||
}
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithTag("todo_task_card").assertDoesNotExist()
|
||||
}
|
||||
|
||||
private fun taskCount(
|
||||
completed: Int,
|
||||
total: Int,
|
||||
): String {
|
||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
return context.getString(R.string.todo_task_count, completed, total)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user