diff --git a/packages/agent/src/api/views-registry.kinds.test.ts b/packages/agent/src/api/views-registry.kinds.test.ts index 4fc2827d135ac..67de3b17bfa9a 100644 --- a/packages/agent/src/api/views-registry.kinds.test.ts +++ b/packages/agent/src/api/views-registry.kinds.test.ts @@ -23,7 +23,13 @@ function fixturePlugin(): Plugin { { id: "vk-system", label: "Sys", viewKind: "system", bundleUrl: "x" }, { id: "vk-release", label: "Rel", viewKind: "release", bundleUrl: "x" }, { id: "vk-dev", label: "Dev", viewKind: "developer", bundleUrl: "x" }, - { id: "vk-preview", label: "Prev", viewKind: "preview", bundleUrl: "x" }, + { + id: "vk-preview", + label: "Prev", + viewKind: "preview", + group: "wallet", + bundleUrl: "x", + }, // legacy gate still maps to developer { id: "vk-legacy", label: "Legacy", developerOnly: true, bundleUrl: "x" }, ], @@ -106,4 +112,12 @@ describe("listViews kind filtering", () => { "vk-system", ]); }); + + it("preserves app-shell grouping metadata for client launcher curation", async () => { + await register(); + const grouped = listViews({ includeAllKinds: true }).find( + (view) => view.id === "vk-preview", + ); + expect(grouped?.group).toBe("wallet"); + }); }); diff --git a/packages/core/src/types/plugin.ts b/packages/core/src/types/plugin.ts index e4cf8e0eb52e8..283b3b6aded47 100644 --- a/packages/core/src/types/plugin.ts +++ b/packages/core/src/types/plugin.ts @@ -754,6 +754,8 @@ export interface ViewDeclaration { path?: string; /** Sort priority in the view manager — lower appears first. Default 100. */ order?: number; + /** Optional named group shared with app-shell page registrations. */ + group?: string; /** Tags for search and discovery (e.g. ["finance", "crypto"]). */ tags?: string[]; /** diff --git a/packages/ui/src/components/pages/WalletSectionNav.tsx b/packages/ui/src/components/pages/WalletSectionNav.tsx index e4fb0beeaa500..32163506c0661 100644 --- a/packages/ui/src/components/pages/WalletSectionNav.tsx +++ b/packages/ui/src/components/pages/WalletSectionNav.tsx @@ -1,3 +1,9 @@ +/** + * Wallet section navigation renders sub-tabs from app-shell pages that declare + * the wallet group. The wallet inventory page owns the root `/wallet` tab while + * plugin pages join or leave the section through their own registration data. + */ + import { useSyncExternalStore } from "react"; import { type AppShellPageRegistration, diff --git a/packages/ui/src/components/pages/launcher-curation.test.ts b/packages/ui/src/components/pages/launcher-curation.test.ts index edeea5f89a1cc..ad25ca0762079 100644 --- a/packages/ui/src/components/pages/launcher-curation.test.ts +++ b/packages/ui/src/components/pages/launcher-curation.test.ts @@ -96,14 +96,26 @@ describe("curateLauncherPages", () => { expect(ids(page)).toEqual(["chat", "wallet"]); }); - it("keeps hyperliquid/polymarket out of the launcher (wallet sub-views)", () => { + it("keeps wallet-group sub-pages out of the launcher", () => { const page = curateLauncherPages( - [entry("wallet"), entry("hyperliquid"), entry("polymarket")], + [ + entry("wallet"), + entry("perps", { group: "wallet" }), + entry("predictions", { group: "wallet" }), + ], { isAosp: false, enabledKinds: ENABLED, cloudActive: true }, ); expect(ids(page)).toEqual(["wallet"]); }); + it("shows the same pages as ordinary apps when they do not declare a group", () => { + const page = curateLauncherPages( + [entry("wallet"), entry("perps"), entry("predictions")], + { isAosp: false, enabledKinds: ENABLED, cloudActive: true }, + ); + expect(ids(page)).toEqual(["wallet", "perps", "predictions"]); + }); + it("gates native-OS tiles to the AOSP fork", () => { const views = [ entry("wallet"), @@ -284,12 +296,12 @@ describe("curateLauncherPages — full realistic view set", () => { entry("shopify"), entry("facewear", { viewKind: "preview" }), entry("smartglasses", { viewKind: "preview" }), - // Wallet + duplicate registrations + sub-views. + // Wallet + duplicate registrations + grouped sub-views. entry("wallet", { viewKind: "system" }), entry("inventory", { builtin: true, viewKind: "system" }), entry("wallet.inventory"), - entry("hyperliquid"), - entry("polymarket"), + entry("perps", { group: "wallet" }), + entry("predictions", { group: "wallet" }), // Automations + duplicates folded to one. entry("automations", { viewKind: "system" }), entry("triggers", { builtin: true }), diff --git a/packages/ui/src/components/pages/launcher-curation.ts b/packages/ui/src/components/pages/launcher-curation.ts index d8a047c6f6ca7..df73522908ea0 100644 --- a/packages/ui/src/components/pages/launcher-curation.ts +++ b/packages/ui/src/components/pages/launcher-curation.ts @@ -16,10 +16,11 @@ * declared, so the whole set hides together in production. * * Curation is a blocklist + canonical dedup, not a fixed allow-list: known apps - * are ordered, removed apps are hidden, duplicate registrations collapse to one - * tile, and everything else that is genuinely loaded and visible still appears - * so installing a new plugin app keeps working. Native-OS tiles (phone/messages/ - * contacts/camera/files) only appear on the AOSP ElizaOS fork. + * are ordered, removed apps are hidden, grouped sub-pages collapse under their + * parent tile, duplicate registrations collapse to one tile, and everything else + * that is genuinely loaded and visible still appears so installing a new plugin + * app keeps working. Native-OS tiles (phone/messages/contacts/camera/files) only + * appear on the AOSP ElizaOS fork. */ import { @@ -89,8 +90,7 @@ export const LAUNCHER_AOSP_ONLY_IDS: readonly string[] = * Views that never appear in the launcher grid: * - shell surfaces reached another way (views/apps launchers; background + * voice are set from Settings/chat; character-select is inline), - * - removed apps (companion, model tester, shopify, wearables), - * - wallet sub-views (hyperliquid/polymarket open from inside the Wallet app). + * - removed apps (companion, model tester, shopify, wearables). */ export const LAUNCHER_HIDDEN_IDS: ReadonlySet = new Set([ "views", @@ -106,9 +106,6 @@ export const LAUNCHER_HIDDEN_IDS: ReadonlySet = new Set([ "shopify", "facewear", "smartglasses", - // Wallet sub-views — reached from inside the Wallet app, not the launcher. - "hyperliquid", - "polymarket", ]); /** @@ -170,6 +167,13 @@ function launcherViewKind(canonicalId: string, entry: ViewEntry) { return resolveViewKind(entry); } +function isGroupedLauncherSubPage( + canonicalId: string, + entry: ViewEntry, +): boolean { + return entry.group === "wallet" && canonicalId !== "wallet"; +} + /** * Score competing registrations for the same canonical id so the richest one * wins the single tile (a loaded standalone view beats an app-shell alias beats @@ -238,6 +242,7 @@ export function curateLauncherPages( for (const entry of entries) { const canonicalId = canonicalLauncherId(entry.id); if (LAUNCHER_HIDDEN_IDS.has(canonicalId)) continue; + if (isGroupedLauncherSubPage(canonicalId, entry)) continue; // Cloud-only tiles (e.g. the Cloud Applications dashboard) never surface // unless the user is signed into Eliza Cloud. if (LAUNCHER_CLOUD_IDS.has(canonicalId) && !cloudActive) continue; diff --git a/packages/ui/src/hooks/useAvailableViews.ts b/packages/ui/src/hooks/useAvailableViews.ts index d46ca2284422f..1fa84deaa264c 100644 --- a/packages/ui/src/hooks/useAvailableViews.ts +++ b/packages/ui/src/hooks/useAvailableViews.ts @@ -72,6 +72,8 @@ export interface ViewRegistryEntry { tags?: string[]; /** Sort priority for launcher/nav surfaces (lower = earlier). */ order?: number; + /** Optional named group shared with app-shell page registrations. */ + group?: string; /** * When true, the view only appears when Developer Mode is enabled. * Equivalent to `viewKind: "developer"`. @@ -317,6 +319,7 @@ function appShellPageToViewEntry( developerOnly: page.developerOnly, viewKind: page.viewKind, order: page.order, + group: page.group, backgroundPolicy: page.backgroundPolicy, visibleInManager: true, builtin: false, diff --git a/packages/ui/src/hooks/view-catalog.ts b/packages/ui/src/hooks/view-catalog.ts index e564b41ee3bc4..9bd8ddd53de76 100644 --- a/packages/ui/src/hooks/view-catalog.ts +++ b/packages/ui/src/hooks/view-catalog.ts @@ -85,6 +85,8 @@ export interface ViewEntry { viewKind?: ViewKind; /** Sort priority for launcher/nav surfaces (lower = earlier). */ order?: number; + /** Optional named group shared with app-shell page registrations. */ + group?: string; /** Source records (one is set depending on `kind`). */ view?: ViewRegistryEntry; app?: RegistryAppInfo; @@ -125,6 +127,7 @@ export function viewToEntry(view: ViewRegistryEntry): ViewEntry { developerOnly: view.developerOnly, viewKind: view.viewKind, order: view.order, + group: view.group, view, }; } diff --git a/plugins/plugin-hyperliquid/src/plugin.ts b/plugins/plugin-hyperliquid/src/plugin.ts index b0c35cb9b8662..67c3395119479 100644 --- a/plugins/plugin-hyperliquid/src/plugin.ts +++ b/plugins/plugin-hyperliquid/src/plugin.ts @@ -145,6 +145,7 @@ export const hyperliquidPlugin: Plugin = { "Hyperliquid perpetual markets — positions, trading status, and market data", icon: "TrendingUp", path: "/hyperliquid", + group: "wallet", modalities: ["gui", "xr", "tui"], bundlePath: "dist/views/bundle.js", componentExport: "HyperliquidView", diff --git a/plugins/plugin-polymarket/src/plugin.ts b/plugins/plugin-polymarket/src/plugin.ts index 9b973587ff80a..4c0dd9c190699 100644 --- a/plugins/plugin-polymarket/src/plugin.ts +++ b/plugins/plugin-polymarket/src/plugin.ts @@ -123,6 +123,7 @@ export const polymarketPlugin: Plugin = { "Polymarket prediction markets — market discovery, orderbook, and positions", icon: "BarChart2", path: "/polymarket", + group: "wallet", modalities: ["gui", "xr", "tui"], bundlePath: "dist/views/bundle.js", componentExport: "PolymarketView",