fix(tui): status bar model, snapshot events, session-start selection

Three fixes:

1. Status bar now shows model/provider from ProviderStatusChanged:
   - RootReducer sets currentModel and providerType when the message arrives
   - providerType is LOCAL for llama/local providers, REMOTE otherwise
   - Removes the stale '(no model) (local)' display

2. SessionSnapshot now carries recent event history:
   - New EventEntryDto in the protocol with timestamp/type/detail
   - SessionSnapshot.recentEvents populated by bridge from event store
   - Bridge reads last 7 display-relevant events per session
   - TUI processSessionSnapshotMessage maps them to TuiEventEntry
   - Completed sessions finally show their event history

3. SessionStarted now switches selectedId to the new session:
   - RootReducer cross-field weave updates selectedId alongside sessionEntered
   - Previously kept the old selectedId, navigating user into the wrong session
   - Works whether starting a session from the list or during replay
This commit is contained in:
2026-05-26 13:22:48 +04:00
parent 6770e3bf0a
commit 450b492937
6 changed files with 121 additions and 4 deletions
@@ -5,6 +5,7 @@ import com.correx.apps.server.protocol.ServerMessage
import com.correx.apps.tui.input.Action
import com.correx.apps.tui.state.DisplayState
import com.correx.apps.tui.state.InputMode
import com.correx.apps.tui.state.ProviderType
import com.correx.apps.tui.state.TuiState
import com.correx.apps.tui.state.displayState
import org.slf4j.LoggerFactory
@@ -138,11 +139,25 @@ object RootReducer {
}
// Auto-enter a newly started session when SessionStarted arrives.
// When a ProviderStatusChanged arrives, update model/provider labels.
is Action.ServerEventReceived -> {
if (action.message is ServerMessage.SessionStarted) {
withBgReset.copy(sessionEntered = true)
} else {
withBgReset
val msg = action.message
when {
msg is ServerMessage.SessionStarted -> {
withBgReset.copy(
sessionEntered = true,
sessions = withBgReset.sessions.copy(selectedId = msg.sessionId.value),
)
}
msg is ServerMessage.ProviderStatusChanged -> {
withBgReset.copy(
currentModel = msg.providerId,
providerType = if (msg.providerId.contains("llama", ignoreCase = true) ||
msg.providerId.contains("local", ignoreCase = true)
) ProviderType.LOCAL else ProviderType.REMOTE,
)
}
else -> withBgReset
}
}
@@ -217,6 +217,13 @@ object SessionsReducer {
)
}
val status = if (firstPending != null) "PAUSED awaiting approval" else msg.state.status
val eventEntries = msg.recentEvents.map { entry ->
TuiEventEntry(
timestamp = formatTime(entry.timestamp),
type = entry.type,
detail = entry.detail,
)
}
val summary = SessionSummary(
id = msg.sessionId.value,
status = status,
@@ -226,6 +233,7 @@ object SessionsReducer {
currentStage = msg.state.currentStageId,
currentStageId = msg.state.currentStageId,
pendingApproval = approvalInfo,
recentEvents = eventEntries,
)
val selected = sessionState.selectedId ?: msg.sessionId.value
return sessionState.copy(
@@ -1,12 +1,14 @@
package com.correx.apps.tui.reducer
import com.correx.apps.server.protocol.ClientMessage
import com.correx.apps.server.protocol.ProviderHealthDto
import com.correx.apps.server.protocol.RiskSummaryDto
import com.correx.apps.server.protocol.ServerMessage
import com.correx.apps.tui.input.Action
import com.correx.apps.tui.state.ConnectionState
import com.correx.apps.tui.state.DisplayState
import com.correx.apps.tui.state.InputMode
import com.correx.apps.tui.state.ProviderType
import com.correx.apps.tui.state.SessionSummary
import com.correx.apps.tui.state.SessionsState
import com.correx.apps.tui.state.TuiState
@@ -341,4 +343,48 @@ class RootReducerTest {
assertEquals(null, state.sessions.sessions.find { it.id == "s1" }?.pendingApproval)
assertEquals("r1", state.sessions.sessions.find { it.id == "s2" }?.pendingApproval?.requestId)
}
@Test
fun `ProviderStatusChanged updates currentModel and providerType`() {
val initial = TuiState(snapshotPhase = false, currentModel = null, providerType = ProviderType.LOCAL)
val msg = ServerMessage.ProviderStatusChanged(
providerId = "llama.cpp",
status = ProviderHealthDto("llama.cpp", "healthy", null),
)
val (state, _) = RootReducer.reduce(initial, Action.ServerEventReceived(msg), fixedClock)
assertEquals("llama.cpp", state.currentModel)
assertEquals(ProviderType.LOCAL, state.providerType)
}
@Test
fun `ProviderStatusChanged for remote provider sets providerType to REMOTE`() {
val initial = TuiState(snapshotPhase = false, currentModel = null, providerType = ProviderType.LOCAL)
val msg = ServerMessage.ProviderStatusChanged(
providerId = "openai",
status = ProviderHealthDto("openai", "healthy", null),
)
val (state, _) = RootReducer.reduce(initial, Action.ServerEventReceived(msg), fixedClock)
assertEquals("openai", state.currentModel)
assertEquals(ProviderType.REMOTE, state.providerType)
}
@Test
fun `SessionStarted switches selectedId to new session when another session is selected`() {
val initial = TuiState(
snapshotPhase = false,
sessions = SessionsState(
sessions = listOf(session("s1"), session("s2")),
selectedId = "s1",
),
)
val startedMsg = ServerMessage.SessionStarted(
sessionId = SessionId("s2"),
workflowId = "wf",
sequence = 1L,
sessionSequence = 1L,
)
val (state, _) = RootReducer.reduce(initial, Action.ServerEventReceived(startedMsg), fixedClock)
assertEquals("s2", state.sessions.selectedId)
assertTrue(state.sessionEntered)
}
}