feat: add app status tracking to the backend (#17163)

This does ~95% of the backend work required to integrate the AI work.

Most left to integrate from the tasks branch is just frontend, which
will be a lot smaller I believe.

The real difference between this branch and that one is the abstraction
-- this now attaches statuses to apps, and returns the latest status
reported as part of a workspace.

This change enables us to have a similar UX to in the tasks branch, but
for agents other than Claude Code as well. Any app can report status
now.
This commit is contained in:
Kyle Carberry
2025-03-31 10:55:44 -04:00
committed by GitHub
parent 489641d0be
commit 8ea956fc11
35 changed files with 1668 additions and 69 deletions

View File

@ -259,6 +259,7 @@ type data struct {
workspaceAgentVolumeResourceMonitors []database.WorkspaceAgentVolumeResourceMonitor
workspaceAgentDevcontainers []database.WorkspaceAgentDevcontainer
workspaceApps []database.WorkspaceApp
workspaceAppStatuses []database.WorkspaceAppStatus
workspaceAppAuditSessions []database.WorkspaceAppAuditSession
workspaceAppStatsLastInsertID int64
workspaceAppStats []database.WorkspaceAppStat
@ -3697,6 +3698,34 @@ func (q *FakeQuerier) GetLatestCryptoKeyByFeature(_ context.Context, feature dat
return latestKey, nil
}
func (q *FakeQuerier) GetLatestWorkspaceAppStatusesByWorkspaceIDs(_ context.Context, ids []uuid.UUID) ([]database.WorkspaceAppStatus, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
// Map to track latest status per workspace ID
latestByWorkspace := make(map[uuid.UUID]database.WorkspaceAppStatus)
// Find latest status for each workspace ID
for _, appStatus := range q.workspaceAppStatuses {
if !slices.Contains(ids, appStatus.WorkspaceID) {
continue
}
current, exists := latestByWorkspace[appStatus.WorkspaceID]
if !exists || appStatus.CreatedAt.After(current.CreatedAt) {
latestByWorkspace[appStatus.WorkspaceID] = appStatus
}
}
// Convert map to slice
appStatuses := make([]database.WorkspaceAppStatus, 0, len(latestByWorkspace))
for _, status := range latestByWorkspace {
appStatuses = append(appStatuses, status)
}
return appStatuses, nil
}
func (q *FakeQuerier) GetLatestWorkspaceBuildByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) (database.WorkspaceBuild, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
@ -7488,6 +7517,21 @@ func (q *FakeQuerier) GetWorkspaceAppByAgentIDAndSlug(ctx context.Context, arg d
return q.getWorkspaceAppByAgentIDAndSlugNoLock(ctx, arg)
}
func (q *FakeQuerier) GetWorkspaceAppStatusesByAppIDs(_ context.Context, ids []uuid.UUID) ([]database.WorkspaceAppStatus, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
statuses := make([]database.WorkspaceAppStatus, 0)
for _, status := range q.workspaceAppStatuses {
for _, id := range ids {
if status.AppID == id {
statuses = append(statuses, status)
}
}
}
return statuses, nil
}
func (q *FakeQuerier) GetWorkspaceAppsByAgentID(_ context.Context, id uuid.UUID) ([]database.WorkspaceApp, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
@ -9584,6 +9628,31 @@ InsertWorkspaceAppStatsLoop:
return nil
}
func (q *FakeQuerier) InsertWorkspaceAppStatus(_ context.Context, arg database.InsertWorkspaceAppStatusParams) (database.WorkspaceAppStatus, error) {
err := validateDatabaseType(arg)
if err != nil {
return database.WorkspaceAppStatus{}, err
}
q.mutex.Lock()
defer q.mutex.Unlock()
status := database.WorkspaceAppStatus{
ID: arg.ID,
CreatedAt: arg.CreatedAt,
WorkspaceID: arg.WorkspaceID,
AgentID: arg.AgentID,
AppID: arg.AppID,
NeedsUserAttention: arg.NeedsUserAttention,
State: arg.State,
Message: arg.Message,
Uri: arg.Uri,
Icon: arg.Icon,
}
q.workspaceAppStatuses = append(q.workspaceAppStatuses, status)
return status, nil
}
func (q *FakeQuerier) InsertWorkspaceBuild(_ context.Context, arg database.InsertWorkspaceBuildParams) error {
if err := validateDatabaseType(arg); err != nil {
return err