diff --git a/InterlinedList/ViewModels/GitHubConnectionViewModel.cs b/InterlinedList/ViewModels/GitHubConnectionViewModel.cs
new file mode 100644
index 0000000..9db6962
--- /dev/null
+++ b/InterlinedList/ViewModels/GitHubConnectionViewModel.cs
@@ -0,0 +1,130 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using InterlinedList.Services;
+
+namespace InterlinedList.ViewModels;
+
+///
+/// The GitHub panel in Connected Accounts: what the link is, the Reconnect for
+/// GitHub Issues handoff, the separate Manage organization access
+/// handoff, and an access check that is honest about what it can and cannot
+/// prove.
+///
+///
+/// Composes rather than re-reading link state,
+/// so this panel and the GitHub-backed-list flow can never disagree about whether
+/// GitHub is connected.
+///
+///
+///
+/// Why there is no scope indicator. #76 asks that link state come from
+/// GET /api/user/identities and that a scope we cannot detect be said
+/// plainly rather than guessed. Both matter here:
+///
+///
+/// /api/user/identities is the only per-user signal and
+/// carries no scope field — provider, username, profile/avatar URLs,
+/// connectedAt, lastVerifiedAt, and that is all (verified live
+/// 2026-09-16).
+/// /api/auth/github/status is a red herring for
+/// this purpose, exactly as CLAUDE.md warns: it answers
+/// { configured: true, clientId: "Ov23li9eXYK1i6psJW6G", manageOrgAccessUrl:
+/// "…/settings/connections/applications/Ov23li9eXYK1i6psJW6G" } — server
+/// configuration, not user grants. Its one genuinely useful field is
+/// manageOrgAccessUrl, which nothing else publishes.
+///
+///
+/// So a sign-in-only link is indistinguishable from an Issues-capable one until a
+/// call behaves differently, and this panel says so instead of drawing a
+/// confident green tick.
+///
+///
+public partial class GitHubConnectionViewModel : ObservableObject
+{
+ private readonly SessionService _session;
+
+ public GitHubLinkViewModel Link { get; }
+
+ [ObservableProperty]
+ private bool isChecking;
+
+ [ObservableProperty]
+ private string? accessCheckResult;
+
+ public GitHubConnectionViewModel(SessionService session)
+ {
+ _session = session;
+ Link = new GitHubLinkViewModel(session);
+ Link.PropertyChanged += (_, e) =>
+ {
+ if (e.PropertyName is nameof(GitHubLinkViewModel.LinkState) or nameof(GitHubLinkViewModel.IsLinked))
+ OnPropertyChanged(nameof(DefaultRepoLine));
+ };
+ }
+
+ ///
+ /// The user's githubDefaultRepo — the repository
+ /// GET /api/github/issues falls back to when called without one, and a
+ /// sensible pre-selection in any repo picker. Null on the test account, so the
+ /// "not set" branch is the observed one.
+ ///
+ public string DefaultRepoLine => Link.DefaultRepo is { Length: > 0 } repo
+ ? $"Default repository: {repo}"
+ : "No default repository set — pickers will ask every time.";
+
+ [RelayCommand]
+ private async Task LoadAsync() => await Link.LoadAsync();
+
+ ///
+ /// Tries the one read that a sign-in-only link would most plausibly differ on,
+ /// and reports exactly what came back — including when the answer is
+ /// ambiguous.
+ ///
+ ///
+ /// The ambiguity is real and worth naming rather than papering over: bare
+ /// GET /api/github/repos returns 200 [] on the account this was
+ /// built against, which owns no repositories while being properly linked. An
+ /// empty array is therefore not evidence of a missing scope, and a non-empty
+ /// one is not proof of one either — GitHub lists public repositories without
+ /// the repo scope. What this check can do is turn a refusal
+ /// into the right remedy, and turn "nothing happened" into a sentence the user
+ /// can act on.
+ ///
+ ///
+ [RelayCommand]
+ private async Task CheckIssuesAccessAsync()
+ {
+ if (!Link.IsLinked)
+ {
+ AccessCheckResult = "GitHub isn't connected to this account yet, so there's nothing to check.";
+ return;
+ }
+
+ IsChecking = true;
+ try
+ {
+ var repos = await _session.Api.GetGitHubReposAsync(null);
+
+ AccessCheckResult = repos.Count > 0
+ ? $"{repos.Count:N0} repositor{(repos.Count == 1 ? "y" : "ies")} readable through the link. " +
+ "That confirms the link works — it still can't prove the Issues scope on its own, since " +
+ "public repositories list without it. If creating a GitHub-backed list is refused, reconnect."
+ : "No repositories came back, and that's ambiguous: this GitHub account may simply own none " +
+ "(true of the account this app was built against), or the link may be sign-in-only. " +
+ "Reconnect for GitHub Issues to rule the second one out. For an organization's " +
+ "repositories, use the organization filter in the GitHub-backed list flow.";
+ }
+ catch (InterlinedApiException ex)
+ {
+ AccessCheckResult = Link.ExplainFailure(ex);
+ }
+ catch (Exception ex) when (ex is System.Net.Http.HttpRequestException or System.Text.Json.JsonException)
+ {
+ AccessCheckResult = "Couldn't reach InterlinedList to check GitHub access.";
+ }
+ finally
+ {
+ IsChecking = false;
+ }
+ }
+}
diff --git a/InterlinedList/Views/ConnectedAccountsView.xaml b/InterlinedList/Views/ConnectedAccountsView.xaml
index 6234744..78f014e 100644
--- a/InterlinedList/Views/ConnectedAccountsView.xaml
+++ b/InterlinedList/Views/ConnectedAccountsView.xaml
@@ -114,6 +114,8 @@
Margin="0,0,0,8"
Visibility="{Binding IsLoading, Converter={StaticResource BoolToVisibility}}"/>
+
+
diff --git a/InterlinedList/Views/GitHubConnectionPanel.xaml b/InterlinedList/Views/GitHubConnectionPanel.xaml
new file mode 100644
index 0000000..b539116
--- /dev/null
+++ b/InterlinedList/Views/GitHubConnectionPanel.xaml
@@ -0,0 +1,205 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/InterlinedList/Views/GitHubConnectionPanel.xaml.cs b/InterlinedList/Views/GitHubConnectionPanel.xaml.cs
new file mode 100644
index 0000000..0f220d8
--- /dev/null
+++ b/InterlinedList/Views/GitHubConnectionPanel.xaml.cs
@@ -0,0 +1,74 @@
+using System.Windows;
+using System.Windows.Controls;
+using InterlinedList.ViewModels;
+
+namespace InterlinedList.Views;
+
+///
+/// The GitHub panel for Connected Accounts — status, Reconnect for GitHub
+/// Issues, Manage organization access, and an honest access check.
+///
+///
+/// Self-contained with its own view model, so hosting costs one element and
+/// ConnectedAccountsViewModel needs no changes at all. That matters here
+/// beyond tidiness: PR #154 is editing that view model's LoadAsync and
+/// appending a LinkedIn panel to the end of the same view, so this lands in a
+/// different region of the file and touches none of the same members.
+///
+///
+///
+/// is the one piece of host coupling, and it is
+/// deliberately valueless: bind it to anything that changes when the host
+/// reloads — {Binding Identities.Count} works, because
+/// ObservableCollection raises PropertyChanged for Count on
+/// both the Clear() and the re-fill — and the panel refreshes alongside
+/// the host's existing Refresh button. It also has its own Check again, so
+/// the binding is optional.
+///
+///
+public partial class GitHubConnectionPanel : UserControl
+{
+ private readonly GitHubConnectionViewModel _vm;
+
+ public GitHubConnectionPanel()
+ {
+ InitializeComponent();
+
+ _vm = new GitHubConnectionViewModel(Services.AppServices.Session);
+ DataContext = _vm;
+
+ Loaded += OnLoaded;
+ }
+
+ ///
+ /// Any value that changes when the host refreshes; the panel reloads in step.
+ /// The value itself is never read.
+ ///
+ public static readonly DependencyProperty ReloadSignalProperty =
+ DependencyProperty.Register(nameof(ReloadSignal), typeof(object), typeof(GitHubConnectionPanel),
+ new PropertyMetadata(null, OnReloadSignalChanged));
+
+ public object? ReloadSignal
+ {
+ get => GetValue(ReloadSignalProperty);
+ set => SetValue(ReloadSignalProperty, value);
+ }
+
+ private static void OnReloadSignalChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ var panel = (GitHubConnectionPanel)d;
+
+ // Only once the control is live, so the initial binding pass doesn't
+ // duplicate the load in OnLoaded.
+ if (panel.IsLoaded)
+ _ = panel._vm.LoadCommand.ExecuteAsync(null);
+ }
+
+ private async void OnLoaded(object sender, RoutedEventArgs e)
+ {
+ // Loaded fires again when the view is swapped back in, which is exactly
+ // when a re-read is wanted: the user may have been away in the browser
+ // approving scopes.
+ await _vm.LoadCommand.ExecuteAsync(null);
+ }
+}