diff --git a/apps/backend/internal/agent/lifecycle/event_types.go b/apps/backend/internal/agent/lifecycle/event_types.go index 5ce4c00c26..7f6aa4f6d2 100644 --- a/apps/backend/internal/agent/lifecycle/event_types.go +++ b/apps/backend/internal/agent/lifecycle/event_types.go @@ -225,6 +225,10 @@ type GitStatusData struct { Files interface{} `json:"files,omitempty"` BranchAdditions int `json:"branch_additions,omitempty"` BranchDeletions int `json:"branch_deletions,omitempty"` + // RepositoryName identifies which repository this status belongs to in + // multi-repo task workspaces. Empty for single-repo. Carried through to + // the frontend so the Changes panel can render per-repo group headers. + RepositoryName string `json:"repository_name,omitempty"` } type GitCommitData struct { diff --git a/apps/backend/internal/agent/lifecycle/events.go b/apps/backend/internal/agent/lifecycle/events.go index 60b54997d6..554719394f 100644 --- a/apps/backend/internal/agent/lifecycle/events.go +++ b/apps/backend/internal/agent/lifecycle/events.go @@ -247,6 +247,7 @@ func (p *EventPublisher) PublishGitStatus(execution *AgentExecution, update *age Files: update.Files, BranchAdditions: update.BranchAdditions, BranchDeletions: update.BranchDeletions, + RepositoryName: update.RepositoryName, }, }) } diff --git a/apps/backend/internal/agent/lifecycle/events_git_status_test.go b/apps/backend/internal/agent/lifecycle/events_git_status_test.go new file mode 100644 index 0000000000..7a7bef440e --- /dev/null +++ b/apps/backend/internal/agent/lifecycle/events_git_status_test.go @@ -0,0 +1,62 @@ +package lifecycle + +import ( + "context" + "testing" + "time" + + agentctl "github.com/kandev/kandev/internal/agentctl/client" + "github.com/kandev/kandev/internal/common/logger" + "github.com/kandev/kandev/internal/events" + "github.com/kandev/kandev/internal/events/bus" +) + +// Verifies the fix for "Changes panel shows no header / no existing changes". +// agentctl tags every per-repo GitStatusUpdate with RepositoryName; that field +// must survive the lifecycle PublishGitStatus translation so the orchestrator +// (and thus the frontend) sees it. +func TestPublishGitStatus_PropagatesRepositoryName(t *testing.T) { + log, _ := logger.NewLogger(logger.LoggingConfig{Level: "error", Format: "json"}) + eventBus := bus.NewMemoryEventBus(log) + pub := NewEventPublisher(eventBus, log) + + received := make(chan *bus.Event, 1) + subj := events.BuildGitEventSubject("sess-multi") + sub, err := eventBus.Subscribe(subj, func(_ context.Context, ev *bus.Event) error { + received <- ev + return nil + }) + if err != nil { + t.Fatalf("subscribe: %v", err) + } + defer func() { _ = sub.Unsubscribe() }() + + exec := &AgentExecution{ + ID: "exec-1", + TaskID: "task-1", + SessionID: "sess-multi", + } + pub.PublishGitStatus(exec, &agentctl.GitStatusUpdate{ + Timestamp: time.Now(), + RepositoryName: "frontend", + Branch: "feature/x", + Modified: []string{"src/app.tsx"}, + Files: map[string]agentctl.FileInfo{"src/app.tsx": {Path: "src/app.tsx"}}, + }) + + select { + case ev := <-received: + payload, ok := ev.Data.(*GitEventPayload) + if !ok || payload == nil { + t.Fatalf("expected *GitEventPayload, got %T", ev.Data) + } + if payload.Status == nil { + t.Fatal("expected non-nil Status on payload") + } + if payload.Status.RepositoryName != "frontend" { + t.Errorf("repository_name was dropped: got %q", payload.Status.RepositoryName) + } + case <-time.After(2 * time.Second): + t.Fatal("timed out waiting for git status event") + } +}