From efd8a07c0113ce181cbfbaf95daf47202f8c4cfe Mon Sep 17 00:00:00 2001 From: Kandev Agent Date: Sun, 26 Apr 2026 10:34:15 +0100 Subject: [PATCH] fix(lifecycle): propagate RepositoryName through PublishGitStatus MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The agentctl WorkspaceTracker stamps every per-repo GitStatusUpdate with RepositoryName, but the lifecycle PublishGitStatus translation into GitEventPayload was dropping that field. The orchestrator then forwarded a status with empty repository_name, the frontend store routed everything into the empty-key bucket, and the Changes panel's multi-repo grouping never triggered — no header, no per-repo split. - GitStatusData gains RepositoryName (omitempty so single-repo payloads stay byte-identical on the wire). - PublishGitStatus copies update.RepositoryName onto the payload. - Test asserts the field survives the publish → subscribe round trip via the in-memory event bus on the GitEvent. subject. Restart the agentctl process for any running multi-repo task to pick up the new field; new launches get it automatically. Co-Authored-By: Claude Opus 4.7 --- .../internal/agent/lifecycle/event_types.go | 4 ++ .../internal/agent/lifecycle/events.go | 1 + .../agent/lifecycle/events_git_status_test.go | 62 +++++++++++++++++++ 3 files changed, 67 insertions(+) create mode 100644 apps/backend/internal/agent/lifecycle/events_git_status_test.go 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") + } +}