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:
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user