diff --git a/desktop/src/features/identity-archive/hooks.ts b/desktop/src/features/identity-archive/hooks.ts
index 5f93c9f522..a00153ad51 100644
--- a/desktop/src/features/identity-archive/hooks.ts
+++ b/desktop/src/features/identity-archive/hooks.ts
@@ -101,7 +101,7 @@ export function useUnarchiveIdentityMutation() {
export type IdentityArchiveActions = {
/** Render guard only — the relay re-verifies authority on submit. */
canArchive: boolean;
- /** `undefined` while the snapshot loads — defer flair + Manage until known. */
+ /** `undefined` while the snapshot loads — defer flair + mutation affordances until known. */
isArchived: boolean | undefined;
isPending: boolean;
archive: () => void;
diff --git a/desktop/src/features/profile/ui/UserProfileAgentActions.tsx b/desktop/src/features/profile/ui/UserProfileAgentActions.tsx
index 510814dd15..b3b87d42ce 100644
--- a/desktop/src/features/profile/ui/UserProfileAgentActions.tsx
+++ b/desktop/src/features/profile/ui/UserProfileAgentActions.tsx
@@ -63,9 +63,8 @@ export function UserProfileAgentSettingsMenu({
onToggleAutoStart !== undefined;
const autoStartSwitchId = `user-profile-agent-auto-start-${actionKey}`;
const hasPrimaryActions = Boolean(onDuplicatePersona || onExportPersona);
- const hasArchiveAction =
- archiveActions?.canArchive === true &&
- archiveActions.isArchived !== undefined;
+ const hasArchiveAction = archiveActions?.canArchive === true;
+ const archiveStateKnown = archiveActions?.isArchived !== undefined;
const shouldConfirmAgentDelete =
managedAgent !== undefined && onDelete !== undefined;
const hasManageActions = hasArchiveAction || Boolean(onDelete);
@@ -146,7 +145,7 @@ export function UserProfileAgentSettingsMenu({
) : null}
{hasArchiveAction && archiveActions ? (
- archiveActions.isArchived ? (
+ archiveActions.isArchived === true ? (
setArchiveConfirmOpen(true)}
+ disabled={isPending || !archiveStateKnown}
+ onSelect={(event) => {
+ if (!archiveStateKnown) {
+ event.preventDefault();
+ return;
+ }
+ setArchiveConfirmOpen(true);
+ }}
>
- {archiveActions.isPending ? "Archiving…" : archiveLabel}
+ {archiveActions.isPending
+ ? "Archiving…"
+ : archiveStateKnown
+ ? archiveLabel
+ : "Checking archive status…"}
)
) : null}
@@ -186,11 +195,12 @@ export function UserProfileAgentSettingsMenu({
) : null}
- {hasArchiveAction && archiveActions ? (
+ {hasArchiveAction && archiveActions && archiveStateKnown ? (
{
+ if (archiveActions.isArchived !== false) return;
archiveActions.archive();
setArchiveConfirmOpen(false);
}}
@@ -245,8 +255,7 @@ export function UserProfileAgentSettingsMenuSlot({
personaActionKey?: string;
viewerIsOwner: boolean;
}) {
- const canShowArchiveAction =
- archiveActions.canArchive && archiveActions.isArchived !== undefined;
+ const canShowArchiveAction = archiveActions.canArchive;
const settingsActionPending =
isAgentActionPending || archiveActions.isPending;
const sharedProps = {
diff --git a/desktop/src/testing/e2eBridge.ts b/desktop/src/testing/e2eBridge.ts
index eaaf6eaa46..0b6092921c 100644
--- a/desktop/src/testing/e2eBridge.ts
+++ b/desktop/src/testing/e2eBridge.ts
@@ -106,12 +106,13 @@ type E2eConfig = {
stallWebsocketSends?: boolean;
userSearchDelayMs?: number;
// NIP-IA gate inputs — see tests/helpers/bridge.ts:MockBridgeOptions for
- // semantics. These three drive the archive-button gate matrix in
+ // semantics. These options drive the archive-button gate matrix in
// tests/e2e/identity-archive.spec.ts; they're plumbed into:
- // - `list_archived_identities` (archivedIdentities)
+ // - `list_archived_identities` (archivedIdentities / archivedIdentitiesDelayMs)
// - `resolve_oa_owner` (oaOwnerIsMe)
// - `resetMockRelayMembers` (relayRole)
archivedIdentities?: string[];
+ archivedIdentitiesDelayMs?: number;
oaOwnerIsMe?: boolean;
relayRole?: "owner" | "admin" | "member" | null;
// Descriptors returned by the mocked `pick_and_upload_media` /
@@ -7649,6 +7650,10 @@ export function maybeInstallE2eTauriMocks() {
return { owner, is_me: isMe };
}
case "list_archived_identities": {
+ const delayMs = activeConfig?.mock?.archivedIdentitiesDelayMs ?? 0;
+ if (delayMs > 0) {
+ await new Promise((resolve) => window.setTimeout(resolve, delayMs));
+ }
const archived = activeConfig?.mock?.archivedIdentities ?? [];
return { archived };
}
diff --git a/desktop/tests/e2e/identity-archive.spec.ts b/desktop/tests/e2e/identity-archive.spec.ts
index d6c5d0e0ab..d4992c9369 100644
--- a/desktop/tests/e2e/identity-archive.spec.ts
+++ b/desktop/tests/e2e/identity-archive.spec.ts
@@ -97,6 +97,26 @@ test.describe("NIP-IA archive button gate", () => {
).toBeVisible();
});
+ test("case 3b — verified OA owner viewing Alice while snapshot is pending: Archive visible but disabled", async ({
+ page,
+ }) => {
+ await installMockBridge(page, {
+ relayRole: null,
+ oaOwnerIsMe: true,
+ archivedIdentities: [],
+ archivedIdentitiesDelayMs: 60_000,
+ });
+ await openAliceProfile(page);
+ await openProfileSettingsMenu(page);
+
+ const archiveButton = page.getByTestId("user-profile-archive-identity");
+ await expect(archiveButton).toBeVisible();
+ await expect(archiveButton).toBeDisabled();
+ await expect(archiveButton).toContainText("Checking archive status…");
+ await archiveButton.click({ force: true });
+ await expect(page.getByTestId("archive-confirm-dialog")).toHaveCount(0);
+ });
+
test("case 4 — no authority viewing Alice: Archive hidden", async ({
page,
}) => {
diff --git a/desktop/tests/helpers/bridge.ts b/desktop/tests/helpers/bridge.ts
index 2d1cc2561f..82114a38e7 100644
--- a/desktop/tests/helpers/bridge.ts
+++ b/desktop/tests/helpers/bridge.ts
@@ -134,6 +134,8 @@ type MockBridgeOptions = {
* "Archived on this relay" flair + Unarchive button.
*/
archivedIdentities?: string[];
+ /** Delay (ms) for `list_archived_identities`; lets specs assert pending snapshot UI. */
+ archivedIdentitiesDelayMs?: number;
/**
* Drives the `is_me` field of `resolve_oa_owner`. When true, the harness
* reports the active identity as the verified NIP-OA owner of the viewee