feat(tui): render workflow progress as dim router-feed narration lines

This commit is contained in:
2026-06-03 15:07:31 +04:00
parent 46c567c835
commit e9a87febc4
3 changed files with 85 additions and 5 deletions
+34 -3
View File
@@ -47,6 +47,35 @@ func inferCategory(t string) string {
}
}
// narrationLine maps a progress frame to a dim router-feed line, or "" if the
// frame is not a narration trigger.
func narrationLine(msg protocol.ServerMessage) string {
switch msg.Type {
case protocol.TypeStageStarted:
return "→ " + msg.StageID
case protocol.TypeStageCompleted:
return "✓ " + msg.StageID
case protocol.TypeStageFailed:
if msg.Reason != "" {
return "⚠ " + msg.StageID + ": " + msg.Reason
}
return "⚠ " + msg.StageID
case protocol.TypeSessionPaused:
return "⏸ paused"
case protocol.TypeSessionResumed:
return "▶ resumed"
case protocol.TypeSessionCompleted:
return "✓ workflow complete"
case protocol.TypeSessionFailed:
if msg.Reason != "" {
return "✗ workflow failed: " + msg.Reason
}
return "✗ workflow failed"
default:
return ""
}
}
// 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)
@@ -55,6 +84,11 @@ func (m *Model) applyServer(msg protocol.ServerMessage) {
if msg.IsEventBearing() && msg.SessionID != "" {
m.ensureSession(msg.SessionID)
}
// Inject dim narration lines for progress frames. Runs in addition to the
// per-type handling below (which feeds the events panel separately).
if line := narrationLine(msg); line != "" {
m.appendRouter(msg.SessionID, RouterEntry{Role: "narration", Content: line})
}
switch msg.Type {
case protocol.TypeSessionAnnounced:
m.onSessionAnnounced(msg)
@@ -102,19 +136,16 @@ func (m *Model) applyServer(msg protocol.ServerMessage) {
s.Tools = nil
s.addEvent(msg.OccurredAt, "StageStarted", msg.StageID)
}
m.appendRouter(msg.SessionID, RouterEntry{"stage", "⟐ stage " + msg.StageID + " started"})
case protocol.TypeStageCompleted:
if s := m.session(msg.SessionID); s != nil {
s.CurrentStage = ""
s.addEvent(msg.OccurredAt, "StageCompleted", msg.StageID)
}
m.appendRouter(msg.SessionID, RouterEntry{"stage", "⟐ stage " + msg.StageID + " completed"})
case protocol.TypeStageFailed:
if s := m.session(msg.SessionID); s != nil {
s.CurrentStage = ""
s.addEvent(msg.OccurredAt, "StageFailed", msg.StageID)
}
m.appendRouter(msg.SessionID, RouterEntry{"stage", "⟐ stage " + msg.StageID + " failed"})
case protocol.TypeInferenceStarted:
if s := m.session(msg.SessionID); s != nil {
s.Active = true