From 7aea030aee9dbd91530c8ce6358f30fe15096c5d Mon Sep 17 00:00:00 2001 From: James Maes Date: Fri, 3 Jul 2026 15:40:40 -0500 Subject: [PATCH 1/2] Implementation plan for Accounts mode + workspace redesign (#16) Six phases from the P1 accessibility wave through wizard/Sync All and stable release; reconciles #22 findings into the redesign; records standing decisions and risks. Co-Authored-By: Claude Fable 5 --- docs/plans/accounts-mode.md | 156 ++++++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 docs/plans/accounts-mode.md diff --git a/docs/plans/accounts-mode.md b/docs/plans/accounts-mode.md new file mode 100644 index 0000000..c10e8b2 --- /dev/null +++ b/docs/plans/accounts-mode.md @@ -0,0 +1,156 @@ +# Implementation plan: Accounts mode + workspace redesign + +Delivers issue #16 (Accounts mode) built directly into the layout redesign +direction recorded on that issue, with issue #22's findings reconciled along the +way. Tracker of record: GitHub Issues; this document is the executable plan. + +**Decisions already made (owner-confirmed on #16):** Windows Credential Manager +for tokens · one account = one org · Sync All · CLI parity (`--account`). +**Defaults standing unless overridden on #16:** NavigationView with Quick Sync +pinned · Sync All runs accounts sequentially · accounts remember the full +settings bundle and show a status badge · no account cap. + +Every phase ends the same way: 0-warning build, all tests green, format clean, +app launch verified, gated PR `dev -> main`, and a `v0.1.0-beta.N` tag so each +increment is installable and self-update keeps getting exercised. + +--- + +## Phase 0 — P1 accessibility & theming wave (issue #22) + +Prerequisite, not part of the redesign; everything here transfers unchanged +into the new layout. + +1. `StatusFormat.BrushFor` → resolve WinUI semantic brushes per call from + `Application.Current.Resources` (`SystemFillColorSuccessBrush` Done, + `SystemFillColorCriticalBrush` Failed, `SystemFillColorCautionBrush` + Canceled, `SystemFillColorAttentionBrush` Cloning/Pulling, neutral Queued). + Add `GlyphFor(SyncStatus)` returning Segoe Fluent Icons glyphs and a + `FontIcon` beside the status text so state is never color-alone. +2. `MainWindow.xaml` inline error: `Foreground="Crimson"` → + `{ThemeResource SystemFillColorCriticalBrush}`. +3. `AutomationProperties.LiveSetting="Polite"` on StatusText; raise UIA + notifications for run summary and per-repo failures + (`FrameworkElementAutomationPeer.RaiseNotificationEvent`). +4. `AutomationProperties.Name` on per-row checkboxes (repo name), select-all, + Resolve links (repo-specific), overall ProgressBar, both ProgressRings. + +Verification: build/launch; Accessibility Insights or Narrator spot-check is an +owner follow-up (headless agent cannot hear announcements). Tag `beta.5`. + +## Phase 1 — Accounts foundation (libraries + CLI; no UI) + +New in `gclo.ViewModels` (app-layer library, headless-testable): + +- `Account` model: `Id` (guid), `Name` (unique, case-insensitive), + `Description`, `Organization`, `TargetRoot`, `CreateOrgSubfolder`, + `MaxConcurrency`, `LastSyncUtc`, `LastSyncSummary` (counts string). + Metadata JSON at `%LOCALAPPDATA%\gclo\accounts.json` — never contains tokens. +- `ITokenVault` { `Store(accountId, token)`, `TryRetrieve(accountId)`, + `Delete(accountId)` } — implementation `CredentialManagerVault` using + `Windows.Security.Credentials.PasswordVault`, resource name + `gclo:account:`; `InMemoryVault` fake for tests. +- `AccountsStore`: CRUD + rename-safe persistence (same never-throw discipline + as `AppSettings`); deleting an account deletes its vault entry. +- CLI (`gclo.Cli`): `gclo accounts` (list: name, org, target, last sync); + `gclo sync --account ` resolves registry + vault (error 2 if the vault + entry is missing); explicit `--org/--token-*` flags override account values. + `docs/CLI.md` section. + +Tests: store CRUD/uniqueness/corrupt-file recovery; vault fake round-trip + +delete-on-remove; CLI resolution and override precedence. The real PasswordVault +is thin glue — verified by launch-time smoke, not unit tests. Tag `beta.6`. + +## Phase 2 — Navigation shell + +- `MainWindow` becomes a `NavigationView` (left pane): pinned "Quick Sync" + item, accounts from `AccountsStore` (name + status dot + last-sync tooltip), + footer "+ Add account". Content region hosts one `WorkspacePage` + (UserControl) bound to a `WorkspaceViewModel`. +- `WorkspaceViewModel` = today's `MainViewModel` generalized: constructed from + an `Account` (persistent) or the ephemeral Quick Sync account (today's + behavior, nothing saved). One cached VM per account per session so switching + accounts never loses a running sync; nav badge reflects each VM's state + (running/ok/failed). +- Menu bar survives (File/View/Help); Settings keeps global defaults whichseed + new accounts. + +Tests: WorkspaceViewModel construction from account values; nav-badge state +mapping (pure VM). Tag `beta.7`. + +## Phase 3 — Workspace redesign (the #16-comment direction) + +Inside `WorkspacePage`, replacing the stacked form: + +1. **Connection chip**: org · connected-state · repo count · destination + + Edit (opens wizard for accounts; inline flyout for Quick Sync). Org-lookup + feedback lives here — the multiplexed StatusText dies. +2. **Hero table card**: header + ListView on + `LayerFillColorDefaultBrush`, `CornerRadius=8`; attached toolbar = + selection summary ("12 of 87 selected"), filter box, options flyout (gear: + parallelism, org subfolder), Refresh, and ONE accent CTA "Sync N repos". + During a run the toolbar's right side swaps to progress (bar + "34 of 87" + + Cancel); progress scoped to the run's selected set (fixes the #22 + progress-honesty P2). +3. **Results InfoBar** (severity-aware): summary + inline Retry failed / Open + folder; replaces permanent buttons and the stale Retry visibility bug. +4. **Designed empty states**: no token → connect card (+ "how to create a + token" link, fixing that P2); connected → "Load N repositories from {org}"; + filter-no-match state. +5. **Reconciled #22 items in this phase**: input disabling while running + (CanEditInputs), min window size via `OverlappedPresenter`, sort-header + accessibility, table row composed automation names, access keys + + accelerators (Ctrl+R / Ctrl+Enter / Esc), microcopy fixes ("Parallel + clones", placement preview wording, ellipsis, pluralization). + +Tests: VM-level (selection summary, run-scoped progress, InfoBar state, +CanEditInputs); XAML verified by build + launch. Tag `beta.8`. + +## Phase 4 — Account wizard + Sync All + +- `AccountWizardDialog`: 1) name + description → 2) token (stored to vault, + validated live via `IOrganizationLister`) → 3) org picker (dropdown from the + token; manual entry fallback) → 4) destination + options (seeded from + Settings defaults) → summary/save. Same dialog pre-filled = Edit. Delete + confirms and removes the vault entry. +- **Sync All** (nav pane footer button): sequentially runs each account's + WorkspaceViewModel sync; per-account rolled-up progress via nav badges; + cancel stops the queue after the in-flight account. +- Activity log gains account context in messages (never tokens). + +Tests: wizard VM validation per step; Sync All ordering/cancellation with fake +clients. Tag `beta.9`. + +## Phase 5 — Close-out & release + +- Reconcile remaining #22 P3s; README rewrite for accounts + new screenshots + (issue #10 — needs owner or an interactive session for captures). +- Docs: README, CLI.md, SECURITY.md (vault storage note), RELEASING.md + untouched. +- Owner gate: confirm self-update beta-chain works installed (#6), then tag + **v0.1.0** stable; winget first submission (#7) follows. + +--- + +## Risks & watch items + +- **PasswordVault in unpackaged builds**: works without package identity, but + verify early in Phase 1 on the unpackaged launch path (it is the dev loop). + Fallback if it misbehaves: `CredRead/CredWrite` P/Invoke behind the same + `ITokenVault`. +- **One VM per account with concurrent runs**: memory/dispatcher pressure is + fine at ~5 accounts, but Sync All is deliberately sequential — do not let a + future "parallel accounts" ask sneak in without revisiting disk/API limits. +- **NavigationView + MenuBar chrome**: two top-level chrome systems can fight; + prototype the shell (Phase 2) before styling it (Phase 3). +- **Quick Sync regression risk**: it must behave exactly like today's app; + its VM tests are the regression net. + +## Execution notes + +- Order is strict: 0 → 1 → 2 → 3 → 4 → 5; each phase is a separate PR through + the required checks; no phase starts until the previous merged. +- Multi-agent builds continue to use pinned cross-agent contracts (the pattern + that has held since the productionize wave); single-writer per file area. +- Any change to the four standing defaults happens as a comment on #16 before + the affected phase starts. -- 2.54.0 From 3a87f9ef9cfd29aa108706e88b730cd4da67c171 Mon Sep 17 00:00:00 2001 From: James Maes Date: Sat, 4 Jul 2026 12:55:47 -0500 Subject: [PATCH 2/2] Phase 0: P1 accessibility and theming wave (#22) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Status brushes resolve theme- and high-contrast-aware WinUI semantic resources per call (SystemFillColor Success/Critical/Caution/Attention) instead of cached color literals; a Segoe Fluent status glyph sits beside the text so state is never color-alone - Inline row errors use SystemFillColorCriticalBrush - StatusText is a real live region: LiveSetting=Polite plus an explicit LiveRegionChanged raise on every update (XAML never raises it automatically — confirmed against Microsoft docs by verification) - Per-repo failures raise UIA notifications via the ListView peer (coalesced with ImportantMostRecent); AnnouncementRequested event on the view model with two pinning tests - AutomationProperties.Name on row/select-all checkboxes, Resolve links, overall ProgressBar, and both ProgressRings - App-level RequestedTheme set at startup from settings so app-resource theme lookups agree with the visible theme Verification: 8 doc-grounded claims checked, 1 found wrong (the silent live region) and fixed before ship. 116/116 tests. Co-Authored-By: Claude Fable 5 --- .claude/settings.json | 6 +++ gclo.Engine.Tests/MainViewModelTests.cs | 36 ++++++++++++++++++ gclo.ViewModels/MainViewModel.cs | 10 +++++ gclo/App.xaml.cs | 15 ++++++++ gclo/MainWindow.xaml | 23 ++++++++++-- gclo/MainWindow.xaml.cs | 41 ++++++++++++++++++++ gclo/StatusFormat.cs | 50 ++++++++++++++++++------- 7 files changed, 164 insertions(+), 17 deletions(-) create mode 100644 .claude/settings.json diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..8b4893a --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,6 @@ +{ + "enabledPlugins": { + "posthog@claude-plugins-official": true, + "playwright@claude-plugins-official": true + } +} diff --git a/gclo.Engine.Tests/MainViewModelTests.cs b/gclo.Engine.Tests/MainViewModelTests.cs index ec5312a..22dbee8 100644 --- a/gclo.Engine.Tests/MainViewModelTests.cs +++ b/gclo.Engine.Tests/MainViewModelTests.cs @@ -176,6 +176,42 @@ public sealed class MainViewModelTests Directory.Delete(vm.TargetFolder, recursive: true); } + [Fact] + public async Task Sync_RepoFailure_RaisesAssistiveAnnouncement() + { + _git.CloneHandler = (url, path, token, onProgress, ct) => + Path.GetFileName(path) == "bravo" + ? Task.FromException(new InvalidOperationException("boom")) + : Task.CompletedTask; + + var vm = await CreateLoadedViewModelAsync(Repo("alpha"), Repo("bravo")); + var announcements = new List(); + vm.AnnouncementRequested += announcements.Add; + + await vm.SyncCommand.ExecuteAsync(null); + + var announcement = Assert.Single(announcements); + Assert.Contains("bravo", announcement); + Assert.Contains("failed", announcement); + Assert.Contains("boom", announcement); + + Directory.Delete(vm.TargetFolder, recursive: true); + } + + [Fact] + public async Task Sync_AllSucceed_RaisesNoAnnouncements() + { + var vm = await CreateLoadedViewModelAsync(Repo("alpha"), Repo("bravo")); + var announcements = new List(); + vm.AnnouncementRequested += announcements.Add; + + await vm.SyncCommand.ExecuteAsync(null); + + Assert.Empty(announcements); + + Directory.Delete(vm.TargetFolder, recursive: true); + } + [Fact] public async Task Sync_CancelCommand_CancelsRun_AndReportsCanceled() { diff --git a/gclo.ViewModels/MainViewModel.cs b/gclo.ViewModels/MainViewModel.cs index a9d864d..44797e7 100644 --- a/gclo.ViewModels/MainViewModel.cs +++ b/gclo.ViewModels/MainViewModel.cs @@ -561,6 +561,13 @@ public sealed partial class MainViewModel : ObservableObject, IDisposable // ---------------------------------------------------------------- progress + /// + /// Raised with a message that assistive technology should announce. Per-repo + /// failures use this channel; run summaries land in , + /// whose live region already announces changes. Raised on the UI thread. + /// + public event Action? AnnouncementRequested; + /// Applies one engine progress report to the table. Runs on the UI thread. private void HandleProgress(RepoProgress report) { @@ -580,6 +587,9 @@ public sealed partial class MainViewModel : ObservableObject, IDisposable if (report.Status == SyncStatus.Failed) { _log.Error($"{report.RepoName} failed: {report.Error}"); + // Failures never reach StatusText (which has a live region), so assistive + // technology hears them only through this explicit channel. + AnnouncementRequested?.Invoke($"{report.RepoName} failed. {report.Error}"); } if (report.Status is SyncStatus.Done or SyncStatus.Failed or SyncStatus.Canceled) diff --git a/gclo/App.xaml.cs b/gclo/App.xaml.cs index ae2cda4..0c4c833 100644 --- a/gclo/App.xaml.cs +++ b/gclo/App.xaml.cs @@ -1,3 +1,4 @@ +using gclo.ViewModels; using Microsoft.UI.Xaml; namespace gclo @@ -16,6 +17,20 @@ namespace gclo public App() { InitializeComponent(); + + // App-level so Application.Current.Resources theme lookups (the status + // brushes) agree with the visible theme. Only settable here, before any + // window exists; mid-session theme changes still use the root element's + // RequestedTheme, and status brushes adopt them as row statuses update. + string theme = AppSettings.Load().Theme; + if (theme == "Light") + { + RequestedTheme = ApplicationTheme.Light; + } + else if (theme == "Dark") + { + RequestedTheme = ApplicationTheme.Dark; + } } /// diff --git a/gclo/MainWindow.xaml b/gclo/MainWindow.xaml index 6c37a5a..2e630ba 100644 --- a/gclo/MainWindow.xaml +++ b/gclo/MainWindow.xaml @@ -78,6 +78,7 @@ Height="20" Margin="0,0,0,8" VerticalAlignment="Bottom" + AutomationProperties.Name="Loading organizations" IsActive="{x:Bind ViewModel.IsLoadingOrgs, Mode=OneWay}" /> @@ -134,6 +135,7 @@ Width="20" Height="20" VerticalAlignment="Center" + AutomationProperties.Name="Loading repositories" IsActive="{x:Bind ViewModel.IsLoadingRepos, Mode=OneWay}" />