diff --git a/apps/tui-go/internal/app/server.go b/apps/tui-go/internal/app/server.go index 2e31c2f1..29567fc3 100644 --- a/apps/tui-go/internal/app/server.go +++ b/apps/tui-go/internal/app/server.go @@ -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 diff --git a/apps/tui-go/internal/app/stage_feed_test.go b/apps/tui-go/internal/app/stage_feed_test.go new file mode 100644 index 00000000..03a00574 --- /dev/null +++ b/apps/tui-go/internal/app/stage_feed_test.go @@ -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) + } +} diff --git a/apps/tui-go/internal/app/view.go b/apps/tui-go/internal/app/view.go index 2402009b..54fe7ac3 100644 --- a/apps/tui-go/internal/app/view.go +++ b/apps/tui-go/internal/app/view.go @@ -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