refactor(cleanup-01): move approval truth to SessionSummary.pendingApproval
Add pendingApproval: ApprovalInfo? = null to SessionSummary and remove the global approval: ApprovalState slot from TuiState. All production code derives active approval from the selected session; RootReducer bridges ApprovalState in/out of the selected session for the duration of the existing ApprovalReducer (removed in Cleanup 03).
This commit is contained in:
@@ -47,7 +47,6 @@ fun main(args: Array<String>) {
|
|||||||
fun dispatch(action: Action) {
|
fun dispatch(action: Action) {
|
||||||
val (next, effects) = RootReducer.reduce(state, action)
|
val (next, effects) = RootReducer.reduce(state, action)
|
||||||
log.debug("got connection state after reducers: {}", next.connection)
|
log.debug("got connection state after reducers: {}", next.connection)
|
||||||
log.debug("got approval state after reducers: {}", next.approval)
|
|
||||||
log.debug("got input state after reducers: {}", next.input)
|
log.debug("got input state after reducers: {}", next.input)
|
||||||
log.debug(
|
log.debug(
|
||||||
"got session state after reducers: {}",
|
"got session state after reducers: {}",
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import dev.tamboui.widgets.paragraph.Paragraph
|
|||||||
|
|
||||||
fun inputBarWidget(state: TuiState): Paragraph {
|
fun inputBarWidget(state: TuiState): Paragraph {
|
||||||
val dimStyle = Style.create().dim().gray()
|
val dimStyle = Style.create().dim().gray()
|
||||||
val hasApproval = state.approval.active != null
|
val hasApproval = state.sessions.sessions.find { it.id == state.sessions.selectedId }?.pendingApproval != null
|
||||||
val hasSession = state.sessions.selectedId != null
|
val hasSession = state.sessions.selectedId != null
|
||||||
|
|
||||||
val sessionName = state.sessions.sessions
|
val sessionName = state.sessions.sessions
|
||||||
|
|||||||
@@ -23,18 +23,21 @@ fun routerPanelWidget(state: TuiState): Paragraph {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val activeApproval = state.sessions.sessions.find { it.id == state.sessions.selectedId }?.pendingApproval
|
||||||
val overlayLines: List<Line> = when {
|
val overlayLines: List<Line> = when {
|
||||||
state.approvalOverlayVisible && state.approval.active != null -> {
|
state.approvalOverlayVisible && activeApproval != null -> {
|
||||||
val a = state.approval.active
|
|
||||||
buildList {
|
buildList {
|
||||||
add(Line.from(Span.styled("╭─ approval [alt+h to hide] ─────────────╮", Style.create().yellow())))
|
add(Line.from(Span.styled("╭─ approval [alt+h to hide] ─────────────╮", Style.create().yellow())))
|
||||||
val tierLine = "│ ⚠ ${a.tier} │ ${a.toolName ?: "no tool name"} │ ${a.riskSummary.take(12)}... │"
|
val tierLine = "│ ⚠ ${activeApproval.tier} │ ${activeApproval.toolName ?: "no tool name"} │ ${
|
||||||
|
activeApproval.riskSummary.take(12)
|
||||||
|
}... │"
|
||||||
add(Line.from(Span.styled(tierLine, Style.create().yellow())))
|
add(Line.from(Span.styled(tierLine, Style.create().yellow())))
|
||||||
val previewLine = "│ ${(a.preview ?: "no preview").take(40)}… │"
|
val previewLine = "│ ${(activeApproval.preview ?: "no preview").take(40)}… │"
|
||||||
add(Line.from(Span.styled(previewLine, dimStyle)))
|
add(Line.from(Span.styled(previewLine, dimStyle)))
|
||||||
add(Line.from(Span.styled("╰──────────────────────────────────────────╯", Style.create().yellow())))
|
add(Line.from(Span.styled("╰──────────────────────────────────────────╯", Style.create().yellow())))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
state.eventOverlayVisible -> {
|
state.eventOverlayVisible -> {
|
||||||
val selectedSession = state.sessions.sessions.firstOrNull { it.id == state.sessions.selectedId }
|
val selectedSession = state.sessions.sessions.firstOrNull { it.id == state.sessions.selectedId }
|
||||||
val events = selectedSession?.recentEvents ?: emptyList()
|
val events = selectedSession?.recentEvents ?: emptyList()
|
||||||
@@ -47,6 +50,7 @@ fun routerPanelWidget(state: TuiState): Paragraph {
|
|||||||
add(Line.from(Span.styled("╰──────────────────────────────────────────╯", Style.create().magenta())))
|
add(Line.from(Span.styled("╰──────────────────────────────────────────╯", Style.create().magenta())))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> emptyList()
|
else -> emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ fun statusBarWidget(state: TuiState): Paragraph {
|
|||||||
add(modelSpan)
|
add(modelSpan)
|
||||||
add(sep)
|
add(sep)
|
||||||
add(sessionsSpan)
|
add(sessionsSpan)
|
||||||
if (state.approval.active != null) {
|
if (state.sessions.sessions.find { it.id == state.sessions.selectedId }?.pendingApproval != null) {
|
||||||
add(sep)
|
add(sep)
|
||||||
add(Span.styled("[⚠ approval]", Style.create().yellow()))
|
add(Span.styled("[⚠ approval]", Style.create().yellow()))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,11 +24,14 @@ object InputReducer {
|
|||||||
) to emptyList()
|
) to emptyList()
|
||||||
|
|
||||||
is Action.OpenNewSessionPrompt -> state.copy(inputMode = InputMode.ROUTER, inputBuffer = "") to emptyList()
|
is Action.OpenNewSessionPrompt -> state.copy(inputMode = InputMode.ROUTER, inputBuffer = "") to emptyList()
|
||||||
is Action.OpenSteeringPrompt -> if (state.approval.active != null) {
|
is Action.OpenSteeringPrompt -> {
|
||||||
|
val active = state.sessions.sessions.find { it.id == state.sessions.selectedId }?.pendingApproval
|
||||||
|
if (active != null) {
|
||||||
state.copy(inputMode = InputMode.STEER, inputBuffer = "") to emptyList()
|
state.copy(inputMode = InputMode.STEER, inputBuffer = "") to emptyList()
|
||||||
} else {
|
} else {
|
||||||
state to emptyList()
|
state to emptyList()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
is Action.EnterSteer -> state.copy(inputMode = InputMode.STEER, inputBuffer = "") to emptyList()
|
is Action.EnterSteer -> state.copy(inputMode = InputMode.STEER, inputBuffer = "") to emptyList()
|
||||||
is Action.OpenFilter -> state.copy(inputMode = InputMode.FILTER, inputBuffer = "") to emptyList()
|
is Action.OpenFilter -> state.copy(inputMode = InputMode.FILTER, inputBuffer = "") to emptyList()
|
||||||
@@ -49,7 +52,7 @@ object InputReducer {
|
|||||||
|
|
||||||
InputMode.STEER -> {
|
InputMode.STEER -> {
|
||||||
val text = state.inputBuffer.trim()
|
val text = state.inputBuffer.trim()
|
||||||
val active = state.approval.active
|
val active = state.sessions.sessions.find { it.id == state.sessions.selectedId }?.pendingApproval
|
||||||
if (active != null && text.isNotBlank()) {
|
if (active != null && text.isNotBlank()) {
|
||||||
state.copy(inputMode = InputMode.ROUTER, inputBuffer = "") to listOf(
|
state.copy(inputMode = InputMode.ROUTER, inputBuffer = "") to listOf(
|
||||||
Effect.SendWs(
|
Effect.SendWs(
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.correx.apps.tui.reducer
|
|||||||
|
|
||||||
import com.correx.apps.server.protocol.ServerMessage
|
import com.correx.apps.server.protocol.ServerMessage
|
||||||
import com.correx.apps.tui.input.Action
|
import com.correx.apps.tui.input.Action
|
||||||
|
import com.correx.apps.tui.state.ApprovalState
|
||||||
import com.correx.apps.tui.state.TuiState
|
import com.correx.apps.tui.state.TuiState
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
|
||||||
@@ -55,16 +56,23 @@ object RootReducer {
|
|||||||
val (afterInput, ie) = InputReducer.reduce(state, action)
|
val (afterInput, ie) = InputReducer.reduce(state, action)
|
||||||
log.debug("sessions state before reducers={}", state.sessions)
|
log.debug("sessions state before reducers={}", state.sessions)
|
||||||
val (sessions, se) = SessionsReducer.reduce(afterInput.sessions, prevInputMode, prevInputBuffer, action, clock)
|
val (sessions, se) = SessionsReducer.reduce(afterInput.sessions, prevInputMode, prevInputBuffer, action, clock)
|
||||||
log.debug("approval state before reducers={}", state.approval)
|
val selectedId = afterInput.sessions.selectedId
|
||||||
val (approval, ae) = ApprovalReducer.reduce(afterInput.approval, prevInputMode, approvalAction)
|
val bridgedApproval = ApprovalState(
|
||||||
|
active = afterInput.sessions.sessions.find { it.id == selectedId }?.pendingApproval,
|
||||||
|
)
|
||||||
|
val (approval, ae) = ApprovalReducer.reduce(bridgedApproval, prevInputMode, approvalAction)
|
||||||
|
val approvalJustArrived = approval.active != null && bridgedApproval.active == null
|
||||||
|
val sessionsWithApproval = sessions.copy(
|
||||||
|
sessions = sessions.sessions.map { s ->
|
||||||
|
if (s.id == selectedId) s.copy(pendingApproval = approval.active) else s
|
||||||
|
},
|
||||||
|
)
|
||||||
log.debug("connection state before reducers={}", state.connection)
|
log.debug("connection state before reducers={}", state.connection)
|
||||||
val (connection, ce) = ConnectionReducer.reduce(afterInput.connection, action)
|
val (connection, ce) = ConnectionReducer.reduce(afterInput.connection, action)
|
||||||
log.debug("provider state before reducers={}", state.provider)
|
log.debug("provider state before reducers={}", state.provider)
|
||||||
val (provider, pe) = ProviderReducer.reduce(afterInput.provider, action)
|
val (provider, pe) = ProviderReducer.reduce(afterInput.provider, action)
|
||||||
val approvalJustArrived = approval.active != null && afterInput.approval.active == null
|
|
||||||
val withSubReducers = afterInput.copy(
|
val withSubReducers = afterInput.copy(
|
||||||
sessions = sessions,
|
sessions = sessionsWithApproval,
|
||||||
approval = approval,
|
|
||||||
connection = connection,
|
connection = connection,
|
||||||
provider = provider,
|
provider = provider,
|
||||||
approvalOverlayVisible = if (approvalJustArrived) true else afterInput.approvalOverlayVisible,
|
approvalOverlayVisible = if (approvalJustArrived) true else afterInput.approvalOverlayVisible,
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ data class TuiState(
|
|||||||
val connection: ConnectionState = ConnectionState(),
|
val connection: ConnectionState = ConnectionState(),
|
||||||
val sessions: SessionsState = SessionsState(),
|
val sessions: SessionsState = SessionsState(),
|
||||||
val input: InputState = InputState(),
|
val input: InputState = InputState(),
|
||||||
val approval: ApprovalState = ApprovalState(),
|
|
||||||
val provider: ProviderState = ProviderState(),
|
val provider: ProviderState = ProviderState(),
|
||||||
val inputMode: InputMode = InputMode.ROUTER,
|
val inputMode: InputMode = InputMode.ROUTER,
|
||||||
val inputBuffer: String = "",
|
val inputBuffer: String = "",
|
||||||
@@ -65,6 +64,7 @@ data class SessionSummary(
|
|||||||
val lastResponseText: String? = null,
|
val lastResponseText: String? = null,
|
||||||
val tools: List<TuiToolRecord> = emptyList(),
|
val tools: List<TuiToolRecord> = emptyList(),
|
||||||
val recentEvents: List<TuiEventEntry> = emptyList(),
|
val recentEvents: List<TuiEventEntry> = emptyList(),
|
||||||
|
val pendingApproval: ApprovalInfo? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class ApprovalInfo(
|
data class ApprovalInfo(
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ import com.correx.apps.server.protocol.ApprovalDecision
|
|||||||
import com.correx.apps.server.protocol.ClientMessage
|
import com.correx.apps.server.protocol.ClientMessage
|
||||||
import com.correx.apps.tui.input.Action
|
import com.correx.apps.tui.input.Action
|
||||||
import com.correx.apps.tui.state.ApprovalInfo
|
import com.correx.apps.tui.state.ApprovalInfo
|
||||||
import com.correx.apps.tui.state.ApprovalState
|
|
||||||
import com.correx.apps.tui.state.InputMode
|
import com.correx.apps.tui.state.InputMode
|
||||||
|
import com.correx.apps.tui.state.SessionSummary
|
||||||
|
import com.correx.apps.tui.state.SessionsState
|
||||||
import com.correx.apps.tui.state.TuiState
|
import com.correx.apps.tui.state.TuiState
|
||||||
import org.junit.jupiter.api.Assertions.assertEquals
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
import org.junit.jupiter.api.Assertions.assertTrue
|
import org.junit.jupiter.api.Assertions.assertTrue
|
||||||
@@ -22,6 +23,21 @@ class InputReducerTest {
|
|||||||
preview = null,
|
preview = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private val sessionWithApproval = SessionSummary(
|
||||||
|
id = "sess-1",
|
||||||
|
status = "running",
|
||||||
|
workflowId = "wf",
|
||||||
|
lastEventAt = 0L,
|
||||||
|
pendingApproval = activeApproval,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun stateWithActiveApproval(inputMode: InputMode = InputMode.ROUTER, inputBuffer: String = "") =
|
||||||
|
TuiState(
|
||||||
|
inputMode = inputMode,
|
||||||
|
inputBuffer = inputBuffer,
|
||||||
|
sessions = SessionsState(sessions = listOf(sessionWithApproval), selectedId = "sess-1"),
|
||||||
|
)
|
||||||
|
|
||||||
private fun reduce(
|
private fun reduce(
|
||||||
state: TuiState = TuiState(),
|
state: TuiState = TuiState(),
|
||||||
action: Action,
|
action: Action,
|
||||||
@@ -37,7 +53,7 @@ class InputReducerTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `OpenSteeringPrompt sets mode to STEER when approval active`() {
|
fun `OpenSteeringPrompt sets mode to STEER when approval active`() {
|
||||||
val initial = TuiState(approval = ApprovalState(active = activeApproval))
|
val initial = stateWithActiveApproval()
|
||||||
val (state, effects) = reduce(state = initial, action = Action.OpenSteeringPrompt)
|
val (state, effects) = reduce(state = initial, action = Action.OpenSteeringPrompt)
|
||||||
assertEquals(InputMode.STEER, state.inputMode)
|
assertEquals(InputMode.STEER, state.inputMode)
|
||||||
assertEquals("", state.inputBuffer)
|
assertEquals("", state.inputBuffer)
|
||||||
@@ -118,11 +134,7 @@ class InputReducerTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `SubmitInput in STEER mode with active approval emits ApprovalResponse STEER`() {
|
fun `SubmitInput in STEER mode with active approval emits ApprovalResponse STEER`() {
|
||||||
val (state, effects) = reduce(
|
val (state, effects) = reduce(
|
||||||
state = TuiState(
|
state = stateWithActiveApproval(inputMode = InputMode.STEER, inputBuffer = "steer this way"),
|
||||||
inputMode = InputMode.STEER,
|
|
||||||
inputBuffer = "steer this way",
|
|
||||||
approval = ApprovalState(active = activeApproval),
|
|
||||||
),
|
|
||||||
action = Action.SubmitInput,
|
action = Action.SubmitInput,
|
||||||
)
|
)
|
||||||
assertEquals(InputMode.ROUTER, state.inputMode)
|
assertEquals(InputMode.ROUTER, state.inputMode)
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ class RootReducerTest {
|
|||||||
assertEquals(initial.inputMode, state.inputMode)
|
assertEquals(initial.inputMode, state.inputMode)
|
||||||
assertEquals(initial.inputBuffer, state.inputBuffer)
|
assertEquals(initial.inputBuffer, state.inputBuffer)
|
||||||
assertEquals(initial.sessions, state.sessions)
|
assertEquals(initial.sessions, state.sessions)
|
||||||
assertEquals(initial.approval, state.approval)
|
|
||||||
assertEquals(initial.provider, state.provider)
|
assertEquals(initial.provider, state.provider)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user