From 49940311badfa068b2f41c0e4bd7fa25635cb1a1 Mon Sep 17 00:00:00 2001 From: Anso Date: Sat, 29 Aug 2026 03:30:53 +0000 Subject: [PATCH] fix(console): clarify Pilot Agent host-console unavailability (#1868) Replace the generic upgrade message on Pilot Agent nodes with transport-specific copy while preserving DAP-remote behavior. - Add resolveHostConsoleLockMessage() to return Pilot-specific copy when nodeMode === 'pilot_agent'; otherwise keep generic upgrade message. - Update ViewRouter to use the helper in the locked branch. - Add unit tests for the new helper and component behavior. - Sync docs/features/host-console.mdx to reflect the Pilot Agent message. Closes #1855 --- docs/features/host-console.mdx | 2 +- .../components/EditorLayout/ViewRouter.tsx | 17 +++--- .../__tests__/ViewRouter.test.tsx | 15 ++++- .../lib/routing/hostConsoleCapability.test.ts | 59 ++++++++++++++++++- .../src/lib/routing/hostConsoleCapability.ts | 41 +++++++++++++ 5 files changed, 122 insertions(+), 12 deletions(-) diff --git a/docs/features/host-console.mdx b/docs/features/host-console.mdx index 306fbd04..08d3831e 100644 --- a/docs/features/host-console.mdx +++ b/docs/features/host-console.mdx @@ -113,6 +113,6 @@ The Host Console is one of the most powerful features in Sencho and is treated a - Console appears for users with the **admin** role. If you are an admin and still do not see it, refresh the page. On a remote node, the Console tab may show a lock card when that node does not support Host Console (for example a Pilot Agent node, or a Distributed API Proxy node that does not advertise Host Console). + Console appears for users with the **admin** role. If you are an admin and still do not see it, refresh the page. On a remote node, the Console tab may show a lock card when that node does not support Host Console. For a Pilot Agent node, the card states that Host Console is not available through Pilot Agent yet. For a Distributed API Proxy node that does not advertise Host Console, the card names the node's version and suggests upgrading it. diff --git a/frontend/src/components/EditorLayout/ViewRouter.tsx b/frontend/src/components/EditorLayout/ViewRouter.tsx index 7e381e14..ff147b99 100644 --- a/frontend/src/components/EditorLayout/ViewRouter.tsx +++ b/frontend/src/components/EditorLayout/ViewRouter.tsx @@ -4,7 +4,7 @@ import { Skeleton } from '@/components/ui/skeleton'; import { useAuth } from '@/context/AuthContext'; import { useLicense } from '@/context/LicenseContext'; import { useNodes } from '@/context/NodeContext'; -import { resolveHostConsoleCapability } from '@/lib/routing/hostConsoleCapability'; +import { resolveHostConsoleCapability, resolveHostConsoleLockMessage } from '@/lib/routing/hostConsoleCapability'; import { LockCard } from '../ui/LockCard'; import { CapabilityGate } from '../CapabilityGate'; import { HubOnlyGate } from '../HubOnlyGate'; @@ -207,17 +207,16 @@ export function ViewRouter({ }); if (capState === 'loading') return ; if (capState === 'locked') { - const nodeName = activeNode.name; - const version = activeNodeMeta?.version; - let versionHint = `${nodeName} does not advertise this capability.`; - if (version && version !== 'unknown' && version !== '0.0.0-dev') { - versionHint = `${nodeName} is running v${version}.`; - } + const { title, body } = resolveHostConsoleLockMessage({ + nodeMode: activeNode.mode, + nodeName: activeNode.name, + version: activeNodeMeta?.version, + }); return ( ); } diff --git a/frontend/src/components/EditorLayout/__tests__/ViewRouter.test.tsx b/frontend/src/components/EditorLayout/__tests__/ViewRouter.test.tsx index bbe6660a..76a3d4f5 100644 --- a/frontend/src/components/EditorLayout/__tests__/ViewRouter.test.tsx +++ b/frontend/src/components/EditorLayout/__tests__/ViewRouter.test.tsx @@ -128,12 +128,25 @@ describe('ViewRouter host-console', () => { it('shows a lock card for Community + legacy remote without mounting HostConsole', () => { vi.mocked(NodeContext.useNodes).mockReturnValue({ - activeNode: { id: 2, name: 'Legacy', type: 'remote' }, + activeNode: { id: 2, name: 'Legacy', type: 'remote', mode: 'proxy' }, activeNodeMeta: { version: '0.95.0', capabilities: ['host-console'], fetchedAt: 1 }, } as unknown as ReturnType); render(); expect(screen.queryByTestId('host-console')).toBeNull(); expect(screen.getByText(/Host Console is not available on this node/i)).toBeTruthy(); + expect(screen.getByText(/Legacy is running v0\.95\.0\. Upgrade the node to use this feature\./i)).toBeTruthy(); + }); + + it('shows Pilot-specific copy for a pilot_agent node without mounting HostConsole', () => { + vi.mocked(NodeContext.useNodes).mockReturnValue({ + activeNode: { id: 4, name: 'Pilot', type: 'remote', mode: 'pilot_agent' }, + activeNodeMeta: { version: '0.97.1', capabilities: [], fetchedAt: 1 }, + } as unknown as ReturnType); + render(); + expect(screen.queryByTestId('host-console')).toBeNull(); + expect(screen.getByText(/Host Console is not available through Pilot Agent yet/i)).toBeTruthy(); + expect(screen.getByText(/Host Console is currently available on the local node and Distributed API Proxy remotes\./i)).toBeTruthy(); + expect(screen.queryByText(/Upgrade the node to use this feature\./i)).toBeNull(); }); it('mounts Host Console for Admiral + legacy remote after meta resolves', async () => { diff --git a/frontend/src/lib/routing/hostConsoleCapability.test.ts b/frontend/src/lib/routing/hostConsoleCapability.test.ts index f43141f1..b218f065 100644 --- a/frontend/src/lib/routing/hostConsoleCapability.test.ts +++ b/frontend/src/lib/routing/hostConsoleCapability.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import { resolveHostConsoleCapability } from './hostConsoleCapability'; +import { resolveHostConsoleCapability, resolveHostConsoleLockMessage } from './hostConsoleCapability'; describe('resolveHostConsoleCapability', () => { it('returns loading when the active node is unresolved', () => { @@ -92,3 +92,60 @@ describe('resolveHostConsoleCapability', () => { })).toBe('locked'); }); }); + +describe('resolveHostConsoleLockMessage', () => { + it('returns Pilot-specific copy for a pilot_agent node regardless of version', () => { + expect(resolveHostConsoleLockMessage({ + nodeMode: 'pilot_agent', + nodeName: 'Pilot', + version: '0.97.1', + })).toEqual({ + title: 'Host Console is not available through Pilot Agent yet', + body: 'Host Console is currently available on the local node and Distributed API Proxy remotes.', + }); + }); + + it('returns the upgrade copy for a proxy node with a real version', () => { + expect(resolveHostConsoleLockMessage({ + nodeMode: 'proxy', + nodeName: 'Peer', + version: '0.95.0', + })).toEqual({ + title: 'Host Console is not available on this node', + body: 'Peer is running v0.95.0. Upgrade the node to use this feature.', + }); + }); + + it('returns the no-capability copy for a proxy node with a placeholder version', () => { + expect(resolveHostConsoleLockMessage({ + nodeMode: 'proxy', + nodeName: 'Peer', + version: '0.0.0-dev', + })).toEqual({ + title: 'Host Console is not available on this node', + body: 'Peer does not advertise this capability. Upgrade the node to use this feature.', + }); + }); + + it('returns the no-capability copy for a proxy node with an unknown version', () => { + expect(resolveHostConsoleLockMessage({ + nodeMode: 'proxy', + nodeName: 'Peer', + version: 'unknown', + })).toEqual({ + title: 'Host Console is not available on this node', + body: 'Peer does not advertise this capability. Upgrade the node to use this feature.', + }); + }); + + it('treats an undefined mode as the generic proxy fallback', () => { + expect(resolveHostConsoleLockMessage({ + nodeMode: undefined, + nodeName: 'Peer', + version: null, + })).toEqual({ + title: 'Host Console is not available on this node', + body: 'Peer does not advertise this capability. Upgrade the node to use this feature.', + }); + }); +}); diff --git a/frontend/src/lib/routing/hostConsoleCapability.ts b/frontend/src/lib/routing/hostConsoleCapability.ts index 71004993..b5e2b5df 100644 --- a/frontend/src/lib/routing/hostConsoleCapability.ts +++ b/frontend/src/lib/routing/hostConsoleCapability.ts @@ -2,6 +2,8 @@ import { HOST_CONSOLE_CAPABILITY, HOST_CONSOLE_COMMUNITY_CAPABILITY, } from '@/lib/capabilities'; +import type { NodeMode } from '@/context/NodeContext'; +import { formatVersion } from '@/lib/version'; export type HostConsoleCapabilityState = 'loading' | 'allowed' | 'locked'; @@ -47,3 +49,42 @@ export function resolveHostConsoleCapability( if (!licenseReady) return 'loading'; return isPaid ? 'allowed' : 'locked'; } + +export interface HostConsoleLockMessageInput { + /** Active node mode. Pilot Agent tunnels do not carry Host Console yet. */ + nodeMode: NodeMode | undefined; + nodeName: string; + version: string | null | undefined; +} + +/** + * Lock-card copy for a node whose Host Console capability is missing. + * + * Pilot Agent nodes cannot advertise the Host Console capability because the + * interactive console path is not wired through the Pilot tunnel, so an + * upgrade would not enable it. They get transport-specific copy instead of the + * generic "upgrade the node" instruction. Every other mode (proxy remote or + * missing metadata) keeps the generic upgrade message: version-aware when a + * real version is present, otherwise a no-capability hint. + */ +export function resolveHostConsoleLockMessage( + input: HostConsoleLockMessageInput, +): { title: string; body: string } { + const { nodeMode, nodeName, version } = input; + + if (nodeMode === 'pilot_agent') { + return { + title: 'Host Console is not available through Pilot Agent yet', + body: 'Host Console is currently available on the local node and Distributed API Proxy remotes.', + }; + } + + const formatted = formatVersion(version); + const versionHint = formatted + ? `${nodeName} is running ${formatted}.` + : `${nodeName} does not advertise this capability.`; + return { + title: 'Host Console is not available on this node', + body: `${versionHint} Upgrade the node to use this feature.`, + }; +}