fix: surface session branch in frontend (#358)

Co-authored-by: Swyam Sharma <cruzer@Swyams-MacBook-Pro.local>
This commit is contained in:
swyam sharma 2026-06-21 14:57:37 +05:30 committed by GitHub
parent 78cdc6d5ca
commit 34a9fdb736
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 34 additions and 6 deletions

View File

@ -1261,6 +1261,8 @@ components:
properties: properties:
activity: activity:
$ref: '#/components/schemas/DomainActivity' $ref: '#/components/schemas/DomainActivity'
branch:
type: string
createdAt: createdAt:
format: date-time format: date-time
type: string type: string

View File

@ -112,12 +112,14 @@ type CleanupSessionsQuery struct {
} }
// SessionView is the session wire shape: the domain read model plus the // SessionView is the session wire shape: the domain read model plus the
// session's attributed pull requests in the curated SessionPRFacts shape. One // display-safe branch name and the session's attributed pull requests in the
// session can own many PRs (e.g. a stack), so prs is a list. The embedded // curated SessionPRFacts shape. One session can own many PRs (e.g. a stack), so
// domain.Session.PRs field is json:"-"; this curated prs is what serializes. // prs is a list. The embedded domain.Session.Metadata and domain.Session.PRs
// fields are json:"-"; these curated fields are what serialize.
type SessionView struct { type SessionView struct {
domain.Session domain.Session
PRs []SessionPRFacts `json:"prs"` Branch string `json:"branch,omitempty"`
PRs []SessionPRFacts `json:"prs"`
} }
// ListSessionsResponse is the body of GET /api/v1/sessions. // ListSessionsResponse is the body of GET /api/v1/sessions.

View File

