Skip to content

feat: support devcontainer agents in ui and unify backend #18332

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 29 commits into from
Jun 17, 2025

Conversation

mafredri
Copy link
Member

@mafredri mafredri commented Jun 11, 2025

This PR adds consolidates two container endpoints on the backend and improves the frontend devcontainer support by showing names and displaying apps as appropriate.

With this change, the frontend now has knowledge of the subagent and we can also display things like port forwards.

The frontend was updated to show dev container labels on the border as well as subagent connection status. The recreation flow was also adjusted a bit to show placeholder app icons when relevant.

Support for apps was also added, although these are still WIP on the backend. And the port forwarding utility was added in since the sub agents now provide the necessary info.

Fixes coder/internal#666

image

@mafredri mafredri force-pushed the mafredri/feat-agent-devcontainer-injection-6 branch from c9f4ca4 to 18e1593 Compare June 13, 2025 17:35
@mafredri mafredri changed the title feat: expand devcontainer subagent support in ui and improve backend feat: support devcontainer agents in ui and unify backend Jun 13, 2025
@mafredri mafredri force-pushed the mafredri/feat-agent-devcontainer-injection-6 branch 2 times, most recently from fc1a236 to 414b6f4 Compare June 13, 2025 18:44
@mafredri mafredri force-pushed the mafredri/feat-agent-devcontainer-injection-6 branch from 414b6f4 to ecfe483 Compare June 13, 2025 19:06
@mafredri mafredri marked this pull request as ready for review June 13, 2025 20:00
@mafredri mafredri force-pushed the mafredri/feat-agent-devcontainer-injection-6 branch from 8f12d26 to 79e1844 Compare June 13, 2025 20:08
Copy link
Contributor

@DanielleMaywood DanielleMaywood left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

haven't managed to read all the Go yet but will read it by Monday

