feat(tui): render stage lifecycle frames as dim lines in the router feed
Stage started/completed/failed events are now injected into the router transcript (role "stage") and rendered in Faint colour, giving the operator a chronological in-feed view of workflow progress alongside user/router/tool turns.
This commit is contained in:
@@ -102,16 +102,19 @@ 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
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/correx/tui-go/internal/protocol"
|
||||
)
|
||||
|
||||
// stageFeedModel builds a minimal model with the given router messages for the
|
||||
// selected session and calls routerRows so we can inspect the rendered lines.
|
||||
func stageFeedModel(entries []RouterEntry) Model {
|
||||
m := NewModel(nil)
|
||||
m.width, m.height = 120, 40
|
||||
m.theme = NewTheme(SoftBlue)
|
||||
sess := Session{ID: "s1", Status: "ACTIVE"}
|
||||
m.sessions = []Session{sess}
|
||||
m.selectedID = "s1"
|
||||
m.sessionEntered = true
|
||||
m.routerMessages["s1"] = entries
|
||||
return m
|
||||
}
|
||||
|
||||
// TestStageFrameAppearsInRouterFeed verifies that TypeStageStarted is injected
|
||||
// into the router transcript as a dim "stage" entry and renders in routerRows.
|
||||
func TestStageFrameAppearsInRouterFeed(t *testing.T) {
|
||||
m := stageFeedModel(nil)
|
||||
|
||||
// Simulate server delivering a StageStarted message.
|
||||
m.applyServer(protocol.ServerMessage{
|
||||
Type: protocol.TypeStageStarted,
|
||||
SessionID: "s1",
|
||||
StageID: "plan",
|
||||
})
|
||||
|
||||
rows := m.routerRows(80, 20)
|
||||
joined := strings.Join(rows, "\n")
|
||||
if !strings.Contains(joined, "plan") {
|
||||
t.Errorf("routerRows should contain stage name 'plan', got:\n%s", joined)
|
||||
}
|
||||
}
|
||||
|
||||
// TestStageCompletedAppearsInRouterFeed checks StageCompleted is also injected.
|
||||
func TestStageCompletedAppearsInRouterFeed(t *testing.T) {
|
||||
m := stageFeedModel(nil)
|
||||
|
||||
m.applyServer(protocol.ServerMessage{
|
||||
Type: protocol.TypeStageCompleted,
|
||||
SessionID: "s1",
|
||||
StageID: "write_script",
|
||||
})
|
||||
|
||||
rows := m.routerRows(80, 20)
|
||||
joined := strings.Join(rows, "\n")
|
||||
if !strings.Contains(joined, "write_script") {
|
||||
t.Errorf("routerRows should contain stage name 'write_script', got:\n%s", joined)
|
||||
}
|
||||
}
|
||||
|
||||
// TestStageFailedAppearsInRouterFeed checks StageFailed is also injected.
|
||||
func TestStageFailedAppearsInRouterFeed(t *testing.T) {
|
||||
m := stageFeedModel(nil)
|
||||
|
||||
m.applyServer(protocol.ServerMessage{
|
||||
Type: protocol.TypeStageFailed,
|
||||
SessionID: "s1",
|
||||
StageID: "verify",
|
||||
})
|
||||
|
||||
rows := m.routerRows(80, 20)
|
||||
joined := strings.Join(rows, "\n")
|
||||
if !strings.Contains(joined, "verify") {
|
||||
t.Errorf("routerRows should contain stage name 'verify', got:\n%s", joined)
|
||||
}
|
||||
}
|
||||
|
||||
// TestStageFrameInterspersedWithRouterTurns verifies stage lines appear between
|
||||
// real router turns in the correct order.
|
||||
func TestStageFrameInterspersedWithRouterTurns(t *testing.T) {
|
||||
m := stageFeedModel([]RouterEntry{
|
||||
{"user", "start the task"},
|
||||
})
|
||||
|
||||
m.applyServer(protocol.ServerMessage{
|
||||
Type: protocol.TypeStageStarted,
|
||||
SessionID: "s1",
|
||||
StageID: "plan",
|
||||
})
|
||||
m.routerMessages["s1"] = append(m.routerMessages["s1"], RouterEntry{"router", "I will plan this out."})
|
||||
|
||||
rows := m.routerRows(80, 30)
|
||||
joined := strings.Join(rows, "\n")
|
||||
|
||||
if !strings.Contains(joined, "start the task") {
|
||||
t.Errorf("user turn missing from router rows:\n%s", joined)
|
||||
}
|
||||
if !strings.Contains(joined, "plan") {
|
||||
t.Errorf("stage frame missing from router rows:\n%s", joined)
|
||||
}
|
||||
if !strings.Contains(joined, "I will plan") {
|
||||
t.Errorf("router turn missing from router rows:\n%s", joined)
|
||||
}
|
||||
}
|
||||
@@ -354,6 +354,8 @@ func (m Model) routerRows(w, h int) []string {
|
||||
}
|
||||
case "tool":
|
||||
rows = append(rows, t.span("· tool output ("+itoaLen(e.Content)+" chars) — ^x to view", t.P.Dim))
|
||||
case "stage":
|
||||
rows = append(rows, t.span(e.Content, t.P.Faint))
|
||||
}
|
||||
}
|
||||
// keep last h rows
|
||||
|
||||
Reference in New Issue
Block a user