@ -427,7 +427,7 @@ func writeSessionPRError(w http.ResponseWriter, r *http.Request, err error) {
} }
func sessionView(s domain.Session) SessionView { func sessionView(s domain.Session) SessionView {
return SessionView{Session: s, PRs: sessionPRFacts(s.PRs)} return SessionView{Session: s, Branch: s.Metadata.Branch, PRs: sessionPRFacts(s.PRs)}
} }
func sessionViews(sessions []domain.Session) []SessionView { func sessionViews(sessions []domain.Session) []SessionView {

View File

@ -170,6 +170,9 @@ func TestSessionsRoutes_DefaultToStubsWithoutService(t *testing.T) {
func TestSessionsAPI_ListSpawnGetAndActions(t *testing.T) { func TestSessionsAPI_ListSpawnGetAndActions(t *testing.T) {
svc := newFakeSessionService() svc := newFakeSessionService()
s := svc.sessions["ao-1"]
s.Metadata = domain.SessionMetadata{Branch: "qa/modal-worker", WorkspacePath: "/tmp/private-worktree", RuntimeHandleID: "runtime-1", Prompt: "private prompt"}
svc.sessions["ao-1"] = s
srv := newSessionTestServer(t, svc) srv := newSessionTestServer(t, svc)
body, status, _ := doRequest(t, srv, "GET", "/api/v1/sessions?project=ao", "") body, status, _ := doRequest(t, srv, "GET", "/api/v1/sessions?project=ao", "")
@ -183,6 +186,22 @@ func TestSessionsAPI_ListSpawnGetAndActions(t *testing.T) {
if len(list.Sessions) != 1 || list.Sessions[0].ID != "ao-1" || list.Sessions[0].Status != string(domain.StatusIdle) || list.Sessions[0].TerminalHandleID != "ao-1/terminal_0" { if len(list.Sessions) != 1 || list.Sessions[0].ID != "ao-1" || list.Sessions[0].Status != string(domain.StatusIdle) || list.Sessions[0].TerminalHandleID != "ao-1/terminal_0" {
t.Fatalf("list = %#v", list) t.Fatalf("list = %#v", list)
} }
if list.Sessions[0].Branch != "qa/modal-worker" {
t.Fatalf("branch = %q, want qa/modal-worker", list.Sessions[0].Branch)
}
var rawList struct {
Sessions []map[string]any `json:"sessions"`
}
mustJSON(t, body, &rawList)
if _, ok := rawList.Sessions[0]["metadata"]; ok {
t.Fatalf("list leaked metadata: %s", body)
}
if _, ok := rawList.Sessions[0]["workspacePath"]; ok {
t.Fatalf("list leaked workspacePath: %s", body)
}
if _, ok := rawList.Sessions[0]["prompt"]; ok {
t.Fatalf("list leaked prompt: %s", body)
}
body, status, _ = doRequest(t, srv, "POST", "/api/v1/sessions", `{"projectId":"ao","issueId":"ISS-1","kind":"worker","harness":"codex","prompt":"fix"}`) body, status, _ = doRequest(t, srv, "POST", "/api/v1/sessions", `{"projectId":"ao","issueId":"ISS-1","kind":"worker","harness":"codex","prompt":"fix"}`)
if status != http.StatusCreated { if status != http.StatusCreated {
@ -388,6 +407,7 @@ type sessionBody struct {
Kind string `json:"kind"` Kind string `json:"kind"`
Harness string `json:"harness"` Harness string `json:"harness"`
DisplayName string `json:"displayName"` DisplayName string `json:"displayName"`
Branch string `json:"branch"`
Status string `json:"status"` Status string `json:"status"`
TerminalHandleID string `json:"terminalHandleId"` TerminalHandleID string `json:"terminalHandleId"`
} }

View File

@ -446,6 +446,7 @@ export interface components {
}; };
ControllersSessionView: { ControllersSessionView: {
activity: components["schemas"]["DomainActivity"]; activity: components["schemas"]["DomainActivity"];
branch?: string;
/** Format: date-time */ /** Format: date-time */
createdAt: string; createdAt: string;
displayName?: string; displayName?: string;

View File

@ -63,6 +63,7 @@ describe("useWorkspaceQuery", () => {
terminalHandleId: "term-1", terminalHandleId: "term-1",
displayName: "fix-bug", displayName: "fix-bug",
harness: "claude-code", harness: "claude-code",
branch: "qa/modal-worker",
status: "mergeable", status: "mergeable",
isTerminated: false, isTerminated: false,
updatedAt: "2026-06-10T16:15:04Z", updatedAt: "2026-06-10T16:15:04Z",
@ -96,12 +97,14 @@ describe("useWorkspaceQuery", () => {
terminalHandleId: "term-1", terminalHandleId: "term-1",
title: "fix-bug", title: "fix-bug",
provider: "claude-code", provider: "claude-code",
branch: "qa/modal-worker",
status: "mergeable", status: "mergeable",
}); });
expect(workspace.sessions[1]).toMatchObject({ expect(workspace.sessions[1]).toMatchObject({
id: "sess-2", id: "sess-2",
title: "sess-2", title: "sess-2",
provider: "codex", provider: "codex",
branch: "session/sess-2",
status: "working", status: "working",
}); });
}); });

View File

@ -53,7 +53,7 @@ async function fetchWorkspaces(): Promise<WorkspaceSummary[]> {
title: session.displayName ?? session.issueId ?? session.id, title: session.displayName ?? session.issueId ?? session.id,
provider: toAgentProvider(session.harness), provider: toAgentProvider(session.harness),
kind: session.kind === "orchestrator" ? "orchestrator" : session.kind === "worker" ? "worker" : undefined, kind: session.kind === "orchestrator" ? "orchestrator" : session.kind === "worker" ? "worker" : undefined,
branch: `session/${session.id}`, branch: session.branch ?? `session/${session.id}`,
status: toSessionStatus(session.status, session.isTerminated), status: toSessionStatus(session.status, session.isTerminated),
createdAt: session.createdAt, createdAt: session.createdAt,
updatedAt: session.updatedAt, updatedAt: session.updatedAt,