feat(tui): event-derived ordering + retire Kotlin TUI
Rebuild the Go TUI transcript from the event stream instead of optimistic local echoes. ChatTurnEvent now maps to ServerMessage.ChatTurn (chat.turn); the TUI auto-vivifies sessions and renders user+router turns from events. SessionStarted is de-special-cased into SessionAnnounced (still carries workflowId), dropping the router.response shadow and optimistic echoes. Delete the unused Kotlin TUI (apps/tui) and add a kotlinx<->Go golden-fixture test to lock the wire protocol. Gitignore server runtime logs.
This commit is contained in:
@@ -50,9 +50,24 @@ func inferCategory(t string) string {
|
||||
// applyServer mutates the model for a single (non-buffered) server message.
|
||||
func (m *Model) applyServer(msg protocol.ServerMessage) {
|
||||
debugLog("SRV type=%s session=%s reason=%s", msg.Type, msg.SessionID, msg.Reason)
|
||||
// Auto-vivify: any event for an unknown session creates it, so session
|
||||
// existence is derived from the event stream, not a dedicated control frame.
|
||||
if msg.IsEventBearing() && msg.SessionID != "" {
|
||||
m.ensureSession(msg.SessionID)
|
||||
}
|
||||
switch msg.Type {
|
||||
case protocol.TypeSessionStarted:
|
||||
m.onSessionStarted(msg)
|
||||
case protocol.TypeSessionAnnounced:
|
||||
m.onSessionAnnounced(msg)
|
||||
case protocol.TypeChatTurn:
|
||||
m.routerConnected = true
|
||||
role := "router"
|
||||
if msg.Role == "USER" {
|
||||
role = "user"
|
||||
}
|
||||
m.appendRouter(msg.SessionID, RouterEntry{role, msg.Content})
|
||||
if s := m.session(msg.SessionID); s != nil {
|
||||
s.LastEventAt = nowMillis()
|
||||
}
|
||||
case protocol.TypeSessionPaused:
|
||||
label := "PAUSED"
|
||||
if msg.Reason == "APPROVAL_PENDING" {
|
||||
@@ -183,9 +198,6 @@ func (m *Model) applyServer(msg protocol.ServerMessage) {
|
||||
} else {
|
||||
m.providerType = "REMOTE"
|
||||
}
|
||||
case protocol.TypeRouterResponse:
|
||||
m.routerConnected = true
|
||||
m.appendRouter(msg.SessionID, RouterEntry{"router", msg.Content})
|
||||
case protocol.TypeWorkflowList:
|
||||
m.workflows = m.workflows[:0]
|
||||
for _, w := range msg.Workflows {
|
||||
@@ -227,30 +239,23 @@ func sessionIDOf(msg protocol.ServerMessage) string {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) onSessionStarted(msg protocol.ServerMessage) {
|
||||
hadOptimistic := false
|
||||
cleaned := m.sessions[:0:0]
|
||||
for _, s := range m.sessions {
|
||||
if s.Status == "STARTING" {
|
||||
hadOptimistic = true
|
||||
continue
|
||||
}
|
||||
cleaned = append(cleaned, s)
|
||||
}
|
||||
cleaned = append(cleaned, Session{
|
||||
ID: msg.SessionID, Status: "ACTIVE", WorkflowID: msg.WorkflowID,
|
||||
Name: msg.WorkflowID, LastEventAt: nowMillis(),
|
||||
})
|
||||
m.sessions = cleaned
|
||||
if hadOptimistic || m.selectedID == "" {
|
||||
m.selectedID = msg.SessionID
|
||||
// onSessionAnnounced fills in a session's workflow identity (the announce is the
|
||||
// only event carrying workflowId) and applies auto-focus. The session entry itself
|
||||
// was already created by the auto-vivify path in applyServer.
|
||||
func (m *Model) onSessionAnnounced(msg protocol.ServerMessage) {
|
||||
s := m.ensureSession(msg.SessionID)
|
||||
s.Status = "ACTIVE"
|
||||
if msg.WorkflowID != "" {
|
||||
s.WorkflowID = msg.WorkflowID
|
||||
s.Name = msg.WorkflowID
|
||||
}
|
||||
s.LastEventAt = nowMillis()
|
||||
if m.pendingWorkflowFocus {
|
||||
m.selectedID = msg.SessionID
|
||||
m.sessionEntered = true
|
||||
m.pendingWorkflowFocus = false
|
||||
} else {
|
||||
m.sessionEntered = true
|
||||
} else if m.selectedID == "" {
|
||||
m.selectedID = msg.SessionID
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user