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