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(