From 2bef4b96a57d82ef94db7c9fb1b96c4368e863bb Mon Sep 17 00:00:00 2001 From: kami Date: Wed, 3 Jun 2026 19:09:28 +0400 Subject: [PATCH] =?UTF-8?q?feat(tui):=20Task=203.5=20=E2=80=94=20decode=20?= =?UTF-8?q?+=20render=20router.narration=20as=20bright=20narration=5Fllm?= =?UTF-8?q?=20lines?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add TypeRouterNarration constant, mark it event-bearing, decode golden test, applyServer case appending RouterEntry{Role:"narration_llm"} with metrics, and routerRows render branch showing bright โ—† prefix with FgStrong content + metrics suffix. --- apps/tui-go/internal/app/server.go | 6 ++++++ apps/tui-go/internal/app/view.go | 13 +++++++++++++ apps/tui-go/internal/protocol/golden_test.go | 11 +++++++++++ apps/tui-go/internal/protocol/protocol.go | 4 +++- 4 files changed, 33 insertions(+), 1 deletion(-) diff --git a/apps/tui-go/internal/app/server.go b/apps/tui-go/internal/app/server.go index 26632bab..540f602a 100644 --- a/apps/tui-go/internal/app/server.go +++ b/apps/tui-go/internal/app/server.go @@ -92,6 +92,12 @@ func (m *Model) applyServer(msg protocol.ServerMessage) { switch msg.Type { case protocol.TypeSessionAnnounced: m.onSessionAnnounced(msg) + case protocol.TypeRouterNarration: + entry := RouterEntry{Role: "narration_llm", Content: msg.Content} + if msg.LatencyMs != nil && msg.TotalTokens != nil { + entry.Metrics = &TurnMetrics{LatencyMs: *msg.LatencyMs, TotalTokens: *msg.TotalTokens} + } + m.appendRouter(msg.SessionID, entry) case protocol.TypeChatTurn: m.routerConnected = true role := "router" diff --git a/apps/tui-go/internal/app/view.go b/apps/tui-go/internal/app/view.go index 5b2c8daf..19bc19e9 100644 --- a/apps/tui-go/internal/app/view.go +++ b/apps/tui-go/internal/app/view.go @@ -358,6 +358,19 @@ 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 "narration_llm": + diamond := lipgloss.NewStyle().Foreground(t.P.Accent).Background(t.P.Bg).Bold(true).Render("โ—†") + lines := wrap(e.Content, w-2) + for i, ln := range lines { + if i == 0 { + rows = append(rows, diamond+" "+t.span(ln, t.P.FgStrong)) + } else { + rows = append(rows, " "+t.span(ln, t.P.FgStrong)) + } + } + if s := metricsSuffix(e.Metrics); s != "" { + rows = append(rows, t.span(s, t.P.Faint)) + } case "narration", "stage": rows = append(rows, t.span(e.Content, t.P.Dim)) } diff --git a/apps/tui-go/internal/protocol/golden_test.go b/apps/tui-go/internal/protocol/golden_test.go index df34c382..c6bb5568 100644 --- a/apps/tui-go/internal/protocol/golden_test.go +++ b/apps/tui-go/internal/protocol/golden_test.go @@ -99,6 +99,17 @@ func TestDecodeGoldenServerFrames(t *testing.T) { } }, }, + { + name: "router.narration carries content + metrics", + json: `{"type":"router.narration","sessionId":"s1","content":"moving on","stageId":"validate","latencyMs":2000,"totalTokens":120,"sequence":10,"sessionSequence":5}`, + wantType: TypeRouterNarration, + eventBear: true, + check: func(t *testing.T, m ServerMessage) { + if m.Content != "moving on" || m.LatencyMs == nil || *m.LatencyMs != 2000 || m.TotalTokens == nil || *m.TotalTokens != 120 { + t.Fatalf("router.narration: %+v", m) + } + }, + }, { name: "snapshot_complete (non-event control frame)", json: `{"type":"snapshot_complete"}`, diff --git a/apps/tui-go/internal/protocol/protocol.go b/apps/tui-go/internal/protocol/protocol.go index 03d899b7..c5639fd1 100644 --- a/apps/tui-go/internal/protocol/protocol.go +++ b/apps/tui-go/internal/protocol/protocol.go @@ -44,6 +44,7 @@ const ( TypeModelChanged = "model.changed" TypeModelList = "model.list" TypeResourceStatus = "resource.status" + TypeRouterNarration = "router.narration" ) // ServerMessage is a flat decode of every server->client variant. Field names @@ -186,7 +187,8 @@ func (m ServerMessage) IsEventBearing() bool { TypeInferenceStarted, TypeInferenceDone, TypeInferenceTimeout, TypeToolStarted, TypeToolCompleted, TypeToolFailed, TypeToolRejected, TypeToolAssessed, - TypeApprovalRequired, TypeApprovalResolved, TypeWorkspaceBound, TypeArtifactCreated: + TypeApprovalRequired, TypeApprovalResolved, TypeWorkspaceBound, TypeArtifactCreated, + TypeRouterNarration: return true default: return false