Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
f8391fc
chore(porch): 787 init pir
amrmelsayed Jun 7, 2026
0021c59
[PIR #787] Plan draft
amrmelsayed Jun 7, 2026
608487a
chore(porch): 787 plan-approval gate-requested
amrmelsayed Jun 7, 2026
375ae08
chore(porch): 787 plan-approval gate-approved
amrmelsayed Jun 8, 2026
cf0a58f
chore(porch): 787 implement phase-transition
amrmelsayed Jun 8, 2026
21ff84b
[PIR #787] Flow reviewRequests + isDraft through pr-list forge concept
amrmelsayed Jun 8, 2026
a1d3f9a
[PIR #787] Carry reviewRequests + isDraft through OverviewPR mapping
amrmelsayed Jun 8, 2026
c4aa15f
[PIR #787] Sort PR sidebar (mine/review-requested/others) + draft badge
amrmelsayed Jun 8, 2026
d5d20b7
[PIR #787] Update builder thread for implement phase
amrmelsayed Jun 8, 2026
1a1844d
chore(porch): 787 dev-approval gate-requested
amrmelsayed Jun 8, 2026
226fc6d
[PIR #787] Correct thread note: test failures were stale artifacts
amrmelsayed Jun 8, 2026
fddc7b9
[PIR #787] Populate gitlab reviewRequests + isDraft from real glab fi…
amrmelsayed Jun 9, 2026
f1df53b
[PIR #787] Update thread: gitlab fields now populated from glab
amrmelsayed Jun 9, 2026
8195ee4
[PIR #787] Document verified rationale for gitea reviewRequests/isDra…
amrmelsayed Jun 9, 2026
841ad23
[PIR #787] Update thread: gitea verified against docs, defaults correct
amrmelsayed Jun 9, 2026
d12168b
chore(porch): 787 dev-approval gate-approved
amrmelsayed Jun 9, 2026
a782e9c
chore(porch): 787 review phase-transition
amrmelsayed Jun 9, 2026
a41e143
[PIR #787] Review + retrospective
amrmelsayed Jun 9, 2026
4975846
chore(porch): 787 record PR #1019
amrmelsayed Jun 9, 2026
2747e5e
chore(porch): 787 review build-complete
amrmelsayed Jun 9, 2026
c2ab33c
chore(porch): 787 pr gate-requested
amrmelsayed Jun 9, 2026
0084246
chore(porch): 787 pr gate-approved
amrmelsayed Jun 9, 2026
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
132 changes: 132 additions & 0 deletions codev/plans/787-vscode-pr-sidebar-sort-mine-fi.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# PIR Plan: VSCode PR sidebar — sort mine first, then review-requested, then others; draft badge

## Understanding

The Codev sidebar's Pull Requests view (`packages/vscode/src/views/pull-requests.ts`) renders
`data.pendingPRs` in whatever order Tower returns them (`getChildren()` does a bare `.map`, no sort).
A reviewer scanning the list has no fast path to "my PRs" or "PRs waiting on my review" — they sit
interleaved with everyone else's. Draft PRs look identical to ready ones.

Issue #787 asks for a **single flat list** (no group headers, no collapsible sections) sorted by a
three-bucket comparator, with drafts visually marked. The "me" identity is already solved:
`OverviewData.currentUser` is populated server-side (`overview.ts:959-961`, via
`fetchCurrentUserCached`) and already consumed by `views/backlog.ts:122,156` for the assignee swap —
this view should reuse the exact same identity source.

The blocker is that the data needed for buckets 1–2 and the draft badge isn't flowed through yet:
- `PrListItem` (`forge-contracts.ts:64-73`) has `author`, `createdAt`, `reviewDecision` but **no**
`reviewRequests` and **no** `isDraft`.
- `OverviewPR` (`packages/types/src/api.ts:227-235`) likewise carries neither.
- The GitHub data is already reachable: the Team view's GraphQL fetches both `isDraft` and
`reviewRequests(first: 20){ nodes { requestedReviewer { ... on User { login } } } }`
(`team-github.ts:119,122`). We don't reuse that query (it's per-team-member and search-scoped);
instead we extend the existing `pr-list` forge concept, which already powers `pendingPRs`.

## Proposed Change

Flow `reviewRequests: string[]` and `isDraft: boolean` from the `pr-list` forge concept all the way
to the VSCode tree item, then sort + badge in the view. Five layers, bottom-up:

1. **Forge concept (data source).** Extend `gh pr list --json` to also request `isDraft` and
`reviewRequests`, then normalize `reviewRequests` to a `string[]` of user logins via `jq` (gh
returns objects; teams have no `login` and are dropped). The github script is the only one that
gains real data; gitlab/gitea scripts emit safe defaults (`reviewRequests: []`, `isDraft: false`)
so the `string[]`/`boolean` contract holds across forges.

2. **Contract type.** Add `reviewRequests: string[]` and `isDraft: boolean` to `PrListItem`.

3. **API type.** Add the same two fields to `OverviewPR` (`packages/types/src/api.ts`).

4. **Overview server mapping.** In `overview.ts:859-867`, map the new fields through with defensive
defaults (`pr.reviewRequests ?? []`, `pr.isDraft ?? false`) so a forge that omits them never
produces `undefined` on the wire.

5. **VSCode view.** In `pull-requests.ts` `getChildren()`:
- Compute `me = data.currentUser?.toLowerCase()` (same idiom as `backlog.ts:122`).
- Sort a copy of `data.pendingPRs` with the comparator below.
- Append ` (draft)` to the label for drafts (and overlay a distinct icon — `git-pull-request-draft`
ThemeIcon — so it's visible even when the label is truncated).

### Sort comparator

```
bucket(pr):
if me && pr.author?.toLowerCase() === me -> 0 (mine — wins even if I'm also a reviewer)
if me && pr.reviewRequests.map(lower).includes(me) -> 1 (review-requested)
else -> 2 (everything else)

compare(a, b):
if bucket(a) !== bucket(b) -> bucket(a) - bucket(b)
else -> b.createdAt.localeCompare(a.createdAt) (createdAt DESC within bucket)
```

When `me` is undefined (gh unavailable / not authenticated), every PR lands in bucket 2, so the sort
collapses to a stable `createdAt`-desc ordering — the partitioning is skipped silently, exactly as
the acceptance criteria require. ISO-8601 `createdAt` strings compare correctly lexicographically, so
`localeCompare` gives true chronological order without `Date` parsing.

## Files to Change

- `packages/codev/scripts/forge/github/pr-list.sh` — add `isDraft,reviewRequests` to `--json`; pipe
through `jq` to flatten `reviewRequests` to `[login]` and pass `isDraft` through.
- `packages/codev/scripts/forge/gitlab/pr-list.sh` — `jq`-normalize to add `reviewRequests: []`,
`isDraft: false` (and the existing fields) so the contract holds.
- `packages/codev/scripts/forge/gitea/pr-list.sh:13-24` — extend the existing `jq` map with
`reviewRequests: []`, `isDraft: false`.
- `packages/codev/src/lib/forge-contracts.ts:64-73` — add `reviewRequests: string[]`, `isDraft: boolean`
to `PrListItem`, with doc comments noting the cross-forge defaults.
- `packages/types/src/api.ts:227-235` — add `reviewRequests: string[]`, `isDraft: boolean` to `OverviewPR`.
- `packages/codev/src/agent-farm/servers/overview.ts:859-867` — map both fields through with `?? []`
/ `?? false` defaults.
- `packages/vscode/src/views/pull-requests.ts` — add the comparator + draft badge in `getChildren()`.

### Tests

- `packages/codev/src/agent-farm/__tests__/overview.test.ts` — extend the PR-mapping tests
(around :1829) to assert `reviewRequests`/`isDraft` flow through and default correctly when the
forge omits them.
- `packages/codev/src/__tests__/forge.test.ts` / `github.test.ts` — update any `PrListItem` fixtures
that fail typecheck once the fields are required.
- VSCode view comparator: extract the comparator to a small pure exported function
(e.g. `comparePendingPRs(a, b, me)`) so it's unit-testable without a VSCode host, and add a focused
test (mine-first, review-requested-second, createdAt-desc tiebreak, null-`me` fallback,
mine-beats-also-reviewer). Place it next to existing vscode view tests if a harness exists; if the
vscode package has no unit-test harness, keep the function exported and cover the comparator logic
via a codev-side test or document the manual check.

## Risks & Alternatives Considered

- **Risk: `gh pr list --json reviewRequests` shape.** gh returns reviewRequests as objects (users
have `login`, teams have `slug`/`name`, no `login`). Mitigation: `jq '[.reviewRequests[].login //
empty]'` extracts user logins and silently drops teams — matches the issue's "currentUser in
reviewRequests" (currentUser is always a user login). I'll verify the exact gh JSON shape
empirically with `gh pr list --json reviewRequests` before finalizing the jq filter.
- **Risk: making the fields required breaks existing `PrListItem` consumers/fixtures.** Mitigation:
required-with-default at every boundary (jq emits them for all three forges; overview mapping
defaults defensively). Typecheck will surface any fixture that needs updating; I'll fix those.
- **Alternative: reuse the Team view's GraphQL query** (`team-github.ts`). Rejected — it's
per-member, search-scoped, and team-oriented; the `pr-list` concept is the existing repo-wide PR
source feeding `pendingPRs`, so extending it is the smaller, correctly-scoped change and keeps the
forge-abstraction boundary intact (gitlab/gitea get defaults, not a GitHub-only query).
- **Alternative: group headers / collapsible sections.** Explicitly out of scope per the issue — flat
sorted list only.
- **Alternative: sort server-side in overview.ts.** Rejected — `currentUser` identity and the
reviewer-centric ordering are a presentation concern; `pendingPRs` is a shared payload (also feeds
the dashboard), so the per-viewer sort belongs in the VSCode view, mirroring how `backlog.ts` does
its own "mine" filtering client-side.

## Test Plan

- **Unit:**
- `comparePendingPRs`: mine-first, then review-requested, then others; createdAt-desc tiebreak
within a bucket; mine-beats-also-a-reviewer (bucket 0); null/undefined `me` → stable createdAt-desc.
- overview mapping: `reviewRequests`/`isDraft` flow through; default to `[]`/`false` when the forge
omits them.
- **Build:** `pnpm --filter @cluesmith/codev-types build && pnpm --filter @cluesmith/codev build`
(types first), plus the vscode package build, all from the worktree.
- **Manual (at the `dev-approval` gate):** Open the Codev sidebar → Pull Requests view against a repo
with a mix of (a) PR I authored, (b) PR requesting my review, (c) unrelated PRs, and (d) a draft.
Verify: my PR is at the top, review-requested next, others last; within each bucket newest-first;
the draft shows `(draft)` + the draft icon. Then sign out / break `gh auth` (or simulate
`currentUser` null) and confirm the list falls back to createdAt-desc with no crash and no
partitioning.
30 changes: 30 additions & 0 deletions codev/projects/787-vscode-pr-sidebar-sort-mine-fi/status.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
id: '787'
title: vscode-pr-sidebar-sort-mine-fi
protocol: pir
phase: review
plan_phases: []
current_plan_phase: null
gates:
plan-approval:
status: approved
requested_at: '2026-06-07T08:35:18.619Z'
approved_at: '2026-06-08T02:26:40.834Z'
dev-approval:
status: approved
requested_at: '2026-06-08T02:38:27.436Z'
approved_at: '2026-06-09T06:18:33.048Z'
pr:
status: approved
requested_at: '2026-06-09T06:24:19.086Z'
approved_at: '2026-06-09T06:32:13.401Z'
iteration: 1
build_complete: false
history: []
started_at: '2026-06-07T08:32:29.828Z'
updated_at: '2026-06-09T06:32:13.404Z'
pr_history:
- phase: review
pr_number: 1019
branch: builder/pir-787
created_at: '2026-06-09T06:20:53.393Z'
pr_ready_for_human: false
2 changes: 2 additions & 0 deletions codev/resources/lessons-learned.md
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,8 @@ Generalizable wisdom extracted from review documents, ordered by impact. Updated

- [From 916] A shared cache that nulls itself on a *transient* failure correlates the failure across every consumer at once. The VSCode sidebar's four data-views share one `OverviewCache`; each renders a falsy read as `[]`, so a brief SSE drop/reconnect that nulled the cache blanked all four simultaneously, then recovered on the next event. Fix: hold last-known-good — a not-connected state or a failed fetch returns without clobbering; only a successful fetch commits. Crucially, distinguish *failure-null* from *legitimately-empty*: the source-of-truth (`TowerClient.getOverview()`) returns `null` only on request failure and a real object with empty arrays for a genuinely empty workspace, so "keep last value on null" never masks real emptiness. The discriminating diagnostic was that one sibling view (Workspace) stayed populated because it reads a *different* source — when N views empty together but one doesn't, suspect a shared dependency of the N, not N independent bugs.

- [From 787] When adding a field to a multi-forge concept contract (`pr-list`, `issue-list`, …), per-CLI data availability diverges — do not assume parity across `gh`/`glab`/`tea`. Verify each empirically (live output or, failing that, the CLI's own `--help` and official docs): `gh pr list --json` exposes `isDraft` + `reviewRequests` (reviewer objects → flatten `.login`, dropping teams); `glab mr list --output json` exposes `draft` + `reviewers[].username` (same command, just add jq); but `tea pulls list` exposes *neither* (its JSON is limited to the selectable `--fields`, which omit draft/reviewers — the data exists only via raw `tea api`). Populate where the existing command can; default safely (`[]`/`false`) where it can't, and document the verified reason in the script. Defaulting a field the CLI *does* expose silently drops working data; rewriting a script onto a different command (e.g. `tea api`) to reach an unavailable field is a separate, larger change. Make the new required fields safe end-to-end by defaulting at the server mapping (`?? []` / `?? false`) so a non-conforming forge degrades rather than emitting `undefined`.

---

*Last updated: 2026-04-17 (Maintenance run 0007 — v3.0.0 pre-release)*
Expand Down
67 changes: 67 additions & 0 deletions codev/reviews/787-vscode-pr-sidebar-sort-mine-fi.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# PIR Review: VSCode PR sidebar — sort mine first, then review-requested, then others; draft badge

Fixes #787

## Summary

The Codev sidebar's Pull Requests view rendered `data.pendingPRs` in arbitrary forge order, with no fast scan-path to the reviewer's own PRs or the ones awaiting their review, and no way to tell drafts apart. This change flows two new fields (`reviewRequests: string[]`, `isDraft: boolean`) from the `pr-list` forge concept through `PrListItem` → `OverviewPR` → the overview-server mapping, then sorts the view into a single flat list (mine → review-requested → others, newest-first within each bucket) and marks drafts with a `(draft)` suffix plus a draft icon. The "me" identity reuses the already-populated `OverviewData.currentUser` (same source the backlog view uses), so no new lookup was added.

## Files Changed

- `packages/codev/scripts/forge/github/pr-list.sh` (+9 / -2) — add `isDraft,reviewRequests` to `gh --json`; jq flattens reviewer objects to logins
- `packages/codev/scripts/forge/gitlab/pr-list.sh` (+10 / -2) — populate `isDraft`/`reviewRequests` from glab's real `draft`/`reviewers[].username`
- `packages/codev/scripts/forge/gitea/pr-list.sh` (+13 / -2) — default both fields (`tea pulls list` exposes neither); documented rationale
- `packages/codev/src/lib/forge-contracts.ts` (+9 / -0) — `PrListItem` gains `reviewRequests` + `isDraft`
- `packages/types/src/api.ts` (+9 / -0) — `OverviewPR` gains `reviewRequests` + `isDraft`
- `packages/codev/src/agent-farm/servers/overview.ts` (+2 / -0) — map both through with `?? []` / `?? false`
- `packages/codev/src/agent-farm/__tests__/overview.test.ts` (+26 / -0) — flow-through + default coverage
- `packages/vscode/src/views/pull-requests-sort.ts` (+46 / -0, new) — pure `comparePendingPRs` / `sortPendingPRs`
- `packages/vscode/src/views/pull-requests.ts` (+9 / -1) — sort + draft badge in `getChildren()`
- `packages/vscode/src/__tests__/pull-requests-sort.test.ts` (+85 / -0, new) — comparator unit tests

(Plus `codev/plans/787-*.md`, `codev/reviews/787-*.md`, `codev/state/pir-787_thread.md`, `codev/resources/lessons-learned.md`, and porch `status.yaml`.)

## Commits

- `21ff84bb` [PIR #787] Flow reviewRequests + isDraft through pr-list forge concept
- `a1d3f9a5` [PIR #787] Carry reviewRequests + isDraft through OverviewPR mapping
- `c4aa15f5` [PIR #787] Sort PR sidebar (mine/review-requested/others) + draft badge
- `d5d20b70` [PIR #787] Update builder thread for implement phase
- `226fc6d9` [PIR #787] Correct thread note: test failures were stale artifacts
- `fddc7b98` [PIR #787] Populate gitlab reviewRequests + isDraft from real glab fields
- `f1df53b6` [PIR #787] Update thread: gitlab fields now populated from glab
- `8195ee4b` [PIR #787] Document verified rationale for gitea reviewRequests/isDraft defaults
- `841ad23d` [PIR #787] Update thread: gitea verified against docs, defaults correct

## Test Results

- `npm run build`: ✓ pass (porch `build` check, 7.2s)
- `npm test`: ✓ pass (porch `tests` check, 20.3s)
- vscode vitest suite: ✓ 360 pass (7 new in `pull-requests-sort.test.ts`)
- codev overview suite: ✓ 164 pass (2 new mapping tests)
- vscode `check-types` (tsc --noEmit): ✓ pass
- Manual verification (human, at `dev-approval` gate): reviewed the running worktree and approved.
- Forge field verification: github verified live (PR #593) + `gh --json`; gitlab verified live against `gitlab-org/cli` + GitLab docs; gitea verified via `tea --help` + official Gitea tea CLI docs.

## Architecture Updates

No `arch.md` changes needed. This change rides two patterns already documented there: the forge-concept abstraction (`arch.md` §"forge concept commands") and the VSCode host-side pure-helper pattern for testable view logic (`arch.md` #920/#1067 — `views/backlog-filter.ts`). `pull-requests-sort.ts` mirrors that exact pattern; no new module boundary or architectural concept was introduced — two fields were added to an existing contract.

## Lessons Learned Updates

Added one entry to `codev/resources/lessons-learned.md` ([From 787]): when adding a field to a multi-forge concept contract, per-CLI data availability diverges (`gh` and `glab` expose draft/reviewers; `tea pulls list` does not) — verify each empirically rather than assuming parity, populate where the existing command can, default safely where it can't, and document the verified reason. This was the non-obvious finding of the work and is reusable for anyone extending `pr-list` / `issue-list` / similar concepts.

## Things to Look At During PR Review

- **The comparator** (`pull-requests-sort.ts`): bucket 0 = mine (`author === me`, wins even if I'm also a reviewer), 1 = review-requested (`me ∈ reviewRequests`), 2 = others; `createdAt`-desc tiebreak via lexicographic compare on ISO-8601 strings (no `Date` parsing). `me` undefined → all bucket 2 → stable createdAt-desc fallback (the gh-unavailable case from the acceptance criteria). Identity matching is case-insensitive.
- **github jq filter**: `[.reviewRequests[].login // empty]` keeps user reviewers and drops team reviewers (Team nodes have no `.login`). Verified against real PR #593 and a mixed user+team sample.
- **gitea defaults are deliberate, not a stub oversight**: `tea pulls list`'s selectable `--fields` omit draft/reviewers, and its JSON is field-limited, so the data is unreachable via that command (only via raw `tea api`). The script comment records this. github + gitlab are fully populated.
- **Defensive defaults at the server boundary** (`overview.ts`: `?? []` / `?? false`) mean a forge that omits the fields degrades cleanly instead of emitting `undefined` on the wire.

## How to Test Locally

- **View diff**: VSCode sidebar → right-click builder pir-787 → **Review Diff**
- **Run dev server**: VSCode sidebar → **Run Dev Server**, or `afx dev pir-787`
- **What to verify**:
- In the Pull Requests view, against a repo with a mix of authored / review-requested / unrelated PRs plus a draft: your PRs sort first, review-requested next, others last; newest-first within each bucket; the draft shows `(draft)` + the draft icon.
- Break `gh auth` (or simulate `currentUser` null) and confirm the list falls back to createdAt-desc with no partitioning and no crash.
Loading
Loading