Skip to content

Desktop Account & Vault Settings (accounts, devices, notifications, vault management, email change)#847

Merged
ericvicenti merged 65 commits into
mainfrom
feat/account-settings-2
Jul 11, 2026
Merged

Desktop Account & Vault Settings (accounts, devices, notifications, vault management, email change)#847
ericvicenti merged 65 commits into
mainfrom
feat/account-settings-2

Conversation

@ericvicenti

Copy link
Copy Markdown
Collaborator

Summary

Unifies the account & vault settings experience across the desktop app and the web vault, sharing UI through @shm/ui so the two stay consistent. Adds a desktop Account Settings page and Vault Settings view, then rebuilds the web vault's unlocked UI on the same shared components.

Shared account-settings UI (@shm/ui)

One set of components, consumed by both platforms (each injects its own data/actions):

  • AccountSettingsLayout (account sidebar + Vault Settings entry + Add account / Import key) and AccountSettingsTabs (Account / Notifications / Devices).
  • AccountProfilePanel — Account tab: identity card, view/copy ID, password-protected export key, delete account (confirmation).
  • NotificationEmailSettings — per-account email notifications.
  • DelegatedKeysList — Devices tab.
  • AccountProfileForm — the create/edit profile form (avatar + name + description + optional email-notification opt-in), backend-agnostic and built only from @shm/ui primitives so the lean vault can use it. Both create-account dialogs now render this one component.
  • VaultSecuritySettings, ImportKeyDialog, CodeInput, ChangeEmailDialog, SetMasterPasswordDialog, NotificationServerDialog.

Desktop

  • New account-settings route + page: sidebar of accounts, per-account tabs (Account / Notifications / Devices), and a Vault Settings view. Active account and tab persist in the route.
  • Vault Settings: switch local ⇄ remote vault (connect opens the standard login/register dialog with custom server URL; daemon merges local identities into the remote vault), read-only vault URL + sync status, Force sync, non-destructive switch-to-local (keeps keys), Log out (clears keys). Vault settings were removed from the App Settings window.
  • Security (shared VaultSecuritySettings): set/change master password, change email, notification server host; passkey changes redirect to the browser.
  • Create account now has the email-notifications opt-in checkbox, matching the web vault.

