Skip to content

Lists: create a GitHub-backed list from a repository (#73) - #175

Merged
Adron merged 2 commits into
mainfrom
issue-73-github-backed-list
Sep 24, 2026
Merged

Adron merged 2 commits into
mainfrom
issue-73-github-backed-list

Conversation

@Adron

@Adron Adron commented Sep 16, 2026

Copy link
Copy Markdown
Member

Stack

main → #142 (issue-72-github-service, the nine /api/github/* endpoints) → this PR → #74 (repo link / private-repo tag / refresh) → #76 (reconnect handoff). Base is issue-72-github-service, not main.

What this adds

The GitHub-backed List tab of the new-list flow from /help/lists, end to end.

Piece File
Two-tab flow + the whole GitHub tab Views/NewListTabs.xaml(.cs), ViewModels/NewListTabsViewModel.cs
Repo picker (my repos / an org, filtered) ViewModels/GitHubRepoPickerViewModel.cs
Link state + the two browser handoffs ViewModels/GitHubLinkViewModel.cs
GitHub icon on GitHub-backed lists Views/GitHubListBadge.xaml(.cs), Views/GitHubMark.xaml(.cs)
One-request badge cache Services/GitHubListIndex.cs
Create + backing reads Services/InterlinedApiClient.GitHubLists.cs
source/githubRepo/githubRepoPrivate Models/GitHubListBacking.cs
POST /api/lists (github) envelope Models/GitHubBackedListCreated.cs

Flow: pick a repository → the title defaults to the repo name (and is not clobbered afterwards if you type over it) → optional parent list → Public list toggle → create.

Hosting cost in ListsView.xaml: 5 lines

That file is contended by #161/#163/#164, so this follows the #55/#56 pattern — a self-contained UserControl with its own view model, context in via dependency properties:

<local:NewListTabs x:Name="NewListTabsPanel" ParentListSource="{Binding Lists}" ListsRefreshCommand="{Binding LoadListsCommand}"/>

plus Visibility="{Binding IsLocalTabSelected, ElementName=NewListTabsPanel, …}" on the three existing local-form controls (the host's title/description/Create form is the Local tab — nothing was rebuilt, moved or duplicated), and one <local:GitHubListBadge ListId="{Binding Id}"/> in the browser's item template. ListsViewModel is untouched; so is ListsView.xaml.cs; so is Models/ListSummary.cs.

Two corrections to the epic, both from live probes

1. There is no client paging to do — but an ?org= is mandatory. The epic asked for a "paginated" picker. GET /api/github/repos returns the complete set in one bare array (559 for ?org=github, 278 for ?org=dotnet — well past GitHub's 100/page ceiling): no envelope, no Link header, ?page=/?per_page= change nothing. So the picker loads once and filters locally. What it does need is an ?org=: the bare call returns only the linked account's own repositories, which is [] here.

2. Exactly 2000 is a truncation, and the cap held on every further org I tried. #142 found microsoft/google/apache each at exactly 2000. Twenty more orgs this round:

exactly 2000 (truncated) natural counts
Azure, mozilla, IBM intel 1360, adobe 1123, elastic 958, hashicorp 943, alibaba 540, tencent 296, openai 272, rust-lang 246, vercel 240, nodejs 231, angular 214, facebook 177, python 96, kubernetes 79, spring-projects 76, golang 61, gitlab-org 2

Six organizations now sit on exactly 2000. The picker therefore says "the server returns at most 2,000 per organization, so this is the first 2,000, not all of them" rather than implying completeness. The slim four-key projection (full_name, name, private, owner_login) was also uniform across ~13,000 further items.

The acceptance criterion that cannot be met literally

Missing Issues scope is detected up front and offers the reconnect handoff rather than failing at create.

There is no scope introspection anywhere in the API. /api/user/identities reports provider, username and timestamps — no scopes. /api/auth/github/status reports { configured, clientId, manageOrgAccessUrl }, i.e. whether the server has a GitHub OAuth app, not what this user granted. A sign-in-only link is therefore indistinguishable from an Issues-capable one until a call comes back empty or refused.

So rather than guess, the panel says so plainly (GitHubLinkViewModel.ScopeNote) and does the useful half:

  • gates the tab on real link state from /api/user/identities — never on an empty collection, because GET /api/github/repos returns 200 [] on an account that is linked;
  • keeps "Reconnect for GitHub Issues" visible the whole time, not only after a failure;
  • names the specific remedy on failure — reconnect for the Issues scope, or Manage organization access, which a reconnect genuinely cannot fix (re-running OAuth with unchanged scopes returns silently), surfaced as its own action from manageOrgAccessUrl.

Verified live (test account, bearer sync-token, 2026-09-16)

  • GET /api/user/identities → provider: github, InterlinedListMessenger. Re-confirms GitHub: InterlinedApiClient.GitHub.cs service + models for all nine /api/github/* endpoints #142: the account is linked, so 200 [] from /repos means "owns no repos", not "not linked".
  • GET /api/auth/github/status → { configured: true, clientId: Ov23li9eXYK1i6psJW6G, manageOrgAccessUrl: …/settings/connections/applications/Ov23li9eXYK1i6psJW6G }.
  • GET /api/github/repos?org=… × 20 orgs — see the table.
  • GET /api/lists and GET /api/lists?all=1 both carry source/githubRepo/githubRepoPrivate ("local"/null/null). ?all=1 is what GitHubListIndex uses.
  • POST /api/lists { title: "ZZ claude-probe gh-norepo", source: "github" } with no githubRepo → 400 "githubRepo is required for GitHub-backed lists (format: owner/repo)", and nothing was created (a following GET /api/lists still showed exactly the one pre-existing list). That is the contract CreateGitHubBackedListAsync targets, verified without creating any repository linkage.
  • dotnet build -c Debug and -c Release — both green, 0 warnings.

Deliberately not called

  • The GitHub-backed create itself. Executing it would wire a real GitHub repository to a real list on shared test infrastructure. The flow is built and left unexercised; per this repo's read-after-write rule refreshStatus is nullable and the caller re-reads. Needs a live create on a throwaway repo before it can be called verified.
  • POST /api/github/issues, the PATCH, and the comments endpoint — they write to real GitHub repositories (GitHub: issue create / update / comment from list rows #75's territory).
  • The account's own content was left alone; the only object created was the ZZ claude-probe list above, deleted and confirmed gone with a closing GET.

Notes for review

Closes #73

🤖 Generated with Claude Code

Adds the second tab of the new-list flow from /help/lists — Local List /
GitHub-backed List — as a self-contained control, plus the service call and
the browser badge behind it.

- `NewListTabs` (view + `NewListTabsViewModel`) owns the tab choice and the
  whole GitHub tab: repo picker, title defaulting to the repository name,
  optional parent list, Public list toggle. Hosted in `ListsView.xaml` with
  one element and three Visibility attributes — the host's existing
  title/description form *is* the Local tab, so nothing was rebuilt or moved.
- `GitHubRepoPickerViewModel` loads "My repositories" (bare `/api/github/repos`)
  or an organization's (`?org=`), filters locally and reports honestly when the
  server truncates.
- `GitHubLinkViewModel` reads link state from `/api/user/identities` and owns
  the two browser handoffs — "Reconnect for GitHub Issues" (`?link=true`) and
  "Manage organization access" (`manageOrgAccessUrl`), which are different
  remedies.
- `GitHubListBadge` marks GitHub-backed lists in the browser, answering from a
  one-request `GitHubListIndex` rather than a fetch per row.
- `InterlinedApiClient.GitHubLists.cs` adds the GitHub-backed create and the
  backing reads; `GitHubListBacking` projects `source`/`githubRepo`/
  `githubRepoPrivate` out of the raw list JSON so this doesn't collide with
  the open PR extending `ListSummary`.

Corrects the epic on two points, both probed live: `/api/github/repos` needs no
client paging (the server returns the complete set in one bare array) but does
need an `?org=` to show anything but your own; and exactly 2000 rows is a server
truncation, now confirmed on six large organizations, so the picker says "first
2,000" instead of implying completeness.

The GitHub-backed create was deliberately never executed — it would wire a real
GitHub repository to a real list. Its contract was verified without creating
anything: `POST /api/lists { title, source: "github" }` with no `githubRepo`
answers 400 "githubRepo is required for GitHub-backed lists (format:
owner/repo)" and creates nothing.

Closes #73

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…onverters

Two changes.

1. ListsView.xaml conflict: #73 puts a local-tab Visibility on the "Create list"
   button; #18 wraps that button in a StackPanel alongside "Define columns".
   Resolved by keeping the StackPanel and moving the visibility binding ONTO it,
   so both buttons hide on the GitHub-backed tab — which is correct, since that
   tab has its own create flow and "Define columns" is meaningless for a
   GitHub-backed list whose columns come from the repo's issue fields. Leaving
   the binding on just the one button would have left "Define columns" visible
   on the wrong tab.

2. Consolidated the shared visibility converters into CommonConverters.cs.

   This merge produced CS0101/CS0111 for BOTH InverseBoolToVisibilityConverter
   (GitHubViewConverters.cs vs ListsViewConverters.cs) and
   NotNullToVisibilityConverter (GitHubViewConverters.cs vs AiConverters.cs) —
   the fifth and sixth instances of the same failure in this merge pass. All
   implementations were byte-identical.

   Rather than delete one copy again, both now live in CommonConverters.cs,
   which carries a comment explaining why: parallel PRs adding the same
   converter to different files always merge cleanly and always break the
   compiler, and these are namespace-visible without a using directive, so a
   per-view copy is never necessary.

   Note: the first attempt used a regex spanning the preceding comment block and
   swallowed a neighbouring class (NullOrEmptyToVisibilityConverter), breaking
   eight XAML files. Redone by anchoring on the declaration line and balancing
   braces. Every converter is now declared exactly once — verified by counting.

Debug and Release both build clean; Sync.Core.Tests 38/38.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Adron
Adron merged commit e11e41c into main Sep 24, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

GitHub: create a GitHub-backed list from a repository

1 participant