Skip to content

feat(ui): vault popover + hub-discovery + OAuth vault-hint - #115

Merged
unforced merged 1 commit into
mainfrom
ag-unforced-dev
May 12, 2026
Merged

feat(ui): vault popover + hub-discovery + OAuth vault-hint#115
unforced merged 1 commit into
mainfrom
ag-unforced-dev

Conversation

@unforced

Copy link
Copy Markdown
Contributor

Summary

Implements Phase 2 of the merged Notes UI audit — §2 of design/2026-05-12-notes-ui-audit.md, §5 ship-sequence item #1.

Replaces the header <select> switcher with a popover that surfaces the operator's full hub-side vault list alongside the locally-connected ones. The named gap (Aaron runs four vaults — boulder/default/gitcoin/techne — but Notes only shows the ones already connected, so each new vault means another trip to /add) closes here.

What's in this PR

  • VaultPopover component (src/components/VaultPopover.tsx) — two sections (Connected / Available from your hub) plus a "Manage vaults →" footer; outside-click + Escape to close; two variants (header floating, inline for the mobile menu).
  • buildVaultPopoverRows() pure diffing function (URL match via normalizeVaultUrl) so the Connected-vs-Available split is testable without React.
  • fetchHubVaults() + hubOriginForVault() in src/lib/vault/hub-discovery.ts. Returns the parsed WellKnownVaultEntry[] or null on any failure (network, non-2xx, malformed JSON).
  • beginOAuth(..., options) — new fourth arg with params: Record<string,string>. Appended last and guarded so caller params can never overwrite standard OAuth/PKCE params.
  • Header.tsx desktop + mobile wired to the new component; standalone "Manage" button removed (the popover footer owns it now).

Design calls made

  1. Hub-origin discovery (the §2 open question). Derived from VaultRecord.issuer rather than storing a new field. Rationale:
    • issuer is already captured at OAuth time (OAuthCallback.tsx:54) and under hub-as-issuer semantically equals the hub origin (per the comment at probe.ts:10-13).
    • Standalone-vault case: issuer === vault URL → well-known fetch returns null or empty vaults → Available section omitted. Graceful degradation, no caller code to special-case.
    • No VaultRecord schema change, no migration, no field that could drift out of sync with issuer.
  2. OAuth vault=<name> hint plumbing. Last-step URL decoration on authorizeUrl.searchParams via a new options.params bag, exactly as suggested in the design doc. Guarded against standard-param collisions for defense-in-depth.
  3. Path A on hub coordination. Notes sends vault=<name> blind. Pre-#240 hubs ignore it; the consent screen renders the picker as today. Future hubs can adopt the hint to pre-select on consent — no Notes change required when they do. Hub steward coordination tracked as a follow-up, not a blocker here.

What's out of scope (per design doc §5)

Version + changelog

  • package.json 0.3.14 → 0.3.15-rc.1 (rc.N convention per parachute-patterns/patterns/governance.md rule 2).
  • CHANGELOG entry under ## Unreleased.

Test plan

  • Type-check clean (bun run typecheck)
  • Lint clean (bun run lint)
  • All tests pass (bun run test → 667/667, 24 new)
  • Manual smoke: open popover on a multi-vault hub, verify Connected list, Available list (other hub-known vaults), click switch, click Connect → OAuth flow with vault=<name> in the URL
  • Mobile: hamburger menu → inline variant of the popover
  • Standalone-vault deploy (Available section should be hidden)

🤖 Generated with Claude Code

Replace the header `<select>` with a popover that lists locally-connected
vaults alongside the operator's hub-side vault list (fetched from
`<hub>/.well-known/parachute.json`). Implements §2 of the merged
design doc `design/2026-05-12-notes-ui-audit.md` — the first item in
the §5 ship sequence.

- New `VaultPopover` component (`src/components/VaultPopover.tsx`) with
  `header` and `inline` variants. Connected section lets the user
  switch active vault with one click; Available section adds a
  per-vault "Connect" button that kicks OAuth against the hub.
  Outside-click + Escape close.
- Pure `buildVaultPopoverRows()` for the diffing logic (URL match via
  `normalizeVaultUrl`) so the connected-vs-available split is testable
  without React.
- New `fetchHubVaults()` + `hubOriginForVault()` in
  `src/lib/vault/hub-discovery.ts`. Hub origin is derived from
  `VaultRecord.issuer` (already captured at OAuth time in
  `OAuthCallback.tsx`) — no new VaultRecord field, no migration.
  Under a standalone-vault deployment the well-known fetch returns
  no peers and the Available section is omitted (graceful).
- `beginOAuth` now accepts an `options.params` bag, appended last to
  the authorize URL and guarded so caller-supplied params can never
  overwrite standard OAuth/PKCE params. Used by the popover to send
  `vault=<name>` as a Path A hint (design doc §2): pre-#240 hubs
  ignore it, future hubs can pre-select the consent screen with no
  Notes change.
- Header.tsx desktop + mobile both replaced — the standalone "Manage"
  button is gone since the popover footer owns the link to /vaults.

Tests: 24 new (13 hub-discovery, 11 popover + diffing + component,
2 oauth params). 667/667 pass, typecheck clean, lint clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@unforced
unforced merged commit 051c404 into main May 12, 2026
@unforced
unforced deleted the ag-unforced-dev branch May 12, 2026 15:51
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.

1 participant