+ ) : null}
+
+ {/* While probing (or probe error) and no saved config, neither auth UI shows —
+ show a hint instead of a blank gap. */}
+ {hasUrl && !authResolved ? (
+
{g.probing}
+ ) : null}
+
+ {error ? (
+
+
+ {error}
+
+ ) : null}
+
+
+
+
+
+
+ )
+}
+
diff --git a/apps/desktop/src/components/boot-failure-overlay.tsx b/apps/desktop/src/components/boot-failure-overlay.tsx
index 5836f335aac..d00f6388c44 100644
--- a/apps/desktop/src/components/boot-failure-overlay.tsx
+++ b/apps/desktop/src/components/boot-failure-overlay.tsx
@@ -38,10 +38,16 @@ export function BootFailureOverlay() {
const [remoteReauth, setRemoteReauth] = useState(null)
const visible = Boolean(boot.error) && !boot.running
+
// While first-run onboarding owns the picker/flow we let it surface its own
// progress; the recovery overlay is for hard failures, which it covers via a
// higher z-index regardless of onboarding state.
- const suppressed = onboarding.flow.status !== 'idle' && onboarding.flow.status !== 'error'
+ // Thin client: when the gateway connect form is showing (no remote configured
+ // yet), suppress the boot-failure overlay — the "no remote" error is expected
+ // and the user is already looking at the form to fix it.
+ const suppressed =
+ (onboarding.flow.status !== 'idle' && onboarding.flow.status !== 'error') ||
+ onboarding.gatewayMode
useEffect(() => {
if (!visible) {
diff --git a/apps/desktop/src/components/desktop-onboarding-overlay.test.tsx b/apps/desktop/src/components/desktop-onboarding-overlay.test.tsx
index 930280faf9d..520f716885d 100644
--- a/apps/desktop/src/components/desktop-onboarding-overlay.test.tsx
+++ b/apps/desktop/src/components/desktop-onboarding-overlay.test.tsx
@@ -27,7 +27,8 @@ function setProviders(providers: OAuthProvider[]) {
requested: false,
firstRunSkipped: false,
manual: false,
- localEndpoint: false
+ localEndpoint: false,
+ gatewayMode: false
} satisfies DesktopOnboardingState)
}
@@ -51,7 +52,8 @@ afterEach(() => {
requested: false,
firstRunSkipped: false,
manual: false,
- localEndpoint: false
+ localEndpoint: false,
+ gatewayMode: false
})
})
diff --git a/apps/desktop/src/components/desktop-onboarding-overlay.tsx b/apps/desktop/src/components/desktop-onboarding-overlay.tsx
index 7ea1c11ffb4..a4e50daf5f4 100644
--- a/apps/desktop/src/components/desktop-onboarding-overlay.tsx
+++ b/apps/desktop/src/components/desktop-onboarding-overlay.tsx
@@ -8,9 +8,11 @@ import { Codicon } from '@/components/ui/codicon'
import { ErrorIcon } from '@/components/ui/error-state'
import { Input } from '@/components/ui/input'
import { Loader } from '@/components/ui/loader'
+import type { DesktopConnectionProbeResult } from '@/global'
import { getGlobalModelOptions } from '@/hermes'
import { useI18n } from '@/i18n'
-import { Check, ChevronDown, ChevronLeft, ChevronRight, ExternalLink, KeyRound, Loader2, Terminal } from '@/lib/icons'
+import { isThinClient } from '@/lib/build-mode'
+import { AlertCircle, Check, ChevronDown, ChevronLeft, ChevronRight, ExternalLink, Globe, KeyRound, Loader2, LogIn, Terminal } from '@/lib/icons'
import { isProviderSetupErrorMessage } from '@/lib/provider-setup-errors'
import { cn } from '@/lib/utils'
import { $desktopBoot, type DesktopBootState } from '@/store/boot'
@@ -25,19 +27,24 @@ import {
DEFAULT_MANUAL_ONBOARDING_REASON,
DEFAULT_ONBOARDING_REASON,
dismissFirstRunOnboarding,
+ exitGatewayMode,
+ gatewayOauthLogin,
type OnboardingContext,
type OnboardingFlow,
peekPendingProviderOAuth,
recheckExternalSignin,
refreshOnboarding,
+ saveGatewayConnection,
saveOnboardingApiKey,
setOnboardingCode,
setOnboardingMode,
setOnboardingModel,
+ startGatewayOnboarding,
startProviderOAuth,
submitOnboardingCode
} from '@/store/onboarding'
import type { ModelOptionProvider, OAuthProvider } from '@/types/hermes'
+import { GatewayConnectForm } from './OnboardingGatewayConnection'
interface DesktopOnboardingOverlayProps {
enabled: boolean
@@ -267,7 +274,10 @@ export function DesktopOnboardingOverlay({ enabled, onCompleted, requestGateway
// The user chose "I'll choose a provider later" on first run. Stay out of the
// way on every subsequent launch — they re-enter via Settings → Providers
// (manual mode), which sets manual=true and bypasses this gate.
- if (onboarding.firstRunSkipped && !onboarding.manual) {
+ // Thin client: never respect the skip — there's no "choose later" escape from
+ // the gateway form (it's the only path), and a skipped thin client has no
+ // working backend to fall back on.
+ if (onboarding.firstRunSkipped && !onboarding.manual && !isThinClient()) {
return null
}
@@ -288,7 +298,9 @@ export function DesktopOnboardingOverlay({ enabled, onCompleted, requestGateway
// In manual mode the app is already configured, so the flow is "ready"
// immediately — no runtime gate needed. Otherwise wait for the readiness
// check (configured === false) before showing the picker.
- const ready = onboarding.manual || (enabled && onboarding.configured === false)
+ // Gateway mode (thin client or user picked "remote gateway") is also ready
+ // immediately — there's no provider list to wait for.
+ const ready = onboarding.manual || onboarding.gatewayMode || (enabled && onboarding.configured === false)
const showPicker = flow.status === 'idle' || flow.status === 'success'
// The final "you're in" screen drops the card chrome and floats centered on
// the surface — same bare, cinematic treatment as the connecting overlay.
@@ -332,7 +344,11 @@ export function DesktopOnboardingOverlay({ enabled, onCompleted, requestGateway
{reason ? : null}
{ready ? (
- showPicker ? (
+ onboarding.gatewayMode && showPicker ? (
+
+ ) : showPicker ? (
) : (
@@ -397,6 +413,7 @@ function Header() {
)
}
+
export const FEATURED_ID = 'nous'
const SHOW_ALL_KEY = 'hermes-onboarding-show-all-v1'
@@ -491,15 +508,27 @@ export function Picker({ ctx }: { ctx: OnboardingContext }) {
In manual mode the overlay already has a close affordance, so the
"choose later" escape would be redundant — hide it. */}
{manual ? : }
-
+
+
+
+
)
@@ -811,6 +840,10 @@ function FlowPanel({
return
}
+ if (flow.status === 'gateway_connected') {
+ return {t.onboarding.gateway.connecting}
+ }
+
if (flow.status === 'confirming_model') {
return
}
diff --git a/apps/desktop/src/components/gateway-connecting-overlay.test.tsx b/apps/desktop/src/components/gateway-connecting-overlay.test.tsx
index e5e49315985..0fa287d41f9 100644
--- a/apps/desktop/src/components/gateway-connecting-overlay.test.tsx
+++ b/apps/desktop/src/components/gateway-connecting-overlay.test.tsx
@@ -42,7 +42,8 @@ function resetStores() {
requested: false,
firstRunSkipped: false,
manual: false,
- localEndpoint: false
+ localEndpoint: false,
+ gatewayMode: false
})
}
diff --git a/apps/desktop/src/i18n/en.ts b/apps/desktop/src/i18n/en.ts
index f7841f760a3..5bccc561675 100644
--- a/apps/desktop/src/i18n/en.ts
+++ b/apps/desktop/src/i18n/en.ts
@@ -1735,7 +1735,27 @@ export const en: Translations = {
price: (input, output) => `${input} in / ${output} out per Mtok`,
change: 'Change',
startChatting: 'Begin',
- docs: provider => `${provider} docs`
+ docs: provider => `${provider} docs`,
+ connectRemoteGateway: 'Connect to a remote gateway',
+ gateway: {
+ urlLabel: 'Gateway URL',
+ urlHint: 'Base URL for the remote Hermes gateway. Path prefixes are supported, e.g. /hermes.',
+ probing: 'Checking how this gateway authenticates…',
+ probeError: 'Could not reach this gateway yet. Check the URL — the auth method will appear once it responds.',
+ signIn: 'Sign in',
+ signInWith: provider => `Sign in with ${provider}`,
+ passwordHint: 'This gateway uses a username and password. Sign in to authorize this app.',
+ oauthHint: provider => `This gateway uses OAuth. Sign in with ${provider} to authorize this app.`,
+ tokenLabel: 'Session token',
+ tokenPlaceholder: 'Paste session token',
+ tokenHint: 'The dashboard session token used for REST and WebSocket access.',
+ connect: 'Connect',
+ connecting: 'Connecting',
+ saveFailed: 'Could not save the gateway connection.',
+ signInFailed: 'Sign-in failed. Try again.',
+ backToProviders: 'Back to providers',
+ identityProvider: 'the gateway'
+ }
},
modelPicker: {
diff --git a/apps/desktop/src/i18n/ja.ts b/apps/desktop/src/i18n/ja.ts
index 741712a6d3c..d273dafba41 100644
--- a/apps/desktop/src/i18n/ja.ts
+++ b/apps/desktop/src/i18n/ja.ts
@@ -1859,7 +1859,27 @@ export const ja = defineLocale({
price: (input, output) => `${input} 入力 / ${output} 出力 per Mtok`,
change: '変更',
startChatting: '始める',
- docs: provider => `${provider} ドキュメント`
+ docs: provider => `${provider} ドキュメント`,
+ connectRemoteGateway: 'リモートゲートウェイに接続',
+ gateway: {
+ urlLabel: 'ゲートウェイ URL',
+ urlHint: 'リモート Hermes ゲートウェイのベース URL。パスプレフィックス対応(例: /hermes)。',
+ probing: 'ゲートウェイの認証方式を確認中…',
+ probeError: 'ゲートウェイに接続できません。URL を確認してください — 応答後に認証方式が表示されます。',
+ signIn: 'サインイン',
+ signInWith: provider => `${provider} でサインイン`,
+ passwordHint: 'このゲートウェイはユーザー名とパスワードを使用します。サインインしてアプリを認証してください。',
+ oauthHint: provider => `このゲートウェイは OAuth を使用します。${provider} でサインインしてアプリを認証してください。`,
+ tokenLabel: 'セッショントークン',
+ tokenPlaceholder: 'セッショントークンを貼り付け',
+ tokenHint: 'REST および WebSocket アクセスに使用されるダッシュボードセッショントークン。',
+ connect: '接続',
+ connecting: '接続中',
+ saveFailed: 'ゲートウェイ接続を保存できませんでした。',
+ signInFailed: 'サインインに失敗しました。再試行してください。',
+ backToProviders: 'プロバイダーに戻る',
+ identityProvider: 'ゲートウェイ'
+ }
},
modelPicker: {
diff --git a/apps/desktop/src/i18n/types.ts b/apps/desktop/src/i18n/types.ts
index 54534f915e7..50fa9ca2683 100644
--- a/apps/desktop/src/i18n/types.ts
+++ b/apps/desktop/src/i18n/types.ts
@@ -1393,6 +1393,26 @@ export interface Translations {
change: string
startChatting: string
docs: (provider: string) => string
+ connectRemoteGateway: string
+ gateway: {
+ urlLabel: string
+ urlHint: string
+ probing: string
+ probeError: string
+ signIn: string
+ signInWith: (provider: string) => string
+ passwordHint: string
+ oauthHint: (provider: string) => string
+ tokenLabel: string
+ tokenPlaceholder: string
+ tokenHint: string
+ connect: string
+ connecting: string
+ saveFailed: string
+ signInFailed: string
+ backToProviders: string
+ identityProvider: string
+ }
}
modelPicker: {
diff --git a/apps/desktop/src/i18n/zh-hant.ts b/apps/desktop/src/i18n/zh-hant.ts
index fbf2ed0822b..9354d4c5ffe 100644
--- a/apps/desktop/src/i18n/zh-hant.ts
+++ b/apps/desktop/src/i18n/zh-hant.ts
@@ -1797,7 +1797,27 @@ export const zhHant = defineLocale({
price: (input, output) => `${input} 輸入 / ${output} 輸出 每 Mtok`,
change: '變更',
startChatting: '開始',
- docs: provider => `${provider} 文件`
+ docs: provider => `${provider} 文件`,
+ connectRemoteGateway: '連線到遠端閘道',
+ gateway: {
+ urlLabel: '閘道 URL',
+ urlHint: '遠端 Hermes 閘道的基礎 URL。支援路徑前綴,例如 /hermes。',
+ probing: '正在檢查閘道認證方式…',
+ probeError: '暫時無法連線到此閘道。請檢查 URL — 認證方式將在閘道回應後顯示。',
+ signIn: '登入',
+ signInWith: provider => `使用 ${provider} 登入`,
+ passwordHint: '此閘道使用使用者名稱和密碼。登入以授權此應用。',
+ oauthHint: provider => `此閘道使用 OAuth。使用 ${provider} 登入以授權此應用。`,
+ tokenLabel: '工作階段權杖',
+ tokenPlaceholder: '貼上工作階段權杖',
+ tokenHint: '用於 REST 和 WebSocket 存取的儀表板工作階段權杖。',
+ connect: '連線',
+ connecting: '正在連線',
+ saveFailed: '無法儲存閘道連線。',
+ signInFailed: '登入失敗。請重試。',
+ backToProviders: '返回提供商列表',
+ identityProvider: '閘道'
+ }
},
modelPicker: {
diff --git a/apps/desktop/src/i18n/zh.ts b/apps/desktop/src/i18n/zh.ts
index 6d029367eae..0bbbd9771e9 100644
--- a/apps/desktop/src/i18n/zh.ts
+++ b/apps/desktop/src/i18n/zh.ts
@@ -1909,7 +1909,27 @@ export const zh: Translations = {
price: (input, output) => `${input} 输入 / ${output} 输出每 Mtok`,
change: '更改',
startChatting: '开始',
- docs: provider => `${provider} 文档`
+ docs: provider => `${provider} 文档`,
+ connectRemoteGateway: '连接到远程网关',
+ gateway: {
+ urlLabel: '网关地址',
+ urlHint: '远程 Hermes 网关的基础 URL。支持路径前缀,例如 /hermes。',
+ probing: '正在检查网关认证方式…',
+ probeError: '暂时无法连接到此网关。请检查 URL — 认证方式将在网关响应后显示。',
+ signIn: '登录',
+ signInWith: provider => `使用 ${provider} 登录`,
+ passwordHint: '此网关使用用户名和密码。登录以授权此应用。',
+ oauthHint: provider => `此网关使用 OAuth。使用 ${provider} 登录以授权此应用。`,
+ tokenLabel: '会话令牌',
+ tokenPlaceholder: '粘贴会话令牌',
+ tokenHint: '用于 REST 和 WebSocket 访问的仪表板会话令牌。',
+ connect: '连接',
+ connecting: '正在连接',
+ saveFailed: '无法保存网关连接。',
+ signInFailed: '登录失败。请重试。',
+ backToProviders: '返回提供商列表',
+ identityProvider: '网关'
+ }
},
modelPicker: {
diff --git a/apps/desktop/src/store/onboarding.test.ts b/apps/desktop/src/store/onboarding.test.ts
index 17e9964cc81..534864a2621 100644
--- a/apps/desktop/src/store/onboarding.test.ts
+++ b/apps/desktop/src/store/onboarding.test.ts
@@ -35,6 +35,7 @@ function baseState(overrides: Partial = {}): DesktopOnbo
firstRunSkipped: false,
manual: false,
localEndpoint: false,
+ gatewayMode: false,
...overrides
}
}
diff --git a/apps/desktop/src/store/onboarding.ts b/apps/desktop/src/store/onboarding.ts
index 9ef3754be7b..d76220f51ca 100644
--- a/apps/desktop/src/store/onboarding.ts
+++ b/apps/desktop/src/store/onboarding.ts
@@ -12,6 +12,7 @@ import {
submitOAuthCode,
validateProviderCredential
} from '@/hermes'
+import { isThinClient } from '@/lib/build-mode'
import { evaluateRuntimeReadiness, type RuntimeReadinessResult } from '@/lib/runtime-readiness'
import { notify, notifyError } from '@/store/notifications'
import type { ModelOptionProvider, OAuthProvider, OAuthStartResponse } from '@/types/hermes'
@@ -20,7 +21,7 @@ type PkceStart = Extract
type DeviceStart = Extract
type LoopbackStart = Extract
-export type OnboardingMode = 'apikey' | 'oauth'
+export type OnboardingMode = 'apikey' | 'oauth' | 'gateway'
export type OnboardingFlow =
| { status: 'idle' }
@@ -34,6 +35,14 @@ export type OnboardingFlow =
| { provider: OAuthProvider; start: OAuthStartResponse; status: 'submitting' }
| { copied: boolean; provider: OAuthProvider; status: 'external_pending' }
| { provider: OAuthProvider; status: 'success' }
+ | {
+ // Gateway onboarding: a remote gateway was configured (URL + auth),
+ // and the connection was saved+applied. The overlay shows a brief
+ // "connected" confirm before completing — the apply already reloads
+ // the window, so this is mostly a transitional state.
+ gatewayUrl: string
+ status: 'gateway_connected'
+ }
| {
// After successful credential acquisition, before completing
// onboarding: show the user which model they're getting and let
@@ -77,6 +86,11 @@ export interface DesktopOnboardingState {
* custom endpoint"). Forces the API-key form with the local option
* preselected instead of the OAuth picker. */
localEndpoint: boolean
+ /** True when the overlay should show the gateway-connection form instead
+ * of the provider picker. Set on thin-client builds (where
+ * there's no local backend to configure providers for), or when the user
+ * picks "Connect to a remote gateway" from the thick-client picker. */
+ gatewayMode: boolean
}
export interface OnboardingContext {
@@ -156,7 +170,8 @@ const INITIAL: DesktopOnboardingState = {
requested: false,
firstRunSkipped: readCachedSkipped(),
manual: false,
- localEndpoint: false
+ localEndpoint: false,
+ gatewayMode: false
}
export const $desktopOnboarding = atom(INITIAL)
@@ -430,6 +445,105 @@ export function startManualLocalEndpoint(reason: null | string = null) {
})
}
+// Open the onboarding overlay on the gateway-connection form. Used on the
+// thin client's first run (no local backend → must configure a remote
+// gateway), and on the thick client when the user picks "Connect to a
+// remote gateway" from the provider picker. The form drives the same
+// desktop bridge IPC (probe / save / apply / oauth-login / test) as the
+// Settings → Gateway page — just wrapped in the onboarding chrome.
+export function startGatewayOnboarding(reason: null | string = null) {
+ pendingProviderOAuthId = null
+ patch({
+ gatewayMode: true,
+ manual: false,
+ mode: 'gateway',
+ requested: true,
+ localEndpoint: false,
+ reason: reason ? reason.trim() || DEFAULT_ONBOARDING_REASON : null,
+ flow: { status: 'idle' }
+ })
+}
+
+// Save + apply a remote gateway connection from the onboarding form. On
+// success, `applyConnectionConfig` reloads the window — so we just set the
+// transitional "gateway_connected" flow state and let the reload handle
+// the rest. On failure, surface the error in the form.
+export async function saveGatewayConnection(
+ url: string,
+ authMode: 'oauth' | 'token',
+ token: string | undefined
+): Promise<{ ok: boolean; message?: string }> {
+ const trimmedUrl = url.trim()
+
+ if (!trimmedUrl || !/^https?:\/\//i.test(trimmedUrl)) {
+ return { ok: false, message: 'Enter a valid gateway URL (https://…).' }
+ }
+
+ try {
+ await window.hermesDesktop?.saveConnectionConfig({
+ mode: 'remote',
+ remoteUrl: trimmedUrl,
+ remoteAuthMode: authMode,
+ remoteToken: authMode === 'token' ? token?.trim() || undefined : undefined
+ })
+ // applyConnectionConfig restarts the backend / reloads the window.
+ await window.hermesDesktop?.applyConnectionConfig({
+ mode: 'remote',
+ remoteUrl: trimmedUrl,
+ remoteAuthMode: authMode,
+ remoteToken: authMode === 'token' ? token?.trim() || undefined : undefined
+ })
+ setFlow({ status: 'gateway_connected', gatewayUrl: trimmedUrl })
+
+ return { ok: true }
+ } catch (error) {
+ const msg = error instanceof Error ? error.message : String(error)
+
+ return { ok: false, message: msg }
+ }
+}
+
+// Initiate OAuth sign-in against a remote gateway from the onboarding form.
+// Mirrors GatewaySettings.signIn: save the URL + oauth mode first (so the login
+// window has a target), then open the login window. On success, apply the
+// connection which reloads the window.
+export async function gatewayOauthLogin(
+ url: string
+): Promise<{ ok: boolean; connected: boolean; message?: string }> {
+ const trimmedUrl = url.trim()
+
+ if (!trimmedUrl) {
+ return { ok: false, connected: false, message: 'Enter a gateway URL first.' }
+ }
+
+ try {
+ await window.hermesDesktop?.saveConnectionConfig({
+ mode: 'remote',
+ remoteUrl: trimmedUrl,
+ remoteAuthMode: 'oauth'
+ })
+
+ const result = await window.hermesDesktop?.oauthLoginConnectionConfig(trimmedUrl)
+
+ if (result?.connected) {
+ await window.hermesDesktop?.applyConnectionConfig({
+ mode: 'remote',
+ remoteUrl: trimmedUrl,
+ remoteAuthMode: 'oauth'
+ })
+ setFlow({ status: 'gateway_connected', gatewayUrl: trimmedUrl })
+
+ return { ok: true, connected: true }
+ }
+
+ return { ok: false, connected: false, message: 'Sign-in was not completed.' }
+ } catch (error) {
+ const msg = error instanceof Error ? error.message : String(error)
+
+ return { ok: false, connected: false, message: msg }
+ }
+}
+
// One-shot hand-off used when the dedicated Providers settings page launches a
// specific provider's sign-in: we open the manual onboarding overlay AND
// remember which provider to start, so the overlay drives that exact OAuth
@@ -461,7 +575,7 @@ export function clearPendingProviderOAuth() {
export function closeManualOnboarding() {
pendingProviderOAuthId = null
- patch({ manual: false, requested: false, localEndpoint: false, flow: { status: 'idle' } })
+ patch({ manual: false, requested: false, localEndpoint: false, gatewayMode: false, flow: { status: 'idle' } })
}
export function completeDesktopOnboarding() {
@@ -479,7 +593,8 @@ export function completeDesktopOnboarding() {
requested: false,
firstRunSkipped: false,
manual: false,
- localEndpoint: false
+ localEndpoint: false,
+ gatewayMode: false
})
}
@@ -492,13 +607,21 @@ export function completeDesktopOnboarding() {
export function dismissFirstRunOnboarding() {
clearPoll()
writeCachedSkipped(true)
- patch({ firstRunSkipped: true, requested: false, manual: false, localEndpoint: false, flow: { status: 'idle' } })
+ patch({ firstRunSkipped: true, requested: false, manual: false, localEndpoint: false, gatewayMode: false, flow: { status: 'idle' } })
}
export function setOnboardingMode(mode: OnboardingMode) {
patch({ mode })
}
+// Exit the gateway connect form and return to the provider picker.
+// Only used on the thick client where the user can go back from the
+// gateway form to the provider list. On thin client the gateway form
+// is the only option, so the back button is hidden.
+export function exitGatewayMode() {
+ patch({ gatewayMode: false, mode: 'oauth', flow: { status: 'idle' } })
+}
+
export async function refreshOnboarding(ctx: OnboardingContext) {
// Manual mode (user opened the selector from a working app): never
// auto-dismiss on runtime-ready — the whole point is to let them add /
@@ -510,6 +633,34 @@ export async function refreshOnboarding(ctx: OnboardingContext) {
return false
}
+ // Thin client: there's no local backend, so the runtime check always
+ // fails. Instead of showing the provider picker, check if a remote
+ // gateway is already configured. If it is, the boot will succeed and
+ // onCompleted fires. If not, show the gateway connect form.
+ if (isThinClient()) {
+ const config = await window.hermesDesktop?.getConnectionConfig?.().catch(() => null)
+
+ if (config?.mode === 'remote' && config.remoteUrl) {
+ // A remote gateway is configured — let the boot proceed. The gateway
+ // boot hook will either connect or surface a reauth failure.
+ completeDesktopOnboarding()
+ ctx.onCompleted?.()
+
+ return true
+ }
+
+ // No remote gateway configured — show the gateway connect form.
+ writeCachedConfigured(false)
+ patch({
+ configured: false,
+ gatewayMode: true,
+ mode: 'gateway',
+ reason: 'Connect to a remote gateway to get started.'
+ })
+
+ return false
+ }
+
const runtime = await checkRuntime(ctx)
if (runtime.ready) {
diff --git a/scripts/slice_tests.py b/scripts/slice_tests.py
new file mode 100644
index 00000000000..e69de29bb2d