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.`,
+ };
+}