Web vault

  • Replaced the legacy VaultView / AccountsView / NotificationsView shell with the shared AccountSettingsView (sidebar + tabs + Vault Settings), wired to the Valtio store.
  • Routing: account/tab selection lives in the URL hash (/vault#/a/<principal>/<tab>) so the server never sees it and reloads need only /vault; /settings stays a real path (the desktop deep-links to it for passkey management). Passkey unlock restores the previous account/tab.
  • Notification email entry, Edit Email, and the create-account notification checkbox brought in line with the shared components.

Theme tokens

  • Defined the full --color-* contract in the vault to match the desktop (sidebar / selection / chart tokens), mapped to the vault palette so selected-sidebar state is visible and dark-mode-aware.
  • Added theme-tokens.test.ts, which fails the build if the vault is ever missing a --color-* token the desktop defines — turning a recurring invisible-UI bug into a compile-time error.

Change vault email (full stack)

  • Vault server: changeEmailStart/changeEmailVerify accept the daemon secret-credential bearer in addition to the browser cookie (browser path unchanged; bearer path carries the anti-phishing binding in request/response). New bearer-authed GET /vault/api/vault-email.
  • Proto + Go daemon: GetVaultEmail / ChangeVaultEmailStart / ChangeVaultEmailVerify RPCs proxying to the vault via the active remote connection.
  • Master password set/change and notification-server host are similarly bridged daemon ⇄ vault (Argon2id parity verified by cross-platform test vectors).

Verification

  • Desktop + vault typecheck clean; Go builds; web vault production build succeeds; 193 vault tests pass (store, api-service, account-settings view, theme-token contract, crypto vectors).

Review focus / not runtime-tested

  • The live remote-vault connect/merge and end-to-end email-change round-trip need the running daemon + vault server + real email send — compiled/typechecked but not executed here. The vault-server bearer-auth branch for email change is security-sensitive.
  • Notifications parity is partial: the shared UI is in place, but desktop still uses the gateway notify-service while the web vault uses seed-signed registration. Full parity needs a daemon API to sign notification requests with the account key (noted as a TODO in NotificationEmailSettings).
  • The legacy drag-to-reorder accounts affordance was not carried into the new sidebar.

🤖 Generated with Claude Code

@ericvicenti ericvicenti marked this pull request as draft July 7, 2026 13:28
@ericvicenti ericvicenti force-pushed the feat/account-settings-2 branch from 9dd7206 to a5fbf49 Compare July 8, 2026 14:50
ericvicenti and others added 28 commits July 10, 2026 18:35
Add an "Account Settings" entry to the logged-in account dropdown that
opens a new desktop page for managing accounts. The page has an account
sidebar (with Add account / Import key) and a tabbed detail view:

- Account: edit profile, view public profile, copy ID, export key
  (password dialog), and delete account (removes the key from the cloud
  vault + all signed-in devices, with confirmation).
- Notifications: per-account email notification settings, ported from the
  web vault's AccountNotificationsSection, reusing the desktop
  notification-config hooks.
- Devices: list of the account's delegated keys via ListCapabilities.

The active account and tab are persisted in the route so a refresh
restores the same place, using the shared PageTabs component.

Share an ImportKeyDialog component in @shm/ui between desktop and the web
vault (refactor the vault's ImportAccountDialog to consume it); make two
latent type-only imports in @shm/ui input/label correct so the vault's
verbatimModuleSyntax build passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…lt settings

- Add Account / Notifications / Devices tabs to the per-account view, using
  the shared PageTabs component; the active account and tab are persisted in
  the route so a refresh restores the same place.
- Notifications tab: per-account email notification settings, ported from the
  web vault, reusing the desktop notification-config hooks.
- Devices tab: lists the account's delegated keys via ListCapabilities.
- Add a Vault Settings entry above the account list (route view: 'vault') for
  vault-wide settings: switch between local and remote storage, connect to a
  remote vault via the standard login/register dialog (custom server URL
  supported), read-only URL while connected, force sync, non-destructive
  switch-to-local (keeps keys), and log out (clears local keys -> zero
  accounts). Remote connect relies on the daemon's automatic local->remote
  identity merge in finishConnection.
- Remove the vault settings from the App Settings window (VaultBackendSettings
  and its now-obsolete test); vault management now lives in Vault Settings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bring the web vault's "change email address" flow to the desktop Vault
Settings page, sharing the UI with the vault and authenticating the
operation via the daemon's existing remote-vault credential.

Vault server (vault/src):
- changeEmailStart/changeEmailVerify now accept the daemon secret-credential
  bearer in addition to a browser cookie session. The browser path is
  unchanged; the bearer path carries the anti-phishing binding in the
  request/response instead of an httpOnly cookie. Add a bearer-authed
  GET /vault/api/vault-email to read the current vault user email.

Proto + Go daemon:
- Add GetVaultEmail / ChangeVaultEmailStart / ChangeVaultEmailVerify RPCs
  (regenerated). The daemon proxies to the vault server using the active
  remote connection's bearer credential.

Shared UI (@shm/ui):
- Extract CodeInput and a new two-step ChangeEmailDialog into @shm/ui, used
  by both the desktop dialog and the web vault. The vault's routed
  change-email pages are replaced with the shared dialog opened from
  SettingsView; obsolete views/routes/store actions are removed. Fix the
  code-input border to use border-border (border-input was invisible against
  the background) and autofocus the first cell.

Desktop:
- Vault Settings shows the current vault email and a Change button (when
  connected to a remote vault) that opens the shared dialog, backed by the
  new daemon RPC hooks.

Browser email-change flow and all vault tests are preserved (98 vault tests
pass); Go builds; desktop + vault typecheck clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bring the web vault's "set master password" workflow to the desktop Vault
Settings page, sharing UI with the vault, and add a master-password fallback
to the vault sign-in flow.

Crypto / daemon:
- The daemon derives the password credential entirely in-process: Argon2id
  (golang.org/x/crypto/argon2) -> HKDF-SHA256 split -> XChaCha20-Poly1305 wrap
  of the in-daemon DEK. Verified byte-for-byte identical to the vault's
  hash-wasm + WebCrypto derivation (password_test.go: cross-platform vector +
  wrap round-trip), so a password set on desktop unlocks the vault on the web.
  The plaintext password and the DEK never leave the daemon.
- New RPCs GetVaultPasswordStatus + SetVaultMasterPassword (proxy to the vault
  using the active remote credential; daemon picks add vs change).

Vault server:
- addPassword/changePassword accept the daemon secret-credential bearer in
  addition to a browser cookie (requireVaultAccess). Browser flow unchanged;
  updated the browser-only-routes test to use addSecretCredential.

Shared UI (@shm/ui):
- New PasswordInput (with strength meter + checkPasswordStrength) and
  SetMasterPasswordDialog, used by both the desktop app and the web vault.

Desktop:
- Vault Settings shows master-password status + a Set/Change button backed by
  the new daemon RPC hooks.

Web vault:
- SettingsView opens the shared dialog (one non-navigating store action);
  removed the obsolete AddPasswordView/ChangePasswordView + routes.
- LoginView: passkey stays primary, with the master password behind a fallback
  button that also auto-reveals when a passkey attempt fails (when a password
  is set), authenticating via the existing password-login path.

Verified: go build + vault Go tests pass; vault typecheck + 196 vault tests
pass; desktop typecheck clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…web vault polling

Bring the web vault's "notification server URL" setting to the desktop Vault
Settings page, sharing the UI, and make the value sync between desktop and the
web vault. Also poll the web vault so it picks up changes made elsewhere.

Shared UI (@shm/ui):
- New NotificationServerDialog, used by both the desktop app and the web vault
  (matches the vault UX: current URL, input, server-default hint, save/cancel).

Notification server now lives in the synced vault state (not a device-local
setting):
- The daemon vault State already carried NotificationServerURL with merge-on-
  sync; expose it via GetVaultNotificationServer / SetVaultNotificationServer
  RPCs. Set writes the state via applyMutation and schedules a remote sync, so
  it propagates to the web vault and other devices.
- Desktop: useVaultNotificationServer / useSetVaultNotificationServer hooks;
  Vault Settings reads/writes the synced value. useNotifyServiceHost now prefers
  the vault-state value (falling back to the local app setting, then default),
  so per-account notifications use the synced server. Removed the now-redundant
  Notify service host row from App Settings.
- Web vault: SettingsView opens the shared dialog (backed by the existing
  saveNotificationServerUrl); removed the obsolete ChangeNotifyServerUrlView +
  route + test.

Web vault sync polling:
- RootLayout polls getVault({knownVersion}) every 15s while unlocked; on a
  version change it re-hydrates the vault from fresh server data. Skipped while a
  load/save is in flight to avoid clobbering local state.

Verified: go build + vault Go tests pass; desktop typecheck clean; vault
typecheck + tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extract the web vault's security/settings rows into a shared @shm/ui
VaultSecuritySettings component (built on the shared SettingsSection/SettingsRow
and the already-shared change dialogs) so the web vault and desktop look and
behave the same.

- Web vault SettingsView becomes a thin wrapper around VaultSecuritySettings.
- Desktop Vault Settings is rewritten to the same SettingsSection/SettingsRow
  design: a STORAGE section (local/remote + sync), the shared security settings
  (passkey, master password, email, notify server) when connected to a remote
  vault, the notify server alone when local, and a REMOTE VAULT log-out section.
- Desktop can't do WebAuthn, so the Passkeys row redirects to the browser
  ("Manage in browser" -> {remoteVaultUrl}/settings) to manage passkeys.
- Remove the now-orphaned desktop change-vault-email-dialog wrapper and the
  unused InfoRow helper.

Verified: desktop + vault typecheck clean; 194 vault tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ts them

Phase 1 (foundation) of unifying the account-settings UI across desktop and the
web vault. Add two routing-agnostic shared components to @shm/ui:

- AccountSettingsLayout: the sidebar (Vault Settings entry + selectable account
  list + Add account / Import key) and the detail pane. Platforms inject the
  account list + selection state and wire onSelect* to their own router.
- AccountSettingsTabs: the per-account Account / Notifications / Devices tab bar
  (activeTab + onTabChange), routable per platform.

The desktop account-settings page now renders these instead of its inline
sidebar and PageTabs, so the layout is shared. Behavior/URLs unchanged.

Web vault routing + adoption comes in the following commits.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the old VaultView shell with the shared @shm/ui
AccountSettingsLayout + AccountSettingsTabs on the web vault, matching the
desktop account-settings experience. Every state now has a URL:

  /                       -> redirect to first account (or /settings)
  /accounts/:accountId    -> account tab
  /accounts/:id/:tab      -> account / notifications / devices tab
  /settings               -> Vault Settings (security)

The new AccountSettingsView reads selection + tab from react-router and
drives the Valtio store (selectAccount, ensureProfileLoaded,
setCreatingAccount, importAccount). Notifications reuse the existing
AccountNotificationsSection; devices render the account delegations; the
account tab shows the account ID (profile/export/delete land in Phase 2).

VaultView/AccountsView/NotificationsView remain for now and are removed in
a later phase.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extract the Account-tab content (identity card, view/copy ID, export key,
edit profile, and a delete "danger zone" with confirmation) into a shared
@shm/ui AccountProfilePanel. Both platforms now render identical UI and
inject only their backend-specific bits:

- desktop: HMIcon avatar, native file-picker export via the daemon,
  useDeleteKey, edit-profile dialog, view-public-profile navigation.
- web vault: <img>/initial avatar, keyfile.create + browser blob download,
  store.deleteAccount, AccountProfileDialog.

The web Account tab now has full parity (it previously only showed the ID).
The desktop delete-confirmation AlertDialog and password export dialog move
into the shared component, removing the duplicated markup.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…p + web

Extract the per-account email-notification UI into a shared @shm/ui
NotificationEmailSettings component so both platforms render identical UI.
Each keeps its own data layer (web signs requests to the notification
server with the account key; desktop talks to the gateway notify-service
over the daemon), mapping its state into normalized props. Platform-only
capabilities are optional props: web's "Register account" step and desktop's
"Resend verification". The web vault gains a free email input (prefilled
with the session email) and an Edit Email action for consistency.

This is the first step toward full parity; the remaining behavioral
unification (desktop adopting the web's seed-signed registration model)
needs a daemon API to sign notification requests with the account key, noted
as a TODO in the shared component.

Tests updated to the new shared UX labels.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extract the delegated-keys ("devices") list into a shared @shm/ui
DelegatedKeysList that renders a normalized DelegatedKeyItem[]. Each platform
maps its own source: the desktop app from the daemon's ListCapabilities
(agent/writer grants), the web vault from the account's stored delegated
sessions (with per-device icons). Removes the duplicated row markup and the
desktop's stray "Coming soon." empty-state copy now that Devices is real.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The web vault's unlocked UI is now fully served by the shared
AccountSettingsView (sidebar + per-account tabs + Vault Settings) wired to
real routes, so the old tabbed VaultView shell and its AccountsView /
NotificationsView panels are dead code. Remove them and their tests.

The one VaultView responsibility not already covered — the "Desktop app
connected" success banner (vaultConnectionSuccessMessage) — moves into
AccountSettingsView. A new AccountSettingsView.test.tsx covers routed
account selection, sidebar clicks, and the connection banner. The obsolete
hash-based account-selection tests are dropped in favor of real path routes.

Note: the legacy drag-to-reorder accounts affordance is not carried over to
the new sidebar; revisit if account ordering is needed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Account and tab selection moved to real router paths (/accounts/:id/:tab),
which the server sees on reload and would require an SPA fallback for
/vault/accounts/*. Restore the prior behavior: selection lives in the URL
hash (/vault#/a/<principal>/<tab>), which browsers never send to the server
and which pushState keeps client-side, so a reload only needs /vault to
resolve.

Vault Settings stays the one server-visible path (/settings) since the
desktop app deep-links to {remoteVaultUrl}/settings for passkey management.
AccountSettingsView now parses location.hash for the selection and navigates
with {pathname: '/', hash} instead of path params; the router drops the
/accounts/* routes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
After a refresh + passkey unlock, the user was bounced to the default
account instead of the screen they left. During unlock the view mounts
(decryptedDEK is set) one tick before loadVaultData populates the accounts,
so the "no valid account selected" redirect fired against an empty account
list and navigated to /settings — clobbering the restored
#/a/<principal>/<tab> hash before the accounts existed to match it.

Guard the redirect on vaultData being loaded: while loading, leave the hash
untouched; once the accounts arrive, the hash resolves to the selected
account and no redirect happens. Adds a regression test for the load gap.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The shared @shm/ui account-settings sidebar styles the selected item with
bg-sidebar-accent / text-sidebar-accent-foreground, but the vault theme only
defined --accent, not the --color-sidebar* tokens. Those utilities resolved
to no color, so the active selection was invisible on the web vault.

Map the sidebar tokens to the vault palette in @theme inline (sidebar-accent
→ accent), making the selection visible and mode-aware, consistent with the
desktop app.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rather than patch missing color tokens one at a time as shared @shm/ui
components surface them, define the full set the desktop app exposes. Adds
the remaining tokens (chart-1..5, selection, selection-strong, and the
sidebar-primary/-foreground/-ring variants); sidebar and selection map to the
vault palette so they stay on-brand and adapt to dark mode automatically.

Adds theme-tokens.test.ts, which fails if the vault is ever missing a
--color-* token the desktop app defines — turning the recurring invisible-UI
bug into a build-time error.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… web

The web vault's create-account form has a "Notify me at <email>" checkbox
that registers the new account for notifications on creation; the desktop
dialog had no such option and was titled "Create Profile".

Add an optional notificationOption checkbox slot to the shared @shm/ui
EditProfileForm, and wire the desktop create-account dialog to show it when a
vault email is available (logged into a remote vault), defaulting on. On
submit it registers the new account's notification config with that email via
the gateway notify-service. Retitle the dialog "Create Account" to match.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…dialogs

The create-account dialogs were two separate implementations: the web vault's
AccountProfileDialog (plain inputs) and the desktop's EditProfileForm
(react-hook-form). Adding the notification checkbox to both was parallel code,
not shared code.

Extract a lean, backend-agnostic @shm/ui AccountProfileForm (avatar + name +
optional description + optional email-notification opt-in) built only from
@shm/ui primitives — no app-context, router, or form-library deps — so the
decoupled vault can consume it. Both platforms now render this one component:

- the vault's AccountProfileDialog becomes a thin dialog wrapper around it
  (covering both create and edit), resolving the existing avatar URL via its
  own store and mapping the picked file back;
- the desktop create dialog renders it directly, mapping to its daemon-backed
  account creation, and gains the description field so the two forms match.

Reverts the one-off notificationOption added to EditProfileForm; the desktop
edit flow and web-app onboarding keep EditProfileForm unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a hover-revealed 3-dots options menu to each account row in the shared
AccountSettingsLayout sidebar (both platforms), using the toolkit's
DropdownMenu. The menu has Copy account ID, Export key, and Delete account.

Export key and Delete account reuse the same dialogs as the Account tab: the
export password dialog and delete confirmation are extracted into shared
@shm/ui ExportKeyDialog / DeleteAccountDialog, which AccountProfilePanel now
also consumes. Each platform wires the per-account actions to its own
backend (web: keyfile blob download + store.deleteAccount; desktop: daemon
export via the native file picker + deleteKey), targeting the hovered account
independently of the one open in the detail pane.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…button

Replace the static "Account Settings" title in the detail pane (above the
tabs) with a shared AccountSettingsHeader showing the account's icon and name
on the left and an "Open Profile" button (external-link icon) on the right.

Both platforms use the shared component and wire navigation their own way:
desktop opens the in-app profile route; the web vault opens the public
profile (createWebHMUrl on the backend host) in a new tab.

Removes the now-redundant "View public profile" action from
AccountProfilePanel (the header supersedes it).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Importing createWebHMUrl pulled @shm/shared/constants into the vault's
browser bundle, whose DEFAULT_GATEWAY_URL reads `process.env.VITE_GATEWAY_URL`
unguarded at module load — throwing "process is not defined" in the Bun
browser runtime and breaking the vault app.

The Open Profile link only needs `${host}/hm/<principal>`, exactly what
createWebHMUrl produces, so build it inline and drop the import. No other
@shm/shared/constants usage remains in the bundle (the only process.env left
is protobuf-es's, which is typeof-guarded).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Render "Add account" as the last item in the account list — styled like an
account row with a dashed plus-in-avatar — instead of a footer action. The
footer now holds only "Import key".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ivider

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ifications

Per-account tabs are now just Devices (default/first) and Notifications. The
old Account tab's profile panel is gone — its actions already live elsewhere:
the identity card is the header, and Copy account ID / Export key / Delete
account are in the row's 3-dots menu. "Edit profile" moves into that menu too
(both platforms), so nothing is lost.

- Shared: AccountSettingsTabs drops 'account' and reorders Devices→Notifications;
  accountSettingsTabSchema narrows to ['devices','notifications']; the sidebar
  options menu gains Edit profile.
- Web + desktop: default tab is 'devices'; the Account tab branch and its
  AccountTabContent/AccountTab are removed; edit-profile is wired at the
  view/page level off the menu. Old #/a/<id>/account hashes fall back to devices.
- Removed the now-unused shared AccountProfilePanel.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@shm/ui and @shm/shared are file: deps that Bun's isolated linker hard-links
into the vault's store; an editor's atomic write breaks that link, so source
edits don't reach the running app until `bun install` re-links AND the
--hot server restarts. Make `bun dev` run `bun install` first (new
`sync-shared` script) so editing a shared component only requires restarting
`bun dev`, not remembering a separate install step.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…show email

Rename the sidebar entry and detail-pane heading from "Vault Settings" to
"Identity Settings". The sidebar entry now shows the signed-in email beneath
the label (a new optional sublabel on the shared SidebarItem): the web vault
passes its session email; the desktop passes the remote vault email (empty,
so hidden, on a local vault).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…te vault

useVaultEmail kept its last value after switching to a local vault, so the
email lingered under the Identity Settings sidebar entry. Gate it on the
remote connection status (useVaultStatus, which is invalidated on
disconnect), so it disappears immediately when switching to local.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ericvicenti and others added 25 commits July 10, 2026 18:35
The web vault rendered a gray initial-in-circle when an account had no profile
image, unlike desktop/web-app which show the jdenticon identicon (via
HMIcon → UIAvatar). Render the shared UIAvatar in the vault's AccountAvatar,
keyed on the same hypermedia id (`hm://<principal>`) so the generated avatar
is identical across platforms.

Also guard Identicon's jdenticon.update() (browser-only) so UIAvatar no longer
throws in SSR/test environments.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Background/scheduled notification syncs resolved the host from the local
default (appStore / NOTIFY_SERVICE_HOST) and ignored the vault's synced
notification server URL — so even when the UI showed the right server (e.g.
the web vault's), the sync hit a different host, set lastSyncError, and the
account read as "not connected to the notification server".

resolveNotifyHost now prefers the connected vault's notification server URL
(grpc getVaultNotificationServer) — matching the web vault and the UI — and
falls back to the local default only when the vault has none.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The daemon's SignData looks up the key by NAME, but buildDesktopSigner passed
the accountUid (public key) as the signing key name. Accounts whose key name
differs from the public key (e.g. imported keys keep their original name)
failed to sign, so their notification sync errored and they showed
"not connected to the notification server" — while an account whose name
happened to equal its public key worked.

Resolve the real key name via daemon listKeys (matching publicKey, like the
export flow) before signing, with a small cache.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The main process has no notify host: NOTIFY_SERVICE_HOST is only injected
into the renderer's Vite build (where the UI shows it), the synced vault URL
was empty, and appStore was unset — so resolveNotifyHost returned null and
every account's background sync failed with "not connected", regardless of
account.

The renderer already passes the resolved host to getConfig on each tab view;
persist it into appStore there (and schedule a re-sync if it changed) so the
main process's background syncs resolve the same host the UI uses.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ncs in

On first load an account may still be syncing in from the remote vault, so
getAccount 404s transiently — ensureProfileLoaded immediately concluded
"not_found" and the name rendered "Profile not found". Add a 'loading' state
(shown as "Loading…") set while fetching, and retry 404s a few times with
backoff before settling on not_found, so a freshly-synced account resolves to
its real name instead of flashing not-found.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a dedicated, greppable logger (seed/vault-merge, prefix "🔑 VAULT-MERGE")
that dumps the local, remote, and merged identity sets during a local→remote
vault sign-in, so a lost signing key can be traced to its source. Suppress all
other logging via SEED_LOG_ONLY (Go subsystems + the desktop winston console),
wired into the mprocs desktop pane so `./dev up` shows only the merge.

Also make GetKey resolve by principal when the name lookup misses: a key's name
is not guaranteed to equal its public key (imported/renamed keys), and callers
routinely pass a principal as the identifier, which otherwise fails with
"named key not found".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
New users arriving from the desktop app already chose to sign in there,
so skip the extra /connect confirmation screen: CreateProfileView now
calls completeVaultConnection directly after createAccount succeeds
(falling back to the confirm screen if the connection fails).

Because completeVaultConnection now runs from /profile/create instead of
/connect, getCurrentVaultBaseURL can no longer derive the vault URL from
the current location path (the daemon rejects the payload on any URL
mismatch, e.g. ".../vault/profile" vs ".../vault"). Derive it from the
origin plus the fixed VAULT_BASENAME, shared with the router basename.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The desktop Vault Connect flow was one-shot and silent: the connect
token and payload poll expired after 2 minutes (shorter than a real
new-user registration with email verification), a single transient
fetch error or remote write conflict killed the flow permanently, and
the failure reason never surfaced anywhere.

- Extend connectionTokenTTL and the poll window to 15 minutes (matching
  the email-code expiry), and the vault server's stored payload TTL to
  match.
- Keep polling through transient payload-fetch errors instead of
  aborting; report the last error if the deadline is reached.
- Retry finishConnection on remote write conflicts (the web app writes
  the vault concurrently right when it posts the payload).
- Log every step of the flow under the "VAULT-CONNECT" prefix on the
  forwarded seed/vault-merge logger.
- Record why the last connect attempt died and expose it as
  last_connect_error on GetVaultStatusResponse so UIs can show it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Instead of waiting on a spinner forever when the daemon's connect
attempt dies, the sign-in dialog now watches lastConnectError from
GetVaultStatus and shows what went wrong with recovery guidance, a
Try Again button that restarts the flow, and Cancel re-enabled.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
After the desktop app is linked, navigate to /connect/success (narrow
layout) instead of landing on the main vault page with a dismissible
banner. The page tells the user to return to the desktop app, with an
"Open desktop app" button (a new hm://open deep link that just brings
the app to the front) and a "Manage my account" button to the vault
home.

ConnectRouteView only redirects to "/" when it never had a pending
request: completing the flow clears the request while the route is
still mounted, and redirecting then would win the race against the
in-flight navigation to /connect/success. Covered by a full-router
regression test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The account dropdown header already shows the current account, so
listing it again as a switch option visually repeated it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The vault registration step (Create your profile) had its own
hand-rolled form that drifted from the shared Create Account dialogs;
it now renders the shared AccountProfileForm.

The shared form adopts the registration step's better layout: name
first with the avatar picker below it (the clickable photo square with
inline preview, replacing the file-input row). All create-account
forms hide the bio/description field; the Edit Profile dialog keeps it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The vault and agents workspaces consume frontend/packages/* as file:
dependencies that bun install copies into node_modules, so editing the
shared packages while `./dev up` was running left the copies stale
until a manual reinstall.

Their dev scripts now run through scripts/watch-file-deps.ts, which
installs deps, starts the server, and watches every file: dependency's
source — re-running bun install and restarting the server on change.
A quiet window after each install stops the watcher from re-triggering
on bun's own writes into the shared packages' node_modules.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The vault's theme tokens were a hand-copied snapshot of the desktop
palette and had drifted (--input, --secondary, --highlight, dark
selection/sidebar, fonts), making shared @shm/ui components render
differently — e.g. grey input fields and grey outline buttons in the
vault where desktop shows near-white.

Move the palette into @shm/ui/theme.css and the shared variants/base
styles (dark variant, base layer, ScrollArea fixes) into
@shm/ui/base.css, imported by all three apps. Drop the vault's global
a/::selection/reset rules so it matches desktop, and align web's one
divergent token (light --secondary) to desktop's value.

theme-tokens.test.ts now asserts every app imports the shared files,
redefines none of the tokens the theme owns, and that the vault's
file:-dep copies are in sync.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…o /settings

The desktop app's "Manage Passkeys" deep-links to /vault/settings, but a
locked or signed-out browser session lost that destination and landed on
the root, which auto-selects the first account. Record a returnToPath
when auth interrupts a protected route and consume it after unlock. The
redirect target is frozen on the first unlocked render: the async store
notification for clearing returnToPath could re-fire the still-mounted
<Navigate> toward '/' and override the redirect (reproduced in the new
settings-deeplink tests).

Password-only sessions also had a broken unlock path:

- /login rendered no password form when arriving with a remembered
  session, because userHasPassword/userHasPasskey were only set by
  preLogin; checkSession now syncs them from the session credentials.
- The locked screen showed an "or" divider with no passkey option above
  it; it now renders the divider only when both options exist — and when
  the password is the only option, skips the screen entirely, forwarding
  straight to the password form.
- PasswordInput declared an autoFocus prop but never forwarded it to the
  input, so the password field never autofocused.
- "Use different email" on /login now logs out an authenticated locked
  session instead of bouncing straight back to /login.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rename all user-facing "master password" copy to just "password" across
the vault web app, the shared security settings UI, and the desktop
toasts, and update the tests that assert on the old wording. Internal
identifiers (SetMasterPasswordDialog, the setVaultMasterPassword RPC)
are unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
LoginView depends on in-memory flow state (email + credential flags from
preLogin or a remembered session). Reloading /vault/login while logged
out had none of it, rendering an empty card with no sign-in form. Wait
for the session check and restart at pre-login when the email is missing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nish "master" rename

The lock screen now renders the password form inline instead of detouring
through /login, titles itself "Unlock your vault", and shows which account
is signed in. Also removes the remaining "master" identifiers: rootKey in
crypto code, SetPasswordDialog in @shm/ui, and setPasswordFromDialog in the
vault store.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UNjCZ6cyJ9uod4mjnns8mc
…file button

Move the settings tabs (Connected Devices / Notifications) into the shared
AccountSettingsHeader alongside a green "My Profile" button, drop the separate
icon+name header, and render the active tab label as an h2. Reorder tabs to lead
with Connected Devices and expose ACCOUNT_SETTINGS_TAB_LABELS. Applied across the
desktop app and the web vault.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the connect-success card with a welcome screen: a personalized
"You're all set, {name}" title from the account profile, a link to account
settings, and a single vibrant-green "Open Seed" button matching the Publish
button's green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
AlertDialogContent's inner wrapper had no flex/gap (unlike DialogContent),
so confirmation dialogs rendered vertically cramped; AlertDialogAction/
Cancel also applied buttonVariants even when asChild-wrapping a styled
Button, concatenating two variants' classes and letting stylesheet order
pick the winner (red destructive buttons with dark text). Fix both in the
shared component, convert the identity-settings dialogs to the canonical
Header/Footer + variant-prop structure, and replace the two divergent
logout confirmations (account dropdown vs Identity Settings) with a shared
LogoutVaultDialog.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UNjCZ6cyJ9uod4mjnns8mc
The success page was restyled to the "You're all set" welcome
(button "Open Seed", link "account settings.") but this test still
asserted the old "Open desktop app"/"Manage my account" text.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012PcXpTnVq45xwRBUMgczVp
@ericvicenti ericvicenti force-pushed the feat/account-settings-2 branch from 05609c1 to c36a218 Compare July 10, 2026 16:38
- vault.go: use %w to wrap lastFetchErr (errorlint)
- password.go: annotate HKDF info-label constants as //nolint:gosec
  (G101 false positive; they are domain-separation strings, not secrets)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012PcXpTnVq45xwRBUMgczVp
@ericvicenti ericvicenti marked this pull request as ready for review July 11, 2026 06:47
@ericvicenti ericvicenti merged commit b052b5b into main Jul 11, 2026
10 checks passed
@ericvicenti ericvicenti deleted the feat/account-settings-2 branch July 11, 2026 06:47
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