diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index 673e9750..aac1d7ee 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -25,7 +25,6 @@ const AgentsPage = lazy(() => import("@/pages/agents/agents-page")); const AgentNewPage = lazy(() => import("@/pages/agents/agent-new")); const AgentDetailPage = lazy(() => import("@/pages/agents/agent-detail")); const FilesPage = lazy(() => import("@/pages/files/files-page")); -const ArtifactsPage = lazy(() => import("@/pages/artifacts/artifacts-page")); const ArtifactPage = lazy(() => import("@/pages/artifacts/artifact-page")); const AutomationsPage = lazy(() => import("@/pages/automations/automations-page")); const PluginsPage = lazy(() => import("@/pages/plugins/plugins-page")); @@ -36,13 +35,10 @@ const LoginsPage = lazy(() => import("@/pages/vault/logins-page")); const TotpPage = lazy(() => import("@/pages/vault/totp-page")); const CardsPage = lazy(() => import("@/pages/vault/cards-page")); const PasskeysPage = lazy(() => import("@/pages/vault/passkeys-page")); -const IntegrationsPage = lazy(() => import("@/pages/integrations/integrations-page")); -const MessagingPage = lazy(() => import("@/pages/messaging/messaging-page")); const BrowserPage = lazy(() => import("@/pages/browser/browser-page")); const ComputerPage = lazy(() => import("@/pages/computer/computer-page")); const VmsPage = lazy(() => import("@/pages/vms/vms-page")); const SshPage = lazy(() => import("@/pages/ssh/ssh-page")); -const AndroidPage = lazy(() => import("@/pages/android/android-page")); const AndroidDevicePage = lazy(() => import("@/pages/android/android-device-page")); const CallsPage = lazy(() => import("@/pages/calls/calls-page")); const RunnersPage = lazy(() => import("@/pages/runners/runners-page")); @@ -204,8 +200,8 @@ export function App() { } /> } /> } /> - } /> - } /> + } /> + : } /> } /> } /> } /> @@ -221,14 +217,14 @@ export function App() { } /> } /> } /> - } /> - } /> + } /> + } /> } /> } /> } /> } /> - } /> - } /> + } /> + : } /> } /> } /> } /> @@ -275,6 +271,13 @@ function NotFoundRedirect() { /** Links from before Mods were Plugins (notifications, chat notes): /mods?mod= → /plugins?plugin=. */ /** My tasks became the Inbox's board: old links (notifications, tickets) open the same task there. */ +/** A page that moved into Settings: its old address, with its query (?tab=, ?connection=), opens the section. */ +function ToSettings({ section }: { section: string }) { + const { search } = useLocation(); + if (section === "integrations" && new URLSearchParams(search).get("tab") === "api-keys") return ; + return ; +} + function MyTasksRedirect() { const params = new URLSearchParams(useLocation().search); if (!params.has("task")) params.set("view", "board"); diff --git a/apps/desktop/src/components/agents/agent-form.tsx b/apps/desktop/src/components/agents/agent-form.tsx index f7c7b884..bcb1c5de 100644 --- a/apps/desktop/src/components/agents/agent-form.tsx +++ b/apps/desktop/src/components/agents/agent-form.tsx @@ -367,7 +367,10 @@ export function AgentForm({ const inherited = useInheritedInstructions(values.workspaceId, undefined, values.projectId); const vmChoices = useVmChoices(); const androidOn = !!boot?.settings.android.enabled; - const sections = SECTIONS.filter((s) => (s.id !== "vm" || vmChoices.available) && (s.id !== "android" || androidOn)); + const integrationsOn = !!boot?.settings.integrations.enabled; + const sections = SECTIONS.filter( + (s) => (s.id !== "vm" || vmChoices.available) && (s.id !== "android" || androidOn) && (s.id !== "tools" || integrationsOn), + ); const { hash } = useLocation(); // Again once the VM or Android section shows up: they wait for the VM status and the settings (links from the VMs page go to #vm). @@ -761,20 +764,22 @@ export function AgentForm({ )} - -
- } - title="Inherit shared integrations" - description="Also use the global and workspace tools, MCP servers and Composio apps." - checked={values.inheritMcp} - onChange={(v) => set("inheritMcp", v)} - /> - - set("mcpServerIds", v)} /> -
-
+ {integrationsOn && ( + +
+ } + title="Inherit shared integrations" + description="Also use the global and workspace tools, MCP servers and Composio apps." + checked={values.inheritMcp} + onChange={(v) => set("inheritMcp", v)} + /> + + set("mcpServerIds", v)} /> +
+
+ )}
- + Manage tools
diff --git a/apps/desktop/src/components/android/android-chip.tsx b/apps/desktop/src/components/android/android-chip.tsx index f4e126db..a0ebf9ce 100644 --- a/apps/desktop/src/components/android/android-chip.tsx +++ b/apps/desktop/src/components/android/android-chip.tsx @@ -114,7 +114,7 @@ export function AndroidChip({ {!isLoading && devices.length === 0 ? (

No phones yet.

-
@@ -160,7 +160,7 @@ export function AndroidChip({
-
@@ -180,7 +180,7 @@ export function AndroidField({ value, onChange }: { value: string[]; onChange: (

No phones yet — connect one and this agent can use it.

@@ -194,7 +194,7 @@ export function AndroidField({ value, onChange }: { value: string[]; onChange: ( Can control - + Manage phones diff --git a/apps/desktop/src/pages/android/android-page.tsx b/apps/desktop/src/components/android/android-overview.tsx similarity index 80% rename from apps/desktop/src/pages/android/android-page.tsx rename to apps/desktop/src/components/android/android-overview.tsx index 955e2828..c76a2941 100644 --- a/apps/desktop/src/pages/android/android-page.tsx +++ b/apps/desktop/src/components/android/android-overview.tsx @@ -7,13 +7,13 @@ import type { AndroidDevice } from "@godmode/shared"; import { Button } from "@/components/ui/button"; import { Skeleton } from "@/components/ui/skeleton"; import { Spinner } from "@/components/ui/spinner"; -import { EmptyState, PageBody, PageHeader } from "@/components/common"; +import { EmptyState } from "@/components/common"; import { ConnectWifiDialog, EditPhoneDialog, ForgetPhoneDialog } from "@/components/android/android-dialogs"; import { CONNECTION_LABEL } from "@/components/android/android-parts"; import { DeviceCard } from "@/components/android/device-card"; import { toastApiError } from "@/components/vault/vault-utils"; import { api, errorMessage } from "@/lib/api"; -import { useAndroidDevices, useAndroidStatus, useSettings } from "@/lib/hooks"; +import { useAndroidDevices, useAndroidStatus } from "@/lib/hooks"; import { qk } from "@/lib/queryKeys"; const STEPS = [ @@ -38,7 +38,7 @@ function SetupSteps() { function DeviceGrid({ devices, onEdit, onForget }: { devices: AndroidDevice[]; onEdit: (d: AndroidDevice) => void; onForget: (d: AndroidDevice) => void }) { return ( -
+
{devices.map((device, i) => ( (null); @@ -86,35 +85,24 @@ export default function AndroidPage() { const adb = status.data?.adb; const pending = status.data?.pending ?? []; - const off = settings?.android.enabled === false; const connected = devices.filter((d) => d.state === "connected").length; return ( -
- } - title="Android phones" - description="Phones connected to this computer over USB or Wi-Fi. Agents see the screen, tap, type, open apps and run commands — you watch live and can take over any time." - actions={ - <> - - - - } - /> - - {off ? ( - } - title="Android phones are turned off" - description="Turn them on in Settings → Android phones to let Godmode look for phones and agents control them." - /> - ) : status.isLoading || list.isLoading ? ( -
+
+
+

{devices.length ? `${connected} of ${devices.length} connected` : "Phones"}

+
+ + +
+
+
+ {status.isLoading || list.isLoading ? ( +
{[0, 1].map((i) => ( ))} @@ -126,7 +114,7 @@ export default function AndroidPage() { description={ status.data?.canInstall ? "adb is Android's free debugging bridge from Google. Godmode installs its own copy (about 16 MB) — nothing else on your computer changes." - : "Install Android's platform-tools (adb) and make sure it's on your PATH, or set its path in Settings → Android phones." + : "Install Android's platform-tools (adb) and make sure it's on your PATH, or set its path below." } action={ status.data?.canInstall && ( @@ -196,10 +184,6 @@ export default function AndroidPage() { /> ) : ( <> -

- {connected} of {devices.length} connected · adb {adb?.version ?? ""} - {adb?.managed ? " (Godmode's copy)" : ""} -

d.state !== "disconnected")} onEdit={setEditing} onForget={setForgetting} /> {devices.some((d) => d.state === "disconnected") && (
@@ -211,7 +195,7 @@ export default function AndroidPage() { )}
)} - +
setEditing(null)} /> diff --git a/apps/desktop/src/components/android/phone-panel.tsx b/apps/desktop/src/components/android/phone-panel.tsx index 1c23ddee..5ab4cb6f 100644 --- a/apps/desktop/src/components/android/phone-panel.tsx +++ b/apps/desktop/src/components/android/phone-panel.tsx @@ -131,7 +131,7 @@ export function PhonePanel({ phones, activity, onHide }: { phones: AndroidDevice
diff --git a/apps/desktop/src/components/artifacts/artifact-view.tsx b/apps/desktop/src/components/artifacts/artifact-view.tsx index 2bf1d8c6..60c542fa 100644 --- a/apps/desktop/src/components/artifacts/artifact-view.tsx +++ b/apps/desktop/src/components/artifacts/artifact-view.tsx @@ -383,7 +383,7 @@ export function ArtifactView({ onOpenChange={setDeleting} onDeleted={() => { onClose?.(); - if (variant === "page") actions.navigate("/artifacts"); + if (variant === "page") actions.navigate("/settings/artifacts"); }} />
diff --git a/apps/desktop/src/pages/artifacts/artifacts-page.tsx b/apps/desktop/src/components/artifacts/artifacts-overview.tsx similarity index 80% rename from apps/desktop/src/pages/artifacts/artifacts-page.tsx rename to apps/desktop/src/components/artifacts/artifacts-overview.tsx index 9bd534b4..8ebdc284 100644 --- a/apps/desktop/src/pages/artifacts/artifacts-page.tsx +++ b/apps/desktop/src/components/artifacts/artifacts-overview.tsx @@ -6,7 +6,7 @@ import { toast } from "sonner"; import type { ArtifactSummary } from "@godmode/shared"; import { ARTIFACT_STARTERS, MASCOT_CHARACTER, MASCOT_COLOR } from "@godmode/shared"; import { AppWindow, ArrowUp, MoreHorizontal, Pin, PinOff, Search, Trash2 } from "lucide-react"; -import { AgentAvatar, EmptyState, PageBody, PageHeader } from "@/components/common"; +import { AgentAvatar, EmptyState } from "@/components/common"; import { Character } from "@/components/character"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -22,7 +22,8 @@ import { useAgents, useArtifact, useArtifacts } from "@/lib/hooks"; import { cn } from "@/lib/utils"; import { useUi } from "@/stores/ui"; -export default function ArtifactsPage() { +/** Dashboards and small apps the agents built: a line to ask for a new one, then the gallery. */ +export function ArtifactsOverview() { const { data: artifacts, isLoading } = useArtifacts(); const [query, setQuery] = useState(""); const list = useMemo(() => { @@ -33,52 +34,45 @@ export default function ArtifactsPage() { const empty = !isLoading && (artifacts?.length ?? 0) === 0; return ( -
- } - title="Artifacts" - description="Dashboards and small apps your agents build for you. Open one, use it, and ask for changes in plain words." - /> - - +
+ - {isLoading ? ( -
- {Array.from({ length: 3 }, (_, i) => ( - - ))} -
- ) : empty ? ( - } - title="Nothing built yet" - description="Describe a dashboard or a tool above, or pick a starting point. Agents also build artifacts when you ask for one in any chat." - /> - ) : ( -
-
-

- Your artifacts {artifacts!.length} -

- {(artifacts?.length ?? 0) > 6 && ( -
- - setQuery(e.target.value)} placeholder="Search" className="h-8 pl-8 text-sm" /> -
- )} -
- {list.length ? ( -
- {list.map((a, i) => ( - - ))} + {isLoading ? ( +
+ {Array.from({ length: 2 }, (_, i) => ( + + ))} +
+ ) : empty ? ( + } + title="Nothing built yet" + description="Describe a dashboard or a tool above, or pick a starting point. Agents also build artifacts when you ask for one in any chat." + /> + ) : ( +
+
+

+ Your artifacts {artifacts!.length} +

+ {(artifacts?.length ?? 0) > 6 && ( +
+ + setQuery(e.target.value)} placeholder="Search" className="h-8 pl-8 text-sm" />
- ) : ( -

Nothing matches “{query}”.

)} -
- )} - +
+ {list.length ? ( +
+ {list.map((a, i) => ( + + ))} +
+ ) : ( +

Nothing matches “{query}”.

+ )} +
+ )}
); } diff --git a/apps/desktop/src/components/automations/app-trigger-fields.tsx b/apps/desktop/src/components/automations/app-trigger-fields.tsx index 4dbbb4bc..5e6e27e2 100644 --- a/apps/desktop/src/components/automations/app-trigger-fields.tsx +++ b/apps/desktop/src/components/automations/app-trigger-fields.tsx @@ -159,7 +159,7 @@ export function AppTriggerFields({ - + Connect another app
@@ -262,7 +262,7 @@ function ConnectHint({ title, description, action }: { title: string; descriptio

{description}

diff --git a/apps/desktop/src/components/browser/profile-use-card.tsx b/apps/desktop/src/components/browser/profile-use-card.tsx index 7b8a3d31..a4db0033 100644 --- a/apps/desktop/src/components/browser/profile-use-card.tsx +++ b/apps/desktop/src/components/browser/profile-use-card.tsx @@ -118,7 +118,7 @@ export function ProfileUseCard() { s.hasApiKey ? ( "browser_use_api_key set" ) : ( - + Add browser-use API key ) diff --git a/apps/desktop/src/components/chat/turn-end.tsx b/apps/desktop/src/components/chat/turn-end.tsx index 8e0b262b..47cfccf3 100644 --- a/apps/desktop/src/components/chat/turn-end.tsx +++ b/apps/desktop/src/components/chat/turn-end.tsx @@ -218,7 +218,7 @@ function SignInAgain({ run, name, action, onSignedIn }: { run: Run; name: string Claude refused the Anthropic API key. Check it, then try again. diff --git a/apps/desktop/src/pages/integrations/integrations-page.tsx b/apps/desktop/src/components/integrations/integrations-overview.tsx similarity index 51% rename from apps/desktop/src/pages/integrations/integrations-page.tsx rename to apps/desktop/src/components/integrations/integrations-overview.tsx index 8f46651e..53b7fc7a 100644 --- a/apps/desktop/src/pages/integrations/integrations-page.tsx +++ b/apps/desktop/src/components/integrations/integrations-overview.tsx @@ -1,22 +1,18 @@ import type { ReactNode } from "react"; -import { useQuery } from "@tanstack/react-query"; import { useSearchParams } from "react-router"; import { motion } from "motion/react"; -import { Boxes, KeyRound, Plug, Server, Wrench } from "lucide-react"; +import { Boxes, Server, Wrench } from "lucide-react"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { PageBody, PageHeader } from "@/components/common"; -import { ApiKeysTab } from "@/components/integrations/api-keys-tab"; import { ApiToolsTab, useApiTools } from "@/components/integrations/api-tools-tab"; import { ComposioTab, useComposioConnections, useComposioStatus } from "@/components/integrations/composio-tab"; import { McpServersTab, useMcpServers } from "@/components/integrations/mcp-servers-tab"; import { NoConnectorsNote } from "@/components/integrations/no-connectors-note"; -import { api } from "@/lib/api"; -import { qk } from "@/lib/queryKeys"; -const TABS = ["composio", "tools", "mcp", "api-keys"] as const; +const TABS = ["composio", "tools", "mcp"] as const; type Tab = (typeof TABS)[number]; -export default function IntegrationsPage() { +/** Composio apps, API tools and MCP servers, a tab each (`?tab=`). */ +export function IntegrationsOverview() { const [params, setParams] = useSearchParams(); const raw = params.get("tab"); const tab: Tab = (TABS as readonly string[]).includes(raw ?? "") ? (raw as Tab) : "composio"; @@ -37,53 +33,38 @@ export default function IntegrationsPage() { const connections = useComposioConnections(composioReady); const servers = useMcpServers(); const tools = useApiTools(); - const secrets = useQuery({ queryKey: qk.appSecrets, queryFn: api.vault.secrets.list }); const counts: Record = { composio: connections.data?.filter((c) => c.status.toUpperCase() === "ACTIVE").length, tools: tools.data?.length, mcp: servers.data?.filter((s) => s.source === "custom").length, - "api-keys": secrets.data?.filter((s) => s.set).length, }; return ( -
- } - title="Integrations" - description="Optional: your agents already sign in to websites with your saved logins and 2FA codes. Add Composio for instant triggers, tools for any API with a key, MCP servers for anything else." - /> - - - - - } label="Composio" count={counts.composio} /> - } label="Tools" count={counts.tools} /> - } label="MCP servers" count={counts.mcp} /> - } label="API keys" count={counts["api-keys"]} /> - - - - - - - - - - - - - - - - - - - setTab("composio")} onOpenTools={() => setTab("tools")} /> - - - - +
+ + + + } label="Composio" count={counts.composio} /> + } label="Tools" count={counts.tools} /> + } label="MCP servers" count={counts.mcp} /> + + + + + + + + + + + + + + + + +
); } diff --git a/apps/desktop/src/components/layout/app-shell.tsx b/apps/desktop/src/components/layout/app-shell.tsx index 6bcf45e1..7537b31f 100644 --- a/apps/desktop/src/components/layout/app-shell.tsx +++ b/apps/desktop/src/components/layout/app-shell.tsx @@ -9,14 +9,11 @@ import { Box, ChevronRight, CreditCard, - Fingerprint, - AppWindow, Globe, MonitorUp, Inbox, KeyRound, Lock, - MessageCircle, MessageSquarePlus, MessagesSquare, MonitorSmartphone, @@ -24,13 +21,10 @@ import { PanelLeftClose, PanelLeftOpen, Phone, - Plug, Puzzle, Search, Server, - Smartphone, Settings, - ShieldCheck, SquareKanban, Workflow, X, @@ -109,8 +103,12 @@ interface NavItem { icon: ReactNode; badge?: number; end?: boolean; + /** Other paths the item stands for (the tabs of one screen). */ + also?: readonly string[]; } +const isActive = (item: NavItem, pathname: string) => [item.to, ...(item.also ?? [])].some((to) => pathname.startsWith(to)); + export function AppShell({ children }: { children: ReactNode }) { const { data: boot } = useBootstrap(); const navigate = useNavigate(); @@ -188,51 +186,37 @@ export function AppShell({ children }: { children: ReactNode }) { { to: "/inbox", label: "Inbox", icon: , badge: inboxCount || undefined }, { to: "/tasks", label: "Tasks", icon: , badge: (attention?.review ?? 0) + (attention?.blocked ?? 0) || undefined }, { to: "/agents", label: "Agents", icon: , badge: runningCount || undefined }, - { to: "/artifacts", label: "Artifacts", icon: }, { to: "/automations", label: "Automations", icon: , badge: attention?.automation || undefined }, { to: "/activity", label: "Activity", icon: }, { to: "/calls", label: "Calls", icon: }, { to: "/plugins", label: "Plugins", icon: , badge: modsWaiting || undefined }, - ]; - const accessNav: NavItem[] = [ - { to: "/vault/logins", label: "Logins", icon: }, - { to: "/vault/2fa", label: "2FA Codes", icon: }, - { to: "/vault/passkeys", label: "Passkeys", icon: }, + { to: "/vault/logins", label: "Passwords", icon: , also: ["/vault/2fa", "/vault/passkeys"] }, { to: "/vault/cards", label: "Cards", icon: }, - { to: "/integrations", label: "Integrations", icon: }, - { to: "/messaging", label: "Messaging", icon: , badge: boot?.counts.messagingRequests || undefined }, ]; const machineNav: NavItem[] = [ { to: "/browser", label: "Browser", icon: }, { to: "/computer", label: "Computer", icon: }, { to: "/vms", label: "Virtual machines", icon: }, { to: "/ssh", label: "SSH servers", icon: }, - { to: "/android", label: "Android phones", icon: }, { to: "/runners", label: "Runners", icon: }, ]; // Easy mode: the everyday pages in plain words up front, every other page under More. - const byPath = new Map([...workNav, ...accessNav, ...machineNav].map((item) => [item.to, item])); + const byPath = new Map([...workNav, ...machineNav].map((item) => [item.to, item])); const easyItem = (to: string, label: string): NavItem => ({ ...byPath.get(to)!, label }); const easyNav: NavItem[] = [ easyItem("/inbox", "Inbox"), easyItem("/agents", "Agents"), easyItem("/automations", "Automations"), - easyItem("/artifacts", "Dashboards & apps"), easyItem("/tasks", "Task board"), easyItem("/vault/logins", "Passwords"), - easyItem("/integrations", "Connected apps"), ]; const easyMore: NavItem[] = [ easyItem("/activity", "History"), easyItem("/calls", "Phone calls"), - easyItem("/messaging", "Chat apps"), - easyItem("/vault/2fa", "2FA codes"), - easyItem("/vault/passkeys", "Passkeys"), easyItem("/vault/cards", "Payment cards"), easyItem("/plugins", "Add-ons"), easyItem("/browser", "Browser"), easyItem("/computer", "Computer control"), - easyItem("/android", "Android phones"), easyItem("/vms", "Virtual machines"), easyItem("/ssh", "Servers"), easyItem("/runners", "Other computers"), @@ -294,7 +278,6 @@ export function AppShell({ children }: { children: ReactNode }) { ) : ( <> - )} @@ -454,7 +437,7 @@ function NavSection({ id, label, items, folded = "icons" }: { id: string; label: ); if (folded === "list") { - const current = items.find((item) => pathname.startsWith(item.to)); + const current = items.find((item) => isActive(item, pathname)); const waiting = items.reduce((sum, item) => sum + (item.badge ?? 0), 0); return ( @@ -477,7 +460,7 @@ function NavSection({ id, label, items, folded = "icons" }: { id: string; label: {header}
{items.map((item) => { - const active = pathname.startsWith(item.to); + const active = isActive(item, pathname); return ( @@ -507,7 +490,7 @@ function NavSection({ id, label, items, folded = "icons" }: { id: string; label: function NavMenuItem({ item }: { item: NavItem }) { const location = useLocation(); - const active = item.end ? location.pathname === item.to || location.pathname.startsWith("/chat/") : location.pathname.startsWith(item.to); + const active = item.end ? location.pathname === item.to || location.pathname.startsWith("/chat/") : isActive(item, location.pathname); return ( s.connected); const qc = useQueryClient(); const lock = useMutation({ @@ -622,13 +611,20 @@ function FooterBar() { - - Settings ({modKey},) + {requests ? `Settings · ${requests} waiting in Messaging` : `Settings (${modKey},)`}
); diff --git a/apps/desktop/src/components/layout/claude-signed-out.tsx b/apps/desktop/src/components/layout/claude-signed-out.tsx index 10c89bd0..5b0757a5 100644 --- a/apps/desktop/src/components/layout/claude-signed-out.tsx +++ b/apps/desktop/src/components/layout/claude-signed-out.tsx @@ -65,7 +65,7 @@ export function ClaudeSignedOutBanner() {

{out.apiKey ? ( ) : !maySignIn ? null : state !== "waiting" ? ( - } - > - - set({ enabled })} /> - - - - } - description="Let agents sign in to apps and websites on a phone with the logins and 2FA codes from your vault." - > - - void setVaultFill(on)} /> - -
    - }>Only agents that may see passwords - }>Passwords only into password fields - }>Every fill audited - } caution> - Not bound to an app - - } caution> - Readable through the shell - -
-
- - } - description="Android Debug Bridge, from Google's platform-tools. Godmode reaches the phones through it." - actions={ - s && !s.adb.installed && s.canInstall ? ( - - ) : undefined - } - > - - {s ? ( - - {s.adb.installed - ? [s.adb.version ? `adb ${s.adb.version}` : "Installed", s.adb.managed ? "Godmode's own copy" : "found on this computer"].join(" · ") - : s.canInstall - ? "Not installed — Godmode can download it from Google" - : "Not installed — install Android's platform-tools"} - - ) : ( - Checking… + } + points={[ + { icon: , text: "Phones and emulators over USB or Wi-Fi" }, + { icon: , text: "Watch live and take over any time" }, + { icon: , text: "Only the agents and chats you give a phone" }, + ]} + > +
+ +
+ {s?.error && ( + + {s.error} + )} - - {s?.adb.path && ( - - {short(s.adb.path)} - - )} - - - - -
+ } + description="Let agents sign in to apps and websites on a phone with the logins and 2FA codes from your vault." + > + + void setVaultFill(on)} /> + +
    + }>Only agents that may see passwords + }>Passwords only into password fields + }>Every fill audited + } caution> + Not bound to an app + + } caution> + Readable through the shell + +
+
+ + } + description="Android Debug Bridge, from Google's platform-tools. Godmode reaches the phones through it." + actions={ + s && !s.adb.installed && s.canInstall ? ( + + ) : undefined + } + > + + {s ? ( + + {s.adb.installed + ? [s.adb.version ? `adb ${s.adb.version}` : "Installed", s.adb.managed ? "Godmode's own copy" : "found on this computer"].join(" · ") + : s.canInstall + ? "Not installed — Godmode can download it from Google" + : "Not installed — install Android's platform-tools"} + + ) : ( + Checking… + )} + + {s?.adb.path && ( + + {short(s.adb.path)} + + )} + + + + +
+
+ ); } diff --git a/apps/desktop/src/components/settings/api-keys-section.tsx b/apps/desktop/src/components/settings/api-keys-section.tsx new file mode 100644 index 00000000..43423cfa --- /dev/null +++ b/apps/desktop/src/components/settings/api-keys-section.tsx @@ -0,0 +1,13 @@ +import { useNavigate } from "react-router"; +import { ApiKeysTab } from "@/components/integrations/api-keys-tab"; +import { SectionHeading } from "./settings-kit"; + +export function ApiKeysSection() { + const navigate = useNavigate(); + return ( +
+ + navigate("/settings/integrations")} onOpenTools={() => navigate("/settings/integrations?tab=tools")} /> +
+ ); +} diff --git a/apps/desktop/src/components/settings/artifacts-section.tsx b/apps/desktop/src/components/settings/artifacts-section.tsx new file mode 100644 index 00000000..19532a5c --- /dev/null +++ b/apps/desktop/src/components/settings/artifacts-section.tsx @@ -0,0 +1,25 @@ +import { AppWindow, History, LayoutDashboard, PanelRight } from "lucide-react"; +import type { Settings } from "@godmode/shared"; +import { ArtifactsOverview } from "@/components/artifacts/artifacts-overview"; +import { useEasyMode } from "@/lib/mode"; +import { FeatureSection } from "./feature-section"; + +export function ArtifactsSection({ settings }: { settings: Settings }) { + const easy = useEasyMode(); + return ( + } + points={[ + { icon: , text: "Dashboards, trackers, planners and calculators" }, + { icon: , text: "Opens next to the chat it was built in" }, + { icon: , text: "Every change is a version you can go back to" }, + ]} + > + + + ); +} diff --git a/apps/desktop/src/components/settings/calls-section.tsx b/apps/desktop/src/components/settings/calls-section.tsx index c4ad8228..8fe7c0f4 100644 --- a/apps/desktop/src/components/settings/calls-section.tsx +++ b/apps/desktop/src/components/settings/calls-section.tsx @@ -192,7 +192,7 @@ function SetupStatus({ setup }: { setup: ReturnType }) { ) : ( <> No ElevenLabs key yet. Add one under{" "} - + Integrations → API keys . diff --git a/apps/desktop/src/components/settings/feature-section.tsx b/apps/desktop/src/components/settings/feature-section.tsx new file mode 100644 index 00000000..d60b7fd5 --- /dev/null +++ b/apps/desktop/src/components/settings/feature-section.tsx @@ -0,0 +1,88 @@ +import { useId, type ReactNode } from "react"; +import { motion } from "motion/react"; +import type { Settings } from "@godmode/shared"; +import { Button } from "@/components/ui/button"; +import { Switch } from "@/components/ui/switch"; +import { cn } from "@/lib/utils"; +import { useSettingsPatch } from "./settings-kit"; + +export const FEATURES = ["artifacts", "integrations", "messaging", "android"] as const; +export type Feature = (typeof FEATURES)[number]; + +export const featureOn = (settings: Pick | undefined, feature: Feature) => !!settings?.[feature].enabled; + +/** + * An optional feature's home in Settings: its switch and what it does at the top, then the feature itself — or, while + * it is off, what it would bring and a way to turn it on. + */ +export function FeatureSection({ + feature, + settings, + title, + summary, + icon, + points, + children, +}: { + feature: Feature; + settings: Settings; + title: string; + summary: string; + icon: ReactNode; + points: { icon: ReactNode; text: string }[]; + children: ReactNode; +}) { + const { patch } = useSettingsPatch(); + const on = featureOn(settings, feature); + const id = useId(); + const set = (enabled: boolean) => patch({ [feature]: { enabled } }); + const off = `${title} ${title.endsWith("s") ? "are" : "is"} off`; + + return ( +
+
+
+

{title}

+

{summary}

+
+ +
+ + {on ? ( + + {children} + + ) : ( + +
{icon}
+

{off}

+

Agents don't get its tools while it's off. Turn it on whenever you need it.

+
    + {points.map((p) => ( +
  • + {p.icon} + {p.text} +
  • + ))} +
+ +
+ )} +
+ ); +} diff --git a/apps/desktop/src/components/settings/integrations-section.tsx b/apps/desktop/src/components/settings/integrations-section.tsx new file mode 100644 index 00000000..4ca83cec --- /dev/null +++ b/apps/desktop/src/components/settings/integrations-section.tsx @@ -0,0 +1,25 @@ +import { Boxes, Plug, Server, Wrench } from "lucide-react"; +import type { Settings } from "@godmode/shared"; +import { IntegrationsOverview } from "@/components/integrations/integrations-overview"; +import { useEasyMode } from "@/lib/mode"; +import { FeatureSection } from "./feature-section"; + +export function IntegrationsSection({ settings }: { settings: Settings }) { + const easy = useEasyMode(); + return ( + } + points={[ + { icon: , text: "Gmail, Slack, Notion and more through Composio" }, + { icon: , text: "Any API with a key, without agents seeing it" }, + { icon: , text: "Your own MCP servers" }, + ]} + > + + + ); +} diff --git a/apps/desktop/src/components/settings/messaging-section.tsx b/apps/desktop/src/components/settings/messaging-section.tsx new file mode 100644 index 00000000..cc4d97b7 --- /dev/null +++ b/apps/desktop/src/components/settings/messaging-section.tsx @@ -0,0 +1,25 @@ +import { KeyRound, MessageCircle, MessagesSquare, UserCheck } from "lucide-react"; +import type { Settings } from "@godmode/shared"; +import { MessagingOverview } from "@/components/messaging/messaging-overview"; +import { useEasyMode } from "@/lib/mode"; +import { FeatureSection } from "./feature-section"; + +export function MessagingSection({ settings }: { settings: Settings }) { + const easy = useEasyMode(); + return ( + } + points={[ + { icon: , text: "Slack, Telegram and Microsoft Teams" }, + { icon: , text: "Private: you approve who may write" }, + { icon: , text: "Bot tokens sealed in your vault" }, + ]} + > + + + ); +} diff --git a/apps/desktop/src/components/settings/voice-section.tsx b/apps/desktop/src/components/settings/voice-section.tsx index b5b41d1e..c5c13f46 100644 --- a/apps/desktop/src/components/settings/voice-section.tsx +++ b/apps/desktop/src/components/settings/voice-section.tsx @@ -294,7 +294,7 @@ export function VoiceSection({ settings }: { settings: Settings }) { {(usesOpenAi || v.ttsProvider === "elevenlabs") && ( } title="API keys live in the vault"> Add your {[usesOpenAi && "OpenAI", v.ttsProvider === "elevenlabs" && "ElevenLabs"].filter(Boolean).join(" and ")} key under{" "} - + Integrations → API keys . Keys are encrypted and never shown again. diff --git a/apps/desktop/src/components/vault/passwords-header.tsx b/apps/desktop/src/components/vault/passwords-header.tsx new file mode 100644 index 00000000..37871b7f --- /dev/null +++ b/apps/desktop/src/components/vault/passwords-header.tsx @@ -0,0 +1,57 @@ +import type { ReactNode } from "react"; +import { NavLink } from "react-router"; +import { useQuery } from "@tanstack/react-query"; +import { Fingerprint, KeyRound, ShieldCheck } from "lucide-react"; +import { PageHeader } from "@/components/common"; +import { api } from "@/lib/api"; +import { qk } from "@/lib/queryKeys"; +import { cn } from "@/lib/utils"; + +export const PASSWORD_TABS = ["/vault/logins", "/vault/2fa", "/vault/passkeys"] as const; + +/** One screen for what agents sign in with: the title, a tab each for logins, 2FA codes and passkeys, and the tab's actions. */ +export function PasswordsHeader({ actions }: { actions?: ReactNode }) { + const logins = useQuery({ queryKey: qk.credentialList("all", ""), queryFn: () => api.credentials.list({ workspaceId: "all" }) }); + const codes = useQuery({ queryKey: qk.totpList("all"), queryFn: () => api.totp.list({ workspaceId: "all" }) }); + const passkeys = useQuery({ queryKey: qk.passkeyList("all"), queryFn: () => api.passkeys.list({ workspaceId: "all" }) }); + const tabs = [ + { to: PASSWORD_TABS[0], label: "Logins", icon: , count: logins.data?.length }, + { to: PASSWORD_TABS[1], label: "2FA codes", icon: , count: codes.data?.length }, + { to: PASSWORD_TABS[2], label: "Passkeys", icon: , count: passkeys.data?.length }, + ]; + return ( + <> + } + title="Passwords" + description="The logins, 2FA codes and passkeys your agents sign in with. Encrypted on this computer; agents never see the secrets." + actions={actions} + className="pb-4 @2xl:pb-5" + /> +
+ +
+ + ); +} diff --git a/apps/desktop/src/lib/mode.ts b/apps/desktop/src/lib/mode.ts index 0acfc57a..0adfef2e 100644 --- a/apps/desktop/src/lib/mode.ts +++ b/apps/desktop/src/lib/mode.ts @@ -20,18 +20,13 @@ export function useInterfaceMode(): InterfaceMode { /** Easy mode titles and descriptions for the top-level pages, by path (the sidebar uses the same names). */ export const EASY_PAGES: Record = { - "/artifacts": { title: "Dashboards & apps" }, "/tasks": { title: "Task board" }, "/automations": { description: "Let your agents start work by themselves: on a schedule, or when something happens in your apps." }, "/files": { description: "What your agents keep: their files, notes and memory. Look through it, tidy it up, and put back what you deleted." }, "/activity": { title: "History", description: "Everything your agents did." }, "/calls": { title: "Phone calls" }, "/plugins": { title: "Add-ons", description: "Extra skills for your agents. Add one, switch it off or remove it any time." }, - "/vault/logins": { title: "Passwords", description: "The logins your agents may use. Encrypted on this computer; agents never see the passwords." }, - "/vault/2fa": { title: "2FA codes" }, "/vault/cards": { title: "Payment cards" }, - "/integrations": { title: "Connected apps", description: "Optional: your agents already sign in to your websites with your saved logins and 2FA codes. Connect an app only so they react the moment something arrives." }, - "/messaging": { title: "Chat apps", description: "Talk to your agents from Slack, Telegram or Microsoft Teams." }, "/computer": { title: "Computer control" }, "/ssh": { title: "Servers" }, "/runners": { title: "Other computers" }, diff --git a/apps/desktop/src/pages/android/android-device-page.tsx b/apps/desktop/src/pages/android/android-device-page.tsx index 7d3895ad..5f44584c 100644 --- a/apps/desktop/src/pages/android/android-device-page.tsx +++ b/apps/desktop/src/pages/android/android-device-page.tsx @@ -208,7 +208,7 @@ export default function AndroidDevicePage() { description={list.isError ? errorMessage(list.error) : "It may have been forgotten. Plug it in again and it shows up as a new phone."} action={ @@ -223,7 +223,7 @@ export default function AndroidDevicePage() {
diff --git a/apps/desktop/src/pages/artifacts/artifact-page.tsx b/apps/desktop/src/pages/artifacts/artifact-page.tsx index dc7d47b4..e85d6eaf 100644 --- a/apps/desktop/src/pages/artifacts/artifact-page.tsx +++ b/apps/desktop/src/pages/artifacts/artifact-page.tsx @@ -15,13 +15,13 @@ export default function ArtifactPage() {
- navigate("/artifacts")} footer={} /> + navigate("/settings/artifacts")} footer={} />
); diff --git a/apps/desktop/src/pages/settings/settings-page.tsx b/apps/desktop/src/pages/settings/settings-page.tsx index 7975db01..80876f16 100644 --- a/apps/desktop/src/pages/settings/settings-page.tsx +++ b/apps/desktop/src/pages/settings/settings-page.tsx @@ -3,6 +3,7 @@ import { Navigate, NavLink, useParams } from "react-router"; import { AnimatePresence, motion } from "motion/react"; import { Activity, + AppWindow, AudioLines, BrainCircuit, Box, @@ -15,10 +16,13 @@ import { Globe, HeartPulse, Info, + KeyRound, KeySquare, Laptop, + MessageCircle, MonitorUp, Phone, + Plug, RefreshCw, ScrollText, Settings2, @@ -32,6 +36,8 @@ import { EmptyState, PageBody, PageHeader } from "@/components/common"; import { AboutSection } from "@/components/settings/about-section"; import { AiSection } from "@/components/settings/ai-section"; import { AndroidSection } from "@/components/settings/android-section"; +import { ApiKeysSection } from "@/components/settings/api-keys-section"; +import { ArtifactsSection } from "@/components/settings/artifacts-section"; import { BackupSection } from "@/components/settings/backup-section"; import { BillingSection } from "@/components/settings/billing-section"; import { BrowserSection } from "@/components/settings/browser-section"; @@ -41,12 +47,15 @@ import { CloudSection } from "@/components/settings/cloud-section"; import { ComputerSection } from "@/components/settings/computer-section"; import { ConnectSection } from "@/components/settings/connect-section"; import { DevicesSection } from "@/components/settings/devices-section"; +import { FEATURES, featureOn } from "@/components/settings/feature-section"; import { FilesSection } from "@/components/settings/files-section"; import { GeneralSection } from "@/components/settings/general-section"; import { InstructionsSection } from "@/components/settings/instructions-section"; +import { IntegrationsSection } from "@/components/settings/integrations-section"; import { LicenseSection } from "@/components/settings/license-section"; import { LogsSection } from "@/components/settings/logs-section"; import { MemorySection } from "@/components/settings/memory-section"; +import { MessagingSection } from "@/components/settings/messaging-section"; import { PhoneSection } from "@/components/settings/phone-section"; import { SecuritySection } from "@/components/settings/security-section"; import { SystemSection } from "@/components/settings/system-section"; @@ -66,11 +75,15 @@ const SECTIONS = [ { id: "browser", label: "Browser", icon: , group: "Preferences" }, { id: "computer", label: "Computer", icon: , group: "Preferences" }, { id: "vms", label: "Virtual machines", icon: , group: "Preferences" }, - { id: "android", label: "Android phones", icon: , group: "Preferences" }, { id: "voice", label: "Voice", icon: , group: "Preferences" }, { id: "calls", label: "Phone calls", icon: , group: "Preferences" }, { id: "memory", label: "Memory", icon: , group: "Preferences" }, + { id: "artifacts", label: "Artifacts", icon: , group: "Features" }, + { id: "integrations", label: "Integrations", icon: , group: "Features" }, + { id: "messaging", label: "Messaging", icon: , group: "Features" }, + { id: "android", label: "Android phones", icon: , group: "Features" }, { id: "files", label: "Files", icon: , group: "Data & privacy" }, + { id: "api-keys", label: "API keys", icon: , group: "Data & privacy" }, { id: "security", label: "Security", icon: , group: "Data & privacy" }, { id: "backup", label: "Backup", icon: , group: "Data & privacy" }, { id: "phone", label: "Phone", icon: , group: "App" }, @@ -96,6 +109,10 @@ const EASY: Partial> = { voice: { label: "Voice", group: "Everyday" }, calls: { label: "Phone calls", group: "Everyday" }, phone: { label: "Phone app", group: "Everyday" }, + artifacts: { label: "Dashboards & apps", group: "Extras" }, + integrations: { label: "Connected apps", group: "Extras" }, + messaging: { label: "Chat apps", group: "Extras" }, + android: { label: "Android phones", group: "Extras" }, security: { label: "Security", group: "Account" }, backup: { label: "Backup", group: "Account" }, license: { label: "License", group: "Account" }, @@ -110,7 +127,7 @@ const EASY_ADVANCED_LABEL: Partial> = { }; /** Sections that render without the settings document (they use their own endpoints). */ -const STANDALONE: SectionId[] = ["files", "backup", "devices", "connect", "cloud", "license", "billing", "system", "cleanup", "about"]; +const STANDALONE: SectionId[] = ["files", "api-keys", "backup", "devices", "connect", "cloud", "license", "billing", "system", "cleanup", "about"]; export default function SettingsPage() { const { section } = useParams(); @@ -145,11 +162,15 @@ export default function SettingsPage() { browser: () => , computer: () => , vms: () => , + artifacts: () => , + integrations: () => , + messaging: () => , android: () => , voice: () => , calls: () => , memory: () => , files: () => , + "api-keys": () => , security: () => , backup: () => , phone: () => , @@ -191,11 +212,29 @@ export default function SettingsPage() { } type NavEntry = { id: SectionId; label: string; icon: ReactNode; group: string }; +type Marks = Partial>; + +/** Next to the optional features: "Off", or what waits for the human there (people who asked to talk to a bot). */ +function useMarks(): Marks { + const { data: boot } = useBootstrap(); + if (!boot) return {}; + const marks: Marks = {}; + for (const feature of FEATURES) if (!featureOn(boot.settings, feature)) marks[feature] = Off; + const requests = featureOn(boot.settings, "messaging") ? boot.counts.messagingRequests : 0; + if (requests) + marks.messaging = ( + + {requests} + + ); + return marks; +} function SectionNav({ active }: { active: SectionId }) { const easy = useEasyMode(); const [showAdvanced, setShowAdvanced] = useState(false); - if (!easy) return ; + const marks = useMarks(); + if (!easy) return ; const everyday: NavEntry[] = (Object.keys(EASY) as SectionId[]).map((id) => ({ ...SECTIONS.find((s) => s.id === id)!, ...EASY[id]! })); const advanced: NavEntry[] = SECTIONS.flatMap((s) => EASY[s.id] ? [] : [{ ...s, label: EASY_ADVANCED_LABEL[s.id] ?? s.label, group: "Advanced" }], @@ -205,6 +244,7 @@ function SectionNav({ active }: { active: SectionId }) { {s.icon} {s.label} + {marks[s.id] && {marks[s.id]}}
); diff --git a/apps/desktop/src/pages/vault/logins-page.tsx b/apps/desktop/src/pages/vault/logins-page.tsx index 1d70fbdf..29af12d2 100644 --- a/apps/desktop/src/pages/vault/logins-page.tsx +++ b/apps/desktop/src/pages/vault/logins-page.tsx @@ -7,12 +7,13 @@ import type { Credential, TotpEntry } from "@godmode/shared"; import { Button } from "@/components/ui/button"; import { Skeleton } from "@/components/ui/skeleton"; import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from "@/components/ui/input-group"; -import { EmptyState, Kbd, PageBody, PageHeader } from "@/components/common"; +import { EmptyState, Kbd, PageBody } from "@/components/common"; import { usePageScroll } from "@/components/layout/page-scroll"; import { CredentialDialog, type CredentialPrefill } from "@/components/vault/credential-dialog"; import { CREDENTIAL_GRID, CredentialRow } from "@/components/vault/credential-row"; import { ConfirmDeleteDialog } from "@/components/vault/confirm-dialog"; import { PasswordImportDialog } from "@/components/vault/password-import-dialog"; +import { PasswordsHeader } from "@/components/vault/passwords-header"; import { ScopeFilterSelect } from "@/components/vault/workspace-select"; import { useDebouncedValue } from "@/components/vault/use-debounced-value"; import { isVaultLocked, toastApiError } from "@/components/vault/vault-utils"; @@ -118,10 +119,7 @@ export default function LoginsPage() { return (
- } - title="Logins" - description="Website logins your agents can use — encrypted in your local vault." +