From d2c6789c4d3a9aeb0ef54fb0ddfc0212cf69246a Mon Sep 17 00:00:00 2001 From: kami Date: Mon, 25 May 2026 14:24:20 +0400 Subject: [PATCH] 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). --- .../main/kotlin/com/correx/apps/tui/TuiApp.kt | 1 - .../correx/apps/tui/components/InputBar.kt | 2 +- .../correx/apps/tui/components/RouterPanel.kt | 12 ++++++--- .../correx/apps/tui/components/StatusBar.kt | 2 +- .../correx/apps/tui/reducer/InputReducer.kt | 13 ++++++---- .../correx/apps/tui/reducer/RootReducer.kt | 18 +++++++++---- .../com/correx/apps/tui/state/TuiState.kt | 2 +- .../apps/tui/reducer/InputReducerTest.kt | 26 ++++++++++++++----- .../apps/tui/reducer/RootReducerTest.kt | 1 - 9 files changed, 51 insertions(+), 26 deletions(-) diff --git a/apps/tui/src/main/kotlin/com/correx/apps/tui/TuiApp.kt b/apps/tui/src/main/kotlin/com/correx/apps/tui/TuiApp.kt index 0a82d8aa..a34e276a 100644 --- a/apps/tui/src/main/kotlin/com/correx/apps/tui/TuiApp.kt +++ b/apps/tui/src/main/kotlin/com/correx/apps/tui/TuiApp.kt @@ -47,7 +47,6 @@ fun main(args: Array) { fun dispatch(action: Action) { val (next, effects) = RootReducer.reduce(state, action) 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 session state after reducers: {}", diff --git a/apps/tui/src/main/kotlin/com/correx/apps/tui/components/InputBar.kt b/apps/tui/src/main/kotlin/com/correx/apps/tui/components/InputBar.kt index 65a2586b..d7c05eb4 100644 --- a/apps/tui/src/main/kotlin/com/correx/apps/tui/components/InputBar.kt +++ b/apps/tui/src/main/kotlin/com/correx/apps/tui/components/InputBar.kt @@ -13,7 +13,7 @@ import dev.tamboui.widgets.paragraph.Paragraph fun inputBarWidget(state: TuiState): Paragraph { 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 sessionName = state.sessions.sessions diff --git a/apps/tui/src/main/kotlin/com/correx/apps/tui/components/RouterPanel.kt b/apps/tui/src/main/kotlin/com/correx/apps/tui/components/RouterPanel.kt index 79231836..d24f636e 100644 --- a/apps/tui/src/main/kotlin/com/correx/apps/tui/components/RouterPanel.kt +++ b/apps/tui/src/main/kotlin/com/correx/apps/tui/components/RouterPanel.kt @@ -23,18 +23,21 @@ fun routerPanelWidget(state: TuiState): Paragraph { } } + val activeApproval = state.sessions.sessions.find { it.id == state.sessions.selectedId }?.pendingApproval val overlayLines: List = when { - state.approvalOverlayVisible && state.approval.active != null -> { - val a = state.approval.active + state.approvalOverlayVisible && activeApproval != null -> { buildList { 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()))) - 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("╰──────────────────────────────────────────╯", Style.create().yellow()))) } } + state.eventOverlayVisible -> { val selectedSession = state.sessions.sessions.firstOrNull { it.id == state.sessions.selectedId } val events = selectedSession?.recentEvents ?: emptyList() @@ -47,6 +50,7 @@ fun routerPanelWidget(state: TuiState): Paragraph { add(Line.from(Span.styled("╰──────────────────────────────────────────╯", Style.create().magenta()))) } } + else -> emptyList() } diff --git a/apps/tui/src/main/kotlin/com/correx/apps/tui/components/StatusBar.kt b/apps/tui/src/main/kotlin/com/correx/apps/tui/components/StatusBar.kt index 069139c2..fb838aa7 100644 --- a/apps/tui/src/main/kotlin/com/correx/apps/tui/components/StatusBar.kt +++ b/apps/tui/src/main/kotlin/com/correx/apps/tui/components/StatusBar.kt @@ -36,7 +36,7 @@ fun statusBarWidget(state: TuiState): Paragraph { add(modelSpan) add(sep) add(sessionsSpan) - if (state.approval.active != null) { + if (state.sessions.sessions.find { it.id == state.sessions.selectedId }?.pendingApproval != null) { add(sep) add(Span.styled("[⚠ approval]", Style.create().yellow())) } diff --git a/apps/tui/src/main/kotlin/com/correx/apps/tui/reducer/InputReducer.kt b/apps/tui/src/main/kotlin/com/correx/apps/tui/reducer/InputReducer.kt index 381ccc8b..8410b910 100644 --- a/apps/tui/src/main/kotlin/com/correx/apps/tui/reducer/InputReducer.kt +++ b/apps/tui/src/main/kotlin/com/correx/apps/tui/reducer/InputReducer.kt @@ -24,10 +24,13 @@ object InputReducer { ) to emptyList() is Action.OpenNewSessionPrompt -> state.copy(inputMode = InputMode.ROUTER, inputBuffer = "") to emptyList() - is Action.OpenSteeringPrompt -> if (state.approval.active != null) { - state.copy(inputMode = InputMode.STEER, inputBuffer = "") to emptyList() - } else { - state to emptyList() + 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() + } else { + state to emptyList() + } } is Action.EnterSteer -> state.copy(inputMode = InputMode.STEER, inputBuffer = "") to emptyList() @@ -49,7 +52,7 @@ object InputReducer { InputMode.STEER -> { 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()) { state.copy(inputMode = InputMode.ROUTER, inputBuffer = "") to listOf( Effect.SendWs( diff --git a/apps/tui/src/main/kotlin/com/correx/apps/tui/reducer/RootReducer.kt b/apps/tui/src/main/kotlin/com/correx/apps/tui/reducer/RootReducer.kt index fdec8d53..87a01626 100644 --- a/apps/tui/src/main/kotlin/com/correx/apps/tui/reducer/RootReducer.kt +++ b/apps/tui/src/main/kotlin/com/correx/apps/tui/reducer/RootReducer.kt @@ -2,6 +2,7 @@ package com.correx.apps.tui.reducer import com.correx.apps.server.protocol.ServerMessage import com.correx.apps.tui.input.Action +import com.correx.apps.tui.state.ApprovalState import com.correx.apps.tui.state.TuiState import org.slf4j.LoggerFactory @@ -55,16 +56,23 @@ object RootReducer { val (afterInput, ie) = InputReducer.reduce(state, action) log.debug("sessions state before reducers={}", state.sessions) val (sessions, se) = SessionsReducer.reduce(afterInput.sessions, prevInputMode, prevInputBuffer, action, clock) - log.debug("approval state before reducers={}", state.approval) - val (approval, ae) = ApprovalReducer.reduce(afterInput.approval, prevInputMode, approvalAction) + val selectedId = afterInput.sessions.selectedId + 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) val (connection, ce) = ConnectionReducer.reduce(afterInput.connection, action) log.debug("provider state before reducers={}", state.provider) val (provider, pe) = ProviderReducer.reduce(afterInput.provider, action) - val approvalJustArrived = approval.active != null && afterInput.approval.active == null val withSubReducers = afterInput.copy( - sessions = sessions, - approval = approval, + sessions = sessionsWithApproval, connection = connection, provider = provider, approvalOverlayVisible = if (approvalJustArrived) true else afterInput.approvalOverlayVisible, diff --git a/apps/tui/src/main/kotlin/com/correx/apps/tui/state/TuiState.kt b/apps/tui/src/main/kotlin/com/correx/apps/tui/state/TuiState.kt index 8fb3ccbf..5d75c72b 100644 --- a/apps/tui/src/main/kotlin/com/correx/apps/tui/state/TuiState.kt +++ b/apps/tui/src/main/kotlin/com/correx/apps/tui/state/TuiState.kt @@ -25,7 +25,6 @@ data class TuiState( val connection: ConnectionState = ConnectionState(), val sessions: SessionsState = SessionsState(), val input: InputState = InputState(), - val approval: ApprovalState = ApprovalState(), val provider: ProviderState = ProviderState(), val inputMode: InputMode = InputMode.ROUTER, val inputBuffer: String = "", @@ -65,6 +64,7 @@ data class SessionSummary( val lastResponseText: String? = null, val tools: List = emptyList(), val recentEvents: List = emptyList(), + val pendingApproval: ApprovalInfo? = null, ) data class ApprovalInfo( diff --git a/apps/tui/src/test/kotlin/com/correx/apps/tui/reducer/InputReducerTest.kt b/apps/tui/src/test/kotlin/com/correx/apps/tui/reducer/InputReducerTest.kt index cdebe328..52f76dbf 100644 --- a/apps/tui/src/test/kotlin/com/correx/apps/tui/reducer/InputReducerTest.kt +++ b/apps/tui/src/test/kotlin/com/correx/apps/tui/reducer/InputReducerTest.kt @@ -4,8 +4,9 @@ import com.correx.apps.server.protocol.ApprovalDecision import com.correx.apps.server.protocol.ClientMessage import com.correx.apps.tui.input.Action 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.SessionSummary +import com.correx.apps.tui.state.SessionsState import com.correx.apps.tui.state.TuiState import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertTrue @@ -22,6 +23,21 @@ class InputReducerTest { 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( state: TuiState = TuiState(), action: Action, @@ -37,7 +53,7 @@ class InputReducerTest { @Test 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) assertEquals(InputMode.STEER, state.inputMode) assertEquals("", state.inputBuffer) @@ -118,11 +134,7 @@ class InputReducerTest { @Test fun `SubmitInput in STEER mode with active approval emits ApprovalResponse STEER`() { val (state, effects) = reduce( - state = TuiState( - inputMode = InputMode.STEER, - inputBuffer = "steer this way", - approval = ApprovalState(active = activeApproval), - ), + state = stateWithActiveApproval(inputMode = InputMode.STEER, inputBuffer = "steer this way"), action = Action.SubmitInput, ) assertEquals(InputMode.ROUTER, state.inputMode) diff --git a/apps/tui/src/test/kotlin/com/correx/apps/tui/reducer/RootReducerTest.kt b/apps/tui/src/test/kotlin/com/correx/apps/tui/reducer/RootReducerTest.kt index 4cc4b982..7597cecb 100644 --- a/apps/tui/src/test/kotlin/com/correx/apps/tui/reducer/RootReducerTest.kt +++ b/apps/tui/src/test/kotlin/com/correx/apps/tui/reducer/RootReducerTest.kt @@ -42,7 +42,6 @@ class RootReducerTest { assertEquals(initial.inputMode, state.inputMode) assertEquals(initial.inputBuffer, state.inputBuffer) assertEquals(initial.sessions, state.sessions) - assertEquals(initial.approval, state.approval) assertEquals(initial.provider, state.provider) }