diff --git a/app/src/components/BottomTabBar.tsx b/app/src/components/BottomTabBar.tsx index 40072ca73..c87ed4d70 100644 --- a/app/src/components/BottomTabBar.tsx +++ b/app/src/components/BottomTabBar.tsx @@ -127,7 +127,6 @@ const makeTabs = (t: (key: string) => string) => [ const BottomTabBar = () => { const { t } = useT(); - const tabs = useMemo(() => makeTabs(t), [t]); const location = useLocation(); const navigate = useNavigate(); const { snapshot } = useCoreState(); @@ -142,6 +141,7 @@ const BottomTabBar = () => { // so an absent theme branch can't crash the bar. const tabBarLabels = useAppSelector(state => state.theme?.tabBarLabels ?? 'hover'); const labelsAlwaysVisible = tabBarLabels === 'always'; + const tabs = useMemo(() => makeTabs(t), [t]); const hiddenPaths = ['/', '/login']; if ( diff --git a/app/src/components/EmptyStateCard.tsx b/app/src/components/EmptyStateCard.tsx new file mode 100644 index 000000000..756cf1387 --- /dev/null +++ b/app/src/components/EmptyStateCard.tsx @@ -0,0 +1,54 @@ +import type { ReactNode } from 'react'; + +interface EmptyStateCardProps { + icon: ReactNode; + title: string; + description: string; + actionLabel?: string; + onAction?: () => void; + footer?: ReactNode; + className?: string; +} + +const EmptyStateCard = ({ + icon, + title, + description, + actionLabel, + onAction, + footer, + className = '', +}: EmptyStateCardProps) => { + return ( +
+
+ {icon} +
+

{title}

+

+ {description} +

+ {actionLabel && onAction ? ( + + ) : null} + {footer} +
+ ); +}; + +export default EmptyStateCard; diff --git a/app/src/components/__tests__/BottomTabBar.test.tsx b/app/src/components/__tests__/BottomTabBar.test.tsx index 773882c9d..1160056cb 100644 --- a/app/src/components/__tests__/BottomTabBar.test.tsx +++ b/app/src/components/__tests__/BottomTabBar.test.tsx @@ -54,16 +54,18 @@ function buildStore(opts: BuildStoreOpts = {}) { interface RenderOpts { hasToken?: boolean; companionSessionActive?: boolean; + tokenValue?: string; } async function renderBottomTabBar(pathname = '/home', opts: RenderOpts | boolean = {}) { // Back-compat: previous callsites passed `hasToken` as the 2nd positional arg. const resolved: RenderOpts = typeof opts === 'boolean' ? { hasToken: opts } : opts; const hasToken = resolved.hasToken ?? true; + const tokenValue = resolved.tokenValue ?? 'tok-test'; const { useCoreState } = await import('../../providers/CoreStateProvider'); vi.mocked(useCoreState).mockReturnValue({ snapshot: { - sessionToken: hasToken ? 'tok-test' : null, + sessionToken: hasToken ? tokenValue : null, auth: { isAuthenticated: true, userId: 'u1', user: null, profileId: null }, currentUser: null, onboardingCompleted: true, @@ -123,6 +125,11 @@ describe('BottomTabBar', () => { expect(container.firstChild).toBeNull(); }); + it('still shows the Rewards tab for local sessions', async () => { + await renderBottomTabBar('/home', { tokenValue: 'header.payload.local' }); + expect(screen.getByRole('button', { name: 'Rewards' })).toBeInTheDocument(); + }); + it('renders the pulsing companion dot on the Settings tab when a session is active', async () => { const { container } = await renderBottomTabBar('/home', { companionSessionActive: true }); const settingsBtn = screen.getByRole('button', { name: 'Settings' }); diff --git a/app/src/components/channels/DiscordConfig.tsx b/app/src/components/channels/DiscordConfig.tsx index 6b9a0932e..9e16d4925 100644 --- a/app/src/components/channels/DiscordConfig.tsx +++ b/app/src/components/channels/DiscordConfig.tsx @@ -3,6 +3,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { useOAuthConnectionListener } from '../../hooks/useOAuthConnectionListener'; import { useT } from '../../lib/i18n/I18nContext'; +import { useCoreState } from '../../providers/CoreStateProvider'; import { channelConnectionsApi } from '../../services/api/channelConnectionsApi'; import { callCoreRpc } from '../../services/coreRpcClient'; import { @@ -18,6 +19,7 @@ import type { ChannelConnectionStatus, ChannelDefinition, } from '../../types/channels'; +import { isLocalSessionToken } from '../../utils/localSession'; import { openUrl } from '../../utils/openUrl'; import { restartCoreProcess } from '../../utils/tauriCommands/core'; import ChannelFieldInput from './ChannelFieldInput'; @@ -36,6 +38,11 @@ const DiscordConfig = ({ definition }: DiscordConfigProps) => { const { t } = useT(); const dispatch = useAppDispatch(); const channelConnections = useAppSelector(state => state.channelConnections); + const { snapshot } = useCoreState(); + const isLocalSession = isLocalSessionToken(snapshot.sessionToken); + const visibleAuthModes = definition.auth_modes.filter( + spec => !isLocalSession || (spec.mode !== 'managed_dm' && spec.mode !== 'oauth') + ); const [busyKeys, setBusyKeys] = useState>({}); const [fieldValues, setFieldValues] = useState>>({}); @@ -279,7 +286,13 @@ const DiscordConfig = ({ definition }: DiscordConfigProps) => { )} - {definition.auth_modes.map(spec => { + {isLocalSession && visibleAuthModes.length !== definition.auth_modes.length && ( +
+ {t('channels.localManagedUnavailable')} +
+ )} + + {visibleAuthModes.map(spec => { const compositeKey = `discord:${spec.mode}`; const connection = channelConnections.connections.discord?.[spec.mode]; const status: ChannelConnectionStatus = connection?.status ?? 'disconnected'; diff --git a/app/src/components/channels/TelegramConfig.tsx b/app/src/components/channels/TelegramConfig.tsx index 67a5b9699..10de76184 100644 --- a/app/src/components/channels/TelegramConfig.tsx +++ b/app/src/components/channels/TelegramConfig.tsx @@ -3,6 +3,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { useOAuthConnectionListener } from '../../hooks/useOAuthConnectionListener'; import { useT } from '../../lib/i18n/I18nContext'; +import { useCoreState } from '../../providers/CoreStateProvider'; import { channelConnectionsApi } from '../../services/api/channelConnectionsApi'; import { callCoreRpc } from '../../services/coreRpcClient'; import { @@ -18,6 +19,7 @@ import type { ChannelConnectionStatus, ChannelDefinition, } from '../../types/channels'; +import { isLocalSessionToken } from '../../utils/localSession'; import { openUrl } from '../../utils/openUrl'; import { restartCoreProcess } from '../../utils/tauriCommands/core'; import ChannelFieldInput from './ChannelFieldInput'; @@ -33,6 +35,11 @@ const TelegramConfig = ({ definition }: TelegramConfigProps) => { const { t } = useT(); const dispatch = useAppDispatch(); const channelConnections = useAppSelector(state => state.channelConnections); + const { snapshot } = useCoreState(); + const isLocalSession = isLocalSessionToken(snapshot.sessionToken); + const visibleAuthModes = definition.auth_modes.filter( + spec => !isLocalSession || (spec.mode !== 'managed_dm' && spec.mode !== 'oauth') + ); const MANAGED_DM_CONNECTING_MESSAGE = t('channels.telegram.managedDmConnecting'); const MANAGED_DM_TIMEOUT_MESSAGE = t('channels.telegram.managedDmTimeout'); @@ -340,7 +347,13 @@ const TelegramConfig = ({ definition }: TelegramConfigProps) => { )} - {definition.auth_modes.map(spec => { + {isLocalSession && visibleAuthModes.length !== definition.auth_modes.length && ( +
+ {t('channels.localManagedUnavailable')} +
+ )} + + {visibleAuthModes.map(spec => { const compositeKey = `telegram:${spec.mode}`; const connection = channelConnections.connections.telegram?.[spec.mode]; const status: ChannelConnectionStatus = connection?.status ?? 'disconnected'; diff --git a/app/src/components/channels/__tests__/DiscordConfig.test.tsx b/app/src/components/channels/__tests__/DiscordConfig.test.tsx index 5eed7c343..2377eb098 100644 --- a/app/src/components/channels/__tests__/DiscordConfig.test.tsx +++ b/app/src/components/channels/__tests__/DiscordConfig.test.tsx @@ -1,12 +1,21 @@ import { screen } from '@testing-library/react'; -import { describe, expect, it } from 'vitest'; +import { afterEach, describe, expect, it, vi } from 'vitest'; import { FALLBACK_DEFINITIONS } from '../../../lib/channels/definitions'; import { renderWithProviders } from '../../../test/test-utils'; import DiscordConfig from '../DiscordConfig'; +const coreStateMock = vi.hoisted(() => vi.fn(() => ({ snapshot: { sessionToken: 'jwt-abc' } }))); + +vi.mock('../../../providers/CoreStateProvider', () => ({ useCoreState: () => coreStateMock() })); + const discordDef = FALLBACK_DEFINITIONS.find(d => d.id === 'discord')!; +afterEach(() => { + vi.clearAllMocks(); + coreStateMock.mockReturnValue({ snapshot: { sessionToken: 'jwt-abc' } }); +}); + describe('DiscordConfig', () => { it('renders auth mode labels', () => { renderWithProviders(); @@ -30,4 +39,17 @@ describe('DiscordConfig', () => { const connectButtons = screen.getAllByText('Connect'); expect(connectButtons.length).toBe(3); }); + + it('hides managed channel auth modes for local users', () => { + coreStateMock.mockReturnValue({ snapshot: { sessionToken: 'header.payload.local' } }); + + renderWithProviders(); + + expect( + screen.getByText('Managed channels are not available for local users.') + ).toBeInTheDocument(); + expect(screen.queryByText('OAuth Sign-in')).not.toBeInTheDocument(); + expect(screen.queryByText('Login with OpenHuman')).not.toBeInTheDocument(); + expect(screen.getAllByText('Bot Token').length).toBeGreaterThanOrEqual(1); + }); }); diff --git a/app/src/components/channels/__tests__/TelegramConfig.test.tsx b/app/src/components/channels/__tests__/TelegramConfig.test.tsx index 7bc841e98..871a6984a 100644 --- a/app/src/components/channels/__tests__/TelegramConfig.test.tsx +++ b/app/src/components/channels/__tests__/TelegramConfig.test.tsx @@ -8,6 +8,7 @@ import { openUrl } from '../../../utils/openUrl'; import TelegramConfig from '../TelegramConfig'; const telegramDef = FALLBACK_DEFINITIONS.find(d => d.id === 'telegram')!; +const coreStateMock = vi.hoisted(() => vi.fn(() => ({ snapshot: { sessionToken: 'jwt-abc' } }))); vi.mock('../../../services/api/channelConnectionsApi', () => ({ channelConnectionsApi: { @@ -21,9 +22,11 @@ vi.mock('../../../services/api/channelConnectionsApi', () => ({ })); vi.mock('../../../utils/openUrl', () => ({ openUrl: vi.fn() })); +vi.mock('../../../providers/CoreStateProvider', () => ({ useCoreState: () => coreStateMock() })); afterEach(() => { vi.clearAllMocks(); + coreStateMock.mockReturnValue({ snapshot: { sessionToken: 'jwt-abc' } }); }); describe('TelegramConfig', () => { @@ -98,4 +101,16 @@ describe('TelegramConfig', () => { }); expect(await screen.findByText('Connected')).toBeInTheDocument(); }); + + it('hides managed channel auth modes for local users', () => { + coreStateMock.mockReturnValue({ snapshot: { sessionToken: 'header.payload.local' } }); + + renderWithProviders(); + + expect( + screen.getByText('Managed channels are not available for local users.') + ).toBeInTheDocument(); + expect(screen.queryByText('Login with OpenHuman')).not.toBeInTheDocument(); + expect(screen.getAllByText(/Bot Token/i).length).toBeGreaterThanOrEqual(1); + }); }); diff --git a/app/src/components/settings/SettingsHome.tsx b/app/src/components/settings/SettingsHome.tsx index afcbd0b25..c7b2f5179 100644 --- a/app/src/components/settings/SettingsHome.tsx +++ b/app/src/components/settings/SettingsHome.tsx @@ -2,7 +2,9 @@ import type { ReactNode } from 'react'; import { useNavigate } from 'react-router-dom'; import { useT } from '../../lib/i18n/I18nContext'; +import { useCoreState } from '../../providers/CoreStateProvider'; import { BILLING_DASHBOARD_URL } from '../../utils/links'; +import { isLocalSessionToken } from '../../utils/localSession'; import { openUrl } from '../../utils/openUrl'; import LanguageSelect from '../LanguageSelect'; import SettingsHeader from './components/SettingsHeader'; @@ -28,6 +30,8 @@ const SettingsHome = () => { const navigate = useNavigate(); const { navigateToSettings } = useSettingsNavigation(); const { t } = useT(); + const { snapshot } = useCoreState(); + const isLocalSession = isLocalSessionToken(snapshot.sessionToken); const settingsSections: SettingsSection[] = [ { @@ -150,29 +154,36 @@ const SettingsHome = () => { // Features tile (Screen Awareness / Messaging Channels / Notifications / // Tools) used to live here. Everything under it moved into Advanced // (DeveloperOptionsPanel), so the section is gone from the home menu. - { - label: t('settings.billingAndRewards'), - items: [ - { - id: 'billing', - title: t('settings.billingUsage'), - description: t('settings.billingUsageDesc'), - icon: ( - - - - ), - onClick: () => { - openUrl(BILLING_DASHBOARD_URL).catch(() => {}); - }, - }, - ], - }, + // Billing & Rewards requires a backend-authenticated session. + // Hidden in local/offline mode — no auth headers are sent and the + // billing dashboard would not recognise the session. + ...(!isLocalSession + ? [ + { + label: t('settings.billingAndRewards'), + items: [ + { + id: 'billing', + title: t('settings.billingUsage'), + description: t('settings.billingUsageDesc'), + icon: ( + + + + ), + onClick: () => { + openUrl(BILLING_DASHBOARD_URL).catch(() => {}); + }, + }, + ], + } satisfies SettingsSection, + ] + : []), { label: t('settings.advanced'), items: [ diff --git a/app/src/components/settings/__tests__/SettingsHome.test.tsx b/app/src/components/settings/__tests__/SettingsHome.test.tsx index 5cae6cf87..aef243722 100644 --- a/app/src/components/settings/__tests__/SettingsHome.test.tsx +++ b/app/src/components/settings/__tests__/SettingsHome.test.tsx @@ -3,7 +3,7 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { Provider } from 'react-redux'; import { MemoryRouter } from 'react-router-dom'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { I18nProvider } from '../../../lib/i18n/I18nContext'; import type { Locale } from '../../../lib/i18n/types'; @@ -33,10 +33,13 @@ vi.mock('../hooks/useSettingsNavigation', () => ({ useSettingsNavigation: () => ({ navigateToSettings: mockNavigateToSettings }), })); +const mockClearSession = vi.fn().mockResolvedValue(undefined); +let mockSessionToken: string | null = null; + vi.mock('../../../providers/CoreStateProvider', () => ({ useCoreState: () => ({ - clearSession: vi.fn().mockResolvedValue(undefined), - snapshot: { auth: { userId: null }, currentUser: null }, + clearSession: mockClearSession, + snapshot: { auth: { userId: null }, currentUser: null, sessionToken: mockSessionToken }, }), })); @@ -179,6 +182,27 @@ describe('SettingsHome', () => { }); }); + describe('local session gating', () => { + beforeEach(() => { + // Use a valid local-session token (three parts, last part = 'local') + mockSessionToken = 'header.payload.local'; + }); + + afterEach(() => { + mockSessionToken = null; + }); + + it('hides the Billing & Usage item in local mode', () => { + renderSettingsHome(); + expect(screen.queryByText('Billing & Usage')).not.toBeInTheDocument(); + }); + + it('shows "Billing & Usage" when not in local mode', () => { + mockSessionToken = null; + renderSettingsHome(); + expect(screen.getByText('Billing & Usage')).toBeInTheDocument(); + }); + }); // Clear App Data flow moved to LogoutAndClearActions (rendered on Account // page) — see LogoutAndClearActions.test.tsx. }); diff --git a/app/src/components/settings/panels/ComposioPanel.tsx b/app/src/components/settings/panels/ComposioPanel.tsx index c2747d83a..342e7f47f 100644 --- a/app/src/components/settings/panels/ComposioPanel.tsx +++ b/app/src/components/settings/panels/ComposioPanel.tsx @@ -15,6 +15,8 @@ import { useEffect, useRef, useState } from 'react'; import { useT } from '../../../lib/i18n/I18nContext'; +import { useCoreState } from '../../../providers/CoreStateProvider'; +import { isLocalSessionToken } from '../../../utils/localSession'; import { type ComposioModeStatus, openhumanComposioClearApiKey, @@ -30,11 +32,18 @@ interface ComposioPanelProps { /** When true, render without the SettingsHeader chrome (used when embedded * inside the onboarding custom wizard). */ embedded?: boolean; + /** Whether OpenHuman-managed auth should be offered. Defaults to true for + * cloud-authenticated sessions and false otherwise. */ + managedAuthEnabled?: boolean; } -const ComposioPanel = ({ embedded = false }: ComposioPanelProps = {}) => { +const ComposioPanel = ({ embedded = false, managedAuthEnabled }: ComposioPanelProps = {}) => { const { t } = useT(); const { navigateBack, breadcrumbs } = useSettingsNavigation(); + const { snapshot } = useCoreState(); + const allowManagedAuth = + managedAuthEnabled ?? + (Boolean(snapshot.sessionToken) && !isLocalSessionToken(snapshot.sessionToken)); const [mode, setMode] = useState('backend'); // Tracks the mode that's actually persisted on disk — distinct from @@ -66,7 +75,8 @@ const ComposioPanel = ({ embedded = false }: ComposioPanelProps = {}) => { if (!isMounted) return; const status: ComposioModeStatus | undefined = res.result; if (!status) return; - const normalizedMode: Mode = status.mode === 'direct' ? 'direct' : 'backend'; + const normalizedMode: Mode = + !allowManagedAuth || status.mode === 'direct' ? 'direct' : 'backend'; setMode(normalizedMode); setPersistedMode(normalizedMode); setApiKeyStored(Boolean(status.api_key_set)); @@ -87,7 +97,7 @@ const ComposioPanel = ({ embedded = false }: ComposioPanelProps = {}) => { clearTimeout(saveStatusTimer.current); } }; - }, []); + }, [allowManagedAuth]); const flashSaved = (status: 'saved' | 'cleared') => { setSaveStatus(status); @@ -228,58 +238,71 @@ const ComposioPanel = ({ embedded = false }: ComposioPanelProps = {}) => { {t('settings.composio.intro')}

- {/* Mode toggle */} -
-
- - {t('settings.composio.routingMode')} - -
- - -
-
-
+ {allowManagedAuth ? ( +
+
+ + {t('settings.composio.routingMode')} + +
+ + +
+
+
+ ) : ( +
+

+ {t('settings.composio.modeDirect')} +

+

+ {t( + 'settings.composio.directOnlyDesc', + 'Managed Composio auth is unavailable here. Enter your own Composio API key or skip this for now.' + )} +

+
+ )} {/* API key field — only when Direct is selected */} {mode === 'direct' && ( diff --git a/app/src/components/settings/panels/DevicesComingSoonPanel.tsx b/app/src/components/settings/panels/DevicesComingSoonPanel.tsx new file mode 100644 index 000000000..a2f76fca9 --- /dev/null +++ b/app/src/components/settings/panels/DevicesComingSoonPanel.tsx @@ -0,0 +1,52 @@ +import { useT } from '../../../lib/i18n/I18nContext'; +import EmptyStateCard from '../../EmptyStateCard'; +import SettingsHeader from '../components/SettingsHeader'; +import { useSettingsNavigation } from '../hooks/useSettingsNavigation'; + +const DevicesComingSoonPanel = () => { + const { t } = useT(); + const { navigateBack, breadcrumbs } = useSettingsNavigation(); + + return ( +
+
+ 0} + onBack={navigateBack} + breadcrumbs={breadcrumbs} + /> +
+ +
+
+
+ ); +}; + +export default DevicesComingSoonPanel; diff --git a/app/src/components/settings/panels/SearchPanel.tsx b/app/src/components/settings/panels/SearchPanel.tsx index cf8157ef1..8a120de9a 100644 --- a/app/src/components/settings/panels/SearchPanel.tsx +++ b/app/src/components/settings/panels/SearchPanel.tsx @@ -1,6 +1,8 @@ import { useEffect, useState } from 'react'; import { useT } from '../../../lib/i18n/I18nContext'; +import { useCoreState } from '../../../providers/CoreStateProvider'; +import { isLocalSessionToken } from '../../../utils/localSession'; import { openhumanGetSearchSettings, openhumanUpdateSearchSettings, @@ -24,9 +26,11 @@ interface EngineOption { requiresKey: boolean; } -const SearchPanel = () => { +const SearchPanel = ({ embedded = false }: { embedded?: boolean }) => { const { t } = useT(); const { navigateBack, breadcrumbs } = useSettingsNavigation(); + const { snapshot } = useCoreState(); + const isLocalSession = isLocalSessionToken(snapshot.sessionToken); const [settings, setSettings] = useState(null); const [status, setStatus] = useState({ kind: 'loading' }); @@ -55,6 +59,9 @@ const SearchPanel = () => { requiresKey: true, }, ]; + const visibleEngines = isLocalSession + ? ENGINES.filter(engine => engine.id !== 'managed') + : ENGINES; useEffect(() => { let cancelled = false; @@ -118,18 +125,26 @@ const SearchPanel = () => { return (
- + {!embedded && ( + + )} -
+

{t('settings.search.description')}

+ {isLocalSession && ( +
+ {t('settings.search.localManagedUnavailable')} +
+ )} + {status.kind === 'loading' && (
{t('common.loading')} @@ -142,7 +157,7 @@ const SearchPanel = () => { className="bg-white dark:bg-neutral-900 rounded-xl border border-neutral-200 dark:border-neutral-800 overflow-hidden" role="radiogroup" aria-label={t('settings.search.engineAria')}> - {ENGINES.map((opt, idx) => { + {visibleEngines.map((opt, idx) => { const selected = opt.id === selectedEngine; const configured = isConfigured(opt.id); const blocked = opt.requiresKey && !configured && selected; diff --git a/app/src/components/settings/panels/__tests__/ComposioPanel.test.tsx b/app/src/components/settings/panels/__tests__/ComposioPanel.test.tsx index 0da2614b1..89acbfa4a 100644 --- a/app/src/components/settings/panels/__tests__/ComposioPanel.test.tsx +++ b/app/src/components/settings/panels/__tests__/ComposioPanel.test.tsx @@ -16,6 +16,10 @@ vi.mock('../../../../utils/tauriCommands', () => ({ openhumanComposioClearApiKey: hoisted.clearApiKey, })); +const coreStateMock = vi.fn(() => ({ snapshot: { sessionToken: 'cloud.jwt.remote' } })); + +vi.mock('../../../../providers/CoreStateProvider', () => ({ useCoreState: () => coreStateMock() })); + vi.mock('../../hooks/useSettingsNavigation', () => ({ useSettingsNavigation: () => ({ navigateBack: vi.fn(), @@ -41,6 +45,7 @@ const directModeWithKey = { result: { mode: 'direct' as const, api_key_set: true describe('ComposioPanel', () => { beforeEach(() => { vi.clearAllMocks(); + coreStateMock.mockReturnValue({ snapshot: { sessionToken: 'cloud.jwt.remote' } }); hoisted.getMode.mockResolvedValue(backendMode); hoisted.setApiKey.mockResolvedValue({ result: { stored: true, mode: 'direct' }, logs: [] }); hoisted.clearApiKey.mockResolvedValue({ result: { cleared: true, mode: 'backend' }, logs: [] }); @@ -104,6 +109,20 @@ describe('ComposioPanel', () => { expect(screen.getByLabelText('Composio API key')).toBeInTheDocument(); }); + test('defaults to direct-only API-key mode when managed auth is unavailable', async () => { + coreStateMock.mockReturnValue({ snapshot: { sessionToken: 'header.payload.local' } }); + const Panel = await importPanel(); + renderWithProviders(); + await waitFor(() => expect(screen.queryByText('Loading…')).toBeNull()); + + expect( + screen.queryByLabelText('Managed (OpenHuman handles it for you)') + ).not.toBeInTheDocument(); + expect(screen.queryByLabelText('Direct (bring your own API key)')).not.toBeInTheDocument(); + expect(screen.getByLabelText('Composio API key')).toBeInTheDocument(); + expect(screen.getByText(/Managed Composio auth is unavailable here/i)).toBeInTheDocument(); + }); + test('saving Direct mode with a key calls setApiKey and masks the field', async () => { const Panel = await importPanel(); renderWithProviders(); diff --git a/app/src/hooks/__tests__/useBackendReachable.test.tsx b/app/src/hooks/__tests__/useBackendReachable.test.tsx new file mode 100644 index 000000000..3463c5166 --- /dev/null +++ b/app/src/hooks/__tests__/useBackendReachable.test.tsx @@ -0,0 +1,36 @@ +import { renderHook, waitFor } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { useBackendReachable } from '../useBackendReachable'; + +vi.mock('../../services/backendUrl', () => ({ + getBackendUrl: vi.fn().mockResolvedValue('http://localhost:5005'), +})); + +describe('useBackendReachable', () => { + beforeEach(() => { + vi.restoreAllMocks(); + }); + + it('reports reachable when fetch resolves with any HTTP response', async () => { + vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response(null, { status: 200 })); + + const { result } = renderHook(() => useBackendReachable()); + expect(result.current).toBe('probing'); + await waitFor(() => expect(result.current).toBe('reachable')); + }); + + it('reports reachable on 4xx (host answered)', async () => { + vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response(null, { status: 404 })); + + const { result } = renderHook(() => useBackendReachable()); + await waitFor(() => expect(result.current).toBe('reachable')); + }); + + it('reports unreachable when fetch rejects (network error)', async () => { + vi.spyOn(globalThis, 'fetch').mockRejectedValue(new TypeError('Failed to fetch')); + + const { result } = renderHook(() => useBackendReachable()); + await waitFor(() => expect(result.current).toBe('unreachable')); + }); +}); diff --git a/app/src/hooks/useBackendReachable.ts b/app/src/hooks/useBackendReachable.ts new file mode 100644 index 000000000..fb48ae629 --- /dev/null +++ b/app/src/hooks/useBackendReachable.ts @@ -0,0 +1,59 @@ +import createDebug from 'debug'; +import { useEffect, useState } from 'react'; + +import { getBackendUrl } from '../services/backendUrl'; + +const log = createDebug('app:backend-probe'); + +export type BackendProbeStatus = 'probing' | 'reachable' | 'unreachable'; + +const PROBE_TIMEOUT_MS = 2500; + +/** + * Probes the configured backend URL once on mount and reports whether it is + * reachable. The "Continue locally" CTA on the Welcome screen is gated on + * `unreachable` so users only see it when the backend OAuth flow can't be + * completed (per issue #2037 AC). + * + * We treat any successful HTTP response (incl. 4xx) as reachable — the goal is + * to confirm the host is online, not that a specific route exists. + */ +export function useBackendReachable(): BackendProbeStatus { + const [status, setStatus] = useState('probing'); + + useEffect(() => { + let cancelled = false; + const controller = new AbortController(); + const timeoutId = window.setTimeout(() => controller.abort(), PROBE_TIMEOUT_MS); + + void (async () => { + try { + const base = await getBackendUrl(); + log('[probe] fetching %s/health', base); + const response = await fetch(`${base}/health`, { + method: 'GET', + signal: controller.signal, + cache: 'no-store', + }); + if (cancelled) return; + // Any HTTP response (even 404) means the host answered — it's online. + log('[probe] response status=%d → reachable', response.status); + setStatus('reachable'); + } catch (err) { + if (cancelled) return; + log('[probe] failed → unreachable: %o', err); + setStatus('unreachable'); + } finally { + window.clearTimeout(timeoutId); + } + })(); + + return () => { + cancelled = true; + controller.abort(); + window.clearTimeout(timeoutId); + }; + }, []); + + return status; +} diff --git a/app/src/lib/composio/hooks.test.ts b/app/src/lib/composio/hooks.test.ts index 14c41fc4f..4333df6c0 100644 --- a/app/src/lib/composio/hooks.test.ts +++ b/app/src/lib/composio/hooks.test.ts @@ -4,6 +4,8 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; const mockListToolkits = vi.fn(); const mockListConnections = vi.fn(); const mockListAgentReadyToolkits = vi.fn(); +const mockOpenhumanComposioGetMode = vi.fn(); +let sessionToken = 'jwt-abc'; vi.mock('./composioApi', () => ({ listToolkits: () => mockListToolkits(), @@ -11,10 +13,27 @@ vi.mock('./composioApi', () => ({ listAgentReadyToolkits: () => mockListAgentReadyToolkits(), })); +vi.mock('../coreState/store', async () => { + const actual = await vi.importActual('../coreState/store'); + return { ...actual, getCoreStateSnapshot: () => ({ snapshot: { sessionToken } }) }; +}); + +vi.mock('../../utils/tauriCommands', async () => { + const actual = await vi.importActual( + '../../utils/tauriCommands' + ); + return { ...actual, openhumanComposioGetMode: () => mockOpenhumanComposioGetMode() }; +}); + describe('useComposioIntegrations', () => { beforeEach(() => { vi.resetModules(); vi.clearAllMocks(); + sessionToken = 'jwt-abc'; + mockOpenhumanComposioGetMode.mockResolvedValue({ + result: { mode: 'backend', api_key_set: true }, + logs: [], + }); }); it('keeps toolkit cards visible when connections fetch fails', async () => { @@ -50,6 +69,27 @@ describe('useComposioIntegrations', () => { expect(result.current.connectionByToolkit.size).toBe(0); expect(result.current.error).toBe('backend unreachable'); }); + + it('skips toolkit fetch and polling for local sessions without a composio api key', async () => { + sessionToken = 'header.payload.local'; + mockOpenhumanComposioGetMode.mockResolvedValue({ + result: { mode: 'direct', api_key_set: false }, + logs: [], + }); + + const { useComposioIntegrations } = await import('./hooks'); + const { result } = renderHook(() => useComposioIntegrations(10)); + + await waitFor(() => { + expect(result.current.loading).toBe(false); + }); + + expect(result.current.toolkits).toEqual([]); + expect(result.current.connectionByToolkit.size).toBe(0); + expect(result.current.error).toBeNull(); + expect(mockListToolkits).not.toHaveBeenCalled(); + expect(mockListConnections).not.toHaveBeenCalled(); + }); }); describe('useAgentReadyComposioToolkits', () => { diff --git a/app/src/lib/composio/hooks.ts b/app/src/lib/composio/hooks.ts index 2372b64fe..6d593ec21 100644 --- a/app/src/lib/composio/hooks.ts +++ b/app/src/lib/composio/hooks.ts @@ -1,5 +1,8 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { isLocalSessionToken } from '../../utils/localSession'; +import { openhumanComposioGetMode } from '../../utils/tauriCommands'; +import { getCoreStateSnapshot } from '../coreState/store'; import { listAgentReadyToolkits, listConnections, listToolkits } from './composioApi'; import { canonicalizeComposioToolkitSlug } from './toolkitSlug'; import type { ComposioConnection } from './types'; @@ -33,10 +36,14 @@ export interface UseComposioIntegrationsResult { * explicit `refresh()` because the allowlist is stable. */ export function useComposioIntegrations(pollIntervalMs = 5_000): UseComposioIntegrationsResult { + const isLocalSession = isLocalSessionToken(getCoreStateSnapshot().snapshot.sessionToken); const [toolkits, setToolkits] = useState([]); const [connections, setConnections] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [fetchEnabled, setFetchEnabled] = useState(() => + isLocalSession ? null : true + ); const mountedRef = useRef(true); useEffect(() => { @@ -46,7 +53,38 @@ export function useComposioIntegrations(pollIntervalMs = 5_000): UseComposioInte }; }, []); + const resolveFetchEnabled = useCallback(async (): Promise => { + if (!isLocalSession) { + if (mountedRef.current) setFetchEnabled(true); + return true; + } + try { + const res = await openhumanComposioGetMode(); + const enabled = Boolean(res.result?.api_key_set); + if (mountedRef.current) setFetchEnabled(enabled); + return enabled; + } catch (err) { + console.warn( + '[composio] failed to resolve direct-mode api key status:', + err instanceof Error ? err.message : String(err) + ); + if (mountedRef.current) setFetchEnabled(false); + return false; + } + }, [isLocalSession]); + const refresh = useCallback(async () => { + const enabled = fetchEnabled ?? (await resolveFetchEnabled()); + if (!enabled) { + if (mountedRef.current) { + setToolkits([]); + setConnections([]); + setError(null); + setLoading(false); + } + return; + } + let nextError: string | null = null; try { const [toolkitsResult, connectionsResult] = await Promise.allSettled([ @@ -81,12 +119,12 @@ export function useComposioIntegrations(pollIntervalMs = 5_000): UseComposioInte } finally { if (mountedRef.current) setLoading(false); } - }, []); + }, [fetchEnabled, resolveFetchEnabled]); // Initial fetch + polling. useEffect(() => { void refresh(); - if (pollIntervalMs <= 0) return; + if (pollIntervalMs <= 0 || fetchEnabled !== true) return; const id = window.setInterval(() => { void listConnections() .then(resp => { @@ -101,7 +139,7 @@ export function useComposioIntegrations(pollIntervalMs = 5_000): UseComposioInte }); }, pollIntervalMs); return () => window.clearInterval(id); - }, [refresh, pollIntervalMs]); + }, [refresh, pollIntervalMs, fetchEnabled]); // [composio-cache] Listen for a window-level "config changed" event // emitted by ComposioPanel when the user flips backend ↔ direct or @@ -115,11 +153,26 @@ export function useComposioIntegrations(pollIntervalMs = 5_000): UseComposioInte useEffect(() => { const onConfigChanged = () => { console.debug('[composio-cache] window:composio:config-changed → refresh()'); + if (isLocalSession) { + void resolveFetchEnabled().then(enabled => { + if (enabled) { + void refresh(); + return; + } + if (mountedRef.current) { + setToolkits([]); + setConnections([]); + setError(null); + setLoading(false); + } + }); + return; + } void refresh(); }; window.addEventListener('composio:config-changed', onConfigChanged); return () => window.removeEventListener('composio:config-changed', onConfigChanged); - }, [refresh]); + }, [isLocalSession, refresh, resolveFetchEnabled]); const connectionByToolkit = useMemo(() => { const map = new Map(); diff --git a/app/src/lib/i18n/ar.ts b/app/src/lib/i18n/ar.ts index 14470ead2..86bfde069 100644 --- a/app/src/lib/i18n/ar.ts +++ b/app/src/lib/i18n/ar.ts @@ -7,6 +7,25 @@ import type { TranslationMap } from './types'; // Arabic (العربية) translations. Each chunk maps to chunks/en-N.ts. // Missing keys fall back to English via I18nContext.resolveEn(). -const ar: TranslationMap = { ...ar1, ...ar2, ...ar3, ...ar4, ...ar5 }; +const ar: TranslationMap = { + ...ar1, + ...ar2, + ...ar3, + ...ar4, + ...ar5, + 'skills.composio.noApiKeyTitle': 'لم يتم إعداد مفتاح Composio API', + 'skills.composio.noApiKeyDescription': + 'يستخدم الوضع المحلي مفتاح Composio API الخاص بك. افتح الإعدادات ← الخيارات المتقدمة ← Composio لإضافته قبل توصيل التكاملات هنا.', + 'skills.composio.noApiKeyCta': 'افتح في الإعدادات', + 'channels.localManagedUnavailable': 'القنوات المُدارة غير متاحة للمستخدمين المحليين.', + 'rewards.localUnavailable': + 'تسجيل الدخول المحلي لا يمنح مكافآت أو قسائم أو رصيد إحالة. لكسب المكافآت، سجّل الخروج ثم تابِع بتسجيل الدخول باستخدام حساب OpenHuman.', + 'rewards.localUnavailableCta': 'افتح إعدادات الحساب', + 'settings.search.localManagedUnavailable': + 'بحث OpenHuman المُدار غير متاح للمستخدمين المحليين. أضف مفتاح Parallel أو Brave الخاص بك لتفعيل البحث على الويب.', + 'devices.comingSoonDescription': + 'إقران الأجهزة قريبًا. ستكون هذه الصفحة مخصصة لإقران أجهزة iPhone وإدارة الأجهزة المتصلة.', + 'welcome.continueLocallyExperimental': 'المتابعة محليًا (تجريبي)', +}; export default ar; diff --git a/app/src/lib/i18n/bn.ts b/app/src/lib/i18n/bn.ts index c0366f2f2..75c5e7802 100644 --- a/app/src/lib/i18n/bn.ts +++ b/app/src/lib/i18n/bn.ts @@ -7,6 +7,25 @@ import type { TranslationMap } from './types'; // Bengali (বাংলা) translations. Each chunk maps to chunks/en-N.ts. // Missing keys fall back to English via I18nContext.resolveEn(). -const bn: TranslationMap = { ...bn1, ...bn2, ...bn3, ...bn4, ...bn5 }; +const bn: TranslationMap = { + ...bn1, + ...bn2, + ...bn3, + ...bn4, + ...bn5, + 'skills.composio.noApiKeyTitle': 'কোনো Composio API Key কনফিগার করা নেই', + 'skills.composio.noApiKeyDescription': + 'লোকাল মোডে আপনার নিজের Composio API key ব্যবহার হয়। এখানে ইন্টিগ্রেশন যুক্ত করার আগে Settings → Advanced → Composio খুলে একটি key যোগ করুন।', + 'skills.composio.noApiKeyCta': 'সেটিংসে খুলুন', + 'channels.localManagedUnavailable': 'লোকাল ব্যবহারকারীদের জন্য ম্যানেজড চ্যানেল উপলভ্য নয়।', + 'rewards.localUnavailable': + 'লোকাল লগইনে কোনো রিওয়ার্ড, কুপন বা রেফারেল ক্রেডিট মেলে না। রিওয়ার্ড পেতে লগ আউট করে একটি OpenHuman অ্যাকাউন্ট দিয়ে সাইন ইন করুন।', + 'rewards.localUnavailableCta': 'অ্যাকাউন্ট সেটিংস খুলুন', + 'settings.search.localManagedUnavailable': + 'লোকাল ব্যবহারকারীদের জন্য OpenHuman Managed সার্চ উপলভ্য নয়। ওয়েব সার্চ চালু করতে আপনার নিজের Parallel বা Brave API key যোগ করুন।', + 'devices.comingSoonDescription': + 'ডিভাইস পেয়ারিং শীঘ্রই আসছে। এই পেজে iPhone পেয়ারিং এবং সংযুক্ত ডিভাইস ম্যানেজ করা যাবে।', + 'welcome.continueLocallyExperimental': 'লোকালি চালিয়ে যান (প্রায়োগিক)', +}; export default bn; diff --git a/app/src/lib/i18n/chunks/ar-1.ts b/app/src/lib/i18n/chunks/ar-1.ts index 806d87fcb..ae45d140b 100644 --- a/app/src/lib/i18n/chunks/ar-1.ts +++ b/app/src/lib/i18n/chunks/ar-1.ts @@ -84,6 +84,8 @@ const ar1: TranslationMap = { 'settings.clearAppDataDesc': 'تسجيل الخروج وحذف جميع البيانات المحلية للتطبيق نهائيًا', 'settings.logOut': 'تسجيل الخروج', 'settings.logOutDesc': 'تسجيل الخروج من حسابك', + 'settings.exitLocalSession': 'Exit local session', + 'settings.exitLocalSessionDesc': 'Return to the sign-in screen', 'settings.language': 'اللغة', 'settings.languageDesc': 'لغة عرض واجهة التطبيق', 'settings.alerts': 'التنبيهات', @@ -511,6 +513,18 @@ const ar1: TranslationMap = { 'Must be a positive integer (use the Unlimited preset for no limit).', 'autonomy.presetUnlimited': 'Unlimited (default)', 'triggers.toggleFailed': '{action} failed for {trigger}: {message}', + 'skills.composio.noApiKeyTitle': 'No Composio API Key Configured', + 'skills.composio.noApiKeyDescription': + 'Local mode uses your own Composio API key. Open Settings → Advanced → Composio to add one before connecting integrations here.', + 'skills.composio.noApiKeyCta': 'Open in Settings', + 'rewards.localUnavailable': + 'Local login does not earn rewards, coupons, or referral credit. To earn rewards, log out and continue by signing in with an OpenHuman account.', + 'rewards.localUnavailableCta': 'Open Account Settings', + 'channels.localManagedUnavailable': 'Managed channels are not available for local users.', + 'settings.search.localManagedUnavailable': + 'OpenHuman Managed search is not available for local users. Add your own Parallel or Brave API key to enable web search.', + 'devices.comingSoonDescription': + 'Device pairing is coming soon. This page will be the home for pairing iPhones and managing connected devices.', 'common.breadcrumb': 'مسار التنقل', 'settings.betaBuild': 'إصدار تجريبي - v{version}', 'migration.vendor.openclaw': 'OpenClaw', diff --git a/app/src/lib/i18n/chunks/ar-3.ts b/app/src/lib/i18n/chunks/ar-3.ts index d2462092d..1d22c8ed9 100644 --- a/app/src/lib/i18n/chunks/ar-3.ts +++ b/app/src/lib/i18n/chunks/ar-3.ts @@ -211,6 +211,9 @@ const ar3: TranslationMap = { 'about.update.status.default': 'التحقق من التحديثات', 'welcome.connectionFailed': 'فشل الاتصال: {status} {statusText}', 'welcome.connectionFailedMsg': 'فشل الاتصال: {message}', + 'welcome.continueLocally': 'Continue locally', + 'welcome.localSessionStarting': 'Starting local session...', + 'welcome.localSessionDesc': 'Uses an offline local profile and skips TinyHumans OAuth.', 'chat.agentChatDesc': 'فتح جلسة محادثة مباشرة مع الوكيل.', 'channels.activeRouteValue': '{channel} عبر {authMode}', 'privacy.dataKind.messages': 'الرسائل', @@ -398,6 +401,7 @@ const ar3: TranslationMap = { 'channels.web.displayName': 'Web', 'channels.web.description': 'Chat via the built-in web UI.', 'channels.web.authMode.managed_dm.description': 'Use the embedded web chat — no setup required.', + 'welcome.continueLocallyExperimental': 'Continue Locally (Experimental)', }; export default ar3; diff --git a/app/src/lib/i18n/chunks/bn-1.ts b/app/src/lib/i18n/chunks/bn-1.ts index 1fad0d088..caad72ca3 100644 --- a/app/src/lib/i18n/chunks/bn-1.ts +++ b/app/src/lib/i18n/chunks/bn-1.ts @@ -84,6 +84,8 @@ const bn1: TranslationMap = { 'settings.clearAppDataDesc': 'সাইন আউট করুন এবং সব লোকাল ডেটা স্থায়ীভাবে মুছুন', 'settings.logOut': 'লগ আউট', 'settings.logOutDesc': 'আপনার অ্যাকাউন্ট থেকে সাইন আউট করুন', + 'settings.exitLocalSession': 'Exit local session', + 'settings.exitLocalSessionDesc': 'Return to the sign-in screen', 'settings.language': 'ভাষা', 'settings.languageDesc': 'অ্যাপ ইন্টারফেসের প্রদর্শন ভাষা', 'settings.alerts': 'সতর্কতা', @@ -520,6 +522,18 @@ const bn1: TranslationMap = { 'Must be a positive integer (use the Unlimited preset for no limit).', 'autonomy.presetUnlimited': 'Unlimited (default)', 'triggers.toggleFailed': '{action} failed for {trigger}: {message}', + 'skills.composio.noApiKeyTitle': 'No Composio API Key Configured', + 'skills.composio.noApiKeyDescription': + 'Local mode uses your own Composio API key. Open Settings → Advanced → Composio to add one before connecting integrations here.', + 'skills.composio.noApiKeyCta': 'Open in Settings', + 'rewards.localUnavailable': + 'Local login does not earn rewards, coupons, or referral credit. To earn rewards, log out and continue by signing in with an OpenHuman account.', + 'rewards.localUnavailableCta': 'Open Account Settings', + 'channels.localManagedUnavailable': 'Managed channels are not available for local users.', + 'settings.search.localManagedUnavailable': + 'OpenHuman Managed search is not available for local users. Add your own Parallel or Brave API key to enable web search.', + 'devices.comingSoonDescription': + 'Device pairing is coming soon. This page will be the home for pairing iPhones and managing connected devices.', 'common.breadcrumb': 'ব্রেডক্রাম্ব নেভিগেশন', 'settings.betaBuild': 'বেটা বিল্ড - v{version}', 'migration.vendor.openclaw': 'OpenClaw', diff --git a/app/src/lib/i18n/chunks/bn-3.ts b/app/src/lib/i18n/chunks/bn-3.ts index 0c9a8b5a2..711bf29ca 100644 --- a/app/src/lib/i18n/chunks/bn-3.ts +++ b/app/src/lib/i18n/chunks/bn-3.ts @@ -215,6 +215,9 @@ const bn3: TranslationMap = { 'about.update.status.default': 'আপডেট পরীক্ষা করুন', 'welcome.connectionFailed': 'সংযোগ ব্যর্থ: {status} {statusText}', 'welcome.connectionFailedMsg': 'সংযোগ ব্যর্থ: {message}', + 'welcome.continueLocally': 'Continue locally', + 'welcome.localSessionStarting': 'Starting local session...', + 'welcome.localSessionDesc': 'Uses an offline local profile and skips TinyHumans OAuth.', 'chat.agentChatDesc': 'এজেন্টের সাথে সরাসরি চ্যাট সেশন খুলুন।', 'channels.activeRouteValue': '{authMode} এর মাধ্যমে {channel}', 'privacy.dataKind.messages': 'বার্তা', @@ -401,6 +404,7 @@ const bn3: TranslationMap = { 'channels.web.displayName': 'Web', 'channels.web.description': 'Chat via the built-in web UI.', 'channels.web.authMode.managed_dm.description': 'Use the embedded web chat — no setup required.', + 'welcome.continueLocallyExperimental': 'Continue Locally (Experimental)', }; export default bn3; diff --git a/app/src/lib/i18n/chunks/de-1.ts b/app/src/lib/i18n/chunks/de-1.ts index 177b3f315..bc1d65960 100644 --- a/app/src/lib/i18n/chunks/de-1.ts +++ b/app/src/lib/i18n/chunks/de-1.ts @@ -532,6 +532,20 @@ const de1: TranslationMap = { 'Must be a positive integer (use the Unlimited preset for no limit).', 'autonomy.presetUnlimited': 'Unlimited (default)', 'triggers.toggleFailed': '{action} failed for {trigger}: {message}', + 'settings.exitLocalSession': 'Exit local session', + 'settings.exitLocalSessionDesc': 'Return to the sign-in screen', + 'skills.composio.noApiKeyTitle': 'No Composio API Key Configured', + 'skills.composio.noApiKeyDescription': + 'Local mode uses your own Composio API key. Open Settings → Advanced → Composio to add one before connecting integrations here.', + 'skills.composio.noApiKeyCta': 'Open in Settings', + 'rewards.localUnavailable': + 'Local login does not earn rewards, coupons, or referral credit. To earn rewards, log out and continue by signing in with an OpenHuman account.', + 'rewards.localUnavailableCta': 'Open Account Settings', + 'channels.localManagedUnavailable': 'Managed channels are not available for local users.', + 'settings.search.localManagedUnavailable': + 'OpenHuman Managed search is not available for local users. Add your own Parallel or Brave API key to enable web search.', + 'devices.comingSoonDescription': + 'Device pairing is coming soon. This page will be the home for pairing iPhones and managing connected devices.', 'common.breadcrumb': 'Navigationspfad', 'settings.betaBuild': 'Beta-Build - v{version}', 'migration.vendor.openclaw': 'OpenClaw', diff --git a/app/src/lib/i18n/chunks/de-3.ts b/app/src/lib/i18n/chunks/de-3.ts index 42a08fa11..2a7ff892f 100644 --- a/app/src/lib/i18n/chunks/de-3.ts +++ b/app/src/lib/i18n/chunks/de-3.ts @@ -413,6 +413,10 @@ const de3: TranslationMap = { 'channels.web.description': 'Chatte über die integrierte Web-Oberfläche.', 'channels.web.authMode.managed_dm.description': 'Nutze den eingebetteten Web-Chat — keine Einrichtung erforderlich.', + 'welcome.continueLocally': 'Continue locally', + 'welcome.continueLocallyExperimental': 'Continue Locally (Experimental)', + 'welcome.localSessionStarting': 'Starting local session...', + 'welcome.localSessionDesc': 'Uses an offline local profile and skips TinyHumans OAuth.', }; export default de3; diff --git a/app/src/lib/i18n/chunks/en-1.ts b/app/src/lib/i18n/chunks/en-1.ts index 16d12a86e..ff664c76b 100644 --- a/app/src/lib/i18n/chunks/en-1.ts +++ b/app/src/lib/i18n/chunks/en-1.ts @@ -285,6 +285,8 @@ const en1: TranslationMap = { 'settings.clearAppDataDesc': 'Sign out and permanently clear all local app data', 'settings.logOut': 'Log out', 'settings.logOutDesc': 'Sign out of your account', + 'settings.exitLocalSession': 'Exit local session', + 'settings.exitLocalSessionDesc': 'Return to the sign-in screen', 'settings.language': 'Language', 'settings.languageDesc': 'Display language for the app interface', 'settings.alerts': 'Alerts', @@ -444,6 +446,10 @@ const en1: TranslationMap = { 'skills.integrations': 'Composio Integrations', 'skills.integrationsSubtitle': 'Cloud-based OAuth connections — sign in with your account and Composio brokers the tokens so agents can read and act on your behalf. No API keys to manage.', + 'skills.composio.noApiKeyTitle': 'No Composio API Key Configured', + 'skills.composio.noApiKeyDescription': + 'Local mode uses your own Composio API key. Open Settings → Advanced → Composio to add one before connecting integrations here.', + 'skills.composio.noApiKeyCta': 'Open in Settings', 'skills.tabs.composio': 'Composio', 'skills.tabs.channels': 'Channels', 'skills.tabs.mcp': 'MCP Servers', @@ -467,6 +473,9 @@ const en1: TranslationMap = { 'rewards.title': 'Rewards', 'rewards.referrals': 'Referrals', 'rewards.coupons': 'Redeem', + 'rewards.localUnavailable': + 'Local login does not earn rewards, coupons, or referral credit. To earn rewards, log out and continue by signing in with an OpenHuman account.', + 'rewards.localUnavailableCta': 'Open Account Settings', 'rewards.credits': 'Credits', 'rewards.referralCode': 'Your referral code', 'rewards.copyCode': 'Copy code', @@ -596,6 +605,7 @@ const en1: TranslationMap = { 'channels.configure': 'Configure Channel', 'channels.setup': 'Setup', 'channels.noChannels': 'No channels configured', + 'channels.localManagedUnavailable': 'Managed channels are not available for local users.', 'channels.addChannel': 'Add Channel', 'channels.status.connected': 'Connected', 'channels.status.disconnected': 'Disconnected', @@ -937,6 +947,8 @@ const en1: TranslationMap = { 'settings.search.engineManagedLabel': 'OpenHuman Managed', 'settings.search.engineManagedDesc': 'Default. Routed through the OpenHuman backend — no API key required.', + 'settings.search.localManagedUnavailable': + 'OpenHuman Managed search is not available for local users. Add your own Parallel or Brave API key to enable web search.', 'settings.search.engineParallelLabel': 'Parallel', 'settings.search.engineParallelDesc': 'Direct Parallel API: search, extract, chat, research, enrich, dataset tools.', @@ -980,6 +992,8 @@ const en1: TranslationMap = { 'devices.betaBadge': 'Beta', 'devices.betaText': 'This feature is currently in beta. Pair iOS phones with this OpenHuman to use them as a remote client.', + 'devices.comingSoonDescription': + 'Device pairing is coming soon. This page will be the home for pairing iPhones and managing connected devices.', 'devices.title': 'Devices', 'devices.pairIphone': 'Pair iPhone', 'devices.noPaired': 'No paired devices', diff --git a/app/src/lib/i18n/chunks/en-3.ts b/app/src/lib/i18n/chunks/en-3.ts index 1f9291f86..c71ecc7e5 100644 --- a/app/src/lib/i18n/chunks/en-3.ts +++ b/app/src/lib/i18n/chunks/en-3.ts @@ -214,6 +214,10 @@ const en3: TranslationMap = { 'about.update.status.default': 'Check for updates', 'welcome.connectionFailed': 'Connection failed: {status} {statusText}', 'welcome.connectionFailedMsg': 'Connection failed: {message}', + 'welcome.continueLocally': 'Continue locally', + 'welcome.continueLocallyExperimental': 'Continue Locally (Experimental)', + 'welcome.localSessionStarting': 'Starting local session...', + 'welcome.localSessionDesc': 'Uses an offline local profile and skips TinyHumans OAuth.', 'chat.agentChatDesc': 'Open a direct chat session with the agent.', 'channels.activeRouteValue': '{channel} via {authMode}', 'privacy.dataKind.messages': 'Messages', diff --git a/app/src/lib/i18n/chunks/es-1.ts b/app/src/lib/i18n/chunks/es-1.ts index 0ece7b6a4..b835e6944 100644 --- a/app/src/lib/i18n/chunks/es-1.ts +++ b/app/src/lib/i18n/chunks/es-1.ts @@ -86,6 +86,8 @@ const es1: TranslationMap = { 'Cerrar sesión y eliminar permanentemente todos los datos locales de la app', 'settings.logOut': 'Cerrar sesión', 'settings.logOutDesc': 'Salir de tu cuenta', + 'settings.exitLocalSession': 'Exit local session', + 'settings.exitLocalSessionDesc': 'Return to the sign-in screen', 'settings.language': 'Idioma', 'settings.languageDesc': 'Idioma de visualización de la interfaz de la app', 'settings.alerts': 'Alertas', @@ -532,6 +534,18 @@ const es1: TranslationMap = { 'Must be a positive integer (use the Unlimited preset for no limit).', 'autonomy.presetUnlimited': 'Unlimited (default)', 'triggers.toggleFailed': '{action} failed for {trigger}: {message}', + 'skills.composio.noApiKeyTitle': 'No Composio API Key Configured', + 'skills.composio.noApiKeyDescription': + 'Local mode uses your own Composio API key. Open Settings → Advanced → Composio to add one before connecting integrations here.', + 'skills.composio.noApiKeyCta': 'Open in Settings', + 'rewards.localUnavailable': + 'Local login does not earn rewards, coupons, or referral credit. To earn rewards, log out and continue by signing in with an OpenHuman account.', + 'rewards.localUnavailableCta': 'Open Account Settings', + 'channels.localManagedUnavailable': 'Managed channels are not available for local users.', + 'settings.search.localManagedUnavailable': + 'OpenHuman Managed search is not available for local users. Add your own Parallel or Brave API key to enable web search.', + 'devices.comingSoonDescription': + 'Device pairing is coming soon. This page will be the home for pairing iPhones and managing connected devices.', 'common.breadcrumb': 'Ruta de navegación', 'settings.betaBuild': 'Compilación beta - v{version}', 'migration.vendor.openclaw': 'OpenClaw', diff --git a/app/src/lib/i18n/chunks/es-3.ts b/app/src/lib/i18n/chunks/es-3.ts index 14b8efa23..bcea0ebba 100644 --- a/app/src/lib/i18n/chunks/es-3.ts +++ b/app/src/lib/i18n/chunks/es-3.ts @@ -218,6 +218,9 @@ const es3: TranslationMap = { 'about.update.status.default': 'Buscar actualizaciones', 'welcome.connectionFailed': 'Conexión fallida: {status} {statusText}', 'welcome.connectionFailedMsg': 'Conexión fallida: {message}', + 'welcome.continueLocally': 'Continue locally', + 'welcome.localSessionStarting': 'Starting local session...', + 'welcome.localSessionDesc': 'Uses an offline local profile and skips TinyHumans OAuth.', 'chat.agentChatDesc': 'Abre una sesión de chat directo con el agente.', 'channels.activeRouteValue': '{channel} vía {authMode}', 'privacy.dataKind.messages': 'Mensajes', @@ -406,6 +409,7 @@ const es3: TranslationMap = { 'channels.web.displayName': 'Web', 'channels.web.description': 'Chat via the built-in web UI.', 'channels.web.authMode.managed_dm.description': 'Use the embedded web chat — no setup required.', + 'welcome.continueLocallyExperimental': 'Continue Locally (Experimental)', }; export default es3; diff --git a/app/src/lib/i18n/chunks/fr-1.ts b/app/src/lib/i18n/chunks/fr-1.ts index c17d13789..21ceb8d4f 100644 --- a/app/src/lib/i18n/chunks/fr-1.ts +++ b/app/src/lib/i18n/chunks/fr-1.ts @@ -86,6 +86,8 @@ const fr1: TranslationMap = { 'Se déconnecter et supprimer définitivement toutes les données locales', 'settings.logOut': 'Se déconnecter', 'settings.logOutDesc': 'Se déconnecter de ton compte', + 'settings.exitLocalSession': 'Exit local session', + 'settings.exitLocalSessionDesc': 'Return to the sign-in screen', 'settings.language': 'Langue', 'settings.languageDesc': "Langue d'affichage de l'interface", 'settings.alerts': 'Alertes', @@ -534,6 +536,18 @@ const fr1: TranslationMap = { 'Must be a positive integer (use the Unlimited preset for no limit).', 'autonomy.presetUnlimited': 'Unlimited (default)', 'triggers.toggleFailed': '{action} failed for {trigger}: {message}', + 'skills.composio.noApiKeyTitle': 'No Composio API Key Configured', + 'skills.composio.noApiKeyDescription': + 'Local mode uses your own Composio API key. Open Settings → Advanced → Composio to add one before connecting integrations here.', + 'skills.composio.noApiKeyCta': 'Open in Settings', + 'rewards.localUnavailable': + 'Local login does not earn rewards, coupons, or referral credit. To earn rewards, log out and continue by signing in with an OpenHuman account.', + 'rewards.localUnavailableCta': 'Open Account Settings', + 'channels.localManagedUnavailable': 'Managed channels are not available for local users.', + 'settings.search.localManagedUnavailable': + 'OpenHuman Managed search is not available for local users. Add your own Parallel or Brave API key to enable web search.', + 'devices.comingSoonDescription': + 'Device pairing is coming soon. This page will be the home for pairing iPhones and managing connected devices.', 'common.breadcrumb': 'Fil d’Ariane', 'settings.betaBuild': 'Build bêta - v{version}', 'migration.vendor.openclaw': 'OpenClaw', diff --git a/app/src/lib/i18n/chunks/fr-3.ts b/app/src/lib/i18n/chunks/fr-3.ts index 943571519..6f0a77ce3 100644 --- a/app/src/lib/i18n/chunks/fr-3.ts +++ b/app/src/lib/i18n/chunks/fr-3.ts @@ -219,6 +219,9 @@ const fr3: TranslationMap = { 'about.update.status.default': 'Rechercher des mises à jour', 'welcome.connectionFailed': 'Connexion échouée : {status} {statusText}', 'welcome.connectionFailedMsg': 'Connexion échouée : {message}', + 'welcome.continueLocally': 'Continue locally', + 'welcome.localSessionStarting': 'Starting local session...', + 'welcome.localSessionDesc': 'Uses an offline local profile and skips TinyHumans OAuth.', 'chat.agentChatDesc': "Ouvrir une session de chat direct avec l'agent.", 'channels.activeRouteValue': '{channel} via {authMode}', 'privacy.dataKind.messages': 'Messages', @@ -407,6 +410,7 @@ const fr3: TranslationMap = { 'channels.web.displayName': 'Web', 'channels.web.description': 'Chat via the built-in web UI.', 'channels.web.authMode.managed_dm.description': 'Use the embedded web chat — no setup required.', + 'welcome.continueLocallyExperimental': 'Continue Locally (Experimental)', }; export default fr3; diff --git a/app/src/lib/i18n/chunks/hi-1.ts b/app/src/lib/i18n/chunks/hi-1.ts index f83ea7543..e92dd8645 100644 --- a/app/src/lib/i18n/chunks/hi-1.ts +++ b/app/src/lib/i18n/chunks/hi-1.ts @@ -83,6 +83,8 @@ const hi1: TranslationMap = { 'settings.clearAppDataDesc': 'साइन आउट करें और सारा लोकल ऐप डेटा हमेशा के लिए मिटाएं', 'settings.logOut': 'लॉग आउट', 'settings.logOutDesc': 'अपने अकाउंट से साइन आउट करें', + 'settings.exitLocalSession': 'Exit local session', + 'settings.exitLocalSessionDesc': 'Return to the sign-in screen', 'settings.language': 'भाषा', 'settings.languageDesc': 'ऐप इंटरफेस की डिस्प्ले भाषा', 'settings.alerts': 'अलर्ट', @@ -517,6 +519,18 @@ const hi1: TranslationMap = { 'Must be a positive integer (use the Unlimited preset for no limit).', 'autonomy.presetUnlimited': 'Unlimited (default)', 'triggers.toggleFailed': '{action} failed for {trigger}: {message}', + 'skills.composio.noApiKeyTitle': 'No Composio API Key Configured', + 'skills.composio.noApiKeyDescription': + 'Local mode uses your own Composio API key. Open Settings → Advanced → Composio to add one before connecting integrations here.', + 'skills.composio.noApiKeyCta': 'Open in Settings', + 'rewards.localUnavailable': + 'Local login does not earn rewards, coupons, or referral credit. To earn rewards, log out and continue by signing in with an OpenHuman account.', + 'rewards.localUnavailableCta': 'Open Account Settings', + 'channels.localManagedUnavailable': 'Managed channels are not available for local users.', + 'settings.search.localManagedUnavailable': + 'OpenHuman Managed search is not available for local users. Add your own Parallel or Brave API key to enable web search.', + 'devices.comingSoonDescription': + 'Device pairing is coming soon. This page will be the home for pairing iPhones and managing connected devices.', 'common.breadcrumb': 'ब्रेडक्रंब नेविगेशन', 'settings.betaBuild': 'बीटा बिल्ड - v{version}', 'migration.vendor.openclaw': 'OpenClaw', diff --git a/app/src/lib/i18n/chunks/hi-3.ts b/app/src/lib/i18n/chunks/hi-3.ts index 0ab30ff9e..3f11035bd 100644 --- a/app/src/lib/i18n/chunks/hi-3.ts +++ b/app/src/lib/i18n/chunks/hi-3.ts @@ -214,6 +214,9 @@ const hi3: TranslationMap = { 'about.update.status.default': 'अपडेट चेक करें', 'welcome.connectionFailed': 'कनेक्शन विफल: {status} {statusText}', 'welcome.connectionFailedMsg': 'कनेक्शन विफल: {message}', + 'welcome.continueLocally': 'Continue locally', + 'welcome.localSessionStarting': 'Starting local session...', + 'welcome.localSessionDesc': 'Uses an offline local profile and skips TinyHumans OAuth.', 'chat.agentChatDesc': 'एजेंट के साथ डायरेक्ट चैट सेशन खोलें।', 'channels.activeRouteValue': '{channel} द्वारा {authMode}', 'privacy.dataKind.messages': 'मैसेज', @@ -403,6 +406,7 @@ const hi3: TranslationMap = { 'channels.web.displayName': 'Web', 'channels.web.description': 'Chat via the built-in web UI.', 'channels.web.authMode.managed_dm.description': 'Use the embedded web chat — no setup required.', + 'welcome.continueLocallyExperimental': 'Continue Locally (Experimental)', }; export default hi3; diff --git a/app/src/lib/i18n/chunks/id-1.ts b/app/src/lib/i18n/chunks/id-1.ts index d27f59ec0..391cb32e0 100644 --- a/app/src/lib/i18n/chunks/id-1.ts +++ b/app/src/lib/i18n/chunks/id-1.ts @@ -83,6 +83,8 @@ const id1: TranslationMap = { 'settings.clearAppDataDesc': 'Keluar dan hapus permanen semua data aplikasi lokal', 'settings.logOut': 'Keluar', 'settings.logOutDesc': 'Keluar dari akun Anda', + 'settings.exitLocalSession': 'Exit local session', + 'settings.exitLocalSessionDesc': 'Return to the sign-in screen', 'settings.language': 'Bahasa', 'settings.languageDesc': 'Bahasa tampilan untuk antarmuka aplikasi', 'settings.alerts': 'Peringatan', @@ -523,6 +525,18 @@ const id1: TranslationMap = { 'Must be a positive integer (use the Unlimited preset for no limit).', 'autonomy.presetUnlimited': 'Unlimited (default)', 'triggers.toggleFailed': '{action} failed for {trigger}: {message}', + 'skills.composio.noApiKeyTitle': 'No Composio API Key Configured', + 'skills.composio.noApiKeyDescription': + 'Local mode uses your own Composio API key. Open Settings → Advanced → Composio to add one before connecting integrations here.', + 'skills.composio.noApiKeyCta': 'Open in Settings', + 'rewards.localUnavailable': + 'Local login does not earn rewards, coupons, or referral credit. To earn rewards, log out and continue by signing in with an OpenHuman account.', + 'rewards.localUnavailableCta': 'Open Account Settings', + 'channels.localManagedUnavailable': 'Managed channels are not available for local users.', + 'settings.search.localManagedUnavailable': + 'OpenHuman Managed search is not available for local users. Add your own Parallel or Brave API key to enable web search.', + 'devices.comingSoonDescription': + 'Device pairing is coming soon. This page will be the home for pairing iPhones and managing connected devices.', 'common.breadcrumb': 'Jejak navigasi', 'settings.betaBuild': 'Build beta - v{version}', 'migration.vendor.openclaw': 'OpenClaw', diff --git a/app/src/lib/i18n/chunks/id-3.ts b/app/src/lib/i18n/chunks/id-3.ts index 72b9d5c9f..923ce7e57 100644 --- a/app/src/lib/i18n/chunks/id-3.ts +++ b/app/src/lib/i18n/chunks/id-3.ts @@ -215,6 +215,9 @@ const id3: TranslationMap = { 'about.update.status.default': 'Periksa pembaruan', 'welcome.connectionFailed': 'Koneksi gagal: {status} {statusText}', 'welcome.connectionFailedMsg': 'Koneksi gagal: {message}', + 'welcome.continueLocally': 'Continue locally', + 'welcome.localSessionStarting': 'Starting local session...', + 'welcome.localSessionDesc': 'Uses an offline local profile and skips TinyHumans OAuth.', 'chat.agentChatDesc': 'Buka sesi chat langsung dengan agen.', 'channels.activeRouteValue': '{channel} lewat {authMode}', 'privacy.dataKind.messages': 'Pesan', @@ -406,6 +409,7 @@ const id3: TranslationMap = { 'channels.web.displayName': 'Web', 'channels.web.description': 'Chat via the built-in web UI.', 'channels.web.authMode.managed_dm.description': 'Use the embedded web chat — no setup required.', + 'welcome.continueLocallyExperimental': 'Continue Locally (Experimental)', }; export default id3; diff --git a/app/src/lib/i18n/chunks/it-1.ts b/app/src/lib/i18n/chunks/it-1.ts index 2d6634cb5..e90953786 100644 --- a/app/src/lib/i18n/chunks/it-1.ts +++ b/app/src/lib/i18n/chunks/it-1.ts @@ -85,6 +85,8 @@ const it1: TranslationMap = { "Disconnetti e cancella permanentemente tutti i dati locali dell'app", 'settings.logOut': 'Disconnetti', 'settings.logOutDesc': 'Disconnetti dal tuo account', + 'settings.exitLocalSession': 'Exit local session', + 'settings.exitLocalSessionDesc': 'Return to the sign-in screen', 'settings.language': 'Lingua', 'settings.languageDesc': "Lingua di visualizzazione dell'interfaccia dell'app", 'settings.alerts': 'Avvisi', @@ -527,6 +529,18 @@ const it1: TranslationMap = { 'Must be a positive integer (use the Unlimited preset for no limit).', 'autonomy.presetUnlimited': 'Unlimited (default)', 'triggers.toggleFailed': '{action} failed for {trigger}: {message}', + 'skills.composio.noApiKeyTitle': 'No Composio API Key Configured', + 'skills.composio.noApiKeyDescription': + 'Local mode uses your own Composio API key. Open Settings → Advanced → Composio to add one before connecting integrations here.', + 'skills.composio.noApiKeyCta': 'Open in Settings', + 'rewards.localUnavailable': + 'Local login does not earn rewards, coupons, or referral credit. To earn rewards, log out and continue by signing in with an OpenHuman account.', + 'rewards.localUnavailableCta': 'Open Account Settings', + 'channels.localManagedUnavailable': 'Managed channels are not available for local users.', + 'settings.search.localManagedUnavailable': + 'OpenHuman Managed search is not available for local users. Add your own Parallel or Brave API key to enable web search.', + 'devices.comingSoonDescription': + 'Device pairing is coming soon. This page will be the home for pairing iPhones and managing connected devices.', 'common.breadcrumb': 'Percorso di navigazione', 'settings.betaBuild': 'Build beta - v{version}', 'migration.vendor.openclaw': 'OpenClaw', diff --git a/app/src/lib/i18n/chunks/it-3.ts b/app/src/lib/i18n/chunks/it-3.ts index 2a918283f..de00a982f 100644 --- a/app/src/lib/i18n/chunks/it-3.ts +++ b/app/src/lib/i18n/chunks/it-3.ts @@ -218,6 +218,9 @@ const it3: TranslationMap = { 'about.update.status.default': 'Verifica aggiornamenti', 'welcome.connectionFailed': 'Connessione fallita: {status} {statusText}', 'welcome.connectionFailedMsg': 'Connessione fallita: {message}', + 'welcome.continueLocally': 'Continue locally', + 'welcome.localSessionStarting': 'Starting local session...', + 'welcome.localSessionDesc': 'Uses an offline local profile and skips TinyHumans OAuth.', 'chat.agentChatDesc': "Apri una sessione di chat diretta con l'agente.", 'channels.activeRouteValue': '{channel} via {authMode}', 'privacy.dataKind.messages': 'Messaggi', @@ -406,6 +409,7 @@ const it3: TranslationMap = { 'channels.web.displayName': 'Web', 'channels.web.description': 'Chat via the built-in web UI.', 'channels.web.authMode.managed_dm.description': 'Use the embedded web chat — no setup required.', + 'welcome.continueLocallyExperimental': 'Continue Locally (Experimental)', }; export default it3; diff --git a/app/src/lib/i18n/chunks/ko-1.ts b/app/src/lib/i18n/chunks/ko-1.ts index d689f7595..215a5f59d 100644 --- a/app/src/lib/i18n/chunks/ko-1.ts +++ b/app/src/lib/i18n/chunks/ko-1.ts @@ -83,6 +83,8 @@ const ko1: TranslationMap = { 'settings.clearAppDataDesc': '로그아웃하고 모든 로컬 앱 데이터를 영구적으로 삭제', 'settings.logOut': '로그아웃', 'settings.logOutDesc': '계정에서 로그아웃', + 'settings.exitLocalSession': 'Exit local session', + 'settings.exitLocalSessionDesc': 'Return to the sign-in screen', 'settings.language': '언어', 'settings.languageDesc': '앱 인터페이스 표시 언어', 'settings.alerts': '알림', @@ -520,6 +522,18 @@ const ko1: TranslationMap = { 'Must be a positive integer (use the Unlimited preset for no limit).', 'autonomy.presetUnlimited': 'Unlimited (default)', 'triggers.toggleFailed': '{action} failed for {trigger}: {message}', + 'skills.composio.noApiKeyTitle': 'No Composio API Key Configured', + 'skills.composio.noApiKeyDescription': + 'Local mode uses your own Composio API key. Open Settings → Advanced → Composio to add one before connecting integrations here.', + 'skills.composio.noApiKeyCta': 'Open in Settings', + 'rewards.localUnavailable': + 'Local login does not earn rewards, coupons, or referral credit. To earn rewards, log out and continue by signing in with an OpenHuman account.', + 'rewards.localUnavailableCta': 'Open Account Settings', + 'channels.localManagedUnavailable': 'Managed channels are not available for local users.', + 'settings.search.localManagedUnavailable': + 'OpenHuman Managed search is not available for local users. Add your own Parallel or Brave API key to enable web search.', + 'devices.comingSoonDescription': + 'Device pairing is coming soon. This page will be the home for pairing iPhones and managing connected devices.', 'common.breadcrumb': '이동 경로', 'settings.betaBuild': '베타 빌드 - v{version}', 'migration.vendor.openclaw': 'OpenClaw', diff --git a/app/src/lib/i18n/chunks/ko-3.ts b/app/src/lib/i18n/chunks/ko-3.ts index 7543d247d..774500eca 100644 --- a/app/src/lib/i18n/chunks/ko-3.ts +++ b/app/src/lib/i18n/chunks/ko-3.ts @@ -213,6 +213,9 @@ const ko3: TranslationMap = { 'about.update.status.default': '업데이트 확인', 'welcome.connectionFailed': '연결 실패: {status} {statusText}', 'welcome.connectionFailedMsg': '연결 실패: {message}', + 'welcome.continueLocally': 'Continue locally', + 'welcome.localSessionStarting': 'Starting local session...', + 'welcome.localSessionDesc': 'Uses an offline local profile and skips TinyHumans OAuth.', 'chat.agentChatDesc': '에이전트와 직접 채팅 세션을 엽니다.', 'channels.activeRouteValue': '{channel} via {authMode}', 'privacy.dataKind.messages': '메시지', @@ -403,5 +406,6 @@ const ko3: TranslationMap = { 'channels.web.displayName': 'Web', 'channels.web.description': 'Chat via the built-in web UI.', 'channels.web.authMode.managed_dm.description': 'Use the embedded web chat — no setup required.', + 'welcome.continueLocallyExperimental': 'Continue Locally (Experimental)', }; export default ko3; diff --git a/app/src/lib/i18n/chunks/pt-1.ts b/app/src/lib/i18n/chunks/pt-1.ts index a05921b98..242109a74 100644 --- a/app/src/lib/i18n/chunks/pt-1.ts +++ b/app/src/lib/i18n/chunks/pt-1.ts @@ -85,6 +85,8 @@ const pt1: TranslationMap = { 'settings.clearAppDataDesc': 'Sair e excluir permanentemente todos os dados locais do app', 'settings.logOut': 'Sair', 'settings.logOutDesc': 'Sair da sua conta', + 'settings.exitLocalSession': 'Exit local session', + 'settings.exitLocalSessionDesc': 'Return to the sign-in screen', 'settings.language': 'Idioma', 'settings.languageDesc': 'Idioma de exibição da interface do app', 'settings.alerts': 'Alertas', @@ -532,6 +534,18 @@ const pt1: TranslationMap = { 'Must be a positive integer (use the Unlimited preset for no limit).', 'autonomy.presetUnlimited': 'Unlimited (default)', 'triggers.toggleFailed': '{action} failed for {trigger}: {message}', + 'skills.composio.noApiKeyTitle': 'No Composio API Key Configured', + 'skills.composio.noApiKeyDescription': + 'Local mode uses your own Composio API key. Open Settings → Advanced → Composio to add one before connecting integrations here.', + 'skills.composio.noApiKeyCta': 'Open in Settings', + 'rewards.localUnavailable': + 'Local login does not earn rewards, coupons, or referral credit. To earn rewards, log out and continue by signing in with an OpenHuman account.', + 'rewards.localUnavailableCta': 'Open Account Settings', + 'channels.localManagedUnavailable': 'Managed channels are not available for local users.', + 'settings.search.localManagedUnavailable': + 'OpenHuman Managed search is not available for local users. Add your own Parallel or Brave API key to enable web search.', + 'devices.comingSoonDescription': + 'Device pairing is coming soon. This page will be the home for pairing iPhones and managing connected devices.', 'common.breadcrumb': 'Trilha de navegação', 'settings.betaBuild': 'Build beta - v{version}', 'migration.vendor.openclaw': 'OpenClaw', diff --git a/app/src/lib/i18n/chunks/pt-3.ts b/app/src/lib/i18n/chunks/pt-3.ts index b43cf61bd..9cdd6fd41 100644 --- a/app/src/lib/i18n/chunks/pt-3.ts +++ b/app/src/lib/i18n/chunks/pt-3.ts @@ -217,6 +217,9 @@ const pt3: TranslationMap = { 'about.update.status.default': 'Verificar atualizações', 'welcome.connectionFailed': 'Falha na conexão: {status} {statusText}', 'welcome.connectionFailedMsg': 'Falha na conexão: {message}', + 'welcome.continueLocally': 'Continue locally', + 'welcome.localSessionStarting': 'Starting local session...', + 'welcome.localSessionDesc': 'Uses an offline local profile and skips TinyHumans OAuth.', 'chat.agentChatDesc': 'Abrir uma sessão de chat direto com o agente.', 'channels.activeRouteValue': '{channel} via {authMode}', 'privacy.dataKind.messages': 'Mensagens', @@ -405,6 +408,7 @@ const pt3: TranslationMap = { 'channels.web.displayName': 'Web', 'channels.web.description': 'Chat via the built-in web UI.', 'channels.web.authMode.managed_dm.description': 'Use the embedded web chat — no setup required.', + 'welcome.continueLocallyExperimental': 'Continue Locally (Experimental)', }; export default pt3; diff --git a/app/src/lib/i18n/chunks/ru-1.ts b/app/src/lib/i18n/chunks/ru-1.ts index 3064c5397..6b6b02fd2 100644 --- a/app/src/lib/i18n/chunks/ru-1.ts +++ b/app/src/lib/i18n/chunks/ru-1.ts @@ -85,6 +85,8 @@ const ru1: TranslationMap = { 'settings.clearAppDataDesc': 'Выйти из аккаунта и удалить все локальные данные приложения', 'settings.logOut': 'Выйти', 'settings.logOutDesc': 'Выйти из своего аккаунта', + 'settings.exitLocalSession': 'Exit local session', + 'settings.exitLocalSessionDesc': 'Return to the sign-in screen', 'settings.language': 'Язык', 'settings.languageDesc': 'Язык отображения интерфейса', 'settings.alerts': 'Оповещения', @@ -522,6 +524,18 @@ const ru1: TranslationMap = { 'Must be a positive integer (use the Unlimited preset for no limit).', 'autonomy.presetUnlimited': 'Unlimited (default)', 'triggers.toggleFailed': '{action} failed for {trigger}: {message}', + 'skills.composio.noApiKeyTitle': 'No Composio API Key Configured', + 'skills.composio.noApiKeyDescription': + 'Local mode uses your own Composio API key. Open Settings → Advanced → Composio to add one before connecting integrations here.', + 'skills.composio.noApiKeyCta': 'Open in Settings', + 'rewards.localUnavailable': + 'Local login does not earn rewards, coupons, or referral credit. To earn rewards, log out and continue by signing in with an OpenHuman account.', + 'rewards.localUnavailableCta': 'Open Account Settings', + 'channels.localManagedUnavailable': 'Managed channels are not available for local users.', + 'settings.search.localManagedUnavailable': + 'OpenHuman Managed search is not available for local users. Add your own Parallel or Brave API key to enable web search.', + 'devices.comingSoonDescription': + 'Device pairing is coming soon. This page will be the home for pairing iPhones and managing connected devices.', 'common.breadcrumb': 'Навигационная цепочка', 'settings.betaBuild': 'Бета-сборка — v{version}', 'migration.vendor.openclaw': 'OpenClaw', diff --git a/app/src/lib/i18n/chunks/ru-3.ts b/app/src/lib/i18n/chunks/ru-3.ts index d4c47e3ee..635fca70a 100644 --- a/app/src/lib/i18n/chunks/ru-3.ts +++ b/app/src/lib/i18n/chunks/ru-3.ts @@ -215,6 +215,9 @@ const ru3: TranslationMap = { 'about.update.status.default': 'Проверить обновления', 'welcome.connectionFailed': 'Ошибка подключения: {status} {statusText}', 'welcome.connectionFailedMsg': 'Ошибка подключения: {message}', + 'welcome.continueLocally': 'Continue locally', + 'welcome.localSessionStarting': 'Starting local session...', + 'welcome.localSessionDesc': 'Uses an offline local profile and skips TinyHumans OAuth.', 'chat.agentChatDesc': 'Открыть прямой чат с агентом.', 'channels.activeRouteValue': '{channel} через {authMode}', 'privacy.dataKind.messages': 'Сообщения', @@ -402,6 +405,7 @@ const ru3: TranslationMap = { 'channels.web.displayName': 'Web', 'channels.web.description': 'Chat via the built-in web UI.', 'channels.web.authMode.managed_dm.description': 'Use the embedded web chat — no setup required.', + 'welcome.continueLocallyExperimental': 'Continue Locally (Experimental)', }; export default ru3; diff --git a/app/src/lib/i18n/chunks/zh-CN-1.ts b/app/src/lib/i18n/chunks/zh-CN-1.ts index c9108b56f..2b8e769a5 100644 --- a/app/src/lib/i18n/chunks/zh-CN-1.ts +++ b/app/src/lib/i18n/chunks/zh-CN-1.ts @@ -83,6 +83,8 @@ const zhCN1: TranslationMap = { 'settings.clearAppDataDesc': '退出登录并永久清除所有本地应用数据', 'settings.logOut': '退出登录', 'settings.logOutDesc': '退出当前账户', + 'settings.exitLocalSession': '退出本地会话', + 'settings.exitLocalSessionDesc': '返回登录页面', 'settings.language': '语言', 'settings.languageDesc': '应用界面显示语言', 'settings.alerts': '通知', @@ -504,6 +506,18 @@ const zhCN1: TranslationMap = { 'Must be a positive integer (use the Unlimited preset for no limit).', 'autonomy.presetUnlimited': 'Unlimited (default)', 'triggers.toggleFailed': '{action} failed for {trigger}: {message}', + 'skills.composio.noApiKeyTitle': 'No Composio API Key Configured', + 'skills.composio.noApiKeyDescription': + 'Local mode uses your own Composio API key. Open Settings → Advanced → Composio to add one before connecting integrations here.', + 'skills.composio.noApiKeyCta': 'Open in Settings', + 'rewards.localUnavailable': + 'Local login does not earn rewards, coupons, or referral credit. To earn rewards, log out and continue by signing in with an OpenHuman account.', + 'rewards.localUnavailableCta': 'Open Account Settings', + 'channels.localManagedUnavailable': 'Managed channels are not available for local users.', + 'settings.search.localManagedUnavailable': + 'OpenHuman Managed search is not available for local users. Add your own Parallel or Brave API key to enable web search.', + 'devices.comingSoonDescription': + 'Device pairing is coming soon. This page will be the home for pairing iPhones and managing connected devices.', 'common.breadcrumb': '面包屑导航', 'settings.betaBuild': '测试版构建 - v{version}', 'migration.vendor.openclaw': 'OpenClaw', diff --git a/app/src/lib/i18n/chunks/zh-CN-3.ts b/app/src/lib/i18n/chunks/zh-CN-3.ts index 0e8a794db..a417beb00 100644 --- a/app/src/lib/i18n/chunks/zh-CN-3.ts +++ b/app/src/lib/i18n/chunks/zh-CN-3.ts @@ -205,6 +205,9 @@ const zhCN3: TranslationMap = { 'about.update.status.default': '检查更新', 'welcome.connectionFailed': '连接失败: {status} {statusText}', 'welcome.connectionFailedMsg': '连接失败: {message}', + 'welcome.continueLocally': '本地继续', + 'welcome.localSessionStarting': '正在启动本地会话...', + 'welcome.localSessionDesc': '使用离线本地配置文件,跳过 TinyHumans OAuth。', 'chat.agentChatDesc': '与智能体进行直接对话。', 'channels.activeRouteValue': '{channel} 通过 {authMode}', 'privacy.dataKind.messages': '消息', @@ -396,6 +399,7 @@ const zhCN3: TranslationMap = { 'channels.web.displayName': 'Web', 'channels.web.description': '通过内置的 Web UI 聊天。', 'channels.web.authMode.managed_dm.description': '使用嵌入式 Web 聊天 — 无需设置。', + 'welcome.continueLocallyExperimental': 'Continue Locally (Experimental)', }; export default zhCN3; diff --git a/app/src/lib/i18n/de.ts b/app/src/lib/i18n/de.ts index 7b2432db1..3950a6739 100644 --- a/app/src/lib/i18n/de.ts +++ b/app/src/lib/i18n/de.ts @@ -7,6 +7,25 @@ import type { TranslationMap } from './types'; // German (Deutsch) translations. Each chunk maps to chunks/en-N.ts. // Missing keys fall back to English via I18nContext.resolveEn(). -const de: TranslationMap = { ...de1, ...de2, ...de3, ...de4, ...de5 }; +const de: TranslationMap = { + ...de1, + ...de2, + ...de3, + ...de4, + ...de5, + 'skills.composio.noApiKeyTitle': 'Kein Composio-API-Schlüssel konfiguriert', + 'skills.composio.noApiKeyDescription': + 'Im lokalen Modus wird dein eigener Composio-API-Schlüssel verwendet. Öffne Einstellungen → Erweitert → Composio, um einen Schlüssel hinzuzufügen, bevor du hier Integrationen verbindest.', + 'skills.composio.noApiKeyCta': 'In den Einstellungen öffnen', + 'channels.localManagedUnavailable': 'Verwaltete Kanäle sind für lokale Benutzer nicht verfügbar.', + 'rewards.localUnavailable': + 'Ein lokaler Login sammelt keine Belohnungen, Gutscheine oder Empfehlungsguthaben. Melde dich ab und anschließend mit einem OpenHuman-Konto an, wenn Belohnungen zählen sollen.', + 'rewards.localUnavailableCta': 'Kontoeinstellungen öffnen', + 'settings.search.localManagedUnavailable': + 'Die von OpenHuman verwaltete Suche ist für lokale Benutzer nicht verfügbar. Füge deinen eigenen Parallel- oder Brave-API-Schlüssel hinzu, um die Websuche zu aktivieren.', + 'devices.comingSoonDescription': + 'Gerätekopplung kommt bald. Diese Seite wird für das Koppeln von iPhones und die Verwaltung verbundener Geräte zuständig sein.', + 'welcome.continueLocallyExperimental': 'Lokal fortfahren (Experimentell)', +}; export default de; diff --git a/app/src/lib/i18n/en.ts b/app/src/lib/i18n/en.ts index 55a09d8ec..05b1e01c2 100644 --- a/app/src/lib/i18n/en.ts +++ b/app/src/lib/i18n/en.ts @@ -90,6 +90,8 @@ const en: TranslationMap = { 'settings.clearAppDataDesc': 'Sign out and permanently clear all local app data', 'settings.logOut': 'Log out', 'settings.logOutDesc': 'Sign out of your account', + 'settings.exitLocalSession': 'Exit local session', + 'settings.exitLocalSessionDesc': 'Return to the sign-in screen', 'settings.language': 'Language', 'settings.betaBuild': 'Beta build - v{version}', 'settings.languageDesc': 'Display language for the app interface', @@ -251,6 +253,10 @@ const en: TranslationMap = { 'skills.integrations': 'Composio Integrations', 'skills.integrationsSubtitle': 'Cloud-based OAuth connections — sign in with your account and Composio brokers the tokens so agents can read and act on your behalf. No API keys to manage.', + 'skills.composio.noApiKeyTitle': 'No Composio API Key Configured', + 'skills.composio.noApiKeyDescription': + 'Local mode uses your own Composio API key. Open Settings → Advanced → Composio to add one before connecting integrations here.', + 'skills.composio.noApiKeyCta': 'Open in Settings', 'skills.tabs.composio': 'Composio', 'skills.tabs.channels': 'Channels', 'skills.tabs.mcp': 'MCP Servers', @@ -277,6 +283,9 @@ const en: TranslationMap = { 'rewards.title': 'Rewards', 'rewards.referrals': 'Referrals', 'rewards.coupons': 'Redeem', + 'rewards.localUnavailable': + 'Local login does not earn rewards, coupons, or referral credit. To earn rewards, log out and continue by signing in with an OpenHuman account.', + 'rewards.localUnavailableCta': 'Open Account Settings', 'rewards.credits': 'Credits', 'rewards.referralCode': 'Your referral code', 'rewards.copyCode': 'Copy code', @@ -420,6 +429,7 @@ const en: TranslationMap = { 'channels.configure': 'Configure Channel', 'channels.setup': 'Setup', 'channels.noChannels': 'No channels configured', + 'channels.localManagedUnavailable': 'Managed channels are not available for local users.', 'channels.addChannel': 'Add Channel', 'channels.status.connected': 'Connected', 'channels.status.disconnected': 'Disconnected', @@ -584,6 +594,8 @@ const en: TranslationMap = { 'settings.search.engineManagedLabel': 'OpenHuman Managed', 'settings.search.engineManagedDesc': 'Default. Routed through the OpenHuman backend — no API key required.', + 'settings.search.localManagedUnavailable': + 'OpenHuman Managed search is not available for local users. Add your own Parallel or Brave API key to enable web search.', 'settings.search.engineParallelLabel': 'Parallel', 'settings.search.engineParallelDesc': 'Direct Parallel API: search, extract, chat, research, enrich, dataset tools.', @@ -627,6 +639,8 @@ const en: TranslationMap = { 'devices.betaBadge': 'Beta', 'devices.betaText': 'This feature is currently in beta. Pair iOS phones with this OpenHuman to use them as a remote client.', + 'devices.comingSoonDescription': + 'Device pairing is coming soon. This page will be the home for pairing iPhones and managing connected devices.', 'devices.title': 'Devices', 'devices.pairIphone': 'Pair iPhone', 'devices.noPaired': 'No paired devices', @@ -1660,6 +1674,10 @@ const en: TranslationMap = { // Welcome: connection error messages 'welcome.connectionFailed': 'Connection failed: {status} {statusText}', 'welcome.connectionFailedMsg': 'Connection failed: {message}', + 'welcome.continueLocally': 'Continue locally', + 'welcome.continueLocallyExperimental': 'Continue Locally (Experimental)', + 'welcome.localSessionStarting': 'Starting local session...', + 'welcome.localSessionDesc': 'Uses an offline local profile and skips TinyHumans OAuth.', // Chat: Agent chat panel description 'chat.agentChatDesc': 'Open a direct chat session with the agent.', diff --git a/app/src/lib/i18n/es.ts b/app/src/lib/i18n/es.ts index fa3153525..bbed6eb01 100644 --- a/app/src/lib/i18n/es.ts +++ b/app/src/lib/i18n/es.ts @@ -7,6 +7,26 @@ import type { TranslationMap } from './types'; // Spanish (Español) translations. Each chunk maps to chunks/en-N.ts. // Missing keys fall back to English via I18nContext.resolveEn(). -const es: TranslationMap = { ...es1, ...es2, ...es3, ...es4, ...es5 }; +const es: TranslationMap = { + ...es1, + ...es2, + ...es3, + ...es4, + ...es5, + 'skills.composio.noApiKeyTitle': 'No hay una API key de Composio configurada', + 'skills.composio.noApiKeyDescription': + 'El modo local usa tu propia API key de Composio. Abre Ajustes → Avanzado → Composio para añadir una antes de conectar integraciones aquí.', + 'skills.composio.noApiKeyCta': 'Abrir en Ajustes', + 'channels.localManagedUnavailable': + 'Los canales gestionados no están disponibles para usuarios locales.', + 'rewards.localUnavailable': + 'El acceso local no obtiene recompensas, cupones ni crédito por referidos. Cierra sesión y continúa iniciando sesión con una cuenta de OpenHuman si quieres que las recompensas cuenten.', + 'rewards.localUnavailableCta': 'Abrir ajustes de la cuenta', + 'settings.search.localManagedUnavailable': + 'La búsqueda gestionada por OpenHuman no está disponible para usuarios locales. Añade tu propia API key de Parallel o Brave para habilitar la búsqueda web.', + 'devices.comingSoonDescription': + 'El emparejamiento de dispositivos llegará pronto. Esta página será el lugar para emparejar iPhones y gestionar dispositivos conectados.', + 'welcome.continueLocallyExperimental': 'Continuar localmente (Experimental)', +}; export default es; diff --git a/app/src/lib/i18n/fr.ts b/app/src/lib/i18n/fr.ts index ca2795040..c67cf74fc 100644 --- a/app/src/lib/i18n/fr.ts +++ b/app/src/lib/i18n/fr.ts @@ -7,6 +7,26 @@ import type { TranslationMap } from './types'; // French (Français) translations. Each chunk maps to chunks/en-N.ts. // Missing keys fall back to English via I18nContext.resolveEn(). -const fr: TranslationMap = { ...fr1, ...fr2, ...fr3, ...fr4, ...fr5 }; +const fr: TranslationMap = { + ...fr1, + ...fr2, + ...fr3, + ...fr4, + ...fr5, + 'skills.composio.noApiKeyTitle': 'Aucune clé API Composio configurée', + 'skills.composio.noApiKeyDescription': + 'Le mode local utilise votre propre clé API Composio. Ouvrez Paramètres → Avancé → Composio pour en ajouter une avant de connecter des intégrations ici.', + 'skills.composio.noApiKeyCta': 'Ouvrir dans les paramètres', + 'channels.localManagedUnavailable': + 'Les canaux gérés ne sont pas disponibles pour les utilisateurs locaux.', + 'rewards.localUnavailable': + 'La connexion locale ne permet pas de gagner des récompenses, des coupons ou du crédit de parrainage. Déconnecte-toi puis connecte-toi avec un compte OpenHuman si tu veux que les récompenses comptent.', + 'rewards.localUnavailableCta': 'Ouvrir les paramètres du compte', + 'settings.search.localManagedUnavailable': + 'La recherche gérée par OpenHuman n’est pas disponible pour les utilisateurs locaux. Ajoutez votre propre clé API Parallel ou Brave pour activer la recherche web.', + 'devices.comingSoonDescription': + 'L’appairage des appareils arrive bientôt. Cette page servira à appairer des iPhone et à gérer les appareils connectés.', + 'welcome.continueLocallyExperimental': 'Continuer en local (Expérimental)', +}; export default fr; diff --git a/app/src/lib/i18n/hi.ts b/app/src/lib/i18n/hi.ts index 56694e538..17ccc0547 100644 --- a/app/src/lib/i18n/hi.ts +++ b/app/src/lib/i18n/hi.ts @@ -7,6 +7,25 @@ import type { TranslationMap } from './types'; // Hindi (हिन्दी) translations. Each chunk maps to chunks/en-N.ts. // Missing keys fall back to English via I18nContext.resolveEn(). -const hi: TranslationMap = { ...hi1, ...hi2, ...hi3, ...hi4, ...hi5 }; +const hi: TranslationMap = { + ...hi1, + ...hi2, + ...hi3, + ...hi4, + ...hi5, + 'skills.composio.noApiKeyTitle': 'कोई Composio API key कॉन्फ़िगर नहीं है', + 'skills.composio.noApiKeyDescription': + 'लोकल मोड आपकी अपनी Composio API key का उपयोग करता है। यहाँ integrations जोड़ने से पहले Settings → Advanced → Composio खोलकर key जोड़ें।', + 'skills.composio.noApiKeyCta': 'Settings में खोलें', + 'channels.localManagedUnavailable': 'लोकल उपयोगकर्ताओं के लिए managed channels उपलब्ध नहीं हैं।', + 'rewards.localUnavailable': + 'लोकल लॉगिन पर rewards, coupons या referral credit नहीं मिलते। rewards पाने के लिए लॉग आउट करें और OpenHuman खाते से साइन इन करें।', + 'rewards.localUnavailableCta': 'Account Settings खोलें', + 'settings.search.localManagedUnavailable': + 'लोकल उपयोगकर्ताओं के लिए OpenHuman Managed search उपलब्ध नहीं है। वेब सर्च चालू करने के लिए अपनी Parallel या Brave API key जोड़ें।', + 'devices.comingSoonDescription': + 'डिवाइस पेयरिंग जल्द आ रही है। यह पेज iPhone पेयर करने और जुड़े हुए डिवाइस प्रबंधित करने का स्थान होगा।', + 'welcome.continueLocallyExperimental': 'लोकल रूप से जारी रखें (प्रायोगिक)', +}; export default hi; diff --git a/app/src/lib/i18n/id.ts b/app/src/lib/i18n/id.ts index 0e55de204..ab5e04e30 100644 --- a/app/src/lib/i18n/id.ts +++ b/app/src/lib/i18n/id.ts @@ -7,6 +7,25 @@ import type { TranslationMap } from './types'; // Bahasa Indonesia translations. Each chunk maps to chunks/en-N.ts. // Missing keys fall back to English via I18nContext.resolveEn(). -const id: TranslationMap = { ...id1, ...id2, ...id3, ...id4, ...id5 }; +const id: TranslationMap = { + ...id1, + ...id2, + ...id3, + ...id4, + ...id5, + 'skills.composio.noApiKeyTitle': 'Belum ada API key Composio yang dikonfigurasi', + 'skills.composio.noApiKeyDescription': + 'Mode lokal menggunakan API key Composio milik Anda sendiri. Buka Pengaturan → Lanjutan → Composio untuk menambahkannya sebelum menghubungkan integrasi di sini.', + 'skills.composio.noApiKeyCta': 'Buka di Pengaturan', + 'channels.localManagedUnavailable': 'Channel terkelola tidak tersedia untuk pengguna lokal.', + 'rewards.localUnavailable': + 'Login lokal tidak mendapatkan reward, kupon, atau kredit referral. Keluar lalu lanjutkan dengan masuk menggunakan akun OpenHuman jika Anda ingin reward dihitung.', + 'rewards.localUnavailableCta': 'Buka Pengaturan Akun', + 'settings.search.localManagedUnavailable': + 'Pencarian OpenHuman Managed tidak tersedia untuk pengguna lokal. Tambahkan API key Parallel atau Brave Anda sendiri untuk mengaktifkan pencarian web.', + 'devices.comingSoonDescription': + 'Pemasangan perangkat akan segera hadir. Halaman ini akan menjadi tempat untuk memasangkan iPhone dan mengelola perangkat yang terhubung.', + 'welcome.continueLocallyExperimental': 'Lanjutkan Secara Lokal (Eksperimental)', +}; export default id; diff --git a/app/src/lib/i18n/it.ts b/app/src/lib/i18n/it.ts index 15cdab7d9..0c4ac79c7 100644 --- a/app/src/lib/i18n/it.ts +++ b/app/src/lib/i18n/it.ts @@ -7,6 +7,26 @@ import type { TranslationMap } from './types'; // Italian (Italiano) translations. Each chunk maps to chunks/en-N.ts. // Missing keys fall back to English via I18nContext.resolveEn(). -const it: TranslationMap = { ...it1, ...it2, ...it3, ...it4, ...it5 }; +const it: TranslationMap = { + ...it1, + ...it2, + ...it3, + ...it4, + ...it5, + 'skills.composio.noApiKeyTitle': 'Nessuna chiave API Composio configurata', + 'skills.composio.noApiKeyDescription': + 'La modalità locale usa la tua chiave API Composio. Apri Impostazioni → Avanzate → Composio per aggiungerne una prima di collegare le integrazioni qui.', + 'skills.composio.noApiKeyCta': 'Apri nelle impostazioni', + 'channels.localManagedUnavailable': + 'I canali gestiti non sono disponibili per gli utenti locali.', + 'rewards.localUnavailable': + "L'accesso locale non guadagna ricompense, coupon o credito referral. Esci e continua accedendo con un account OpenHuman se vuoi che le ricompense vengano conteggiate.", + 'rewards.localUnavailableCta': 'Apri le impostazioni account', + 'settings.search.localManagedUnavailable': + 'La ricerca gestita da OpenHuman non è disponibile per gli utenti locali. Aggiungi la tua chiave API Parallel o Brave per abilitare la ricerca web.', + 'devices.comingSoonDescription': + "L'abbinamento dei dispositivi arriverà presto. Questa pagina sarà il punto centrale per abbinare gli iPhone e gestire i dispositivi connessi.", + 'welcome.continueLocallyExperimental': 'Continua Localmente (Sperimentale)', +}; export default it; diff --git a/app/src/lib/i18n/ko.ts b/app/src/lib/i18n/ko.ts index 3d21380ba..a11c0e30d 100644 --- a/app/src/lib/i18n/ko.ts +++ b/app/src/lib/i18n/ko.ts @@ -2110,6 +2110,19 @@ const ko: TranslationMap = { 'settings.localModel.status.ollamaDocs': 'Ollama 문서', 'settings.localModel.status.thenRetry': '설정 지침을 확인한 다음 런타임에 연결할 수 있게 되면 다시 시도하세요.', + 'skills.composio.noApiKeyTitle': 'Composio API 키가 설정되지 않았습니다', + 'skills.composio.noApiKeyDescription': + '로컬 모드는 사용자의 Composio API 키를 사용합니다. 여기서 통합을 연결하기 전에 설정 → 고급 → Composio에서 키를 추가하세요.', + 'skills.composio.noApiKeyCta': '설정에서 열기', + 'channels.localManagedUnavailable': '로컬 사용자에게는 관리형 채널을 사용할 수 없습니다.', + 'rewards.localUnavailable': + '로컬 로그인으로는 보상, 쿠폰 또는 추천 크레딧을 받을 수 없습니다. 보상을 받으려면 로그아웃한 뒤 OpenHuman 계정으로 로그인하세요.', + 'rewards.localUnavailableCta': '계정 설정 열기', + 'settings.search.localManagedUnavailable': + '로컬 사용자에게는 OpenHuman Managed 검색을 사용할 수 없습니다. 웹 검색을 사용하려면 Parallel 또는 Brave API 키를 추가하세요.', + 'devices.comingSoonDescription': + '기기 페어링은 곧 제공됩니다. 이 페이지는 iPhone 페어링과 연결된 기기 관리를 위한 공간이 됩니다.', + 'welcome.continueLocallyExperimental': '로컬로 계속하기 (실험적)', }; export default ko; diff --git a/app/src/lib/i18n/pt.ts b/app/src/lib/i18n/pt.ts index 578fb41fe..ecabdb95e 100644 --- a/app/src/lib/i18n/pt.ts +++ b/app/src/lib/i18n/pt.ts @@ -7,6 +7,26 @@ import type { TranslationMap } from './types'; // Portuguese (Português) translations. Each chunk maps to chunks/en-N.ts. // Missing keys fall back to English via I18nContext.resolveEn(). -const pt: TranslationMap = { ...pt1, ...pt2, ...pt3, ...pt4, ...pt5 }; +const pt: TranslationMap = { + ...pt1, + ...pt2, + ...pt3, + ...pt4, + ...pt5, + 'skills.composio.noApiKeyTitle': 'Nenhuma chave de API do Composio configurada', + 'skills.composio.noApiKeyDescription': + 'O modo local usa sua própria chave de API do Composio. Abra Configurações → Avançado → Composio para adicionar uma antes de conectar integrações aqui.', + 'skills.composio.noApiKeyCta': 'Abrir nas Configurações', + 'channels.localManagedUnavailable': + 'Canais gerenciados não estão disponíveis para usuários locais.', + 'rewards.localUnavailable': + 'O login local não rende recompensas, cupons nem crédito de indicação. Saia e continue entrando com uma conta OpenHuman se quiser que as recompensas contem.', + 'rewards.localUnavailableCta': 'Abrir configurações da conta', + 'settings.search.localManagedUnavailable': + 'A busca gerenciada pela OpenHuman não está disponível para usuários locais. Adicione sua própria chave de API do Parallel ou Brave para habilitar a busca na web.', + 'devices.comingSoonDescription': + 'O pareamento de dispositivos está chegando em breve. Esta página será o lugar para parear iPhones e gerenciar dispositivos conectados.', + 'welcome.continueLocallyExperimental': 'Continuar Localmente (Experimental)', +}; export default pt; diff --git a/app/src/lib/i18n/ru.ts b/app/src/lib/i18n/ru.ts index 4a89a0137..c404e4644 100644 --- a/app/src/lib/i18n/ru.ts +++ b/app/src/lib/i18n/ru.ts @@ -7,6 +7,25 @@ import type { TranslationMap } from './types'; // Russian (Русский) translations. Each chunk maps to chunks/en-N.ts. // Missing keys fall back to English via I18nContext.resolveEn(). -const ru: TranslationMap = { ...ru1, ...ru2, ...ru3, ...ru4, ...ru5 }; +const ru: TranslationMap = { + ...ru1, + ...ru2, + ...ru3, + ...ru4, + ...ru5, + 'skills.composio.noApiKeyTitle': 'Ключ API Composio не настроен', + 'skills.composio.noApiKeyDescription': + 'Локальный режим использует ваш собственный ключ API Composio. Откройте Настройки → Дополнительно → Composio, чтобы добавить ключ перед подключением интеграций здесь.', + 'skills.composio.noApiKeyCta': 'Открыть в настройках', + 'channels.localManagedUnavailable': 'Управляемые каналы недоступны для локальных пользователей.', + 'rewards.localUnavailable': + 'Локальный вход не приносит награды, купоны или реферальный кредит. Выйдите и войдите с аккаунтом OpenHuman, если хотите, чтобы награды начислялись.', + 'rewards.localUnavailableCta': 'Открыть настройки аккаунта', + 'settings.search.localManagedUnavailable': + 'Поиск OpenHuman Managed недоступен для локальных пользователей. Добавьте свой ключ API Parallel или Brave, чтобы включить веб-поиск.', + 'devices.comingSoonDescription': + 'Сопряжение устройств скоро появится. Эта страница будет местом для подключения iPhone и управления подключёнными устройствами.', + 'welcome.continueLocallyExperimental': 'Продолжить локально (Экспериментально)', +}; export default ru; diff --git a/app/src/lib/i18n/zh-CN.ts b/app/src/lib/i18n/zh-CN.ts index c02fa656a..60419abd2 100644 --- a/app/src/lib/i18n/zh-CN.ts +++ b/app/src/lib/i18n/zh-CN.ts @@ -7,6 +7,24 @@ import type { TranslationMap } from './types'; // Simplified Chinese (简体中文) translations. Each chunk maps to chunks/en-N.ts. // Missing keys fall back to English via I18nContext.resolveEn(). -const zhCN: TranslationMap = { ...zhCN1, ...zhCN2, ...zhCN3, ...zhCN4, ...zhCN5 }; +const zhCN: TranslationMap = { + ...zhCN1, + ...zhCN2, + ...zhCN3, + ...zhCN4, + ...zhCN5, + 'skills.composio.noApiKeyTitle': '尚未配置 Composio API 密钥', + 'skills.composio.noApiKeyDescription': + '本地模式使用你自己的 Composio API 密钥。在此连接集成之前,请打开 设置 → 高级 → Composio 添加一个密钥。', + 'skills.composio.noApiKeyCta': '在设置中打开', + 'channels.localManagedUnavailable': '本地用户无法使用托管频道。', + 'rewards.localUnavailable': + '本地登录不会获得奖励、优惠券或推荐积分。若要累计奖励,请先登出,然后使用 OpenHuman 账号登录。', + 'rewards.localUnavailableCta': '打开账户设置', + 'settings.search.localManagedUnavailable': + '本地用户无法使用 OpenHuman 托管搜索。请添加你自己的 Parallel 或 Brave API 密钥以启用网页搜索。', + 'devices.comingSoonDescription': '设备配对即将推出。此页面将用于配对 iPhone 并管理已连接设备。', + 'welcome.continueLocallyExperimental': '本地继续(实验性)', +}; export default zhCN; diff --git a/app/src/pages/Rewards.tsx b/app/src/pages/Rewards.tsx index 2077e1ac7..f49f5f398 100644 --- a/app/src/pages/Rewards.tsx +++ b/app/src/pages/Rewards.tsx @@ -1,13 +1,17 @@ import createDebug from 'debug'; import { useCallback, useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import EmptyStateCard from '../components/EmptyStateCard'; import PillTabBar from '../components/PillTabBar'; import RewardsCommunityTab from '../components/rewards/RewardsCommunityTab'; import RewardsRedeemTab from '../components/rewards/RewardsRedeemTab'; import RewardsReferralsTab from '../components/rewards/RewardsReferralsTab'; import { useT } from '../lib/i18n/I18nContext'; +import { useCoreState } from '../providers/CoreStateProvider'; import { rewardsApi } from '../services/api/rewardsApi'; import type { RewardsSnapshot } from '../types/rewards'; +import { isLocalSessionToken } from '../utils/localSession'; type RewardsTab = 'referrals' | 'redeem' | 'rewards'; @@ -25,8 +29,11 @@ function errorMessage(err: unknown): string { const Rewards = () => { const { t } = useT(); + const navigate = useNavigate(); + const { snapshot: coreSnapshot } = useCoreState(); + const isLocalSession = isLocalSessionToken(coreSnapshot.sessionToken); const [selectedTab, setSelectedTab] = useState('rewards'); - const [snapshot, setSnapshot] = useState(null); + const [rewardsSnapshot, setRewardsSnapshot] = useState(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); @@ -37,7 +44,7 @@ const Rewards = () => { try { const result = await rewardsApi.getMyRewards(); if (signal?.cancelled) return; - setSnapshot(result); + setRewardsSnapshot(result); log( 'snapshot applied unlockedCount=%d totalCount=%d', result.summary.unlockedCount, @@ -47,7 +54,7 @@ const Rewards = () => { const message = errorMessage(err); log('snapshot load failed error=%s', message); if (signal?.cancelled) return; - setSnapshot(null); + setRewardsSnapshot(null); setError(message); } finally { if (!signal?.cancelled) { @@ -57,12 +64,15 @@ const Rewards = () => { }, []); useEffect(() => { + if (isLocalSession) { + return; + } const signal = { cancelled: false }; void loadRewards(signal); return () => { signal.cancelled = true; }; - }, [loadRewards]); + }, [isLocalSession, loadRewards]); const handleTabChange = useCallback((next: RewardsTab) => { log('tab changed next=%s', next); @@ -74,6 +84,37 @@ const Rewards = () => { void loadRewards(); }, [loadRewards]); + if (isLocalSession) { + return ( +
+
+
+
+ ); + } + return (
@@ -97,7 +138,7 @@ const Rewards = () => { error={error} isLoading={isLoading} onRetry={handleRetry} - snapshot={snapshot} + snapshot={rewardsSnapshot} /> )}
diff --git a/app/src/pages/Settings.tsx b/app/src/pages/Settings.tsx index 8acd62473..bc944d4a8 100644 --- a/app/src/pages/Settings.tsx +++ b/app/src/pages/Settings.tsx @@ -15,7 +15,7 @@ import ComposioPanel from '../components/settings/panels/ComposioPanel'; import ComposioTriagePanel from '../components/settings/panels/ComposioTriagePanel'; import CronJobsPanel from '../components/settings/panels/CronJobsPanel'; import DeveloperOptionsPanel from '../components/settings/panels/DeveloperOptionsPanel'; -import DevicesPanel from '../components/settings/panels/DevicesPanel'; +import DevicesComingSoonPanel from '../components/settings/panels/DevicesComingSoonPanel'; import HeartbeatPanel from '../components/settings/panels/HeartbeatPanel'; import LedgerUsagePanel from '../components/settings/panels/LedgerUsagePanel'; import LocalModelDebugPanel from '../components/settings/panels/LocalModelDebugPanel'; @@ -441,7 +441,7 @@ const Settings = () => { )} /> )} /> {/* Mobile devices */} - )} /> + )} /> {/* About / updates */} )} /> {/* Fallback */} diff --git a/app/src/pages/Skills.tsx b/app/src/pages/Skills.tsx index 4bd88e879..111d63a23 100644 --- a/app/src/pages/Skills.tsx +++ b/app/src/pages/Skills.tsx @@ -8,6 +8,7 @@ import { type ComposioToolkitMeta, KNOWN_COMPOSIO_TOOLKITS, } from '../components/composio/toolkitMeta'; +import EmptyStateCard from '../components/EmptyStateCard'; import { ToastContainer } from '../components/intelligence/Toast'; import PillTabBar from '../components/PillTabBar'; import AutocompleteSetupModal from '../components/skills/AutocompleteSetupModal'; @@ -35,6 +36,7 @@ import { useChannelDefinitions } from '../hooks/useChannelDefinitions'; import { useAgentReadyComposioToolkits, useComposioIntegrations } from '../lib/composio/hooks'; import { canonicalizeComposioToolkitSlug } from '../lib/composio/toolkitSlug'; import { type ComposioConnection, deriveComposioState } from '../lib/composio/types'; +import { getCoreStateSnapshot } from '../lib/coreState/store'; import { useT } from '../lib/i18n/I18nContext'; import { channelConnectionsApi } from '../services/api/channelConnectionsApi'; import { skillsApi, type SkillSummary } from '../services/api/skillsApi'; @@ -43,7 +45,8 @@ import { useAppDispatch, useAppSelector } from '../store/hooks'; import type { ChannelConnectionStatus, ChannelDefinition, ChannelType } from '../types/channels'; import type { ToastNotification } from '../types/intelligence'; import { IS_DEV } from '../utils/config'; -import { subconsciousEscalationsDismiss } from '../utils/tauriCommands'; +import { isLocalSessionToken } from '../utils/localSession'; +import { openhumanComposioGetMode, subconsciousEscalationsDismiss } from '../utils/tauriCommands'; function channelStatusLabel(status: ChannelConnectionStatus, t: (key: string) => string): string { switch (status) { @@ -286,8 +289,8 @@ function ChannelTile({ def, status, icon, testId, onOpen }: ChannelTileProps) { function McpComingSoonPanel() { const { t } = useT(); return ( -
-
+ -
-

- {t('skills.mcpComingSoon.title')} -

-

- {t('skills.mcpComingSoon.description')} -

- - {t('common.comingSoon')} - -
+ } + title={t('skills.mcpComingSoon.title')} + description={t('skills.mcpComingSoon.description')} + footer={ + + {t('common.comingSoon')} + + } + /> + ); +} + +function ComposioApiKeyEmptyState({ onOpenSettings }: { onOpenSettings: () => void }) { + const { t } = useT(); + return ( + + + + } + title={t('skills.composio.noApiKeyTitle')} + description={t('skills.composio.noApiKeyDescription')} + actionLabel={t('skills.composio.noApiKeyCta')} + onAction={onOpenSettings} + /> ); } @@ -362,6 +386,7 @@ export default function Skills() { const channelIcons = useMemo(() => getChannelIcons(t), [t]); const location = useLocation(); const navigate = useNavigate(); + const isLocalSession = isLocalSessionToken(getCoreStateSnapshot().snapshot.sessionToken); const [activeTab, setActiveTab] = useState('composio'); const dispatch = useAppDispatch(); const [defaultChannelBusy, setDefaultChannelBusy] = useState(null); @@ -432,6 +457,8 @@ export default function Skills() { const [installDialogOpen, setInstallDialogOpen] = useState(false); const [uninstallCandidate, setUninstallCandidate] = useState(null); const [toasts, setToasts] = useState([]); + const [hasComposioApiKey, setHasComposioApiKey] = useState(null); + const showLocalComposioApiKeyBanner = isLocalSession && hasComposioApiKey === false; const addToast = useCallback((toast: Omit) => { const newToast: ToastNotification = { ...toast, id: `toast-${Date.now()}-${Math.random()}` }; setToasts(prev => [...prev, newToast]); @@ -511,6 +538,29 @@ export default function Skills() { }; }, [refreshDiscoveredSkills]); + useEffect(() => { + if (!isLocalSession) { + setHasComposioApiKey(null); + return; + } + let cancelled = false; + void openhumanComposioGetMode() + .then(res => { + if (!cancelled) { + setHasComposioApiKey(Boolean(res.result?.api_key_set)); + } + }) + .catch(err => { + if (!cancelled) { + console.warn('[skills][composio] failed to load composio mode status:', err); + setHasComposioApiKey(false); + } + }); + return () => { + cancelled = true; + }; + }, [isLocalSession]); + const bestChannelStatus = (channelId: ChannelType): ChannelConnectionStatus => { const conns = channelConnections.connections[channelId]; if (!conns) return 'disconnected'; @@ -1016,41 +1066,49 @@ export default function Skills() { {t('skills.integrationsSubtitle')}

-
- - navigate('/settings/composio-routing')} /> -
- {composioSortedEntries.length > 0 ? ( -
- {composioSortedEntries.map(({ meta, connection }) => ( -
- setComposioModalToolkit(meta)} - onRetryGlobal={() => void refreshComposio()} - /> -
- ))} -
- ) : ( -

- {t('skills.noResults')} -

)} + {!showLocalComposioApiKeyBanner && ( +
+ + +
+ )} + {!showLocalComposioApiKeyBanner && + (composioSortedEntries.length > 0 ? ( +
+ {composioSortedEntries.map(({ meta, connection }) => ( +
+ setComposioModalToolkit(meta)} + onRetryGlobal={() => void refreshComposio()} + /> +
+ ))} +
+ ) : ( +

+ {t('skills.noResults')} +

+ ))}
)} diff --git a/app/src/pages/Welcome.tsx b/app/src/pages/Welcome.tsx index 39a970758..7a1f43b97 100644 --- a/app/src/pages/Welcome.tsx +++ b/app/src/pages/Welcome.tsx @@ -1,30 +1,41 @@ import createDebug from 'debug'; import { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; import OAuthProviderButton from '../components/oauth/OAuthProviderButton'; import { oauthProviderConfigs } from '../components/oauth/providerConfigs'; import RotatingTetrahedronCanvas from '../components/RotatingTetrahedronCanvas'; import Button from '../components/ui/Button'; import { useT } from '../lib/i18n/I18nContext'; +import { useCoreState } from '../providers/CoreStateProvider'; import { clearBackendUrlCache } from '../services/backendUrl'; import { clearCoreRpcTokenCache, clearCoreRpcUrlCache } from '../services/coreRpcClient'; import { resetCoreMode } from '../store/coreModeSlice'; import { useDeepLinkAuthState } from '../store/deepLinkAuthState'; -import { useAppDispatch } from '../store/hooks'; +import { useAppDispatch, useAppSelector } from '../store/hooks'; +import { resolveTheme, setThemeMode, type ThemeMode } from '../store/themeSlice'; import { clearAllAppData } from '../utils/clearAllAppData'; import { clearStoredCoreMode, clearStoredCoreToken, storeRpcUrl } from '../utils/configPersistence'; import { PRIVACY_POLICY_URL, TERMS_OF_USE_URL } from '../utils/links'; +import { createLocalSessionToken, LOCAL_SESSION_USER } from '../utils/localSession'; import { openUrl } from '../utils/openUrl'; const log = createDebug('app:welcome'); const Welcome = () => { const { t } = useT(); + const navigate = useNavigate(); const dispatch = useAppDispatch(); + const { storeSessionToken } = useCoreState(); const { isProcessing, errorMessage, requiresAppDataReset } = useDeepLinkAuthState(); + const themeMode = useAppSelector(state => state.theme?.mode ?? 'system') as ThemeMode; + const resolvedTheme = resolveTheme(themeMode); + const isDark = resolvedTheme === 'dark'; const [isClearingAppData, setIsClearingAppData] = useState(false); + const [isLocalSigningIn, setIsLocalSigningIn] = useState(false); const [resetError, setResetError] = useState(null); + const [localLoginError, setLocalLoginError] = useState(null); const handleClearAppData = async () => { setIsClearingAppData(true); @@ -52,10 +63,69 @@ const Welcome = () => { dispatch(resetCoreMode()); }; + const handleLocalLogin = async () => { + setIsLocalSigningIn(true); + setLocalLoginError(null); + try { + log('[welcome] local session login requested'); + await storeSessionToken(createLocalSessionToken(), LOCAL_SESSION_USER); + navigate('/onboarding/custom/inference', { replace: true }); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + log('[welcome] local session login failed: %s', message); + setLocalLoginError(message || 'Could not start a local session.'); + setIsLocalSigningIn(false); + } + }; + + const toggleTheme = () => { + dispatch(setThemeMode(isDark ? 'light' : 'dark')); + }; + return (
+
+