feat: wire ChatMode.STEERING toggle in TUI input bar

- 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
This commit is contained in:
2026-05-29 02:27:23 +04:00
parent 3981d8443d
commit fb3ab9b344
10 changed files with 45 additions and 8 deletions
@@ -20,4 +20,5 @@ sealed class KeyEvent {
object CursorLeft : KeyEvent()
object CursorRight : KeyEvent()
object ToggleWorkflows : KeyEvent()
object ToggleSteeringMode : KeyEvent()
}
@@ -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()),
@@ -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
}
@@ -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
@@ -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
}
}
@@ -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()
@@ -69,6 +69,7 @@ object RootReducer {
inputText = prevInputBuffer,
action = action,
clock = clock,
chatMode = afterInput.chatMode,
),
)
log.debug("connection state before reducers={}", state.connection)
@@ -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,
),
),
)
@@ -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<String, List<String>> = 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.
@@ -549,16 +549,28 @@ abstract class SessionOrchestrator(
private suspend fun buildSteeringNoteEntries(sessionId: SessionId): List<ContextEntry> {
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
}
}
}