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.
This commit is contained in:
2026-05-20 00:36:46 +04:00
parent bc755572bd
commit 7ac3a1ff79
12 changed files with 280 additions and 128 deletions
-25
View File
@@ -1,25 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %c{1} [%t] %msg%n"/>
</Console>
</Appenders>
<Loggers>
<Logger name="com.correx" level="DEBUG" additivity="false">
<AppenderRef ref="Console"/>
</Logger>
<Logger name="io.netty" level="WARN" additivity="false">
<AppenderRef ref="Console"/>
</Logger>
<Logger name="org.eclipse.jetty" level="WARN" additivity="false">
<AppenderRef ref="Console"/>
</Logger>
<Logger name="io.ktor" level="INFO" additivity="false">
<AppenderRef ref="Console"/>
</Logger>
<Root level="INFO">
<AppenderRef ref="Console"/>
</Root>
</Loggers>
</Configuration>
@@ -84,8 +84,8 @@ fun main(args: Array<String>) {
val handler = EventHandler { event, _ -> val handler = EventHandler { event, _ ->
if (event is TambouiKeyEvent) { if (event is TambouiKeyEvent) {
mapKey(event, state.input.mode) mapKey(event, state.inputMode)
?.let { KeyResolver.resolve(it, state.input.mode, state.input.text) } ?.let { KeyResolver.resolve(it, state.inputMode, state.inputBuffer) }
?.let(::dispatch) ?.let(::dispatch)
} }
true true
@@ -100,7 +100,7 @@ fun main(args: Array<String>) {
private fun render(frame: Frame, state: TuiState, listState: ListState) { private fun render(frame: Frame, state: TuiState, listState: ListState) {
val area = frame.area() val area = frame.area()
val hasInput = state.input.mode != InputMode.None val hasInput = state.inputMode != InputMode.ROUTER
val hasApproval = state.approval.active != null val hasApproval = state.approval.active != null
val constraints = buildList { val constraints = buildList {
@@ -136,7 +136,7 @@ private fun render(frame: Frame, state: TuiState, listState: ListState) {
} }
if (hasInput) { if (hasInput) {
inputBarWidget(state.input)?.let { frame.renderWidget(it, rects[idx]) } inputBarWidget(state.inputMode, state.inputBuffer)?.let { frame.renderWidget(it, rects[idx]) }
idx++ idx++
} }
@@ -1,7 +1,6 @@
package com.correx.apps.tui.components package com.correx.apps.tui.components
import com.correx.apps.tui.state.InputMode import com.correx.apps.tui.state.InputMode
import com.correx.apps.tui.state.InputState
import dev.tamboui.style.Style import dev.tamboui.style.Style
import dev.tamboui.text.Line import dev.tamboui.text.Line
import dev.tamboui.text.Span 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.block.Borders
import dev.tamboui.widgets.paragraph.Paragraph import dev.tamboui.widgets.paragraph.Paragraph
fun inputBarWidget(input: InputState): Paragraph? { fun inputBarWidget(inputMode: InputMode, inputBuffer: String): Paragraph? {
if (input.mode == InputMode.None) return null if (inputMode == InputMode.ROUTER) return null
val (promptText, promptStyle) = when (input.mode) { val (promptText, promptStyle) = when (inputMode) {
InputMode.WorkflowId -> "new session > " to Style.create().cyan() InputMode.ROUTER -> "" to Style.create()
InputMode.Filter -> "filter > " to Style.create().yellow() InputMode.NAVIGATE -> "" to Style.create()
InputMode.SteeringNote -> "steer > " to Style.create().magenta() InputMode.FILTER -> "filter > " to Style.create().yellow()
else -> "" to Style.create() InputMode.STEER -> "steer > " to Style.create().magenta()
} }
val line = Line.from( val line = Line.from(
Span.styled(promptText, promptStyle), Span.styled(promptText, promptStyle),
Span.raw("${input.text}_") Span.raw("${inputBuffer}_")
) )
val block = Block.builder() val block = Block.builder()
.title("input") .title("input")
@@ -20,4 +20,7 @@ sealed interface Action {
data object Connected : Action data object Connected : Action
data object Disconnected : Action data object Disconnected : Action
data class RetryScheduled(val attempt: Int, val nextRetryAtMs: Long) : Action data class RetryScheduled(val attempt: Int, val nextRetryAtMs: Long) : Action
data object ToggleApprovalOverlay : Action
data object ToggleEventOverlay : Action
data object EnterSteer : Action
} }
@@ -5,11 +5,12 @@ import com.correx.apps.tui.state.InputMode
object KeyResolver { object KeyResolver {
fun resolve(key: KeyEvent, mode: InputMode, inputText: String): Action? = when (mode) { fun resolve(key: KeyEvent, mode: InputMode, inputText: String): Action? = when (mode) {
InputMode.None -> resolveCommandMode(key) InputMode.ROUTER -> resolveRouterMode(key)
else -> resolveInputMode(key, inputText) 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.Quit -> Action.Quit
KeyEvent.NewSession -> Action.OpenNewSessionPrompt KeyEvent.NewSession -> Action.OpenNewSessionPrompt
KeyEvent.Cancel -> Action.CancelSelectedSession KeyEvent.Cancel -> Action.CancelSelectedSession
@@ -22,7 +23,14 @@ object KeyResolver {
else -> null 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.Escape -> Action.CancelInput
KeyEvent.Backspace -> Action.Backspace KeyEvent.Backspace -> Action.Backspace
KeyEvent.Enter -> if (inputText.isBlank()) null else Action.SubmitInput KeyEvent.Enter -> if (inputText.isBlank()) null else Action.SubmitInput
@@ -19,7 +19,7 @@ fun mapKey(event: TambouiKeyEvent, mode: InputMode): KeyEvent? {
KeyCode.CHAR -> { KeyCode.CHAR -> {
val ch = event.character() val ch = event.character()
if (ch.isISOControl()) return null if (ch.isISOControl()) return null
if (mode != InputMode.None) return KeyEvent.CharInput(ch) if (mode != InputMode.ROUTER) return KeyEvent.CharInput(ch)
when (ch) { when (ch) {
'q' -> KeyEvent.Quit 'q' -> KeyEvent.Quit
'n' -> KeyEvent.NewSession 'n' -> KeyEvent.NewSession
@@ -47,7 +47,7 @@ object ApprovalReducer {
approval to emptyList() approval to emptyList()
} }
} }
is Action.SubmitInput -> if (inputMode == InputMode.SteeringNote) { is Action.SubmitInput -> if (inputMode == InputMode.STEER) {
ApprovalState(active = null) to emptyList() ApprovalState(active = null) to emptyList()
} else { } else {
approval to emptyList() approval to emptyList()
@@ -3,58 +3,58 @@ package com.correx.apps.tui.reducer
import com.correx.apps.server.protocol.ApprovalDecision 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.InputMode 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 import com.correx.core.events.types.ApprovalRequestId
object InputReducer { object InputReducer {
@Suppress("CyclomaticComplexMethod") @Suppress("CyclomaticComplexMethod")
fun reduce( fun reduce(
input: InputState, state: TuiState,
activeApproval: ApprovalInfo?,
action: Action, action: Action,
): Pair<InputState, List<Effect>> = when (action) { ): Pair<TuiState, List<Effect>> = when (action) {
is Action.OpenNewSessionPrompt -> InputState(InputMode.WorkflowId, "") to emptyList() is Action.OpenNewSessionPrompt -> state.copy(inputMode = InputMode.ROUTER, inputBuffer = "") to emptyList()
is Action.OpenSteeringPrompt -> if (activeApproval != null) { is Action.OpenSteeringPrompt -> if (state.approval.active != null) {
InputState(InputMode.SteeringNote, "") to emptyList() state.copy(inputMode = InputMode.STEER, inputBuffer = "") to emptyList()
} else { } else {
input to emptyList() state to emptyList()
} }
is Action.OpenFilter -> InputState(InputMode.Filter, "") to emptyList() is Action.EnterSteer -> state.copy(inputMode = InputMode.STEER, inputBuffer = "") to emptyList()
is Action.AppendChar -> input.copy(text = input.text + action.ch) to emptyList() is Action.OpenFilter -> state.copy(inputMode = InputMode.FILTER, inputBuffer = "") to emptyList()
is Action.Backspace -> input.copy(text = input.text.dropLast(1)) to emptyList() is Action.AppendChar -> state.copy(inputBuffer = state.inputBuffer + action.ch) to emptyList()
is Action.CancelInput -> InputState(InputMode.None, "") to emptyList() is Action.Backspace -> state.copy(inputBuffer = state.inputBuffer.dropLast(1)) to emptyList()
is Action.SubmitInput -> when (input.mode) { is Action.CancelInput -> state.copy(inputMode = InputMode.ROUTER, inputBuffer = "") to emptyList()
InputMode.WorkflowId -> { is Action.SubmitInput -> when (state.inputMode) {
val text = input.text.trim() InputMode.ROUTER -> {
val text = state.inputBuffer.trim()
if (text.isNotBlank()) { if (text.isNotBlank()) {
InputState(InputMode.None, "") to listOf( state.copy(inputMode = InputMode.ROUTER, inputBuffer = "") to listOf(
Effect.SendWs(ClientMessage.StartSession(workflowId = text, config = null)), Effect.SendWs(ClientMessage.StartSession(workflowId = text, config = null)),
) )
} else { } else {
InputState(InputMode.None, "") to emptyList() state.copy(inputMode = InputMode.ROUTER, inputBuffer = "") to emptyList()
} }
} }
InputMode.SteeringNote -> { InputMode.STEER -> {
val text = input.text.trim() val text = state.inputBuffer.trim()
if (activeApproval != null && text.isNotBlank()) { val active = state.approval.active
InputState(InputMode.None, "") to listOf( if (active != null && text.isNotBlank()) {
state.copy(inputMode = InputMode.ROUTER, inputBuffer = "") to listOf(
Effect.SendWs( Effect.SendWs(
ClientMessage.ApprovalResponse( ClientMessage.ApprovalResponse(
requestId = ApprovalRequestId(activeApproval.requestId), requestId = ApprovalRequestId(active.requestId),
decision = ApprovalDecision.STEER, decision = ApprovalDecision.STEER,
steeringNote = text, steeringNote = text,
), ),
), ),
) )
} else { } else {
InputState(InputMode.None, "") to emptyList() state.copy(inputMode = InputMode.ROUTER, inputBuffer = "") to emptyList()
} }
} }
InputMode.Filter -> InputState(InputMode.None, "") to emptyList() InputMode.FILTER -> state.copy(inputMode = InputMode.ROUTER, inputBuffer = "") to emptyList()
InputMode.None -> input to emptyList() InputMode.NAVIGATE -> state to emptyList()
} }
else -> input to emptyList() else -> state to emptyList()
} }
} }
@@ -10,17 +10,30 @@ object RootReducer {
clock: () -> Long = System::currentTimeMillis, clock: () -> Long = System::currentTimeMillis,
): Pair<TuiState, List<Effect>> { ): Pair<TuiState, List<Effect>> {
val quitEffects: List<Effect> = if (action is Action.Quit) listOf(Effect.Quit) else emptyList() val quitEffects: List<Effect> = if (action is Action.Quit) listOf(Effect.Quit) else emptyList()
val (input, ie) = InputReducer.reduce(state.input, state.approval.active, action) val prevInputMode = state.inputMode
val (sessions, se) = SessionsReducer.reduce(state.sessions, state.input.mode, state.input.text, action, clock) val prevInputBuffer = state.inputBuffer
val (approval, ae) = ApprovalReducer.reduce(state.approval, state.input.mode, action) val (afterInput, ie) = InputReducer.reduce(state, action)
val (connection, ce) = ConnectionReducer.reduce(state.connection, action) val (sessions, se) = SessionsReducer.reduce(afterInput.sessions, prevInputMode, prevInputBuffer, action, clock)
val (provider, pe) = ProviderReducer.reduce(state.provider, action) val (approval, ae) = ApprovalReducer.reduce(afterInput.approval, prevInputMode, action)
return state.copy( val (connection, ce) = ConnectionReducer.reduce(afterInput.connection, action)
input = input, val (provider, pe) = ProviderReducer.reduce(afterInput.provider, action)
val withSubReducers = afterInput.copy(
sessions = sessions, sessions = sessions,
approval = approval, approval = approval,
connection = connection, connection = connection,
provider = provider, 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)
} }
} }
@@ -7,9 +7,17 @@ import com.correx.apps.tui.input.Action
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.SessionSummary
import com.correx.apps.tui.state.SessionsState 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 com.correx.core.events.types.SessionId
import java.time.Instant
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
object SessionsReducer { object SessionsReducer {
private val timeFormatter = DateTimeFormatter.ofPattern("HH:mm:ss").withZone(ZoneOffset.UTC)
fun reduce( fun reduce(
sessions: SessionsState, sessions: SessionsState,
inputMode: InputMode, inputMode: InputMode,
@@ -17,15 +25,15 @@ object SessionsReducer {
action: Action, action: Action,
clock: () -> Long = System::currentTimeMillis, clock: () -> Long = System::currentTimeMillis,
): Pair<SessionsState, List<Effect>> = when (action) { ): Pair<SessionsState, List<Effect>> = when (action) {
is Action.NavigateUp -> navigateUp(sessions) to emptyList() is Action.NavigateUp -> if (inputMode == InputMode.NAVIGATE) navigateUp(sessions) to emptyList() else sessions to emptyList()
is Action.NavigateDown -> navigateDown(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) { is Action.SubmitInput -> if (inputMode == InputMode.FILTER) {
sessions.copy(filter = inputText) to emptyList() sessions.copy(filter = inputText) to emptyList()
} else { } else {
sessions to emptyList() sessions to emptyList()
} }
is Action.CancelInput -> if (inputMode == InputMode.Filter) { is Action.CancelInput -> if (inputMode == InputMode.FILTER) {
sessions.copy(filter = "") to emptyList() sessions.copy(filter = "") to emptyList()
} else { } else {
sessions to emptyList() sessions to emptyList()
@@ -60,6 +68,9 @@ object SessionsReducer {
return sessions.copy(selectedId = list[newIdx].id) return sessions.copy(selectedId = list[newIdx].id)
} }
private fun formatTime(epochMs: Long): String =
timeFormatter.format(Instant.ofEpochMilli(epochMs))
@Suppress("CyclomaticComplexMethod") @Suppress("CyclomaticComplexMethod")
private fun applyServerMessage( private fun applyServerMessage(
sessions: SessionsState, sessions: SessionsState,
@@ -71,6 +82,7 @@ object SessionsReducer {
id = msg.sessionId.value, id = msg.sessionId.value,
status = "ACTIVE", status = "ACTIVE",
workflowId = msg.workflowId, workflowId = msg.workflowId,
name = msg.workflowId,
lastEventAt = clock(), lastEventAt = clock(),
currentStage = null, currentStage = null,
lastOutput = null, lastOutput = null,
@@ -104,38 +116,145 @@ object SessionsReducer {
clock, clock,
) to listOf(Effect.DisconnectSession) ) to listOf(Effect.DisconnectSession)
is ServerMessage.StageStarted -> sessions.copy( is ServerMessage.StageStarted -> {
val now = clock()
sessions.copy(
sessions = sessions.sessions.map { s -> sessions = sessions.sessions.map { s ->
if (s.id == msg.sessionId.value) { if (s.id == msg.sessionId.value) {
s.copy(currentStage = msg.stageId.value, lastEventAt = clock()) 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 { } else {
s s
} }
}, },
) to emptyList() ) to emptyList()
}
is ServerMessage.StageCompleted -> sessions.copy( is ServerMessage.StageCompleted -> {
val now = clock()
sessions.copy(
sessions = sessions.sessions.map { s -> sessions = sessions.sessions.map { s ->
if (s.id == msg.sessionId.value) s.copy(currentStage = null, lastEventAt = clock()) else 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() ) to emptyList()
}
is ServerMessage.StageFailed -> sessions.copy( is ServerMessage.StageFailed -> {
val now = clock()
sessions.copy(
sessions = sessions.sessions.map { s -> sessions = sessions.sessions.map { s ->
if (s.id == msg.sessionId.value) s.copy(currentStage = null, lastEventAt = clock()) else 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() ) 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.InferenceCompleted -> applyInferenceCompleted(sessions, msg, clock)
is ServerMessage.ToolCompleted -> sessions.copy( is ServerMessage.ToolCompleted -> {
val now = clock()
sessions.copy(
sessions = sessions.sessions.map { s -> sessions = sessions.sessions.map { s ->
if (s.id == msg.sessionId.value) { if (s.id == msg.sessionId.value) {
s.copy(lastOutput = "${msg.toolName}: ${msg.outputSummary}", lastEventAt = clock()) 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 { } else {
s s
} }
}, },
) to emptyList() ) 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() else -> sessions to emptyList()
} }
@@ -144,19 +263,24 @@ object SessionsReducer {
sessions: SessionsState, sessions: SessionsState,
msg: ServerMessage.InferenceCompleted, msg: ServerMessage.InferenceCompleted,
clock: () -> Long, clock: () -> Long,
): Pair<SessionsState, List<Effect>> = sessions.copy( ): Pair<SessionsState, List<Effect>> {
val now = clock()
return sessions.copy(
sessions = sessions.sessions.map { s -> sessions = sessions.sessions.map { s ->
if (s.id == msg.sessionId.value) { if (s.id == msg.sessionId.value) {
val entry = TuiEventEntry(formatTime(now), "InferenceCompleted", msg.stageId.value)
s.copy( s.copy(
lastOutput = msg.outputSummary, lastOutput = msg.outputSummary,
lastResponseText = msg.responseText.takeIf { it.isNotEmpty() }, lastResponseText = msg.responseText.takeIf { it.isNotEmpty() },
lastEventAt = clock(), recentEvents = (s.recentEvents + entry).takeLast(7),
lastEventAt = now,
) )
} else { } else {
s s
} }
}, },
) to emptyList() ) to emptyList()
}
private fun touchSession( private fun touchSession(
sessions: SessionsState, sessions: SessionsState,
@@ -1,6 +1,6 @@
package com.correx.apps.tui.state package com.correx.apps.tui.state
data class InputState( data class InputState(
val mode: InputMode = InputMode.None, val mode: InputMode = InputMode.ROUTER,
val text: String = "", val text: String = "",
) )
@@ -1,6 +1,23 @@
package com.correx.apps.tui.state 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( data class TuiState(
val connection: ConnectionState = ConnectionState(), val connection: ConnectionState = ConnectionState(),
@@ -8,16 +25,29 @@ data class TuiState(
val input: InputState = InputState(), val input: InputState = InputState(),
val approval: ApprovalState = ApprovalState(), val approval: ApprovalState = ApprovalState(),
val provider: ProviderState = ProviderState(), 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<String> = emptyList(),
) )
data class SessionSummary( data class SessionSummary(
val id: String, val id: String,
val status: String, val status: String,
val workflowId: String, val workflowId: String,
val name: String = "",
val lastEventAt: Long, val lastEventAt: Long,
val currentStage: String?, val currentStage: String? = null,
val lastOutput: String?, val currentStageId: String? = null,
val nextStageId: String? = null,
val lastOutput: String? = null,
val lastResponseText: String? = null, val lastResponseText: String? = null,
val tools: List<TuiToolRecord> = emptyList(),
val recentEvents: List<TuiEventEntry> = emptyList(),
) )
data class ApprovalInfo( data class ApprovalInfo(