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, _ ->
|
||||
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<String>) {
|
||||
|
||||
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++
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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<InputState, List<Effect>> = when (action) {
|
||||
is Action.OpenNewSessionPrompt -> InputState(InputMode.WorkflowId, "") to emptyList()
|
||||
is Action.OpenSteeringPrompt -> if (activeApproval != null) {
|
||||
InputState(InputMode.SteeringNote, "") to emptyList()
|
||||
): Pair<TuiState, List<Effect>> = 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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,17 +10,30 @@ object RootReducer {
|
||||
clock: () -> Long = System::currentTimeMillis,
|
||||
): Pair<TuiState, List<Effect>> {
|
||||
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 (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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<SessionsState, List<Effect>> = 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<SessionsState, List<Effect>> = 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<SessionsState, List<Effect>> {
|
||||
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,
|
||||
|
||||
@@ -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 = "",
|
||||
)
|
||||
|
||||
@@ -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<String> = 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<TuiToolRecord> = emptyList(),
|
||||
val recentEvents: List<TuiEventEntry> = emptyList(),
|
||||
)
|
||||
|
||||
data class ApprovalInfo(
|
||||
|
||||
Reference in New Issue
Block a user