fix: wire routerConnected flag, expandable diffs, stage in session list, ArtifactCreated mapping

This commit is contained in:
2026-05-29 17:04:33 +04:00
parent fb3ab9b344
commit 7c55a53a9f
13 changed files with 83 additions and 13 deletions
@@ -5,6 +5,7 @@ import com.correx.apps.server.protocol.RiskSummaryDto
import com.correx.apps.server.protocol.ServerMessage
import com.correx.core.artifactstore.ArtifactStore
import com.correx.core.events.events.ApprovalRequestedEvent
import com.correx.core.events.events.ArtifactCreatedEvent
import com.correx.core.events.events.ChatSessionStartedEvent
import com.correx.core.events.events.InferenceCompletedEvent
import com.correx.core.events.events.WorkflowStartedEvent
@@ -157,6 +158,13 @@ suspend fun domainEventToServerMessage(
)
is ApprovalRequestedEvent -> mapApprovalRequested(p, seq, sessionSequence)
is ArtifactCreatedEvent -> ServerMessage.ArtifactCreated(
sessionId = p.sessionId,
stageId = p.stageId,
artifactId = p.artifactId,
sequence = seq,
sessionSequence = sessionSequence,
)
else -> {
log.debug(
"DomainEventMapper: unmapped payload type={} sessionId={} sequence={}",
@@ -19,6 +19,7 @@ import com.correx.core.events.events.InferenceStartedEvent
import com.correx.core.events.events.InferenceTimeoutEvent
import com.correx.core.events.events.NewEvent
import com.correx.core.events.events.ApprovalRequestedEvent
import com.correx.core.events.events.ArtifactCreatedEvent
import com.correx.core.events.events.OrchestrationPausedEvent
import com.correx.core.events.events.OrchestrationResumedEvent
import com.correx.core.events.events.StageCompletedEvent
@@ -183,6 +184,7 @@ class SessionEventBridge(
is OrchestrationPausedEvent -> EventEntryDto(ts, "SessionPaused", p.reason)
is WorkflowCompletedEvent -> EventEntryDto(ts, "SessionCompleted", "")
is WorkflowFailedEvent -> EventEntryDto(ts, "SessionFailed", p.reason)
is ArtifactCreatedEvent -> EventEntryDto(ts, "ArtifactCreated", p.artifactId.value)
else -> null
}
}
@@ -2,6 +2,7 @@ package com.correx.apps.server.protocol
import com.correx.core.approvals.Tier
import com.correx.core.events.types.ApprovalRequestId
import com.correx.core.events.types.ArtifactId
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import kotlinx.serialization.SerialName
@@ -251,6 +252,18 @@ sealed interface ServerMessage {
override val sessionSequence: Long? = null,
) : ServerMessage, NonEventMessage
// -- Artifacts --
@Serializable
@SerialName("artifact.created")
data class ArtifactCreated(
val sessionId: SessionId,
val stageId: StageId,
val artifactId: ArtifactId,
override val sequence: Long,
override val sessionSequence: Long,
) : ServerMessage, SessionMessage
// -- Router --
@Serializable
@@ -21,4 +21,5 @@ sealed class KeyEvent {
object CursorRight : KeyEvent()
object ToggleWorkflows : KeyEvent()
object ToggleSteeringMode : KeyEvent()
object ToggleDiffExpand : KeyEvent()
}
@@ -26,6 +26,8 @@ fun approvalSurfaceWidget(state: TuiState): Paragraph {
val approval = state.selectedPendingApproval()
val pendingDecision = state.pendingDecision
val diffExpanded = state.diffExpanded
val lines = buildList<Line> {
if (approval == null) {
add(Line.from(Span.styled("no pending approval", dimStyle)))
@@ -43,10 +45,14 @@ fun approvalSurfaceWidget(state: TuiState): Paragraph {
if (preview != null) {
val previewLines = preview.lines()
if (previewLines.isNotEmpty()) {
val visibleLines = previewLines.take(PREVIEW_MAX_LINES)
val visibleLines = if (diffExpanded) {
previewLines
} else {
previewLines.take(PREVIEW_MAX_LINES)
}
for (line in visibleLines) {
if (isDiff) {
val truncated = if (line.length > DIFF_LINE_MAX_LENGTH) {
val truncated = if (line.length > DIFF_LINE_MAX_LENGTH && !diffExpanded) {
line.take(DIFF_LINE_MAX_LENGTH - 3) + "..."
} else {
line
@@ -63,9 +69,12 @@ fun approvalSurfaceWidget(state: TuiState): Paragraph {
add(Line.from(Span.raw(" $line")))
}
}
if (previewLines.size > PREVIEW_MAX_LINES) {
if (!diffExpanded && previewLines.size > PREVIEW_MAX_LINES) {
val remaining = previewLines.size - PREVIEW_MAX_LINES
add(Line.from(Span.styled(" (preview truncated, $remaining more lines)", dimStyle)))
add(Line.from(Span.styled(" (preview truncated, $remaining more lines — ctrl+x to expand)", dimStyle)))
}
if (diffExpanded) {
add(Line.from(Span.styled(" (full diff — ctrl+x to collapse)", dimStyle)))
}
} else {
add(Line.from(Span.styled(" no preview available", dimStyle)))
@@ -87,7 +96,7 @@ fun approvalSurfaceWidget(state: TuiState): Paragraph {
// Approve/reject key hints
add(Line.from(Span.styled(
" ctrl+a approve \u00B7 ctrl+r reject \u00B7 enter submit \u00B7 esc dismiss",
" ctrl+a approve \u00B7 ctrl+r reject \u00B7 ctrl+x toggle diff \u00B7 enter submit \u00B7 esc dismiss",
dimStyle
)))
}
@@ -20,14 +20,13 @@ fun eventHistoryStripWidget(state: TuiState): Paragraph {
} else {
// Stage ID and status line (compact, dim metadata)
val stageInfo = buildString {
if (selectedSession.currentStageId != null) {
append(selectedSession.currentStageId)
val stageId = selectedSession.currentStageId
val stage = selectedSession.currentStage
when {
stageId != null -> append(stageId)
stage != null -> append(stage)
else -> append("(no stage)")
}
if (selectedSession.currentStage != null) {
if (isNotEmpty()) append(" · ")
append(selectedSession.currentStage)
}
if (isEmpty()) append("(no stage)")
append(" ")
append(selectedSession.status)
}
@@ -46,6 +45,7 @@ fun eventHistoryStripWidget(state: TuiState): Paragraph {
"StageFailed", "SessionFailed", "ToolFailed", "ToolRejected" -> Style.create().red()
"SessionPaused", "InferenceTimeout" -> Style.create().yellow()
"StageStarted", "InferenceStarted", "ToolStarted" -> Style.create().blue()
"ArtifactCreated" -> Style.create().magenta()
else -> Style.create()
}
val detail = if (ev.detail.length > DETAIL_MAX) {
@@ -31,7 +31,11 @@ fun sessionListWidget(state: TuiState): Paragraph {
val statusSpan = statusSpan(s)
val rawName = s.name.ifEmpty { s.workflowId }
val truncated = if (rawName.length > NAME_MAX) rawName.take(NAME_MAX - 1) + "" else rawName
add(Line.from(prefix, idSpan, Span.raw(" "), statusSpan, Span.raw(" $truncated")))
val nameAndStage = buildString {
append(truncated)
if (s.currentStageId != null) append(" [${s.currentStageId}]")
}
add(Line.from(prefix, idSpan, Span.raw(" "), statusSpan, Span.raw(" $nameAndStage")))
}
if (overflow > 0) {
add(Line.from(Span.styled("$overflow more —", dimStyle)))
@@ -28,5 +28,6 @@ sealed interface Action {
data object CursorRight : Action
data object ToggleWorkflows : Action
data object CycleChatMode : Action
data object ToggleDiffExpand : Action
data object NoOp : Action
}
@@ -74,6 +74,7 @@ object KeyResolver {
KeyEvent.Cancel -> Action.CancelInput
KeyEvent.ReturnToSessionList -> Action.ReturnToSessionList
KeyEvent.ToggleEventStrip -> Action.ToggleEventStrip
KeyEvent.ToggleDiffExpand -> Action.ToggleDiffExpand
KeyEvent.Tab -> Action.CycleMode
KeyEvent.Backspace -> Action.Backspace
is KeyEvent.CharInput -> Action.AppendChar(key.ch)
@@ -25,6 +25,7 @@ fun mapKey(event: TambouiKeyEvent): KeyEvent? {
event.isChar('h') -> KeyEvent.ShowPendingApproval
event.isChar('w') -> KeyEvent.ToggleWorkflows
event.isChar('s') -> KeyEvent.ToggleSteeringMode
event.isChar('x') -> KeyEvent.ToggleDiffExpand
else -> null
}
}
@@ -118,6 +118,8 @@ object RootReducer {
else withBgReset.copy(eventStripVisible = !withBgReset.eventStripVisible)
}
is Action.ToggleDiffExpand -> withBgReset.copy(diffExpanded = !withBgReset.diffExpanded)
// Input history append on SubmitInput in IN_SESSION + ROUTER (ARCH-HISTORY-1),
// or enter a selected session from the list in IDLE.
is Action.SubmitInput -> {
@@ -167,6 +169,7 @@ object RootReducer {
}
msg is ServerMessage.RouterResponseMessage -> {
withBgReset.copy(
routerConnected = true,
routerMessages = withBgReset.routerMessages.run {
val key = msg.sessionId.value
this + (key to (this[key].orEmpty() + msg.content))
@@ -207,6 +207,7 @@ object SessionsReducer {
is ServerMessage.SessionSnapshot -> processSessionSnapshotMessage(clock, sessions, msg)
is ServerMessage.ApprovalRequired -> processApprovalRequiredMessage(sessions, msg)
is ServerMessage.StageToolManifest -> processStageToolManifestMessage(sessions, msg)
is ServerMessage.ArtifactCreated -> processArtifactCreatedMessage(sessions, msg)
is ServerMessage.WorkflowList -> processWorkflowListMessage(sessions, msg)
else -> sessions to emptyList()
}
@@ -231,6 +232,7 @@ object SessionsReducer {
is ServerMessage.ToolRejected -> msg.sessionId.value
is ServerMessage.ApprovalRequired -> msg.sessionId.value
is ServerMessage.SessionSnapshot -> msg.sessionId.value
is ServerMessage.ArtifactCreated -> msg.sessionId.value
// Control/non-event messages do not count per ARCH-BADGE-1:
is ServerMessage.StageToolManifest -> null
is ServerMessage.SnapshotComplete -> null
@@ -627,6 +629,30 @@ object SessionsReducer {
},
)
private fun processArtifactCreatedMessage(
sessions: SessionsState,
msg: ServerMessage.ArtifactCreated,
): Pair<SessionsState, List<Effect>> {
val now = msg.sequence
val entry = TuiEventEntry(
timestamp = formatTime(now),
type = "ArtifactCreated",
detail = msg.artifactId.value,
)
return sessions.copy(
sessions = sessions.sessions.map { s ->
if (s.id == msg.sessionId.value) {
s.copy(
recentEvents = (s.recentEvents + entry).takeLast(7),
lastEventAt = now,
)
} else {
s
}
},
) to emptyList()
}
private fun processWorkflowListMessage(
sessions: SessionsState,
msg: ServerMessage.WorkflowList,
@@ -47,6 +47,7 @@ data class TuiState(
val providerType: ProviderType = ProviderType.LOCAL,
val routerConnected: Boolean = false,
val routerMessages: Map<String, List<String>> = emptyMap(),
val diffExpanded: Boolean = false,
val chatMode: ChatMode = ChatMode.CHAT,
/**
* True from connect until SnapshotComplete observed. Reset to true on Disconnected.