Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions backend/internal/httpd/apispec/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1261,6 +1261,8 @@ components:
properties:
activity:
$ref: '#/components/schemas/DomainActivity'
branch:
type: string
createdAt:
format: date-time
type: string
Expand Down
10 changes: 6 additions & 4 deletions backend/internal/httpd/controllers/dto.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,12 +112,14 @@ type CleanupSessionsQuery struct {
}

// SessionView is the session wire shape: the domain read model plus the
// session's attributed pull requests in the curated SessionPRFacts shape. One
// session can own many PRs (e.g. a stack), so prs is a list. The embedded
// domain.Session.PRs field is json:"-"; this curated prs is what serializes.
// display-safe branch name and the session's attributed pull requests in the
// curated SessionPRFacts shape. One session can own many PRs (e.g. a stack), so
// 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 {
domain.Session
PRs []SessionPRFacts `json:"prs"`
Branch string `json:"branch,omitempty"`
PRs []SessionPRFacts `json:"prs"`
}

// ListSessionsResponse is the body of GET /api/v1/sessions.
Expand Down
2 changes: 1 addition & 1 deletion backend/internal/httpd/controllers/sessions.go
Original file line number Diff line number Diff line change
Expand Up @@ -427,7 +427,7 @@ func writeSessionPRError(w http.ResponseWriter, r *http.Request, err error) {
}

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 {
Expand Down
20 changes: 20 additions & 0 deletions backend/internal/httpd/controllers/sessions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,9 @@ func TestSessionsRoutes_DefaultToStubsWithoutService(t *testing.T) {

func TestSessionsAPI_ListSpawnGetAndActions(t *testing.T) {
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)

body, status, _ := doRequest(t, srv, "GET", "/api/v1/sessions?project=ao", "")
Expand All @@ -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" {
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"}`)
if status != http.StatusCreated {
Expand Down Expand Up @@ -388,6 +407,7 @@ type sessionBody struct {
Kind string `json:"kind"`
Harness string `json:"harness"`
DisplayName string `json:"displayName"`
Branch string `json:"branch"`
Status string `json:"status"`
TerminalHandleID string `json:"terminalHandleId"`
}
Expand Down
1 change: 1 addition & 0 deletions frontend/src/api/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,7 @@ export interface components {
};
ControllersSessionView: {
activity: components["schemas"]["DomainActivity"];
branch?: string;
/** Format: date-time */
createdAt: string;
displayName?: string;
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/renderer/hooks/useWorkspaceQuery.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ describe("useWorkspaceQuery", () => {
terminalHandleId: "term-1",
displayName: "fix-bug",
harness: "claude-code",
branch: "qa/modal-worker",
status: "mergeable",
isTerminated: false,
updatedAt: "2026-06-10T16:15:04Z",
Expand Down Expand Up @@ -81,12 +82,14 @@ describe("useWorkspaceQuery", () => {
terminalHandleId: "term-1",
title: "fix-bug",
provider: "claude-code",
branch: "qa/modal-worker",
status: "mergeable",
});
expect(workspace.sessions[1]).toMatchObject({
id: "sess-2",
title: "sess-2",
provider: "codex",
branch: "session/sess-2",
status: "working",
});
});
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/renderer/hooks/useWorkspaceQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ async function fetchWorkspaces(): Promise<WorkspaceSummary[]> {
title: session.displayName ?? session.issueId ?? session.id,
provider: toAgentProvider(session.harness),
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),
createdAt: session.createdAt,
updatedAt: session.updatedAt,
Expand Down
Loading