From af70dd92392cb6d40b384a8cb6c1a64b403a4316 Mon Sep 17 00:00:00 2001 From: Louis Knight-Webb Date: Fri, 9 Jan 2026 09:09:11 +0000 Subject: [PATCH] The implementation is complete. Here's a summary of the changes made: (#1871) ## Summary I've implemented the GitHub PR status display on `WorkspaceSummary.tsx`. The changes include: ### Backend Changes 1. **`crates/db/src/models/merge.rs`** - Added a new method `get_latest_pr_status_for_workspaces()` that efficiently queries the latest PR status for all workspaces grouped by archived status. 2. **`crates/server/src/routes/task_attempts/workspace_summary.rs`** - Added `pr_status: Option` field to the `WorkspaceSummary` struct and included the PR status query in the summary endpoint. ### Frontend Changes 3. **`frontend/src/components/ui-new/hooks/useWorkspaces.ts`** - Added `prStatus` to the `SidebarWorkspace` interface and mapped it in `toSidebarWorkspace()`. 4. **`frontend/src/components/ui-new/primitives/WorkspaceSummary.tsx`** - Added: - Import for `GitPullRequestIcon` - `prStatus` prop to the component - Display logic showing: - **PR open**: `GitPullRequestIcon` with `text-brand` (orange) color - **PR merged**: `GitPullRequestIcon` with `text-success` (green) color - **No PR/closed/unknown**: No icon displayed 5. **`frontend/src/components/ui-new/views/WorkspacesSidebar.tsx`** - Passed the `prStatus` prop to both active and archived workspace summaries. ### Generated Types 6. **`shared/types.ts`** - Auto-generated to include the new `pr_status` field in `WorkspaceSummary`. --- crates/db/src/models/merge.rs | 41 +++++++++++++++++++ .../routes/task_attempts/workspace_summary.rs | 11 ++++- .../components/ui-new/hooks/useWorkspaces.ts | 2 + .../ui-new/primitives/WorkspaceSummary.tsx | 17 ++++++++ .../ui-new/views/WorkspacesSidebar.tsx | 2 + shared/types.ts | 6 ++- 6 files changed, 76 insertions(+), 3 deletions(-) diff --git a/crates/db/src/models/merge.rs b/crates/db/src/models/merge.rs index 4d9516cb..336eb233 100644 --- a/crates/db/src/models/merge.rs +++ b/crates/db/src/models/merge.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use sqlx::{FromRow, SqlitePool, Type}; @@ -288,6 +290,45 @@ impl Merge { Ok(rows.into_iter().map(Into::into).collect()) } + + /// Get the latest PR status for each workspace (for workspace summaries) + /// Returns a map of workspace_id -> MergeStatus for workspaces that have PRs + pub async fn get_latest_pr_status_for_workspaces( + pool: &SqlitePool, + archived: bool, + ) -> Result, sqlx::Error> { + #[derive(FromRow)] + struct PrStatusRow { + workspace_id: Uuid, + pr_status: Option, + } + + // Get the latest PR for each workspace by using a subquery to find the max created_at + // Only consider PR merges (not direct merges) + let rows = sqlx::query_as::<_, PrStatusRow>( + r#"SELECT + m.workspace_id, + m.pr_status + FROM merges m + INNER JOIN ( + SELECT workspace_id, MAX(created_at) as max_created_at + FROM merges + WHERE merge_type = 'pr' + GROUP BY workspace_id + ) latest ON m.workspace_id = latest.workspace_id + AND m.created_at = latest.max_created_at + INNER JOIN workspaces w ON m.workspace_id = w.id + WHERE m.merge_type = 'pr' AND w.archived = $1"#, + ) + .bind(archived) + .fetch_all(pool) + .await?; + + Ok(rows + .into_iter() + .filter_map(|row| row.pr_status.map(|status| (row.workspace_id, status))) + .collect()) + } } // Conversion implementations diff --git a/crates/server/src/routes/task_attempts/workspace_summary.rs b/crates/server/src/routes/task_attempts/workspace_summary.rs index e4e1e8f9..18e1e87d 100644 --- a/crates/server/src/routes/task_attempts/workspace_summary.rs +++ b/crates/server/src/routes/task_attempts/workspace_summary.rs @@ -4,6 +4,7 @@ use axum::{Json, extract::State, response::Json as ResponseJson}; use db::models::{ coding_agent_turn::CodingAgentTurn, execution_process::{ExecutionProcess, ExecutionProcessStatus}, + merge::{Merge, MergeStatus}, workspace::Workspace, workspace_repo::WorkspaceRepo, }; @@ -45,6 +46,8 @@ pub struct WorkspaceSummary { pub has_running_dev_server: bool, /// Does this workspace have unseen coding agent turns? pub has_unseen_turns: bool, + /// PR status for this workspace (if any PR exists) + pub pr_status: Option, } /// Response containing summaries for requested workspaces @@ -103,7 +106,10 @@ pub async fn get_workspace_summaries( // 5. Check which workspaces have unseen coding agent turns let unseen_workspaces = CodingAgentTurn::find_workspaces_with_unseen(pool, archived).await?; - // 6. Compute diff stats for each workspace (in parallel) + // 6. Get PR status for each workspace + let pr_statuses = Merge::get_latest_pr_status_for_workspaces(pool, archived).await?; + + // 7. Compute diff stats for each workspace (in parallel) let diff_futures: Vec<_> = workspaces .iter() .map(|ws| { @@ -126,7 +132,7 @@ pub async fn get_workspace_summaries( futures_util::future::join_all(diff_futures).await; let diff_stats: HashMap = diff_results.into_iter().flatten().collect(); - // 7. Assemble response + // 8. Assemble response let summaries: Vec = workspaces .iter() .map(|ws| { @@ -148,6 +154,7 @@ pub async fn get_workspace_summaries( latest_process_status: latest.map(|p| p.status.clone()), has_running_dev_server: dev_server_workspaces.contains(&id), has_unseen_turns: unseen_workspaces.contains(&id), + pr_status: pr_statuses.get(&id).cloned(), } }) .collect(); diff --git a/frontend/src/components/ui-new/hooks/useWorkspaces.ts b/frontend/src/components/ui-new/hooks/useWorkspaces.ts index a2c1e77a..ae9673a1 100644 --- a/frontend/src/components/ui-new/hooks/useWorkspaces.ts +++ b/frontend/src/components/ui-new/hooks/useWorkspaces.ts @@ -25,6 +25,7 @@ export interface SidebarWorkspace { hasUnseenActivity?: boolean; latestProcessCompletedAt?: string; latestProcessStatus?: 'running' | 'completed' | 'failed' | 'killed'; + prStatus?: 'open' | 'merged' | 'closed' | 'unknown'; } // Keep the old export name for backwards compatibility @@ -67,6 +68,7 @@ function toSidebarWorkspace( hasUnseenActivity: summary?.has_unseen_turns, latestProcessCompletedAt: summary?.latest_process_completed_at ?? undefined, latestProcessStatus: summary?.latest_process_status ?? undefined, + prStatus: summary?.pr_status ?? undefined, }; } diff --git a/frontend/src/components/ui-new/primitives/WorkspaceSummary.tsx b/frontend/src/components/ui-new/primitives/WorkspaceSummary.tsx index 50b61753..f8ba8b51 100644 --- a/frontend/src/components/ui-new/primitives/WorkspaceSummary.tsx +++ b/frontend/src/components/ui-new/primitives/WorkspaceSummary.tsx @@ -6,6 +6,7 @@ import { PlayIcon, FileIcon, CircleIcon, + GitPullRequestIcon, } from '@phosphor-icons/react'; import { useTranslation } from 'react-i18next'; import { cn } from '@/lib/utils'; @@ -27,6 +28,7 @@ interface WorkspaceSummaryProps { hasUnseenActivity?: boolean; latestProcessCompletedAt?: string; latestProcessStatus?: 'running' | 'completed' | 'failed' | 'killed'; + prStatus?: 'open' | 'merged' | 'closed' | 'unknown'; onClick?: () => void; className?: string; summary?: boolean; @@ -48,6 +50,7 @@ export function WorkspaceSummary({ hasUnseenActivity = false, latestProcessCompletedAt, latestProcessStatus, + prStatus, onClick, className, summary = false, @@ -120,6 +123,20 @@ export function WorkspaceSummary({ /> )} + {/* PR status icon */} + {prStatus === 'open' && ( + + )} + {prStatus === 'merged' && ( + + )} + {/* Pin icon */} {isPinned && ( onSelectWorkspace(workspace.id)} /> ))} @@ -124,6 +125,7 @@ export function WorkspacesSidebar({ hasUnseenActivity={workspace.hasUnseenActivity} latestProcessCompletedAt={workspace.latestProcessCompletedAt} latestProcessStatus={workspace.latestProcessStatus} + prStatus={workspace.prStatus} onClick={() => onSelectWorkspace(workspace.id)} /> ))} diff --git a/shared/types.ts b/shared/types.ts index e21fb778..2334887c 100644 --- a/shared/types.ts +++ b/shared/types.ts @@ -352,7 +352,11 @@ has_running_dev_server: boolean, /** * Does this workspace have unseen coding agent turns? */ -has_unseen_turns: boolean, }; +has_unseen_turns: boolean, +/** + * PR status for this workspace (if any PR exists) + */ +pr_status: MergeStatus | null, }; export type WorkspaceSummaryResponse = { summaries: Array, };