diff --git a/src/components/AccountManager.tsx b/src/components/AccountManager.tsx
new file mode 100644
index 0000000..e47fe1a
--- /dev/null
+++ b/src/components/AccountManager.tsx
@@ -0,0 +1,158 @@
+import { useObservableMemo } from "applesauce-react/hooks";
+import { Check, User, UserX, UserPlus } from "lucide-react";
+import { toast } from "sonner";
+import accountManager from "@/services/accounts";
+import { useProfile } from "@/hooks/useProfile";
+import { getDisplayName } from "@/lib/nostr-utils";
+import { useAppShell } from "@/components/layouts/AppShellContext";
+import { Button } from "@/components/ui/button";
+import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import Nip05 from "@/components/nostr/nip05";
+
+function AccountCard({
+ account,
+ isActive,
+}: {
+ account: any;
+ isActive: boolean;
+}) {
+ const profile = useProfile(account.pubkey);
+ const displayName = getDisplayName(account.pubkey, profile);
+
+ const handleSwitch = () => {
+ accountManager.setActive(account.id);
+ toast.success("Switched account", {
+ description: `Now using ${displayName}`,
+ });
+ };
+
+ const handleRemove = () => {
+ const confirmRemove = window.confirm(
+ `Remove account ${displayName}? This cannot be undone.`,
+ );
+ if (confirmRemove) {
+ accountManager.removeAccount(account);
+ toast.success("Account removed", {
+ description: `Removed ${displayName}`,
+ });
+ }
+ };
+
+ return (
+
+
+
+
+ {isActive && (
+
+ )}
+
+
+
+
+
+
+
+
{displayName}
+ {profile && (
+
+
+
+ )}
+
+ {account.pubkey.slice(0, 8)}...{account.pubkey.slice(-8)}
+
+
+
+
+ {!isActive && (
+
+ )}
+
+
+
+
+
+ );
+}
+
+/**
+ * AccountManager - Shows all accounts with management actions
+ */
+export default function AccountManager() {
+ const activeAccount = useObservableMemo(() => accountManager.active$, []);
+ const allAccounts = useObservableMemo(() => accountManager.accounts$, []);
+ const { openCommandLauncher } = useAppShell();
+
+ const handleAddAccount = () => {
+ openCommandLauncher();
+ };
+
+ return (
+
+
+
+
+
+ Accounts
+
+
+
+
+ {allAccounts.length === 0 ? (
+
+
+
No accounts yet
+
+ Use the "Add Account" button or type{" "}
+
+ login
+ {" "}
+ in the command launcher
+
+
+ ) : (
+ allAccounts.map((account) => (
+
+ ))
+ )}
+
+
+
+
+
+ Tip: You can also switch accounts from the user menu in the
+ top-right corner
+
+
+
+
+ );
+}
diff --git a/src/components/LogoutHandler.tsx b/src/components/LogoutHandler.tsx
new file mode 100644
index 0000000..35f9e23
--- /dev/null
+++ b/src/components/LogoutHandler.tsx
@@ -0,0 +1,86 @@
+import { useEffect } from "react";
+import { toast } from "sonner";
+import { LogOut } from "lucide-react";
+import accountManager from "@/services/accounts";
+import { useObservableMemo } from "applesauce-react/hooks";
+
+interface LogoutHandlerProps {
+ action: "logout" | "logout-all";
+ all: boolean;
+}
+
+/**
+ * LogoutHandler - Executes logout command actions
+ *
+ * This component handles the result of the /logout command:
+ * - logout: Removes the active account
+ * - logout-all: Removes all accounts
+ */
+export default function LogoutHandler({ action, all }: LogoutHandlerProps) {
+ const activeAccount = useObservableMemo(() => accountManager.active$, []);
+ const allAccounts = useObservableMemo(() => accountManager.accounts$, []);
+
+ useEffect(() => {
+ const handleLogout = () => {
+ if (action === "logout-all" || all) {
+ // Remove all accounts
+ if (allAccounts.length === 0) {
+ toast.info("No accounts to remove", {
+ icon: ,
+ });
+ return;
+ }
+
+ const confirmLogoutAll = window.confirm(
+ `Remove all ${allAccounts.length} account(s)? This cannot be undone.`,
+ );
+
+ if (!confirmLogoutAll) {
+ toast.info("Logout cancelled", {
+ icon: ,
+ });
+ return;
+ }
+
+ // Remove all accounts
+ allAccounts.forEach((account) => {
+ accountManager.removeAccount(account);
+ });
+
+ toast.success("All accounts removed", {
+ description: `Removed ${allAccounts.length} account(s)`,
+ icon: ,
+ });
+ } else {
+ // Remove only active account
+ if (!activeAccount) {
+ toast.info("No active account to remove", {
+ description: "You are not logged in",
+ icon: ,
+ });
+ return;
+ }
+
+ accountManager.removeAccount(activeAccount);
+
+ toast.success("Logged out", {
+ description: `Removed ${activeAccount.pubkey.slice(0, 8)}...${activeAccount.pubkey.slice(-8)}`,
+ icon: ,
+ });
+ }
+ };
+
+ handleLogout();
+ }, [action, all, activeAccount, allAccounts]);
+
+ // This component doesn't render anything visible - it just executes the action
+ return (
+
+
+
+
Processing logout...
+
This window can be closed.
+
+
+ );
+}
diff --git a/src/components/WindowRenderer.tsx b/src/components/WindowRenderer.tsx
index 90debd4..b7b08d7 100644
--- a/src/components/WindowRenderer.tsx
+++ b/src/components/WindowRenderer.tsx
@@ -34,6 +34,8 @@ const SpellbooksViewer = lazy(() =>
import("./SpellbooksViewer").then((m) => ({ default: m.SpellbooksViewer })),
);
const LoginHandler = lazy(() => import("./LoginHandler"));
+const AccountManager = lazy(() => import("./AccountManager"));
+const LogoutHandler = lazy(() => import("./LogoutHandler"));
// Loading fallback component
function ViewerLoading() {
@@ -185,6 +187,14 @@ export function WindowRenderer({ window, onClose }: WindowRendererProps) {
/>
);
break;
+ case "account-manager":
+ content = ;
+ break;
+ case "logout-handler":
+ content = (
+
+ );
+ break;
default:
content = (
diff --git a/src/types/app.ts b/src/types/app.ts
index 006975a..22a406d 100644
--- a/src/types/app.ts
+++ b/src/types/app.ts
@@ -19,7 +19,9 @@ export type AppId =
| "spells"
| "spellbooks"
| "win"
- | "login-handler";
+ | "login-handler"
+ | "account-manager"
+ | "logout-handler";
export interface WindowInstance {
id: string;
diff --git a/src/types/man.ts b/src/types/man.ts
index c20e258..399d62c 100644
--- a/src/types/man.ts
+++ b/src/types/man.ts
@@ -140,6 +140,43 @@ export const manPages: Record = {
},
defaultProps: { action: "open-dialog" },
},
+ accounts: {
+ name: "accounts",
+ section: "1",
+ synopsis: "accounts",
+ description:
+ "View and manage all Nostr accounts in Grimoire. Shows all logged-in accounts with their connection types, allows switching between accounts, and provides account management options.",
+ examples: ["accounts View all accounts"],
+ seeAlso: ["login", "logout", "profile"],
+ appId: "account-manager",
+ category: "System",
+ defaultProps: {},
+ },
+ logout: {
+ name: "logout",
+ section: "1",
+ synopsis: "logout [--all]",
+ description:
+ "Remove the active Nostr account from Grimoire. Use the --all flag to remove all accounts at once.",
+ options: [
+ {
+ flag: "--all",
+ description: "Remove all accounts instead of just the active one",
+ },
+ ],
+ examples: [
+ "logout Remove the active account",
+ "logout --all Remove all accounts",
+ ],
+ seeAlso: ["login", "accounts"],
+ appId: "logout-handler",
+ category: "System",
+ argParser: (args: string[]) => {
+ const all = args.includes("--all");
+ return { action: all ? "logout-all" : "logout", all };
+ },
+ defaultProps: { action: "logout", all: false },
+ },
kinds: {
name: "kinds",
section: "1",