From fb3ab9b3445178da14eedc57dd3ea6e23ef19c2b Mon Sep 17 00:00:00 2001 From: kami Date: Fri, 29 May 2026 02:27:23 +0400 Subject: [PATCH] feat: wire ChatMode.STEERING toggle in TUI input bar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add ChatMode.STEERING toggle via ctrl+s keybinding - Add KeyEvent.ToggleSteeringMode, Action.CycleChatMode - TambouiKeyMapper: map ctrl+s to ToggleSteeringMode - KeyResolver: route ToggleSteeringMode to CycleChatMode (global) - InputReducer: toggle chatMode between CHAT and STEERING - TuiState: add chatMode field (default CHAT) - SessionsReducer: use ctx.chatMode instead of hardcoded CHAT - RootReducer: pass afterInput.chatMode to SessionsReducerContext - InputBar: show blue 'chat' or magenta 'steer' label in status line Activation: ctrl+s toggles mode, 'steer' label appears, submitted ChatInput uses ChatMode.STEERING → router emits SteeringNoteAddedEvent → context builder folds into next stage's context pack --- .../kotlin/com/correx/apps/tui/KeyEvent.kt | 1 + .../correx/apps/tui/components/InputBar.kt | 13 +++++++++-- .../com/correx/apps/tui/input/Action.kt | 1 + .../com/correx/apps/tui/input/KeyResolver.kt | 1 + .../correx/apps/tui/input/TambouiKeyMapper.kt | 1 + .../correx/apps/tui/reducer/InputReducer.kt | 8 +++++++ .../correx/apps/tui/reducer/RootReducer.kt | 1 + .../apps/tui/reducer/SessionsReducer.kt | 3 ++- .../com/correx/apps/tui/state/TuiState.kt | 2 ++ .../orchestration/SessionOrchestrator.kt | 22 ++++++++++++++----- 10 files changed, 45 insertions(+), 8 deletions(-) diff --git a/apps/tui/src/main/kotlin/com/correx/apps/tui/KeyEvent.kt b/apps/tui/src/main/kotlin/com/correx/apps/tui/KeyEvent.kt index af75f160..e00ae9a6 100644 --- a/apps/tui/src/main/kotlin/com/correx/apps/tui/KeyEvent.kt +++ b/apps/tui/src/main/kotlin/com/correx/apps/tui/KeyEvent.kt @@ -20,4 +20,5 @@ sealed class KeyEvent { object CursorLeft : KeyEvent() object CursorRight : KeyEvent() object ToggleWorkflows : KeyEvent() + object ToggleSteeringMode : KeyEvent() } 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 f909b96a..92136c97 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 @@ -18,6 +18,11 @@ private fun promptStyle(inputMode: InputMode): Style = when (inputMode) { InputMode.FILTER -> Style.create().yellow() } +private fun chatModeLabel(state: TuiState): Span = when (state.chatMode) { + com.correx.core.router.ChatMode.CHAT -> Span.styled("chat", Style.create().blue()) + com.correx.core.router.ChatMode.STEERING -> Span.styled("steer", Style.create().magenta()) +} + private fun cursorLine(buffer: String, cursor: Int, inputMode: InputMode): Line { val prompt = Span.styled("▌", promptStyle(inputMode)) if (buffer.isEmpty()) return Line.from(prompt, Span.raw("")) @@ -77,7 +82,9 @@ private fun createRowsForIdleRouter( } return row1 to Line.from( Span.styled(sessionName, Style.create().cyan()), - Span.styled(" · router ", dimStyle), + Span.styled(" · ", dimStyle), + chatModeLabel(state), + Span.styled(" ", dimStyle), Span.styled("tab", Style.create().blue()), Span.styled(" filter ", dimStyle), Span.styled("ctrl+n", Style.create().blue()), @@ -101,7 +108,9 @@ private fun createRowsForInSessionRouter( } return row1 to Line.from( Span.styled(sessionName, Style.create().cyan()), - Span.styled(" · router ", dimStyle), + Span.styled(" · ", dimStyle), + chatModeLabel(state), + Span.styled(" ", dimStyle), Span.styled("tab", Style.create().blue()), Span.styled(" filter ", dimStyle), Span.styled("↑↓", Style.create().blue()), diff --git a/apps/tui/src/main/kotlin/com/correx/apps/tui/input/Action.kt b/apps/tui/src/main/kotlin/com/correx/apps/tui/input/Action.kt index af23d3a6..05a978cd 100644 --- a/apps/tui/src/main/kotlin/com/correx/apps/tui/input/Action.kt +++ b/apps/tui/src/main/kotlin/com/correx/apps/tui/input/Action.kt @@ -27,5 +27,6 @@ sealed interface Action { data object CursorLeft : Action data object CursorRight : Action data object ToggleWorkflows : Action + data object CycleChatMode : Action data object NoOp : Action } diff --git a/apps/tui/src/main/kotlin/com/correx/apps/tui/input/KeyResolver.kt b/apps/tui/src/main/kotlin/com/correx/apps/tui/input/KeyResolver.kt index a8f13159..f6e5eb41 100644 --- a/apps/tui/src/main/kotlin/com/correx/apps/tui/input/KeyResolver.kt +++ b/apps/tui/src/main/kotlin/com/correx/apps/tui/input/KeyResolver.kt @@ -17,6 +17,7 @@ object KeyResolver { KeyEvent.Quit -> Action.Quit KeyEvent.Approve -> Action.ApproveActive KeyEvent.Reject -> Action.RejectActive + KeyEvent.ToggleSteeringMode -> Action.CycleChatMode else -> null } if (global != null) return global diff --git a/apps/tui/src/main/kotlin/com/correx/apps/tui/input/TambouiKeyMapper.kt b/apps/tui/src/main/kotlin/com/correx/apps/tui/input/TambouiKeyMapper.kt index d375ec4b..04fc30d3 100644 --- a/apps/tui/src/main/kotlin/com/correx/apps/tui/input/TambouiKeyMapper.kt +++ b/apps/tui/src/main/kotlin/com/correx/apps/tui/input/TambouiKeyMapper.kt @@ -24,6 +24,7 @@ fun mapKey(event: TambouiKeyEvent): KeyEvent? { event.isChar('e') -> KeyEvent.ToggleEventStrip event.isChar('h') -> KeyEvent.ShowPendingApproval event.isChar('w') -> KeyEvent.ToggleWorkflows + event.isChar('s') -> KeyEvent.ToggleSteeringMode else -> null } } 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 d6734e81..3cb13ab7 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 @@ -7,6 +7,7 @@ import com.correx.apps.tui.state.InputMode import com.correx.apps.tui.state.TuiState import com.correx.apps.tui.state.selectedPendingApproval import com.correx.core.events.types.ApprovalRequestId +import com.correx.core.router.ChatMode object InputReducer { @Suppress("CyclomaticComplexMethod") @@ -21,6 +22,13 @@ object InputReducer { }, ) to emptyList() + is Action.CycleChatMode -> state.copy( + chatMode = when (state.chatMode) { + ChatMode.CHAT -> ChatMode.STEERING + ChatMode.STEERING -> ChatMode.CHAT + }, + ) to emptyList() + is Action.CursorLeft -> state.copy( inputCursor = (state.inputCursor - 1).coerceAtLeast(0), ) to emptyList() 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 f93010b8..a2914342 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 @@ -69,6 +69,7 @@ object RootReducer { inputText = prevInputBuffer, action = action, clock = clock, + chatMode = afterInput.chatMode, ), ) log.debug("connection state before reducers={}", state.connection) diff --git a/apps/tui/src/main/kotlin/com/correx/apps/tui/reducer/SessionsReducer.kt b/apps/tui/src/main/kotlin/com/correx/apps/tui/reducer/SessionsReducer.kt index 75e7ee6d..d2be71bf 100644 --- a/apps/tui/src/main/kotlin/com/correx/apps/tui/reducer/SessionsReducer.kt +++ b/apps/tui/src/main/kotlin/com/correx/apps/tui/reducer/SessionsReducer.kt @@ -29,6 +29,7 @@ data class SessionsReducerContext( val inputText: String, val action: Action, val clock: () -> Long = System::currentTimeMillis, + val chatMode: ChatMode = ChatMode.CHAT, ) object SessionsReducer { @@ -89,7 +90,7 @@ object SessionsReducer { ClientMessage.ChatInput( sessionId = SessionId(sid), text = ctx.inputText, - mode = ChatMode.CHAT, + mode = ctx.chatMode, ), ), ) 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 5ae4cdc5..b6070356 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 @@ -2,6 +2,7 @@ package com.correx.apps.tui.state import com.correx.apps.server.protocol.ApprovalDecision import com.correx.apps.server.protocol.ServerMessage +import com.correx.core.router.ChatMode enum class InputMode { ROUTER, FILTER } @@ -46,6 +47,7 @@ data class TuiState( val providerType: ProviderType = ProviderType.LOCAL, val routerConnected: Boolean = false, val routerMessages: Map> = emptyMap(), + val chatMode: ChatMode = ChatMode.CHAT, /** * True from connect until SnapshotComplete observed. Reset to true on Disconnected. * When true, event-bearing messages are buffered in [pendingEvents]; snapshots are applied immediately. diff --git a/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/SessionOrchestrator.kt b/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/SessionOrchestrator.kt index c882605e..3752061c 100644 --- a/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/SessionOrchestrator.kt +++ b/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/SessionOrchestrator.kt @@ -549,16 +549,28 @@ abstract class SessionOrchestrator( private suspend fun buildSteeringNoteEntries(sessionId: SessionId): List { val events = eventStore.read(sessionId) return events.mapNotNull { event -> - (event.payload as? SteeringNoteAddedEvent)?.let { steering -> - ContextEntry( + when (val p = event.payload) { + is SteeringNoteAddedEvent -> ContextEntry( id = ContextEntryId(UUID.randomUUID().toString()), layer = ContextLayer.L2, - content = steering.content, + content = p.content, sourceType = "steeringNote", - sourceId = steering.stageId?.value ?: sessionId.value, - tokenEstimate = estimateTokens(steering.content), + sourceId = p.stageId?.value ?: sessionId.value, + tokenEstimate = estimateTokens(p.content), role = EntryRole.SYSTEM, ) + is ApprovalDecisionResolvedEvent -> p.userSteering?.let { steering -> + ContextEntry( + id = ContextEntryId(UUID.randomUUID().toString()), + layer = ContextLayer.L2, + content = steering.text, + sourceType = "steeringNote", + sourceId = steering.sessionId.value, + tokenEstimate = estimateTokens(steering.text), + role = EntryRole.USER, + ) + } + else -> null } } }