From 7ac3a1ff798b1f38ed521b97d2a34de59f2ba107 Mon Sep 17 00:00:00 2001 From: kami Date: Wed, 20 May 2026 00:36:46 +0400 Subject: [PATCH] feat(tui): enrich state model and refactor input mode into TuiState Lifts inputMode/inputBuffer out of InputState into top-level TuiState, renames InputMode variants to ROUTER/NAVIGATE/FILTER/STEER, adds TuiToolRecord and TuiEventEntry for per-session tool/event history, expands SessionsReducer to track tool lifecycle and recent events, and adds ToggleApprovalOverlay/ToggleEventOverlay/EnterSteer actions. --- apps/server/src/main/resources/log4j2.xml | 25 -- .../main/kotlin/com/correx/apps/tui/TuiApp.kt | 8 +- .../correx/apps/tui/components/InputBar.kt | 17 +- .../com/correx/apps/tui/input/Action.kt | 3 + .../com/correx/apps/tui/input/KeyResolver.kt | 16 +- .../correx/apps/tui/input/TambouiKeyMapper.kt | 2 +- .../apps/tui/reducer/ApprovalReducer.kt | 2 +- .../correx/apps/tui/reducer/InputReducer.kt | 54 ++--- .../correx/apps/tui/reducer/RootReducer.kt | 29 ++- .../apps/tui/reducer/SessionsReducer.kt | 214 ++++++++++++++---- .../com/correx/apps/tui/state/InputState.kt | 2 +- .../com/correx/apps/tui/state/TuiState.kt | 36 ++- 12 files changed, 280 insertions(+), 128 deletions(-) delete mode 100644 apps/server/src/main/resources/log4j2.xml diff --git a/apps/server/src/main/resources/log4j2.xml b/apps/server/src/main/resources/log4j2.xml deleted file mode 100644 index e5f7d81c..00000000 --- a/apps/server/src/main/resources/log4j2.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - 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 7ddfd44a..a941b45e 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 @@ -84,8 +84,8 @@ fun main(args: Array) { val handler = EventHandler { event, _ -> if (event is TambouiKeyEvent) { - mapKey(event, state.input.mode) - ?.let { KeyResolver.resolve(it, state.input.mode, state.input.text) } + mapKey(event, state.inputMode) + ?.let { KeyResolver.resolve(it, state.inputMode, state.inputBuffer) } ?.let(::dispatch) } true @@ -100,7 +100,7 @@ fun main(args: Array) { private fun render(frame: Frame, state: TuiState, listState: ListState) { val area = frame.area() - val hasInput = state.input.mode != InputMode.None + val hasInput = state.inputMode != InputMode.ROUTER val hasApproval = state.approval.active != null val constraints = buildList { @@ -136,7 +136,7 @@ private fun render(frame: Frame, state: TuiState, listState: ListState) { } if (hasInput) { - inputBarWidget(state.input)?.let { frame.renderWidget(it, rects[idx]) } + inputBarWidget(state.inputMode, state.inputBuffer)?.let { frame.renderWidget(it, rects[idx]) } idx++ } 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 36915eb3..575a0e59 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 @@ -1,7 +1,6 @@ package com.correx.apps.tui.components import com.correx.apps.tui.state.InputMode -import com.correx.apps.tui.state.InputState import dev.tamboui.style.Style import dev.tamboui.text.Line import dev.tamboui.text.Span @@ -11,17 +10,17 @@ import dev.tamboui.widgets.block.BorderType import dev.tamboui.widgets.block.Borders import dev.tamboui.widgets.paragraph.Paragraph -fun inputBarWidget(input: InputState): Paragraph? { - if (input.mode == InputMode.None) return null - val (promptText, promptStyle) = when (input.mode) { - InputMode.WorkflowId -> "new session > " to Style.create().cyan() - InputMode.Filter -> "filter > " to Style.create().yellow() - InputMode.SteeringNote -> "steer > " to Style.create().magenta() - else -> "" to Style.create() +fun inputBarWidget(inputMode: InputMode, inputBuffer: String): Paragraph? { + if (inputMode == InputMode.ROUTER) return null + val (promptText, promptStyle) = when (inputMode) { + InputMode.ROUTER -> "" to Style.create() + InputMode.NAVIGATE -> "" to Style.create() + InputMode.FILTER -> "filter > " to Style.create().yellow() + InputMode.STEER -> "steer > " to Style.create().magenta() } val line = Line.from( Span.styled(promptText, promptStyle), - Span.raw("${input.text}_") + Span.raw("${inputBuffer}_") ) val block = Block.builder() .title("input") 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 474756cc..0276197b 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 @@ -20,4 +20,7 @@ sealed interface Action { data object Connected : Action data object Disconnected : Action data class RetryScheduled(val attempt: Int, val nextRetryAtMs: Long) : Action + data object ToggleApprovalOverlay : Action + data object ToggleEventOverlay : Action + data object EnterSteer : 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 fbff18e8..dac18d27 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 @@ -5,11 +5,12 @@ import com.correx.apps.tui.state.InputMode object KeyResolver { fun resolve(key: KeyEvent, mode: InputMode, inputText: String): Action? = when (mode) { - InputMode.None -> resolveCommandMode(key) - else -> resolveInputMode(key, inputText) + InputMode.ROUTER -> resolveRouterMode(key) + InputMode.NAVIGATE -> resolveNavigateMode(key) + InputMode.FILTER, InputMode.STEER -> resolveTextMode(key, inputText) } - private fun resolveCommandMode(key: KeyEvent): Action? = when (key) { + private fun resolveRouterMode(key: KeyEvent): Action? = when (key) { KeyEvent.Quit -> Action.Quit KeyEvent.NewSession -> Action.OpenNewSessionPrompt KeyEvent.Cancel -> Action.CancelSelectedSession @@ -22,7 +23,14 @@ object KeyResolver { else -> null } - private fun resolveInputMode(key: KeyEvent, inputText: String): Action? = when (key) { + private fun resolveNavigateMode(key: KeyEvent): Action? = when (key) { + KeyEvent.NavUp -> Action.NavigateUp + KeyEvent.NavDown -> Action.NavigateDown + KeyEvent.Escape -> Action.CancelInput + else -> null + } + + private fun resolveTextMode(key: KeyEvent, inputText: String): Action? = when (key) { KeyEvent.Escape -> Action.CancelInput KeyEvent.Backspace -> Action.Backspace KeyEvent.Enter -> if (inputText.isBlank()) null else Action.SubmitInput 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 d4ce129a..9015e907 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 @@ -19,7 +19,7 @@ fun mapKey(event: TambouiKeyEvent, mode: InputMode): KeyEvent? { KeyCode.CHAR -> { val ch = event.character() if (ch.isISOControl()) return null - if (mode != InputMode.None) return KeyEvent.CharInput(ch) + if (mode != InputMode.ROUTER) return KeyEvent.CharInput(ch) when (ch) { 'q' -> KeyEvent.Quit 'n' -> KeyEvent.NewSession diff --git a/apps/tui/src/main/kotlin/com/correx/apps/tui/reducer/ApprovalReducer.kt b/apps/tui/src/main/kotlin/com/correx/apps/tui/reducer/ApprovalReducer.kt index dafb2a02..30b99d81 100644 --- a/apps/tui/src/main/kotlin/com/correx/apps/tui/reducer/ApprovalReducer.kt +++ b/apps/tui/src/main/kotlin/com/correx/apps/tui/reducer/ApprovalReducer.kt @@ -47,7 +47,7 @@ object ApprovalReducer { approval to emptyList() } } - is Action.SubmitInput -> if (inputMode == InputMode.SteeringNote) { + is Action.SubmitInput -> if (inputMode == InputMode.STEER) { ApprovalState(active = null) to emptyList() } else { approval to emptyList() 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 e2d14137..fbd56f4b 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 @@ -3,58 +3,58 @@ package com.correx.apps.tui.reducer 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.InputMode -import com.correx.apps.tui.state.InputState +import com.correx.apps.tui.state.TuiState import com.correx.core.events.types.ApprovalRequestId object InputReducer { @Suppress("CyclomaticComplexMethod") fun reduce( - input: InputState, - activeApproval: ApprovalInfo?, + state: TuiState, action: Action, - ): Pair> = when (action) { - is Action.OpenNewSessionPrompt -> InputState(InputMode.WorkflowId, "") to emptyList() - is Action.OpenSteeringPrompt -> if (activeApproval != null) { - InputState(InputMode.SteeringNote, "") to emptyList() + ): Pair> = when (action) { + 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 { - input to emptyList() + state to emptyList() } - is Action.OpenFilter -> InputState(InputMode.Filter, "") to emptyList() - is Action.AppendChar -> input.copy(text = input.text + action.ch) to emptyList() - is Action.Backspace -> input.copy(text = input.text.dropLast(1)) to emptyList() - is Action.CancelInput -> InputState(InputMode.None, "") to emptyList() - is Action.SubmitInput -> when (input.mode) { - InputMode.WorkflowId -> { - val text = input.text.trim() + is Action.EnterSteer -> state.copy(inputMode = InputMode.STEER, inputBuffer = "") to emptyList() + is Action.OpenFilter -> state.copy(inputMode = InputMode.FILTER, inputBuffer = "") to emptyList() + is Action.AppendChar -> state.copy(inputBuffer = state.inputBuffer + action.ch) to emptyList() + is Action.Backspace -> state.copy(inputBuffer = state.inputBuffer.dropLast(1)) to emptyList() + is Action.CancelInput -> state.copy(inputMode = InputMode.ROUTER, inputBuffer = "") to emptyList() + is Action.SubmitInput -> when (state.inputMode) { + InputMode.ROUTER -> { + val text = state.inputBuffer.trim() if (text.isNotBlank()) { - InputState(InputMode.None, "") to listOf( + state.copy(inputMode = InputMode.ROUTER, inputBuffer = "") to listOf( Effect.SendWs(ClientMessage.StartSession(workflowId = text, config = null)), ) } else { - InputState(InputMode.None, "") to emptyList() + state.copy(inputMode = InputMode.ROUTER, inputBuffer = "") to emptyList() } } - InputMode.SteeringNote -> { - val text = input.text.trim() - if (activeApproval != null && text.isNotBlank()) { - InputState(InputMode.None, "") to listOf( + InputMode.STEER -> { + val text = state.inputBuffer.trim() + val active = state.approval.active + if (active != null && text.isNotBlank()) { + state.copy(inputMode = InputMode.ROUTER, inputBuffer = "") to listOf( Effect.SendWs( ClientMessage.ApprovalResponse( - requestId = ApprovalRequestId(activeApproval.requestId), + requestId = ApprovalRequestId(active.requestId), decision = ApprovalDecision.STEER, steeringNote = text, ), ), ) } else { - InputState(InputMode.None, "") to emptyList() + state.copy(inputMode = InputMode.ROUTER, inputBuffer = "") to emptyList() } } - InputMode.Filter -> InputState(InputMode.None, "") to emptyList() - InputMode.None -> input to emptyList() + InputMode.FILTER -> state.copy(inputMode = InputMode.ROUTER, inputBuffer = "") to emptyList() + InputMode.NAVIGATE -> state to emptyList() } - else -> input to emptyList() + else -> state 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 9fc9e5ea..c7d65cb2 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 @@ -10,17 +10,30 @@ object RootReducer { clock: () -> Long = System::currentTimeMillis, ): Pair> { val quitEffects: List = if (action is Action.Quit) listOf(Effect.Quit) else emptyList() - val (input, ie) = InputReducer.reduce(state.input, state.approval.active, action) - val (sessions, se) = SessionsReducer.reduce(state.sessions, state.input.mode, state.input.text, action, clock) - val (approval, ae) = ApprovalReducer.reduce(state.approval, state.input.mode, action) - val (connection, ce) = ConnectionReducer.reduce(state.connection, action) - val (provider, pe) = ProviderReducer.reduce(state.provider, action) - return state.copy( - input = input, + val prevInputMode = state.inputMode + val prevInputBuffer = state.inputBuffer + val (afterInput, ie) = InputReducer.reduce(state, action) + val (sessions, se) = SessionsReducer.reduce(afterInput.sessions, prevInputMode, prevInputBuffer, action, clock) + val (approval, ae) = ApprovalReducer.reduce(afterInput.approval, prevInputMode, action) + val (connection, ce) = ConnectionReducer.reduce(afterInput.connection, action) + val (provider, pe) = ProviderReducer.reduce(afterInput.provider, action) + val withSubReducers = afterInput.copy( sessions = sessions, approval = approval, connection = connection, provider = provider, - ) to (quitEffects + ie + se + ae + ce + pe) + ) + val final = when (action) { + is Action.ToggleApprovalOverlay -> withSubReducers.copy( + approvalOverlayVisible = !withSubReducers.approvalOverlayVisible, + eventOverlayVisible = false, + ) + is Action.ToggleEventOverlay -> withSubReducers.copy( + eventOverlayVisible = !withSubReducers.eventOverlayVisible, + approvalOverlayVisible = false, + ) + else -> withSubReducers + } + return final to (quitEffects + ie + se + ae + ce + pe) } } 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 b526e7f3..93d97bc0 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 @@ -7,9 +7,17 @@ import com.correx.apps.tui.input.Action 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.ToolDisplayStatus +import com.correx.apps.tui.state.TuiEventEntry +import com.correx.apps.tui.state.TuiToolRecord import com.correx.core.events.types.SessionId +import java.time.Instant +import java.time.ZoneOffset +import java.time.format.DateTimeFormatter object SessionsReducer { + private val timeFormatter = DateTimeFormatter.ofPattern("HH:mm:ss").withZone(ZoneOffset.UTC) + fun reduce( sessions: SessionsState, inputMode: InputMode, @@ -17,15 +25,15 @@ object SessionsReducer { action: Action, clock: () -> Long = System::currentTimeMillis, ): Pair> = when (action) { - is Action.NavigateUp -> navigateUp(sessions) to emptyList() - is Action.NavigateDown -> navigateDown(sessions) to emptyList() - is Action.SubmitInput -> if (inputMode == InputMode.Filter) { + is Action.NavigateUp -> if (inputMode == InputMode.NAVIGATE) navigateUp(sessions) to emptyList() else sessions to emptyList() + is Action.NavigateDown -> if (inputMode == InputMode.NAVIGATE) navigateDown(sessions) to emptyList() else sessions to emptyList() + is Action.SubmitInput -> if (inputMode == InputMode.FILTER) { sessions.copy(filter = inputText) to emptyList() } else { sessions to emptyList() } - is Action.CancelInput -> if (inputMode == InputMode.Filter) { + is Action.CancelInput -> if (inputMode == InputMode.FILTER) { sessions.copy(filter = "") to emptyList() } else { sessions to emptyList() @@ -60,6 +68,9 @@ object SessionsReducer { return sessions.copy(selectedId = list[newIdx].id) } + private fun formatTime(epochMs: Long): String = + timeFormatter.format(Instant.ofEpochMilli(epochMs)) + @Suppress("CyclomaticComplexMethod") private fun applyServerMessage( sessions: SessionsState, @@ -71,6 +82,7 @@ object SessionsReducer { id = msg.sessionId.value, status = "ACTIVE", workflowId = msg.workflowId, + name = msg.workflowId, lastEventAt = clock(), currentStage = null, lastOutput = null, @@ -104,38 +116,145 @@ object SessionsReducer { clock, ) to listOf(Effect.DisconnectSession) - is ServerMessage.StageStarted -> sessions.copy( - sessions = sessions.sessions.map { s -> - if (s.id == msg.sessionId.value) { - s.copy(currentStage = msg.stageId.value, lastEventAt = clock()) - } else { - s - } - }, - ) to emptyList() + is ServerMessage.StageStarted -> { + val now = clock() + sessions.copy( + sessions = sessions.sessions.map { s -> + if (s.id == msg.sessionId.value) { + val entry = TuiEventEntry(formatTime(now), "StageStarted", msg.stageId.value) + s.copy( + currentStage = msg.stageId.value, + currentStageId = msg.stageId.value, + tools = emptyList(), + recentEvents = (s.recentEvents + entry).takeLast(7), + lastEventAt = now, + ) + } else { + s + } + }, + ) to emptyList() + } - is ServerMessage.StageCompleted -> sessions.copy( - sessions = sessions.sessions.map { s -> - if (s.id == msg.sessionId.value) s.copy(currentStage = null, lastEventAt = clock()) else s - }, - ) to emptyList() + is ServerMessage.StageCompleted -> { + val now = clock() + sessions.copy( + sessions = sessions.sessions.map { s -> + if (s.id == msg.sessionId.value) { + val entry = TuiEventEntry(formatTime(now), "StageCompleted", msg.stageId.value) + s.copy( + currentStage = null, + recentEvents = (s.recentEvents + entry).takeLast(7), + lastEventAt = now, + ) + } else { + s + } + }, + ) to emptyList() + } - is ServerMessage.StageFailed -> sessions.copy( - sessions = sessions.sessions.map { s -> - if (s.id == msg.sessionId.value) s.copy(currentStage = null, lastEventAt = clock()) else s - }, - ) to emptyList() + is ServerMessage.StageFailed -> { + val now = clock() + sessions.copy( + sessions = sessions.sessions.map { s -> + if (s.id == msg.sessionId.value) { + val entry = TuiEventEntry(formatTime(now), "StageFailed", msg.stageId.value) + s.copy( + currentStage = null, + recentEvents = (s.recentEvents + entry).takeLast(7), + lastEventAt = now, + ) + } else { + s + } + }, + ) to emptyList() + } + + is ServerMessage.ToolStarted -> { + val now = clock() + sessions.copy( + sessions = sessions.sessions.map { s -> + if (s.id == msg.sessionId.value) { + val record = TuiToolRecord(msg.toolName, msg.tier.level, ToolDisplayStatus.STARTED, null) + s.copy( + tools = (s.tools + record).takeLast(8), + lastEventAt = now, + ) + } else { + s + } + }, + ) to emptyList() + } is ServerMessage.InferenceCompleted -> applyInferenceCompleted(sessions, msg, clock) - is ServerMessage.ToolCompleted -> sessions.copy( - sessions = sessions.sessions.map { s -> - if (s.id == msg.sessionId.value) { - s.copy(lastOutput = "${msg.toolName}: ${msg.outputSummary}", lastEventAt = clock()) - } else { - s - } - }, - ) to emptyList() + is ServerMessage.ToolCompleted -> { + val now = clock() + sessions.copy( + sessions = sessions.sessions.map { s -> + if (s.id == msg.sessionId.value) { + val updatedTools = s.tools.map { t -> + if (t.name == msg.toolName && t.status == ToolDisplayStatus.STARTED) { + t.copy(status = ToolDisplayStatus.COMPLETED) + } else { + t + } + } + val entry = TuiEventEntry(formatTime(now), "ToolCompleted", msg.toolName) + s.copy( + tools = updatedTools, + lastOutput = "${msg.toolName}: ${msg.outputSummary}", + recentEvents = (s.recentEvents + entry).takeLast(7), + lastEventAt = now, + ) + } else { + s + } + }, + ) to emptyList() + } + + is ServerMessage.ToolFailed -> { + val now = clock() + sessions.copy( + sessions = sessions.sessions.map { s -> + if (s.id == msg.sessionId.value) { + val updatedTools = s.tools.map { t -> + if (t.name == msg.toolName && t.status == ToolDisplayStatus.STARTED) { + t.copy(status = ToolDisplayStatus.FAILED) + } else { + t + } + } + s.copy(tools = updatedTools, lastEventAt = now) + } else { + s + } + }, + ) to emptyList() + } + + is ServerMessage.ToolRejected -> { + val now = clock() + sessions.copy( + sessions = sessions.sessions.map { s -> + if (s.id == msg.sessionId.value) { + val updatedTools = s.tools.map { t -> + if (t.name == msg.toolName && t.status == ToolDisplayStatus.STARTED) { + t.copy(status = ToolDisplayStatus.REJECTED) + } else { + t + } + } + s.copy(tools = updatedTools, lastEventAt = now) + } else { + s + } + }, + ) to emptyList() + } else -> sessions to emptyList() } @@ -144,19 +263,24 @@ object SessionsReducer { sessions: SessionsState, msg: ServerMessage.InferenceCompleted, clock: () -> Long, - ): Pair> = sessions.copy( - sessions = sessions.sessions.map { s -> - if (s.id == msg.sessionId.value) { - s.copy( - lastOutput = msg.outputSummary, - lastResponseText = msg.responseText.takeIf { it.isNotEmpty() }, - lastEventAt = clock(), - ) - } else { - s - } - }, - ) to emptyList() + ): Pair> { + val now = clock() + return sessions.copy( + sessions = sessions.sessions.map { s -> + if (s.id == msg.sessionId.value) { + val entry = TuiEventEntry(formatTime(now), "InferenceCompleted", msg.stageId.value) + s.copy( + lastOutput = msg.outputSummary, + lastResponseText = msg.responseText.takeIf { it.isNotEmpty() }, + recentEvents = (s.recentEvents + entry).takeLast(7), + lastEventAt = now, + ) + } else { + s + } + }, + ) to emptyList() + } private fun touchSession( sessions: SessionsState, diff --git a/apps/tui/src/main/kotlin/com/correx/apps/tui/state/InputState.kt b/apps/tui/src/main/kotlin/com/correx/apps/tui/state/InputState.kt index c31f53bf..bcb772d2 100644 --- a/apps/tui/src/main/kotlin/com/correx/apps/tui/state/InputState.kt +++ b/apps/tui/src/main/kotlin/com/correx/apps/tui/state/InputState.kt @@ -1,6 +1,6 @@ package com.correx.apps.tui.state data class InputState( - val mode: InputMode = InputMode.None, + val mode: InputMode = InputMode.ROUTER, val text: String = "", ) 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 bfa6d968..49070a04 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 @@ -1,6 +1,23 @@ package com.correx.apps.tui.state -enum class InputMode { None, WorkflowId, SteeringNote, Filter } +enum class InputMode { ROUTER, NAVIGATE, FILTER, STEER } + +enum class ToolDisplayStatus { REQUESTED, STARTED, COMPLETED, FAILED, REJECTED } + +enum class ProviderType { LOCAL, REMOTE } + +data class TuiToolRecord( + val name: String, + val tier: Int, + val status: ToolDisplayStatus, + val argsPreview: String?, +) + +data class TuiEventEntry( + val timestamp: String, + val type: String, + val detail: String, +) data class TuiState( val connection: ConnectionState = ConnectionState(), @@ -8,16 +25,29 @@ data class TuiState( val input: InputState = InputState(), val approval: ApprovalState = ApprovalState(), val provider: ProviderState = ProviderState(), + val inputMode: InputMode = InputMode.ROUTER, + val inputBuffer: String = "", + val approvalOverlayVisible: Boolean = false, + val eventOverlayVisible: Boolean = false, + val currentModel: String? = null, + val providerType: ProviderType = ProviderType.LOCAL, + val routerConnected: Boolean = false, + val routerMessages: List = emptyList(), ) data class SessionSummary( val id: String, val status: String, val workflowId: String, + val name: String = "", val lastEventAt: Long, - val currentStage: String?, - val lastOutput: String?, + val currentStage: String? = null, + val currentStageId: String? = null, + val nextStageId: String? = null, + val lastOutput: String? = null, val lastResponseText: String? = null, + val tools: List = emptyList(), + val recentEvents: List = emptyList(), ) data class ApprovalInfo(