feat(tui): render workflow progress as dim router-feed narration lines
This commit is contained in:
@@ -0,0 +1,49 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/correx/tui-go/internal/protocol"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTemplateNarrationAppendsDimLines(t *testing.T) {
|
||||||
|
m := NewModel(nil)
|
||||||
|
m.width, m.height = 120, 40
|
||||||
|
m.theme = NewTheme(SoftBlue)
|
||||||
|
sid := "s1"
|
||||||
|
m.selectedID = sid
|
||||||
|
m.sessionEntered = true
|
||||||
|
sess := Session{ID: sid, Status: "ACTIVE"}
|
||||||
|
m.sessions = []Session{sess}
|
||||||
|
|
||||||
|
frames := []protocol.ServerMessage{
|
||||||
|
{Type: protocol.TypeStageStarted, SessionID: sid, StageID: "validate"},
|
||||||
|
{Type: protocol.TypeStageCompleted, SessionID: sid, StageID: "validate"},
|
||||||
|
{Type: protocol.TypeStageFailed, SessionID: sid, StageID: "generate", Reason: "schema mismatch"},
|
||||||
|
{Type: protocol.TypeSessionCompleted, SessionID: sid},
|
||||||
|
}
|
||||||
|
for _, f := range frames {
|
||||||
|
m.applyServer(f)
|
||||||
|
}
|
||||||
|
got := m.routerMessages[sid]
|
||||||
|
if len(got) != 4 {
|
||||||
|
t.Fatalf("want 4 narration entries, got %d: %+v", len(got), got)
|
||||||
|
}
|
||||||
|
for _, e := range got {
|
||||||
|
if e.Role != "narration" {
|
||||||
|
t.Fatalf("want role=narration, got %q", e.Role)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !strings.Contains(got[0].Content, "validate") || !strings.HasPrefix(got[0].Content, "→") {
|
||||||
|
t.Fatalf("stage.started line: %q", got[0].Content)
|
||||||
|
}
|
||||||
|
if !strings.Contains(got[2].Content, "schema mismatch") {
|
||||||
|
t.Fatalf("stage.failed line should carry reason: %q", got[2].Content)
|
||||||
|
}
|
||||||
|
rows := m.routerRows(80, 20)
|
||||||
|
joined := strings.Join(rows, "\n")
|
||||||
|
if strings.Contains(joined, "awaiting router response") {
|
||||||
|
t.Fatalf("placeholder must be gone once feed has entries")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.
|
// applyServer mutates the model for a single (non-buffered) server message.
|
||||||
func (m *Model) applyServer(msg protocol.ServerMessage) {
|
func (m *Model) applyServer(msg protocol.ServerMessage) {
|
||||||
debugLog("SRV type=%s session=%s reason=%s", msg.Type, msg.SessionID, msg.Reason)
|
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 != "" {
|
if msg.IsEventBearing() && msg.SessionID != "" {
|
||||||
m.ensureSession(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 {
|
switch msg.Type {
|
||||||
case protocol.TypeSessionAnnounced:
|
case protocol.TypeSessionAnnounced:
|
||||||
m.onSessionAnnounced(msg)
|
m.onSessionAnnounced(msg)
|
||||||
@@ -102,19 +136,16 @@ func (m *Model) applyServer(msg protocol.ServerMessage) {
|
|||||||
s.Tools = nil
|
s.Tools = nil
|
||||||
s.addEvent(msg.OccurredAt, "StageStarted", msg.StageID)
|
s.addEvent(msg.OccurredAt, "StageStarted", msg.StageID)
|
||||||
}
|
}
|
||||||
m.appendRouter(msg.SessionID, RouterEntry{"stage", "⟐ stage " + msg.StageID + " started"})
|
|
||||||
case protocol.TypeStageCompleted:
|
case protocol.TypeStageCompleted:
|
||||||
if s := m.session(msg.SessionID); s != nil {
|
if s := m.session(msg.SessionID); s != nil {
|
||||||
s.CurrentStage = ""
|
s.CurrentStage = ""
|
||||||
s.addEvent(msg.OccurredAt, "StageCompleted", msg.StageID)
|
s.addEvent(msg.OccurredAt, "StageCompleted", msg.StageID)
|
||||||
}
|
}
|
||||||
m.appendRouter(msg.SessionID, RouterEntry{"stage", "⟐ stage " + msg.StageID + " completed"})
|
|
||||||
case protocol.TypeStageFailed:
|
case protocol.TypeStageFailed:
|
||||||
if s := m.session(msg.SessionID); s != nil {
|
if s := m.session(msg.SessionID); s != nil {
|
||||||
s.CurrentStage = ""
|
s.CurrentStage = ""
|
||||||
s.addEvent(msg.OccurredAt, "StageFailed", msg.StageID)
|
s.addEvent(msg.OccurredAt, "StageFailed", msg.StageID)
|
||||||
}
|
}
|
||||||
m.appendRouter(msg.SessionID, RouterEntry{"stage", "⟐ stage " + msg.StageID + " failed"})
|
|
||||||
case protocol.TypeInferenceStarted:
|
case protocol.TypeInferenceStarted:
|
||||||
if s := m.session(msg.SessionID); s != nil {
|
if s := m.session(msg.SessionID); s != nil {
|
||||||
s.Active = true
|
s.Active = true
|
||||||
|
|||||||
@@ -354,8 +354,8 @@ func (m Model) routerRows(w, h int) []string {
|
|||||||
}
|
}
|
||||||
case "tool":
|
case "tool":
|
||||||
rows = append(rows, t.span("· tool output ("+itoaLen(e.Content)+" chars) — ^x to view", t.P.Dim))
|
rows = append(rows, t.span("· tool output ("+itoaLen(e.Content)+" chars) — ^x to view", t.P.Dim))
|
||||||
case "stage":
|
case "narration", "stage":
|
||||||
rows = append(rows, t.span(e.Content, t.P.Faint))
|
rows = append(rows, t.span(e.Content, t.P.Dim))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// keep last h rows
|
// keep last h rows
|
||||||
|
|||||||
Reference in New Issue
Block a user