feat(chat): live task delegation widget with inline card & sticky progress indicator (#736) (#743)

* feat(chat): live task delegation widget with inline card & sticky progress indicator (#736)

* fix(chat): refine subagent isRunning status and fallback Compose list keys (#743)

* feat(ci): add CodeQL analysis workflow & integrate todos into task widget (#736)

- Add CodeQL workflow with manual build mode for java-kotlin
- Render agent plan & todo items inside live delegation widget

* ci(codeql): add advanced CodeQL workflow with continue-on-error for Kotlin 2.4.10 (#736)

* ci(codeql): use build-mode none to prevent Kotlin 2.4.10 extractor error (#736)

* revert(ci): remove codeql workflow (#736)

* refactor(chat): remove inline task card and rely on sticky top bar (#736)

- Keep StickySubagentBar and SubagentInspectionSheet for top widget UI

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
M57andM57 authored and GitHub committed 2026-07-29 18:15:39 +03:00
1 parent 8ba92f2567
commit 2cf41ead06
8 files changed
+754 -31

No files matched your search

@@ -2,6 +2,7 @@ package com.m57.hermescontrol.ui.chat
import com.m57.hermescontrol.data.ws.WsEvent
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
class ChatWsEventReducerTest {
@@ -161,7 +162,7 @@ class ChatWsEventReducerTest {
}
@Test
fun testSubagentComplete_removesIndicator() {
fun testSubagentComplete_updatesIndicatorToCompleted() {
val initialIndicator =
SubagentIndicator(
type = "subagent.start",
@@ -196,7 +197,12 @@ class ChatWsEventReducerTest {
currentSessionId = "session-1",
)
assertEquals(0, result.state.subagentIndicators.size)
assertEquals(1, result.state.subagentIndicators.size)
val indicator = result.state.subagentIndicators.first()
assertEquals("subagent.complete", indicator.type)
assertEquals("completed", indicator.status)
assertEquals("done", indicator.summary)
assertTrue(indicator.isComplete)
}
@Test
@@ -233,4 +239,140 @@ class ChatWsEventReducerTest {
assertEquals(ToolStatus.RUNNING, updatedMessage.toolStatus)
assertEquals(null, updatedMessage.progressPreview)
}
@Test
fun testSubagentEvent_accumulatesLiveTranscriptLogs() {
val state =
ChatUiState(
currentSessionId = "session-1",
subagentIndicators = emptyList(),
)
val startEvent =
WsEvent.SubagentEvent(
type = "subagent.start",
sessionId = "session-1",
payload =
mapOf(
"subagent_id" to "sub-1",
"goal" to "research api",
"text" to "Initializing subagent",
),
)
val res1 =
ChatWsEventReducer.reduce(
state = state,
streamingState = StreamingState(),
event = startEvent,
currentSessionId = "session-1",
)
val progressEvent =
WsEvent.SubagentEvent(
type = "subagent.progress",
sessionId = "session-1",
payload =
mapOf(
"subagent_id" to "sub-1",
"text" to "Fetching documentation",
),
)
val res2 =
ChatWsEventReducer.reduce(
state = res1.state,
streamingState = StreamingState(),
event = progressEvent,
currentSessionId = "session-1",
)
val indicator = res2.state.subagentIndicators.first()
assertEquals(2, indicator.logs.size)
assertEquals("Initializing subagent", indicator.logs[0].text)
assertEquals("Fetching documentation", indicator.logs[1].text)
assertTrue(indicator.isRunning)
}
@Test
fun testMessageStart_prunesCompletedSubagents() {
val completedSubagent =
SubagentIndicator(
type = "subagent.complete",
goal = "finished task",
subagentId = "sub-1",
status = "completed",
)
val state =
ChatUiState(
currentSessionId = "session-1",
subagentIndicators = listOf(completedSubagent),
)
val startEvent = WsEvent.MessageStart(sessionId = "session-1")
val result =
ChatWsEventReducer.reduce(
state = state,
streamingState = StreamingState(),
event = startEvent,
currentSessionId = "session-1",
)
assertTrue(result.state.subagentIndicators.isEmpty())
}
@Test
fun testToolStart_extractsAgentTodos() {
val state = ChatUiState(currentSessionId = "session-1")
val todoEvent =
WsEvent.ToolStart(
name = "todo",
sessionId = "session-1",
data =
mapOf(
"todos" to
listOf(
mapOf("id" to "1", "content" to "Inspect repo", "status" to "completed"),
mapOf("id" to "2", "content" to "Implement feature", "status" to "in_progress"),
),
),
)
val result =
ChatWsEventReducer.reduce(
state = state,
streamingState = StreamingState(),
event = todoEvent,
currentSessionId = "session-1",
)
assertEquals(2, result.state.todos.size)
assertEquals("Inspect repo", result.state.todos[0].content)
assertTrue(result.state.todos[0].isCompleted)
assertEquals("Implement feature", result.state.todos[1].content)
assertTrue(result.state.todos[1].isInProgress)
}
@Test
fun testHydrateTodosFromMessages_parsesStoredToolMessage() {
val todoMessage =
ChatMessage(
role = MessageRole.TOOL,
toolName = "todo",
content = """{"todos":[{"id":"a","content":"Write tests","status":"completed"}]}""",
)
val state = ChatUiState(messages = listOf(todoMessage), currentSessionId = "session-1")
val event = WsEvent.MessageToken(token = "hello", sessionId = "session-1")
val result =
ChatWsEventReducer.reduce(
state = state,
streamingState = StreamingState(),
event = event,
currentSessionId = "session-1",
)
assertEquals(1, result.state.todos.size)
assertEquals("Write tests", result.state.todos[0].content)
assertTrue(result.state.todos[0].isCompleted)
}
}