Add PR detail expansion and per-workflow CI status - #7
Conversation
- PR cards now expand in-place on tap (like early Twitter) showing: - Per-workflow CI breakdown (failed/passed/pending counts) - Retry job status with remaining/total retries - Action buttons: Open PR, Retry, Retry x3, Cancel - CI badge shows compact workflow format (e.g. "2/5wf·3") - GraphQL query fetches individual check runs via statusCheckRollup contexts - Enrich RetryFlakyJob with workflowAttempts and totalRetries - Runner settings now show pending retry count and recent retry results https://claude.ai/code/session_01PqW1eLdPDdSmtQnCfG1HCn
- Add JUnit 5 test infrastructure to app module (build.gradle.kts) - Extract parseCIContexts to top-level internal function for testability - Change ciStatusText to internal visibility for testability - CIWorkflowInfoTest: status/totalCount computed properties - RetryFlakyJobTest: totalRetries, workflowAttempts, defaults - ParseCIContextsTest: CheckRun/StatusContext parsing, grouping, sorting - CiStatusTextTest: compact workflow count formatting for all CI states - OpenPrsUiStateTest: expand/collapse state, prKey, swipe disable logic - SettingsRetryStatsTest: pending count, recent results filtering/sorting https://claude.ai/code/session_01PqW1eLdPDdSmtQnCfG1HCn
…es, consolidate ActionPill into StatusBadge Extract duplicated AnimatedVisibility enter/exit transitions into file-level constants, hoist RoundedCornerShape allocations out of composables, and replace the private ActionPill with a clickable StatusBadge overload to eliminate near-identical code. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
@codex pls review |
|
To use Codex here, create a Codex account and connect to github. |
There was a problem hiding this comment.
Pull request overview
This PR adds an in-place expandable PR card UI that surfaces per-context/workflow CI status details and retry-job status, and wires new GraphQL data to support that richer CI display.
Changes:
- Add expandable PR cards with an expanded detail section and compact CI badge text.
- Extend GraphQL PR fetching to retrieve
statusCheckRollup.contextsand parse them into per-group CI counts/workflows. - Add retry-job stats surfaced in Settings and introduce JUnit 5-based unit tests for new parsing/formatting logic.
Reviewed changes
Copilot reviewed 15 out of 16 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
android/app/src/main/kotlin/com/ghpr/app/ui/openprs/OpenPrsScreen.kt |
Adds expand/collapse UI, expanded detail actions, and compact CI badge formatting. |
android/app/src/main/kotlin/com/ghpr/app/ui/openprs/OpenPrsViewModel.kt |
Adds expandedPrKey state and toggle handler. |
android/app/src/main/kotlin/com/ghpr/app/ui/components/StatusBadge.kt |
Adds a clickable StatusBadge overload with enabled/disabled behavior. |
android/app/src/main/kotlin/com/ghpr/app/data/OpenPullRequest.kt |
Extends PR model with check counts, workflow/context info, and running flag; adds CIWorkflowInfo. |
android/app/src/main/kotlin/com/ghpr/app/data/GitHubGraphQLClient.kt |
Expands GraphQL fragment to fetch check contexts and adds parseCIContexts() aggregation logic. |
android/app/src/main/kotlin/com/ghpr/app/data/GhprApiClient.kt |
Extends RetryFlakyJob with workflow attempts/timestamps and exposes totalRetries. |
android/app/src/main/kotlin/com/ghpr/app/ui/settings/SettingsViewModel.kt |
Fetches retry jobs and computes pending + recent retry stats into SettingsUiState. |
android/app/src/main/kotlin/com/ghpr/app/ui/settings/SettingsScreen.kt |
Displays pending retry count and recent retry results in Settings UI. |
android/app/src/test/kotlin/com/ghpr/app/ui/settings/SettingsRetryStatsTest.kt |
Unit tests for settings retry-stats filtering/counting behavior. |
android/app/src/test/kotlin/com/ghpr/app/ui/openprs/OpenPrsUiStateTest.kt |
Unit tests for expansion key behavior and swipe enablement logic. |
android/app/src/test/kotlin/com/ghpr/app/ui/openprs/CiStatusTextTest.kt |
Unit tests for compact CI badge text formatting. |
android/app/src/test/kotlin/com/ghpr/app/data/ParseCIContextsTest.kt |
Unit tests for parsing/grouping CI contexts into workflow/context aggregates. |
android/app/src/test/kotlin/com/ghpr/app/data/CIWorkflowInfoTest.kt |
Unit tests for CIWorkflowInfo derived properties. |
android/app/src/test/kotlin/com/ghpr/app/data/RetryFlakyJobTest.kt |
Unit tests for new RetryFlakyJob fields/defaults. |
android/app/build.gradle.kts |
Enables JUnit Platform and adds JUnit 5 + test dependencies for new unit tests. |
.gitignore |
Ignores Rust target/ and a local Cloudflare Wrangler config file. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| internal fun ciStatusText(pr: OpenPullRequest): String { | ||
| val ci = pr.ciState?.uppercase() ?: return "ci" | ||
| val workflows = pr.ciWorkflows | ||
| if (workflows.isEmpty()) return ci.lowercase() | ||
| val totalWf = workflows.size | ||
| return when (ci) { | ||
| "FAILURE", "ERROR" -> { | ||
| val failedWf = workflows.count { it.failureCount > 0 } | ||
| val totalFailedTasks = workflows.sumOf { it.failureCount } | ||
| "${failedWf}/${totalWf}wf\u00B7${totalFailedTasks}" | ||
| } | ||
| "PENDING" -> { | ||
| val doneWf = workflows.count { it.status == "SUCCESS" || it.status == "FAILURE" } | ||
| "${doneWf}/${totalWf}wf" | ||
| } | ||
| "SUCCESS" -> "${totalWf}wf" | ||
| else -> ci.lowercase() | ||
| } |
There was a problem hiding this comment.
ciStatusText() labels the count as wf, but it uses pr.ciWorkflows.size (which includes non-workflow grouped contexts where CIWorkflowInfo.isWorkflow == false, e.g. StatusContext checks). This can produce misleading output like 3wf even when only 1 entry is an actual GitHub Actions workflow. Consider either counting only isWorkflow entries when using the wf suffix, or changing the suffix/logic to reflect that these are generic CI contexts.
| // Only show failed workflows | ||
| if (failedWorkflows.isNotEmpty()) { | ||
| Column(verticalArrangement = Arrangement.spacedBy(3.dp)) { | ||
| failedWorkflows.forEach { wf -> | ||
| WorkflowStatusRow(wf) | ||
| } | ||
| } | ||
| } else if (pr.ciState != null && pr.ciWorkflows.isEmpty()) { |
There was a problem hiding this comment.
The expanded detail view currently filters to failedWorkflows only (pr.ciWorkflows.filter { it.failureCount > 0 }). This contradicts the PR description’s “per-workflow CI breakdown (failed/passed/pending counts)” and prevents users from seeing pending/success workflows when expanded. Consider rendering all workflows with a status indicator and counts, and optionally visually prioritizing failures.
| context | ||
| state | ||
| } | ||
| } |
There was a problem hiding this comment.
The GraphQL query requests statusCheckRollup.contexts(first: 100) but does not fetch pageInfo or paginate. PRs with >100 check contexts will have truncated workflow/context counts and potentially incorrect ciIsRunning/badge output. Consider adding pagination (e.g., pageInfo { hasNextPage endCursor } and follow-up queries) or a clear UI fallback when truncation occurs.
| } | |
| } | |
| pageInfo { | |
| hasNextPage | |
| endCursor | |
| } |
| val groupName = workflowName ?: ctx.optString("name", "check") | ||
| val isWf = workflowName != null | ||
|
|
||
| val counts = workflowMap.getOrPut(groupName) { intArrayOf(0, 0, 0) } | ||
| workflowIsWf.putIfAbsent(groupName, isWf) | ||
|
|
There was a problem hiding this comment.
workflowIsWf.putIfAbsent(groupName, isWf) never upgrades an existing entry from false to true. If a non-workflow context and a real workflow happen to share the same groupName (e.g., a StatusContext/check named the same as a workflow), isWorkflow can be permanently incorrect. Consider setting workflowIsWf[groupName] = (workflowIsWf[groupName] == true) || isWf (or equivalent) so true wins.
| val state: StateFlow<SettingsUiState> = combine( | ||
| baseState, | ||
| _retryJobs, | ||
| ) { base, jobs -> | ||
| val pendingCount = jobs.count { it.status == "active" } | ||
| val recentResults = jobs | ||
| .filter { it.status in listOf("completed", "exhausted", "cancelled") } | ||
| .sortedByDescending { it.updatedAt } | ||
| .take(5) | ||
| base.copy( | ||
| retryPendingCount = pendingCount, | ||
| recentRetryResults = recentResults, | ||
| ) |
There was a problem hiding this comment.
_retryJobs is never cleared when GitHub auth transitions to SignedOut, and the state combine derives retryPendingCount/recentRetryResults from _retryJobs regardless of auth state. This can leave retry stats from a previous session visible after sign-out. Consider clearing _retryJobs on sign-out (e.g., in an authState collector) and/or returning 0/empty when gitHubAuthState is not SignedIn.
…sWorkflow, sign-out - Change misleading "wf" suffix to "ci" in ciStatusText() since counts include non-workflow StatusContext checks - Show all workflows in expanded detail (not just failed), with status-appropriate icons (cross/hourglass/checkmark) - Detect GraphQL pagination truncation (>100 checks) via pageInfo.hasNextPage and append "+" to CI counts - Fix workflowIsWf so true wins over false when same name appears as both workflow and non-workflow entry - Clear _retryJobs on sign-out alongside runnerStatus Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
https://claude.ai/code/session_01PqW1eLdPDdSmtQnCfG1HCn