@mafredri mafredri force-pushed the mafredri/feat-agent-devcontainer-injection-6 branch from abc9999 to d4f208b Compare June 16, 2025 08:48
@mafredri mafredri requested a review from DanielleMaywood June 16, 2025 08:49
Comment on lines +1255 to +1263
for _, id := range subAgentIDs {
err := api.subAgentClient.Delete(deleteCtx, id)
if err != nil {
api.logger.Error(api.ctx, "delete subagent record during shutdown failed",
slog.Error(err),
slog.F("agent_id", id),
)
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: should we do this in parallel instead?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the argument for doing it in parallel? My gut feeling is I want to avoid spamming coderd. If this is something we want, we should allow deleting multiple agents in one request instead. // cc @DanielleMaywood

Copy link
Member

@johnstcn johnstcn Jun 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Main argument is to reduce time to shutdown. Currently it is worst case maximum len(subAgentIDs) * deleteCtx timeout. Not a blocker though.

Copy link
Member Author

@mafredri mafredri Jun 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha 👍🏻. It's btw just 1 * deleteCtx timeout, we give up on deleting the rest if it takes too long.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha. That may be another argument for doing multiple at once - I'd pefer it to be all or nothing.

}

// From codersdk/workspaceagents.go
export interface WorkspaceAgentDevcontainerAgent {
Copy link
Collaborator

@BrunoQuaresma BrunoQuaresma Jun 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This name sounds confusing to me 🤔. From my understanding, we have two agents, WorkspaceAgent, and Devcontainer Agent, is that correct? If yes, why not call DevcontainerAgent to simplify the naming?

I think prefixing the resource names, when not necessary, a bad practice, but I see we do that in a lot of places in coderd, so it is not a blocker, but I would like to bring attention to it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do see your point. Technically I suppose "Workspace" in "WorkspaceAgent" is also somehwhat useless. The main idea with this is to follow existing naming conventions but also to symbolize the dependency tree. A devcontainer agent (a workspace agents subagent) cannot exist without a (parent) workspace agent. Hence it is nested in the naming. Another existing type like WorkspaceAgentContainer also already exists so I don't think it's worth diverging right now without a plan for how to tackle existing names.

const handleRebuildDevcontainer = async () => {
setIsRebuilding(true);
setSubAgentRemoved(true);
let rebuildSucceeded = false;
try {
const response = await fetch(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the FE, we handle requests in two ways: queries or mutations. In this case, you should wrap this request into a mutation. Here is the docs.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When using a mutation, you can invalidate other queries or update their data to reflect the most recent status, instead of doing it manually, so you wouldn't need the isRebuilding or subAgentRemoved statuses. Here is how you can do it. If you need more examples, you can search in the codebase for "invalidateQueries".

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the tip! This was superb, actually solved a couple of issues I wanted to address but didn't know how!

f150bad (#18332)

Copy link
Collaborator

@BrunoQuaresma BrunoQuaresma left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left a few comments related to the FE codebase.

@johnstcn
Copy link
Member

I don't have any blocking comments, but deferring final approval to @DanielleMaywood

@mafredri mafredri force-pushed the mafredri/feat-agent-devcontainer-injection-6 branch from 4032530 to f150bad Compare June 16, 2025 15:59
Comment on lines +94 to +131
onMutate: async () => {
await queryClient.cancelQueries({
queryKey: ["agents", parentAgent.id, "containers"],
});

// Snapshot the previous data for rollback in case of error.
const previousData = queryClient.getQueryData([
"agents",
parentAgent.id,
"containers",
]);

// Optimistically update the devcontainer status to
// "starting" and zero the agent and container to mimic what
// the API does.
queryClient.setQueryData(
["agents", parentAgent.id, "containers"],
(oldData?: WorkspaceAgentListContainersResponse) => {
if (!oldData?.devcontainers) return oldData;
return {
...oldData,
devcontainers: oldData.devcontainers.map((dc) => {
if (dc.id === devcontainer.id) {
return {
...dc,
agent: null,
container: null,
status: "starting",
};
}
return dc;
}),
};
},
);

return { previousData };
},
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@BrunoQuaresma should I keep or remove this? It essentially just leads to a slightly faster UI response.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is up to you! If you think this faster UI experience, worth the amount of code/complexity, go for it.

Copy link
Member Author

@mafredri mafredri Jun 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I think it's a bit better and without it's dependent on ping between user <-> coderd <-> agent, so let's keep for now. Pretty easy to eliminate if it becomes a burden. 👍🏻

Copy link
Member

@johnstcn johnstcn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the backend changes, nothing blocking 👍 Nice work!

Comment on lines -855 to -887
// handleDevcontainersList handles the HTTP request to list known devcontainers.
func (api *API) handleDevcontainersList(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()

api.mu.RLock()
err := api.containersErr
devcontainers := make([]codersdk.WorkspaceAgentDevcontainer, 0, len(api.knownDevcontainers))
for _, dc := range api.knownDevcontainers {
devcontainers = append(devcontainers, dc)
}
api.mu.RUnlock()
if err != nil {
httpapi.Write(ctx, w, http.StatusInternalServerError, codersdk.Response{
Message: "Could not list containers",
Detail: err.Error(),
})
return
}

slices.SortFunc(devcontainers, func(a, b codersdk.WorkspaceAgentDevcontainer) int {
if cmp := strings.Compare(a.WorkspaceFolder, b.WorkspaceFolder); cmp != 0 {
return cmp
}
return strings.Compare(a.ConfigPath, b.ConfigPath)
})

response := codersdk.WorkspaceAgentDevcontainersResponse{
Devcontainers: devcontainers,
}

httpapi.Write(ctx, w, http.StatusOK, response)
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is replaced by api.handleList now?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes 👍🏻

Comment on lines +1103 to +1106
err = api.subAgentClient.Delete(ctx, proc.agent.ID)
if err != nil {
return xerrors.Errorf("delete existing subagent failed: %w", err)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as discussed: this maybe becomes a soft-delete or a conditional re-use if the apps match

mafredri and others added 3 commits June 17, 2025 09:58
@mafredri mafredri merged commit 97474bb into main Jun 17, 2025
39 checks passed
@mafredri mafredri deleted the mafredri/feat-agent-devcontainer-injection-6 branch June 17, 2025 13:06
@github-actions github-actions bot locked and limited conversation to collaborators Jun 17, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Consolidate GET /containers and GET /containers/devcontainers and render devcontainers (instead of containers) in the UI
4 participants