mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
IA revamp: restructure app navigation and settings for everyday users (phased) (#3518)
This commit is contained in:
+33
-41
@@ -4,18 +4,15 @@ import AppRoutesIOS from './AppRoutesIOS';
|
||||
import DefaultRedirect from './components/DefaultRedirect';
|
||||
import ProtectedRoute from './components/ProtectedRoute';
|
||||
import PublicRoute from './components/PublicRoute';
|
||||
import HumanPage from './features/human/HumanPage';
|
||||
import { getIsMobile } from './lib/platform';
|
||||
import Accounts from './pages/Accounts';
|
||||
import Channels from './pages/Channels';
|
||||
import Activity from './pages/Activity';
|
||||
import Home from './pages/Home';
|
||||
import Intelligence from './pages/Intelligence';
|
||||
import Invites from './pages/Invites';
|
||||
import Notifications from './pages/Notifications';
|
||||
import Onboarding from './pages/onboarding/Onboarding';
|
||||
import { PttOverlayPage } from './pages/PttOverlayPage';
|
||||
import Rewards from './pages/Rewards';
|
||||
import Routines from './pages/Routines';
|
||||
import Settings from './pages/Settings';
|
||||
import Skills from './pages/Skills';
|
||||
import WebCallbackPage from './pages/WebCallbackPage';
|
||||
@@ -65,33 +62,34 @@ const AppRoutes = () => {
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Phase 6 — /human merged into /chat (Assistant surface).
|
||||
Preserve the route for back-compat (deep links, iOS share sheets, etc.).
|
||||
iOS AppRoutesIOS still serves /human natively — only desktop redirects. */}
|
||||
<Route path="/human" element={<Navigate to="/chat" replace />} />
|
||||
|
||||
{/* Primary Activity surface — replaces /intelligence (Phase 3). */}
|
||||
<Route
|
||||
path="/human"
|
||||
path="/activity"
|
||||
element={
|
||||
<ProtectedRoute requireAuth={true}>
|
||||
<HumanPage />
|
||||
<Activity />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/intelligence"
|
||||
element={
|
||||
<ProtectedRoute requireAuth={true}>
|
||||
<Intelligence />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
{/* Back-compat: /intelligence → /activity (preserves ?tab= deep links).
|
||||
Deep links such as ?tab=memory or ?tab=agents still resolve but fall
|
||||
back to the tasks tab in prod (dev-only tabs are gated inside Activity). */}
|
||||
<Route path="/intelligence" element={<Navigate to="/activity" replace />} />
|
||||
|
||||
{/* Skills lives at /skills with its 4 sub-tabs (Composio / Channels /
|
||||
MCP Servers / Runners). The scheduled-skills dashboard concept
|
||||
composes INSIDE the Runners sub-tab, not as a separate top-level
|
||||
page — the bottom-bar "Connections" entry has always pointed at
|
||||
/skills to surface Composio integrations + MCP, and that muscle
|
||||
memory is restored here.
|
||||
{/* Connections page lives at /connections (Phase 2 rename from /skills).
|
||||
The old /skills path is kept as a back-compat redirect so bookmarks
|
||||
and deep links continue to work. `?tab=` query params are preserved
|
||||
by Navigate (replace) so existing deep links still land on the right
|
||||
sub-tab.
|
||||
`/workflows/new` is the create-a-skill authoring page.
|
||||
Order matters: keep `/workflows/new` before `/skills` so it wins the
|
||||
prefix match. */}
|
||||
Order matters: keep `/workflows/new` before `/connections` so it wins
|
||||
the prefix match. */}
|
||||
<Route
|
||||
path="/workflows/new"
|
||||
element={
|
||||
@@ -111,7 +109,7 @@ const AppRoutes = () => {
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/skills"
|
||||
path="/connections"
|
||||
element={
|
||||
<ProtectedRoute requireAuth={true}>
|
||||
<Skills />
|
||||
@@ -119,6 +117,9 @@ const AppRoutes = () => {
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Back-compat: /skills → /connections (preserves ?tab= deep links). */}
|
||||
<Route path="/skills" element={<Navigate to="/connections" replace />} />
|
||||
|
||||
{/* Unified chat = agent + connected web apps. Replaces the old
|
||||
/conversations and /accounts routes. */}
|
||||
<Route
|
||||
@@ -130,14 +131,9 @@ const AppRoutes = () => {
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/channels"
|
||||
element={
|
||||
<ProtectedRoute requireAuth={true}>
|
||||
<Channels />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
{/* Back-compat: /channels was an orphaned standalone page; it now
|
||||
redirects to the unified Connections page on the Messaging tab. */}
|
||||
<Route path="/channels" element={<Navigate to="/connections?tab=messaging" replace />} />
|
||||
|
||||
<Route
|
||||
path="/invites"
|
||||
@@ -157,14 +153,10 @@ const AppRoutes = () => {
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/routines"
|
||||
element={
|
||||
<ProtectedRoute requireAuth={true}>
|
||||
<Routines />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
{/* Back-compat: /routines was an orphaned dead page (superseded by the
|
||||
Cron Jobs settings panel). Redirect to Activity → Automations so
|
||||
any surviving deep links land somewhere sensible. */}
|
||||
<Route path="/routines" element={<Navigate to="/activity?tab=automations" replace />} />
|
||||
|
||||
<Route
|
||||
path="/rewards"
|
||||
@@ -175,9 +167,9 @@ const AppRoutes = () => {
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Workflows moved onto the Intelligence page (its own tab). Keep the
|
||||
{/* Workflows moved onto the Activity page (Automations tab). Keep the
|
||||
old /workflows path working as a deep link into that tab. */}
|
||||
<Route path="/workflows" element={<Navigate to="/intelligence?tab=workflows" replace />} />
|
||||
<Route path="/workflows" element={<Navigate to="/activity?tab=automations" replace />} />
|
||||
|
||||
<Route path="/webhooks" element={<Navigate to="/settings/webhooks-triggers" replace />} />
|
||||
|
||||
|
||||
+187
-128
@@ -1,6 +1,7 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
|
||||
import { AVATAR_MENU_ITEMS, NAV_TABS } from '../config/navConfig';
|
||||
import { useT } from '../lib/i18n/I18nContext';
|
||||
import { useCoreState } from '../providers/CoreStateProvider';
|
||||
import { trackEvent } from '../services/analytics';
|
||||
@@ -8,123 +9,105 @@ import { selectCompanionSessionActive } from '../store/companionSlice';
|
||||
import { useAppSelector } from '../store/hooks';
|
||||
import { selectUnreadCount } from '../store/notificationSlice';
|
||||
import { isAccountsFullscreen } from '../utils/accountsFullscreen';
|
||||
import { BILLING_DASHBOARD_URL } from '../utils/links';
|
||||
import { isLocalSessionToken } from '../utils/localSession';
|
||||
import { openUrl } from '../utils/openUrl';
|
||||
import { resolveUserName } from '../utils/userName';
|
||||
|
||||
const makeTabs = (t: (key: string) => string) => [
|
||||
{
|
||||
id: 'home',
|
||||
label: t('nav.home'),
|
||||
path: '/home',
|
||||
icon: (
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.8}
|
||||
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-4 0a2 2 0 01-2-2v-4a2 2 0 012-2h2a2 2 0 012 2v4a2 2 0 01-2 2h-2z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'human',
|
||||
label: t('nav.human'),
|
||||
path: '/human',
|
||||
icon: (
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.8}
|
||||
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14c-4 0-7 2.5-7 6h14c0-3.5-3-6-7-6z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'chat',
|
||||
label: t('nav.chat'),
|
||||
path: '/chat',
|
||||
icon: (
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.8}
|
||||
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'skills',
|
||||
label: t('nav.connections'),
|
||||
path: '/skills',
|
||||
icon: (
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.8}
|
||||
d="M14 10l-2 1m0 0l-2-1m2 1v2.5M20 7l-2 1m2-1l-2-1m2 1v2.5M14 4l-2-1-2 1M4 7l2-1M4 7l2 1M4 7v2.5M12 21l-2-1m2 1l2-1m-2 1v-2.5M6 18l-2-1v-2.5M18 18l2-1v-2.5"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'intelligence',
|
||||
label: t('nav.memory'),
|
||||
path: '/intelligence',
|
||||
icon: (
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.8}
|
||||
d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
// Alerts/Notifications used to be its own bottom tab; moved into
|
||||
// Settings › Notifications since it's a low-traffic destination.
|
||||
// The /notifications route still exists for deep links.
|
||||
{
|
||||
id: 'rewards',
|
||||
label: t('nav.rewards'),
|
||||
path: '/rewards',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.8}
|
||||
d="M12 8v8m0-8l-3-3m3 3l3-3M8 14H6a2 2 0 01-2-2V7a2 2 0 012-2h2m8 9h2a2 2 0 002-2V7a2 2 0 00-2-2h-2M7 19h10"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'settings',
|
||||
label: t('nav.settings'),
|
||||
path: '/settings',
|
||||
icon: (
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.8}
|
||||
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.8}
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
];
|
||||
// ── SVG icons, keyed by tab id ────────────────────────────────────────────────
|
||||
|
||||
function TabIcon({ id }: { id: string }) {
|
||||
switch (id) {
|
||||
case 'home':
|
||||
return (
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.8}
|
||||
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-4 0a2 2 0 01-2-2v-4a2 2 0 012-2h2a2 2 0 012 2v4a2 2 0 01-2 2h-2z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
case 'human':
|
||||
return (
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.8}
|
||||
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14c-4 0-7 2.5-7 6h14c0-3.5-3-6-7-6z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
case 'chat':
|
||||
return (
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.8}
|
||||
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
case 'connections':
|
||||
return (
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.8}
|
||||
d="M14 10l-2 1m0 0l-2-1m2 1v2.5M20 7l-2 1m2-1l-2-1m2 1v2.5M14 4l-2-1-2 1M4 7l2-1M4 7l2 1M4 7v2.5M12 21l-2-1m2 1l2-1m-2 1v-2.5M6 18l-2-1v-2.5M18 18l2-1v-2.5"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
case 'activity':
|
||||
// Reuse the Intelligence/memory lightbulb icon for the Activity tab
|
||||
return (
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.8}
|
||||
d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
case 'settings':
|
||||
return (
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.8}
|
||||
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.8}
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
const getInitials = (name: string): string => {
|
||||
const words = name.trim().split(/\s+/).filter(Boolean);
|
||||
if (words.length === 0) return 'OH';
|
||||
return words
|
||||
.slice(0, 2)
|
||||
.map(word => word[0]?.toUpperCase() ?? '')
|
||||
.join('');
|
||||
};
|
||||
|
||||
// ── Component ─────────────────────────────────────────────────────────────────
|
||||
|
||||
const BottomTabBar = () => {
|
||||
const { t } = useT();
|
||||
@@ -133,6 +116,8 @@ const BottomTabBar = () => {
|
||||
const { snapshot } = useCoreState();
|
||||
const token = snapshot.sessionToken;
|
||||
const [revealed, setRevealed] = useState(false);
|
||||
const [profileMenuOpen, setProfileMenuOpen] = useState(false);
|
||||
const profileMenuRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const activeAccountId = useAppSelector(state => state.accounts.activeAccountId);
|
||||
const unreadCount = useAppSelector(state => selectUnreadCount(state.notifications.items));
|
||||
@@ -142,7 +127,32 @@ 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 isLocalSession = isLocalSessionToken(token);
|
||||
// The avatar button shows the signed-in user's initials.
|
||||
const userInitials = getInitials(resolveUserName(snapshot.currentUser));
|
||||
|
||||
// Resolve translated labels for NAV_TABS once per render cycle.
|
||||
const tabs = useMemo(() => NAV_TABS.map(tab => ({ ...tab, label: t(tab.labelKey) })), [t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!profileMenuOpen) return;
|
||||
|
||||
const onPointerDown = (event: PointerEvent) => {
|
||||
if (profileMenuRef.current?.contains(event.target as Node)) return;
|
||||
setProfileMenuOpen(false);
|
||||
};
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') setProfileMenuOpen(false);
|
||||
};
|
||||
|
||||
document.addEventListener('pointerdown', onPointerDown);
|
||||
document.addEventListener('keydown', onKeyDown);
|
||||
return () => {
|
||||
document.removeEventListener('pointerdown', onPointerDown);
|
||||
document.removeEventListener('keydown', onKeyDown);
|
||||
};
|
||||
}, [profileMenuOpen]);
|
||||
|
||||
const hiddenPaths = ['/', '/login'];
|
||||
if (
|
||||
@@ -189,6 +199,16 @@ const BottomTabBar = () => {
|
||||
navigate(tab.path);
|
||||
};
|
||||
|
||||
const handleAvatarMenuItemClick = (itemId: string, kind: string, target: string) => {
|
||||
setProfileMenuOpen(false);
|
||||
if (kind === 'openUrl') {
|
||||
openUrl(target).catch(() => {});
|
||||
} else {
|
||||
navigate(target);
|
||||
}
|
||||
trackEvent('avatar_menu_item_click', { item_id: itemId });
|
||||
};
|
||||
|
||||
return (
|
||||
// pointer-events-none on the full-width shell so transparent areas (e.g.
|
||||
// beside the centered nav pill) do not steal clicks from sticky footers
|
||||
@@ -217,18 +237,10 @@ const BottomTabBar = () => {
|
||||
const active = isActive(tab.path);
|
||||
const showBadge = tab.id === 'notifications' && unreadCount > 0;
|
||||
const showCompanionDot = tab.id === 'settings' && companionActive;
|
||||
// data-walkthrough attributes for the Joyride walkthrough steps.
|
||||
// Maps tab ids to their walkthrough target names.
|
||||
const walkthroughAttr: Record<string, string> = {
|
||||
chat: 'tab-chat',
|
||||
skills: 'tab-skills',
|
||||
notifications: 'tab-notifications',
|
||||
settings: 'tab-settings',
|
||||
};
|
||||
return (
|
||||
<button
|
||||
key={tab.id}
|
||||
data-walkthrough={walkthroughAttr[tab.id]}
|
||||
data-walkthrough={tab.walkthroughAttr}
|
||||
onClick={() => handleTabClick(tab, active)}
|
||||
className={`group relative flex items-center px-2 py-2 rounded-sm text-sm transition-colors duration-500 ease-[cubic-bezier(0.22,1,0.36,1)] cursor-pointer ${
|
||||
active
|
||||
@@ -241,7 +253,7 @@ const BottomTabBar = () => {
|
||||
: tab.label
|
||||
}>
|
||||
<span className="relative inline-flex flex-shrink-0">
|
||||
{tab.icon}
|
||||
<TabIcon id={tab.id} />
|
||||
{showBadge && (
|
||||
<span className="absolute -top-1 -right-1 min-w-[14px] h-[14px] px-1 rounded-full bg-coral-500 text-[9px] font-bold text-white flex items-center justify-center leading-none">
|
||||
{unreadCount > 9 ? '9+' : unreadCount}
|
||||
@@ -262,6 +274,53 @@ const BottomTabBar = () => {
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
<div
|
||||
className="relative ml-1 border-l border-stone-300 pl-1 dark:border-neutral-700"
|
||||
ref={profileMenuRef}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setProfileMenuOpen(open => !open)}
|
||||
className={`relative flex h-9 w-9 items-center justify-center rounded-sm transition-colors duration-300 cursor-pointer ${
|
||||
profileMenuOpen
|
||||
? 'bg-white text-stone-900 shadow-sm dark:bg-neutral-800 dark:text-neutral-100'
|
||||
: 'bg-transparent text-stone-500 hover:bg-stone-300/50 hover:text-stone-700 dark:text-neutral-400 dark:hover:bg-neutral-800/60 dark:hover:text-neutral-200'
|
||||
}`}
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={profileMenuOpen}
|
||||
aria-label={t('nav.avatarMenu.account')}
|
||||
title={t('nav.avatarMenu.account')}>
|
||||
<span className="flex h-6 w-6 items-center justify-center rounded-full bg-primary-500 text-[10px] font-semibold leading-none text-white">
|
||||
{userInitials}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{profileMenuOpen && (
|
||||
<div
|
||||
role="menu"
|
||||
aria-label={t('nav.avatarMenu.account')}
|
||||
className="absolute bottom-full right-0 mb-2 w-56 overflow-hidden rounded-sm border border-stone-300 bg-white shadow-soft dark:border-neutral-700 dark:bg-neutral-900">
|
||||
<div className="p-1">
|
||||
{AVATAR_MENU_ITEMS.filter(item => !item.cloudOnly || !isLocalSession).map(
|
||||
item => {
|
||||
// Billing target is resolved from the canonical constant rather than the
|
||||
// data-file placeholder so it stays in sync with SettingsHome.
|
||||
const target = item.id === 'billing' ? BILLING_DASHBOARD_URL : item.target;
|
||||
return (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
role="menuitem"
|
||||
onClick={() => handleAvatarMenuItemClick(item.id, item.kind, target)}
|
||||
className="flex w-full items-center rounded-sm px-2 py-2 text-left text-sm text-stone-700 transition-colors hover:bg-stone-100 dark:text-neutral-200 dark:hover:bg-neutral-800">
|
||||
{t(item.labelKey)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
/**
|
||||
* Tests for BottomTabBar — verifies that:
|
||||
* - the tab bar renders when the user has a session token and is on a non-hidden path
|
||||
* - the walkthroughAttr mapping (line 222) is exercised by rendering the tabs
|
||||
* - the tab bar is hidden on '/' and '/login' paths
|
||||
* - 5 tabs are rendered (no Rewards tab, no Human tab), Activity label is present
|
||||
* - Assistant tab is present (was "Chat", id stays 'chat', label now 'Assistant')
|
||||
* - Walkthrough attributes reflect the new ids (tab-connections, tab-activity)
|
||||
* - Avatar menu opens and shows Account / Billing / Rewards / Invites / Wallet
|
||||
* - Clicking an avatar menu item navigates or opens URL
|
||||
* - The bar is hidden on '/' and '/login' paths
|
||||
*
|
||||
* [#1123] Covers the walkthroughAttr object added for the Joyride walkthrough.
|
||||
* Updated for IA Phase 6: Human tab removed; Chat renamed to Assistant.
|
||||
*/
|
||||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
@@ -13,6 +16,7 @@ import { MemoryRouter } from 'react-router-dom';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import accountsReducer from '../../store/accountsSlice';
|
||||
import agentProfileReducer, { setAgentProfilesFromResponse } from '../../store/agentProfileSlice';
|
||||
import companionReducer from '../../store/companionSlice';
|
||||
import notificationReducer from '../../store/notificationSlice';
|
||||
import BottomTabBar from '../BottomTabBar';
|
||||
@@ -21,6 +25,15 @@ import BottomTabBar from '../BottomTabBar';
|
||||
|
||||
vi.mock('../../providers/CoreStateProvider', () => ({ useCoreState: vi.fn() }));
|
||||
|
||||
const agentProfilesApiMock = vi.hoisted(() => ({
|
||||
list: vi.fn(),
|
||||
select: vi.fn(),
|
||||
upsert: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../services/api/agentProfilesApi', () => ({ agentProfilesApi: agentProfilesApiMock }));
|
||||
|
||||
vi.mock('../../utils/config', async importOriginal => {
|
||||
const actual = await importOriginal<typeof import('../../utils/config')>();
|
||||
return { ...actual, APP_ENVIRONMENT: 'development' };
|
||||
@@ -29,20 +42,53 @@ vi.mock('../../utils/config', async importOriginal => {
|
||||
vi.mock('../../utils/accountsFullscreen', () => ({ isAccountsFullscreen: vi.fn(() => false) }));
|
||||
vi.mock('../../services/analytics', () => ({ trackEvent: vi.fn() }));
|
||||
|
||||
// Mock openUrl so tests don't try to open real URLs
|
||||
vi.mock('../../utils/openUrl', () => ({ openUrl: vi.fn().mockResolvedValue(undefined) }));
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────────────────────────
|
||||
|
||||
interface BuildStoreOpts {
|
||||
companionSessionActive?: boolean;
|
||||
}
|
||||
|
||||
const testProfiles = {
|
||||
activeProfileId: 'planner',
|
||||
profiles: [
|
||||
{
|
||||
id: 'default',
|
||||
name: 'Orchestrator',
|
||||
description: 'Default agent',
|
||||
agentId: 'orchestrator',
|
||||
builtIn: true,
|
||||
},
|
||||
{
|
||||
id: 'planner',
|
||||
name: 'Planner',
|
||||
description: 'Plans multi-step work',
|
||||
agentId: 'planner',
|
||||
builtIn: true,
|
||||
avatarUrl: 'https://example.com/planner.png',
|
||||
},
|
||||
{
|
||||
id: 'research',
|
||||
name: 'Research',
|
||||
description: 'Finds and summarizes sources',
|
||||
agentId: 'research',
|
||||
builtIn: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
function buildStore(opts: BuildStoreOpts = {}) {
|
||||
const store = configureStore({
|
||||
reducer: {
|
||||
accounts: accountsReducer,
|
||||
notifications: notificationReducer,
|
||||
companion: companionReducer,
|
||||
agentProfiles: agentProfileReducer,
|
||||
},
|
||||
});
|
||||
store.dispatch(setAgentProfilesFromResponse(testProfiles));
|
||||
if (opts.companionSessionActive) {
|
||||
store.dispatch({
|
||||
type: 'companion/setSessionActive',
|
||||
@@ -56,6 +102,7 @@ interface RenderOpts {
|
||||
hasToken?: boolean;
|
||||
companionSessionActive?: boolean;
|
||||
tokenValue?: string;
|
||||
currentUser?: unknown;
|
||||
}
|
||||
|
||||
async function renderBottomTabBar(pathname = '/home', opts: RenderOpts | boolean = {}) {
|
||||
@@ -68,7 +115,7 @@ async function renderBottomTabBar(pathname = '/home', opts: RenderOpts | boolean
|
||||
snapshot: {
|
||||
sessionToken: hasToken ? tokenValue : null,
|
||||
auth: { isAuthenticated: true, userId: 'u1', user: null, profileId: null },
|
||||
currentUser: null,
|
||||
currentUser: resolved.currentUser ?? null,
|
||||
onboardingCompleted: true,
|
||||
chatOnboardingCompleted: true,
|
||||
analyticsEnabled: false,
|
||||
@@ -106,19 +153,50 @@ async function renderBottomTabBar(pathname = '/home', opts: RenderOpts | boolean
|
||||
describe('BottomTabBar', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
agentProfilesApiMock.select.mockResolvedValue(testProfiles);
|
||||
});
|
||||
|
||||
// [#1123] Covers line 222 — walkthroughAttr object created per-tab inside .map()
|
||||
it('renders navigation tabs with data-walkthrough attributes when session is active', async () => {
|
||||
it('renders exactly 5 tab buttons (Phase 6: Human merged into Assistant)', async () => {
|
||||
await renderBottomTabBar('/home');
|
||||
// Query only buttons inside <nav> to exclude the avatar button
|
||||
const nav = document.querySelector('nav');
|
||||
const navButtons = nav?.querySelectorAll('button:not([aria-haspopup])');
|
||||
expect(navButtons).toHaveLength(5);
|
||||
});
|
||||
|
||||
// The Home tab is always visible and has no walkthrough attr (not in the map)
|
||||
expect(screen.getByRole('button', { name: 'Home' })).toBeInTheDocument();
|
||||
it('does NOT render a Rewards tab', async () => {
|
||||
await renderBottomTabBar('/home');
|
||||
expect(screen.queryByRole('button', { name: 'Rewards' })).toBeNull();
|
||||
});
|
||||
|
||||
// Chat tab has data-walkthrough="tab-chat" (from walkthroughAttr map)
|
||||
const chatBtn = screen.getByRole('button', { name: 'Chat' });
|
||||
expect(chatBtn).toBeInTheDocument();
|
||||
expect(chatBtn).toHaveAttribute('data-walkthrough', 'tab-chat');
|
||||
it('does NOT render a Human tab (Phase 6: merged into Assistant)', async () => {
|
||||
await renderBottomTabBar('/home');
|
||||
expect(screen.queryByRole('button', { name: 'Human' })).toBeNull();
|
||||
});
|
||||
|
||||
it('renders the Activity tab', async () => {
|
||||
await renderBottomTabBar('/home');
|
||||
expect(screen.getByRole('button', { name: 'Activity' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the Assistant tab (was Chat, Phase 6 rename)', async () => {
|
||||
await renderBottomTabBar('/home');
|
||||
const assistantBtn = screen.getByRole('button', { name: 'Assistant' });
|
||||
expect(assistantBtn).toBeInTheDocument();
|
||||
expect(assistantBtn).toHaveAttribute('data-walkthrough', 'tab-chat');
|
||||
});
|
||||
|
||||
it('renders the Connections tab with data-walkthrough="tab-connections"', async () => {
|
||||
await renderBottomTabBar('/home');
|
||||
const connectionsBtn = screen.getByRole('button', { name: 'Connections' });
|
||||
expect(connectionsBtn).toBeInTheDocument();
|
||||
expect(connectionsBtn).toHaveAttribute('data-walkthrough', 'tab-connections');
|
||||
});
|
||||
|
||||
it('renders Activity tab with data-walkthrough="tab-activity"', async () => {
|
||||
await renderBottomTabBar('/home');
|
||||
const activityBtn = screen.getByRole('button', { name: 'Activity' });
|
||||
expect(activityBtn).toHaveAttribute('data-walkthrough', 'tab-activity');
|
||||
});
|
||||
|
||||
it('renders Settings tab with data-walkthrough="tab-settings"', async () => {
|
||||
@@ -132,11 +210,6 @@ 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' });
|
||||
@@ -164,7 +237,8 @@ describe('BottomTabBar', () => {
|
||||
const { trackEvent } = await import('../../services/analytics');
|
||||
await renderBottomTabBar('/home');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Chat' }));
|
||||
// Tab id is still 'chat' (back-compat) even though label is now 'Assistant'.
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Assistant' }));
|
||||
|
||||
expect(trackEvent).toHaveBeenCalledWith('tab_bar_change', {
|
||||
from_tab: 'home',
|
||||
@@ -182,4 +256,65 @@ describe('BottomTabBar', () => {
|
||||
|
||||
expect(trackEvent).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('renders the avatar button with the signed-in user initials', async () => {
|
||||
await renderBottomTabBar('/home', { currentUser: { firstName: 'Ada', lastName: 'Lovelace' } });
|
||||
|
||||
const avatar = screen.getByRole('button', { name: 'Account' });
|
||||
expect(avatar).toHaveTextContent('AL');
|
||||
});
|
||||
|
||||
it('falls back to a generic initial when no user is present', async () => {
|
||||
await renderBottomTabBar('/home', { currentUser: null });
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Account' })).toHaveTextContent('U');
|
||||
});
|
||||
|
||||
it('avatar menu shows Account, Billing, Rewards, Invites, and Wallet items', async () => {
|
||||
await renderBottomTabBar('/home');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Account' }));
|
||||
|
||||
const menu = screen.getByRole('menu', { name: 'Account' });
|
||||
const menuItems = menu.querySelectorAll('[role="menuitem"]');
|
||||
const labels = Array.from(menuItems).map(el => el.textContent?.trim());
|
||||
expect(labels).toContain('Account');
|
||||
expect(labels).toContain('Billing');
|
||||
expect(labels).toContain('Rewards');
|
||||
expect(labels).toContain('Invite a friend');
|
||||
expect(labels).toContain('Wallet');
|
||||
});
|
||||
|
||||
it('clicking Account in avatar menu closes the menu', async () => {
|
||||
await renderBottomTabBar('/home');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Account' }));
|
||||
expect(screen.getByRole('menu', { name: 'Account' })).toBeInTheDocument();
|
||||
|
||||
const accountItem = screen.getByRole('menuitem', { name: 'Account' });
|
||||
fireEvent.click(accountItem);
|
||||
|
||||
// Menu should close after click
|
||||
expect(screen.queryByRole('menu', { name: 'Account' })).toBeNull();
|
||||
});
|
||||
|
||||
it('avatar menu does not show cloud-only items for local session', async () => {
|
||||
// A local session token contains the literal string 'local'
|
||||
await renderBottomTabBar('/home', { tokenValue: 'header.payload.local' });
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Account' }));
|
||||
|
||||
const menu = screen.getByRole('menu', { name: 'Account' });
|
||||
const menuItems = menu.querySelectorAll('[role="menuitem"]');
|
||||
const labels = Array.from(menuItems).map(el => el.textContent?.trim());
|
||||
|
||||
// Account and Wallet are always shown
|
||||
expect(labels).toContain('Account');
|
||||
expect(labels).toContain('Wallet');
|
||||
|
||||
// Cloud-only items should not appear for local sessions
|
||||
expect(labels).not.toContain('Billing');
|
||||
expect(labels).not.toContain('Rewards');
|
||||
expect(labels).not.toContain('Invite a friend');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -110,7 +110,7 @@ export default function RewardsCommunityTab({
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 sm:flex-row">
|
||||
<button
|
||||
onClick={() => navigate('/skills')}
|
||||
onClick={() => navigate('/connections')}
|
||||
className="inline-flex items-center justify-center gap-2 rounded-xl bg-white dark:bg-neutral-900 px-4 py-3 text-sm font-semibold text-primary-700 dark:text-primary-300 shadow-lg transition-transform active:scale-[0.98]">
|
||||
<svg
|
||||
className="w-4 h-4"
|
||||
|
||||
@@ -1,19 +1,15 @@
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import { useDeveloperMode } from '../../hooks/useDeveloperMode';
|
||||
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';
|
||||
import SettingsMenuItem from './components/SettingsMenuItem';
|
||||
import { useSettingsNavigation } from './hooks/useSettingsNavigation';
|
||||
|
||||
interface SettingsSection {
|
||||
label: string;
|
||||
items: SettingsItem[];
|
||||
}
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface SettingsItem {
|
||||
id: string;
|
||||
@@ -25,190 +21,313 @@ interface SettingsItem {
|
||||
rightElement?: ReactNode;
|
||||
}
|
||||
|
||||
interface SettingsGroup {
|
||||
/** Stable identifier for testing and key prop */
|
||||
id: string;
|
||||
/** i18n label shown above the card */
|
||||
label: string;
|
||||
items: SettingsItem[];
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Icon helpers (inline SVG kept as constants to avoid duplication)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const AccountIcon = (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const LanguageIcon = (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M3 5h12M9 3v2m1.048 9.5A18.022 18.022 0 016.412 9m6.088 9h7M11 21l5-10 5 10M12.751 5C11.783 10.77 8.07 15.61 3 18.129"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const AppearanceIcon = (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const DevicesIcon = (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const PersonalityIcon = (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const MascotIcon = (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 21a9 9 0 100-18 9 9 0 000 18zM9 10h.01M15 10h.01M9.5 15c.83.67 1.67 1 2.5 1s1.67-.33 2.5-1"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const PrivacyIcon = (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const NotificationsIcon = (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const DeveloperIcon = (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const AboutIcon = (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const DataSyncIcon = (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Group header (visual separator label above each settings card)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const GroupHeader = ({ label }: { label: string }) =>
|
||||
label ? (
|
||||
<div className="px-1 pt-5 pb-1">
|
||||
<span className="text-xs font-semibold uppercase tracking-wider text-stone-500 dark:text-neutral-400">
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
// Empty label → a plain divider (the doc places Developer & Diagnostics and
|
||||
// About after a divider, not under their own section headers).
|
||||
<div className="mx-1 mt-6 mb-2 border-t border-stone-200 dark:border-neutral-800" />
|
||||
);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main component
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const SettingsHome = () => {
|
||||
const { navigateToSettings } = useSettingsNavigation();
|
||||
const { t } = useT();
|
||||
const { snapshot } = useCoreState();
|
||||
const isLocalSession = isLocalSessionToken(snapshot.sessionToken);
|
||||
const developerMode = useDeveloperMode();
|
||||
|
||||
const settingsSections: SettingsSection[] = [
|
||||
{
|
||||
label: t('settings.general'),
|
||||
items: [
|
||||
{
|
||||
id: 'account',
|
||||
title: t('settings.account'),
|
||||
description: t('settings.accountDesc'),
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
onClick: () => navigateToSettings('account'),
|
||||
},
|
||||
// Alerts (inbox) + Notifications (preferences) now live together under
|
||||
// the Advanced → Notifications hub (see DeveloperOptionsPanel).
|
||||
{
|
||||
id: 'devices',
|
||||
title: 'Devices',
|
||||
description: 'Pair iOS phones with this OpenHuman',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
onClick: () => navigateToSettings('devices'),
|
||||
},
|
||||
{
|
||||
id: 'language',
|
||||
title: t('settings.language'),
|
||||
description: t('settings.languageDesc'),
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M3 5h12M9 3v2m1.048 9.5A18.022 18.022 0 016.412 9m6.088 9h7M11 21l5-10 5 10M12.751 5C11.783 10.77 8.07 15.61 3 18.129"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
rightElement: <LanguageSelect ariaLabel={t('settings.language')} />,
|
||||
},
|
||||
{
|
||||
id: 'appearance',
|
||||
title: t('settings.appearance.title'),
|
||||
description: t('settings.appearance.menuDesc'),
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
onClick: () => navigateToSettings('appearance'),
|
||||
},
|
||||
{
|
||||
id: 'agents-settings',
|
||||
title: t('settings.agentsSection.title'),
|
||||
description: t('settings.agentsSection.menuDesc'),
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 7h10a2 2 0 012 2v6a2 2 0 01-2 2H7a2 2 0 01-2-2V9a2 2 0 012-2zm2 4h.01M15 11h.01M9.5 15h5"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
onClick: () => navigateToSettings('agents-settings'),
|
||||
},
|
||||
{
|
||||
id: 'crypto',
|
||||
title: t('settings.cryptoSection.title'),
|
||||
description: t('settings.cryptoSection.menuDesc'),
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V6m0 10c-1.11 0-2.08-.402-2.599-1M12 16v2m0-12a9 9 0 100 18 9 9 0 000-18z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
onClick: () => navigateToSettings('crypto'),
|
||||
},
|
||||
{
|
||||
id: 'mascot',
|
||||
title: t('settings.mascot.menuTitle'),
|
||||
description: t('settings.mascot.menuDesc'),
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 21a9 9 0 100-18 9 9 0 000 18zM9 10h.01M15 10h.01M9.5 15c.83.67 1.67 1 2.5 1s1.67-.33 2.5-1"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
onClick: () => navigateToSettings('mascot'),
|
||||
},
|
||||
],
|
||||
},
|
||||
// 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.
|
||||
// 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: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H5a3 3 0 00-3 3v8a3 3 0 003 3z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
onClick: () => {
|
||||
openUrl(BILLING_DASHBOARD_URL).catch(() => {});
|
||||
},
|
||||
},
|
||||
],
|
||||
} satisfies SettingsSection,
|
||||
]
|
||||
: []),
|
||||
{
|
||||
label: t('settings.advanced'),
|
||||
items: [
|
||||
{
|
||||
id: 'developer-options',
|
||||
title: t('settings.developerOptions'),
|
||||
description: t('settings.developerOptionsDesc'),
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
onClick: () => navigateToSettings('developer-options'),
|
||||
},
|
||||
],
|
||||
},
|
||||
// --- 👤 Account group ---
|
||||
const accountGroup: SettingsGroup = {
|
||||
id: 'account',
|
||||
label: t('settings.groups.account'),
|
||||
items: [
|
||||
{
|
||||
// The Account row opens the account hub (recovery phrase, team,
|
||||
// connections, privacy, sign-out) — named after what it actually holds.
|
||||
id: 'profile',
|
||||
title: t('pages.settings.accountSection.title'),
|
||||
description: t('pages.settings.accountSection.description'),
|
||||
icon: AccountIcon,
|
||||
onClick: () => navigateToSettings('account'),
|
||||
},
|
||||
{
|
||||
id: 'language',
|
||||
title: t('settings.language'),
|
||||
description: t('settings.languageDesc'),
|
||||
icon: LanguageIcon,
|
||||
rightElement: <LanguageSelect ariaLabel={t('settings.language')} />,
|
||||
},
|
||||
{
|
||||
id: 'appearance',
|
||||
title: t('settings.appearance.title'),
|
||||
description: t('settings.appearance.menuDesc'),
|
||||
icon: AppearanceIcon,
|
||||
onClick: () => navigateToSettings('appearance'),
|
||||
},
|
||||
{
|
||||
id: 'devices',
|
||||
title: t('settings.account.devices'),
|
||||
description: t('settings.account.devicesDesc'),
|
||||
icon: DevicesIcon,
|
||||
onClick: () => navigateToSettings('devices'),
|
||||
},
|
||||
{
|
||||
id: 'data-sync',
|
||||
title: t('settings.dataSync.title'),
|
||||
description: t('settings.dataSync.menuDesc'),
|
||||
icon: DataSyncIcon,
|
||||
onClick: () => navigateToSettings('memory-sync'),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// --- 🤖 Assistant group ---
|
||||
const assistantGroup: SettingsGroup = {
|
||||
id: 'assistant',
|
||||
label: t('settings.groups.assistant'),
|
||||
items: [
|
||||
{
|
||||
id: 'persona',
|
||||
title: t('settings.assistant.personality'),
|
||||
description: t('settings.assistant.personalityDesc'),
|
||||
icon: PersonalityIcon,
|
||||
onClick: () => navigateToSettings('persona'),
|
||||
},
|
||||
{
|
||||
id: 'mascot',
|
||||
title: t('settings.assistant.faceMascot'),
|
||||
description: t('settings.assistant.faceMascotDesc'),
|
||||
icon: MascotIcon,
|
||||
onClick: () => navigateToSettings('mascot'),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// --- 🔒 Privacy group (Security + Approvals moved to Developer & Diagnostics) ---
|
||||
const privacySecurityGroup: SettingsGroup = {
|
||||
id: 'privacy-security',
|
||||
label: t('settings.privacySecurity.privacy'),
|
||||
items: [
|
||||
{
|
||||
id: 'privacy',
|
||||
title: t('settings.privacySecurity.privacy'),
|
||||
description: t('settings.privacySecurity.privacyDesc'),
|
||||
icon: PrivacyIcon,
|
||||
onClick: () => navigateToSettings('privacy'),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// --- 🔔 Notifications group ---
|
||||
const notificationsGroup: SettingsGroup = {
|
||||
id: 'notifications',
|
||||
label: t('settings.groups.notifications'),
|
||||
items: [
|
||||
{
|
||||
id: 'notifications-hub',
|
||||
title: t('settings.notifications.menuTitle'),
|
||||
description: t('settings.notifications.menuDesc'),
|
||||
icon: NotificationsIcon,
|
||||
onClick: () => navigateToSettings('notifications-hub'),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// --- ℹ️ About group (always visible; no section header — just a divider) ---
|
||||
const aboutGroup: SettingsGroup = {
|
||||
id: 'about',
|
||||
label: '',
|
||||
items: [
|
||||
{
|
||||
id: 'about',
|
||||
title: t('settings.about'),
|
||||
description: t('settings.aboutDesc'),
|
||||
icon: AboutIcon,
|
||||
onClick: () => navigateToSettings('about'),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// --- Always-visible groups ---
|
||||
const visibleGroups: SettingsGroup[] = [
|
||||
accountGroup,
|
||||
assistantGroup,
|
||||
privacySecurityGroup,
|
||||
notificationsGroup,
|
||||
];
|
||||
|
||||
// Log Out and Clear App Data now live on the Account page (Settings → Account)
|
||||
// alongside the recovery phrase, team, privacy, and migration entries.
|
||||
// Billing / Rewards / Wallet are NOT in Settings — per the design doc they
|
||||
// live in the avatar menu (monetisation out of the settings tree).
|
||||
|
||||
// --- Developer & Diagnostics (gated) ---
|
||||
// The Developer & Diagnostics entry is hidden when developer mode is off.
|
||||
// About is always accessible — that's where the toggle lives (chicken-and-egg).
|
||||
// No section header — it sits after a divider, then About (per the doc).
|
||||
const developerGroup: SettingsGroup | null = developerMode
|
||||
? {
|
||||
id: 'developer',
|
||||
label: '',
|
||||
items: [
|
||||
{
|
||||
id: 'developer-options',
|
||||
title: t('settings.developerDiagnostics'),
|
||||
description: t('settings.developerDiagnosticsDesc'),
|
||||
icon: DeveloperIcon,
|
||||
onClick: () => navigateToSettings('developer-options'),
|
||||
},
|
||||
],
|
||||
}
|
||||
: null;
|
||||
|
||||
// The layman groups (Account / Assistant / Privacy / Notifications) render as a
|
||||
// single flat card with no section subheadings. Developer & Diagnostics (when
|
||||
// on) and About sit after a divider, each in their own card.
|
||||
const laymanItems: SettingsItem[] = visibleGroups.flatMap(group => group.items);
|
||||
const trailingGroups: SettingsGroup[] = [...(developerGroup ? [developerGroup] : []), aboutGroup];
|
||||
|
||||
return (
|
||||
<div className="z-10 relative">
|
||||
@@ -216,12 +335,12 @@ const SettingsHome = () => {
|
||||
<SettingsHeader />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{/* Flat list — group titles removed for clarity. Destructive
|
||||
actions (Log Out, Clear App Data) now live on the Account page. */}
|
||||
{(() => {
|
||||
const flatItems = settingsSections.flatMap(s => s.items);
|
||||
return flatItems.map((item, index) => (
|
||||
<div className="px-4 pb-5">
|
||||
{/* Merged layman card — no Account/Assistant/… subheadings. */}
|
||||
<div
|
||||
data-testid="settings-group-main"
|
||||
className="rounded-3xl overflow-hidden border border-stone-200 dark:border-neutral-800">
|
||||
{laymanItems.map((item, index) => (
|
||||
<SettingsMenuItem
|
||||
key={item.id}
|
||||
icon={item.icon}
|
||||
@@ -231,11 +350,33 @@ const SettingsHome = () => {
|
||||
testId={`settings-nav-${item.id}`}
|
||||
dangerous={item.dangerous}
|
||||
isFirst={index === 0}
|
||||
isLast={index === flatItems.length - 1}
|
||||
isLast={index === laymanItems.length - 1}
|
||||
rightElement={item.rightElement}
|
||||
/>
|
||||
));
|
||||
})()}
|
||||
))}
|
||||
</div>
|
||||
|
||||
{trailingGroups.map(group => (
|
||||
<div key={group.id} data-testid={`settings-group-${group.id}`}>
|
||||
<GroupHeader label={group.label} />
|
||||
<div className="rounded-3xl overflow-hidden border border-stone-200 dark:border-neutral-800">
|
||||
{group.items.map((item, index) => (
|
||||
<SettingsMenuItem
|
||||
key={item.id}
|
||||
icon={item.icon}
|
||||
title={item.title}
|
||||
description={item.description}
|
||||
onClick={item.onClick}
|
||||
testId={`settings-nav-${item.id}`}
|
||||
dangerous={item.dangerous}
|
||||
isFirst={index === 0}
|
||||
isLast={index === group.items.length - 1}
|
||||
rightElement={item.rightElement}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -8,12 +8,34 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { I18nProvider } from '../../../lib/i18n/I18nContext';
|
||||
import type { Locale } from '../../../lib/i18n/types';
|
||||
import localeReducer from '../../../store/localeSlice';
|
||||
import themeReducer, {
|
||||
type AgentMessageViewMode,
|
||||
type FontSize,
|
||||
type TabBarLabels,
|
||||
type ThemeMode,
|
||||
} from '../../../store/themeSlice';
|
||||
import SettingsHome from '../SettingsHome';
|
||||
|
||||
function makeTestStore(locale: Locale = 'en') {
|
||||
// `useDeveloperMode` combines IS_DEV || developerMode. In tests IS_DEV is
|
||||
// true (Vite test mode), so mock the hook to control the gate explicitly.
|
||||
const devModeHoisted = vi.hoisted(() => ({ value: false }));
|
||||
vi.mock('../../../hooks/useDeveloperMode', () => ({
|
||||
useDeveloperMode: () => devModeHoisted.value,
|
||||
}));
|
||||
|
||||
function makeTestStore(locale: Locale = 'en', developerMode = false) {
|
||||
return configureStore({
|
||||
reducer: { locale: localeReducer },
|
||||
preloadedState: { locale: { current: locale } },
|
||||
reducer: { locale: localeReducer, theme: themeReducer },
|
||||
preloadedState: {
|
||||
locale: { current: locale },
|
||||
theme: {
|
||||
mode: 'system' as ThemeMode,
|
||||
tabBarLabels: 'hover' as TabBarLabels,
|
||||
fontSize: 'medium' as FontSize,
|
||||
agentMessageViewMode: 'bubbles' as AgentMessageViewMode,
|
||||
developerMode,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -59,7 +81,10 @@ vi.mock('../../walkthrough/AppWalkthrough', () => ({ resetWalkthrough: vi.fn() }
|
||||
|
||||
// --- helpers ---
|
||||
|
||||
function renderSettingsHome({ locale = 'en', withI18n = false } = {}) {
|
||||
function renderSettingsHome({ locale = 'en', withI18n = false, developerMode = false } = {}) {
|
||||
// Set the mocked hook value before rendering.
|
||||
devModeHoisted.value = developerMode;
|
||||
|
||||
const content = withI18n ? (
|
||||
<I18nProvider>
|
||||
<SettingsHome />
|
||||
@@ -69,7 +94,7 @@ function renderSettingsHome({ locale = 'en', withI18n = false } = {}) {
|
||||
);
|
||||
|
||||
return render(
|
||||
<Provider store={makeTestStore(locale as Locale)}>
|
||||
<Provider store={makeTestStore(locale as Locale, developerMode)}>
|
||||
<MemoryRouter>{content}</MemoryRouter>
|
||||
</Provider>
|
||||
);
|
||||
@@ -80,30 +105,84 @@ function renderSettingsHome({ locale = 'en', withI18n = false } = {}) {
|
||||
describe('SettingsHome', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
devModeHoisted.value = false;
|
||||
});
|
||||
|
||||
describe('flat menu', () => {
|
||||
// Section headers ("General", "Features & AI", "Billing & Rewards",
|
||||
// "Support", "Danger Zone") were intentionally removed — the menu is
|
||||
// now a single flat list to reduce visual noise.
|
||||
it.each(['General', 'Features & AI', 'Billing & Rewards', 'Support', 'Danger Zone'])(
|
||||
'does not render section header: %s',
|
||||
label => {
|
||||
renderSettingsHome();
|
||||
expect(screen.queryByText(label)).not.toBeInTheDocument();
|
||||
}
|
||||
);
|
||||
|
||||
it('renders the core menu items in a single list', () => {
|
||||
describe('layman groups structure', () => {
|
||||
it('renders the merged layman card and the About container', () => {
|
||||
renderSettingsHome();
|
||||
expect(screen.getByText('Account')).toBeInTheDocument();
|
||||
expect(screen.getByText('Billing & Usage')).toBeInTheDocument();
|
||||
expect(screen.getByText('Advanced')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('settings-nav-account')).toBeInTheDocument();
|
||||
// The layman groups (Account/Assistant/Privacy/Notifications) merge into a
|
||||
// single card with no subheadings; About keeps its own container.
|
||||
expect(screen.getByTestId('settings-group-main')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('settings-group-about')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('settings-group-account')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('settings-group-assistant')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('no longer renders Alerts / Notifications on the home screen', () => {
|
||||
// Both moved into the Advanced → Notifications hub.
|
||||
it('renders the Account group items', () => {
|
||||
renderSettingsHome();
|
||||
// Account group: Account (hub), Language, Appearance, Devices, Data Sync.
|
||||
// Team & members and Data & migration moved out (dev / removed).
|
||||
expect(screen.getByTestId('settings-nav-profile')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('settings-nav-language')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('settings-nav-appearance')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('settings-nav-devices')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('settings-nav-data-sync')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('settings-nav-team')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('settings-nav-migration')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the Assistant group items', () => {
|
||||
renderSettingsHome();
|
||||
// Only Personality + Face/Mascot stay layman-facing; the rest moved to
|
||||
// Developer & Diagnostics.
|
||||
expect(screen.getByTestId('settings-nav-persona')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('settings-nav-mascot')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('settings-nav-voice')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('settings-nav-permissions')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('settings-nav-activity-level')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('settings-nav-screen-intelligence')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('settings-nav-companion')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the Privacy group items', () => {
|
||||
renderSettingsHome();
|
||||
// Privacy stays; Security + Approvals moved to Developer & Diagnostics.
|
||||
expect(screen.getByTestId('settings-nav-privacy')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('settings-nav-security')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('settings-nav-approval-history')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the Notifications group item', () => {
|
||||
renderSettingsHome();
|
||||
expect(screen.getByTestId('settings-nav-notifications-hub')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the About item always (even without developer mode)', () => {
|
||||
renderSettingsHome({ developerMode: false });
|
||||
expect(screen.getByTestId('settings-nav-about')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('old flat section headers are not rendered', () => {
|
||||
// Section headers ("General", "Features & AI", "Billing & Rewards",
|
||||
// "Support", "Danger Zone") were removed in Phase 4.
|
||||
renderSettingsHome();
|
||||
expect(screen.queryByText('Features & AI')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Support')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Danger Zone')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('items no longer on the home screen', () => {
|
||||
it('no longer renders Agents / Crypto section pages on the home screen', () => {
|
||||
// These moved into the Developer & Diagnostics sub-tree (Agents & Autonomy).
|
||||
renderSettingsHome();
|
||||
expect(screen.queryByTestId('settings-nav-agents-settings')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('settings-nav-crypto')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('no longer renders Alerts / stand-alone Notifications on the home screen', () => {
|
||||
// Notifications now lives in its own Notifications group (notifications-hub).
|
||||
renderSettingsHome();
|
||||
expect(screen.queryByTestId('settings-nav-alerts')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('settings-nav-notifications')).not.toBeInTheDocument();
|
||||
@@ -116,78 +195,103 @@ describe('SettingsHome', () => {
|
||||
expect(screen.queryByText('Log out')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('localizes Appearance and Mascot menu items', () => {
|
||||
renderSettingsHome({ locale: 'zh-CN', withI18n: true });
|
||||
|
||||
expect(screen.getByText('外观')).toBeInTheDocument();
|
||||
expect(screen.getByText('选择浅色、深色或跟随系统主题')).toBeInTheDocument();
|
||||
expect(screen.getByText('吉祥物')).toBeInTheDocument();
|
||||
expect(screen.getByText('选择应用内使用的吉祥物颜色')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('no longer renders Features / AI / Rewards / Restart Tour / About on the home screen', () => {
|
||||
it('no longer renders Features / AI Configuration / Rewards / Restart Tour on the home screen', () => {
|
||||
renderSettingsHome();
|
||||
expect(screen.queryByText('Features')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('AI Configuration')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Rewards')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Restart Tour')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('About')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('language selector', () => {
|
||||
it('offers Bahasa Indonesia as a display language', () => {
|
||||
renderSettingsHome();
|
||||
|
||||
expect(screen.getByRole('option', { name: /Bahasa Indonesia/ })).toHaveValue('id');
|
||||
});
|
||||
});
|
||||
|
||||
describe('existing navigation items', () => {
|
||||
it('navigates to account settings when Account is clicked', async () => {
|
||||
describe('navigation — layman groups', () => {
|
||||
it('navigates to account settings when Profile is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderSettingsHome();
|
||||
|
||||
await user.click(screen.getByText('Account').closest('button')!);
|
||||
await user.click(screen.getByTestId('settings-nav-profile'));
|
||||
expect(mockNavigateToSettings).toHaveBeenCalledWith('account');
|
||||
});
|
||||
|
||||
it('navigates to the Agents section when Agents is clicked', async () => {
|
||||
it('navigates to persona when Personality is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderSettingsHome();
|
||||
renderSettingsHome({ withI18n: true });
|
||||
|
||||
// Persona, Agent OS access, etc. now live under the Agents section page.
|
||||
await user.click(screen.getByText('Agents').closest('button')!);
|
||||
expect(mockNavigateToSettings).toHaveBeenCalledWith('agents-settings');
|
||||
await user.click(screen.getByTestId('settings-nav-persona'));
|
||||
expect(mockNavigateToSettings).toHaveBeenCalledWith('persona');
|
||||
});
|
||||
|
||||
it('navigates to the Crypto section when Crypto is clicked', async () => {
|
||||
it('navigates to mascot when Face / Mascot is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderSettingsHome();
|
||||
|
||||
// Recovery phrase + wallet balances now live under the Crypto section page.
|
||||
await user.click(screen.getByText('Crypto').closest('button')!);
|
||||
expect(mockNavigateToSettings).toHaveBeenCalledWith('crypto');
|
||||
await user.click(screen.getByTestId('settings-nav-mascot'));
|
||||
expect(mockNavigateToSettings).toHaveBeenCalledWith('mascot');
|
||||
});
|
||||
|
||||
it('opens billing URL when Billing & Usage is clicked', async () => {
|
||||
const { openUrl } = await import('../../../utils/openUrl');
|
||||
it('navigates to privacy when Privacy is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderSettingsHome();
|
||||
|
||||
await user.click(screen.getByText('Billing & Usage').closest('button')!);
|
||||
expect(openUrl).toHaveBeenCalledWith('https://billing.example.com');
|
||||
await user.click(screen.getByTestId('settings-nav-privacy'));
|
||||
expect(mockNavigateToSettings).toHaveBeenCalledWith('privacy');
|
||||
});
|
||||
|
||||
it('navigates to developer-options when Advanced is clicked', async () => {
|
||||
it('navigates to notifications-hub when Notifications is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderSettingsHome();
|
||||
|
||||
await user.click(screen.getByText('Advanced').closest('button')!);
|
||||
await user.click(screen.getByTestId('settings-nav-notifications-hub'));
|
||||
expect(mockNavigateToSettings).toHaveBeenCalledWith('notifications-hub');
|
||||
});
|
||||
|
||||
it('navigates to about when About is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderSettingsHome();
|
||||
|
||||
await user.click(screen.getByTestId('settings-nav-about'));
|
||||
expect(mockNavigateToSettings).toHaveBeenCalledWith('about');
|
||||
});
|
||||
|
||||
it('does not render Billing & Usage in Settings (billing is in avatar menu)', () => {
|
||||
// Per the IA redesign doc, billing/rewards live in the avatar menu — not in Settings.
|
||||
renderSettingsHome();
|
||||
expect(screen.queryByTestId('settings-nav-billing')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Billing & Usage')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('navigates to developer-options when "Developer & Diagnostics" is clicked (developerMode=true)', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderSettingsHome({ developerMode: true });
|
||||
|
||||
await user.click(screen.getByTestId('settings-nav-developer-options'));
|
||||
expect(mockNavigateToSettings).toHaveBeenCalledWith('developer-options');
|
||||
});
|
||||
});
|
||||
|
||||
describe('developer mode gate', () => {
|
||||
it('hides the developer-options entry when developerMode is off', () => {
|
||||
renderSettingsHome({ developerMode: false });
|
||||
expect(screen.queryByTestId('settings-nav-developer-options')).not.toBeInTheDocument();
|
||||
// The English resolved text should also be absent
|
||||
expect(screen.queryByText('Developer & Diagnostics')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows the developer-options entry when developerMode is on', () => {
|
||||
renderSettingsHome({ developerMode: true });
|
||||
expect(screen.getByTestId('settings-nav-developer-options')).toBeInTheDocument();
|
||||
// useT() resolves to English even without I18nProvider
|
||||
expect(screen.getByText('Developer & Diagnostics')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('local session gating', () => {
|
||||
beforeEach(() => {
|
||||
// Use a valid local-session token (three parts, last part = 'local')
|
||||
@@ -198,17 +302,29 @@ describe('SettingsHome', () => {
|
||||
mockSessionToken = null;
|
||||
});
|
||||
|
||||
it('hides the Billing & Usage item in local mode', () => {
|
||||
it('does not render Billing & Usage in Settings regardless of session type (billing is in avatar menu)', () => {
|
||||
// Billing moved to avatar menu per IA redesign — never shown in Settings.
|
||||
renderSettingsHome();
|
||||
expect(screen.queryByText('Billing & Usage')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows "Billing & Usage" when not in local mode', () => {
|
||||
it('does not render Billing & Usage in Settings even when not in local mode', () => {
|
||||
// Billing moved to avatar menu per IA redesign — never shown in Settings.
|
||||
mockSessionToken = null;
|
||||
renderSettingsHome();
|
||||
expect(screen.getByText('Billing & Usage')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Billing & Usage')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('i18n — Chinese locale', () => {
|
||||
it('localizes Appearance and Mascot menu items', () => {
|
||||
renderSettingsHome({ locale: 'zh-CN', withI18n: true });
|
||||
|
||||
expect(screen.getByText('外观')).toBeInTheDocument();
|
||||
expect(screen.getByText('选择浅色、深色或跟随系统主题')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
// Clear App Data flow moved to LogoutAndClearActions (rendered on Account
|
||||
// page) — see LogoutAndClearActions.test.tsx.
|
||||
});
|
||||
|
||||
@@ -53,7 +53,7 @@ const SettingsMenuItem = ({
|
||||
type="button"
|
||||
data-testid={testId}
|
||||
onClick={onClick}
|
||||
className={`w-full flex items-center justify-between py-3 px-4 bg-white dark:bg-neutral-900 text-stone-900 dark:text-neutral-100 ${borderClasses} hover:bg-stone-50 dark:hover:bg-neutral-800/60 dark:bg-neutral-800/60 dark:hover:bg-neutral-800/60 transition-all duration-200 text-left ${roundedClasses} focus:outline-none focus:ring-0 focus:border-inherit`}>
|
||||
className={`w-full flex items-center justify-between py-3 px-4 bg-stone-50 dark:bg-neutral-900/40 text-stone-900 dark:text-neutral-100 ${borderClasses} hover:bg-stone-100 dark:hover:bg-neutral-800/60 transition-all duration-200 text-left ${roundedClasses} focus:outline-none focus:ring-0 focus:border-inherit`}>
|
||||
{content}
|
||||
</button>
|
||||
);
|
||||
@@ -62,7 +62,7 @@ const SettingsMenuItem = ({
|
||||
return (
|
||||
<div
|
||||
data-testid={testId}
|
||||
className={`w-full flex items-center justify-between py-3 px-4 bg-white dark:bg-neutral-900 text-stone-900 dark:text-neutral-100 ${borderClasses} ${roundedClasses}`}>
|
||||
className={`w-full flex items-center justify-between py-3 px-4 bg-stone-50 dark:bg-neutral-900/40 text-stone-900 dark:text-neutral-100 ${borderClasses} ${roundedClasses}`}>
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -24,6 +24,7 @@ export type SettingsRoute =
|
||||
| 'voice'
|
||||
| 'tools'
|
||||
| 'memory-data'
|
||||
| 'memory-sync'
|
||||
| 'memory-debug'
|
||||
| 'crypto'
|
||||
| 'recovery-phrase'
|
||||
@@ -49,7 +50,9 @@ export type SettingsRoute =
|
||||
| 'mcp-server'
|
||||
| 'dev-workflow'
|
||||
| 'sandbox-settings'
|
||||
| 'devices';
|
||||
| 'permissions'
|
||||
| 'devices'
|
||||
| 'heartbeat';
|
||||
|
||||
export interface BreadcrumbItem {
|
||||
label: string;
|
||||
@@ -110,6 +113,7 @@ export const useSettingsNavigation = (): SettingsNavigationHook => {
|
||||
if (path.includes('/settings/voice-debug')) return 'voice-debug';
|
||||
if (path.includes('/settings/voice')) return 'voice';
|
||||
if (path.includes('/settings/tools')) return 'tools';
|
||||
if (path.includes('/settings/memory-sync')) return 'memory-sync';
|
||||
if (path.includes('/settings/memory-data')) return 'memory-data';
|
||||
if (path.includes('/settings/memory-debug')) return 'memory-debug';
|
||||
if (path.includes('/settings/webhooks-debug')) return 'webhooks-debug';
|
||||
@@ -141,10 +145,12 @@ export const useSettingsNavigation = (): SettingsNavigationHook => {
|
||||
// shorter `agents` (the manage-agents registry panel) so it isn't swallowed.
|
||||
if (path.includes('/settings/agents-settings')) return 'agents-settings';
|
||||
if (path.includes('/settings/sandbox-settings')) return 'sandbox-settings';
|
||||
if (path.includes('/settings/permissions')) return 'permissions';
|
||||
if (path.includes('/settings/agent-access')) return 'agent-access';
|
||||
if (path.includes('/settings/agents')) return 'agents';
|
||||
if (path.includes('/settings/mcp-server')) return 'mcp-server';
|
||||
if (path.includes('/settings/dev-workflow')) return 'dev-workflow';
|
||||
if (path.includes('/settings/heartbeat')) return 'heartbeat';
|
||||
return 'home';
|
||||
};
|
||||
|
||||
@@ -286,6 +292,7 @@ export const useSettingsNavigation = (): SettingsNavigationHook => {
|
||||
case 'notification-routing':
|
||||
case 'mcp-server':
|
||||
case 'dev-workflow':
|
||||
case 'heartbeat':
|
||||
case 'notifications-hub': // Notifications hub section page lives under Advanced.
|
||||
return [settingsCrumb, developerCrumb];
|
||||
|
||||
@@ -301,6 +308,14 @@ export const useSettingsNavigation = (): SettingsNavigationHook => {
|
||||
case 'devices':
|
||||
return [settingsCrumb];
|
||||
|
||||
// Data Sync is a top-level leaf in the Account group (#3301).
|
||||
case 'memory-sync':
|
||||
return [settingsCrumb];
|
||||
|
||||
// Permissions panel lives at the top level of Settings (Assistant group).
|
||||
case 'permissions':
|
||||
return [settingsCrumb];
|
||||
|
||||
// Mascot appearance panel sits at the top level of Settings.
|
||||
case 'mascot':
|
||||
return [settingsCrumb];
|
||||
|
||||
@@ -10,8 +10,10 @@ import { invoke } from '@tauri-apps/api/core';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useAppUpdate } from '../../../hooks/useAppUpdate';
|
||||
import { useDeveloperMode } from '../../../hooks/useDeveloperMode';
|
||||
import { useT } from '../../../lib/i18n/I18nContext';
|
||||
import { useAppSelector } from '../../../store/hooks';
|
||||
import { useAppDispatch, useAppSelector } from '../../../store/hooks';
|
||||
import { selectDeveloperMode, setDeveloperMode } from '../../../store/themeSlice';
|
||||
import { APP_VERSION, LATEST_APP_DOWNLOAD_URL } from '../../../utils/config';
|
||||
import { isTauriEnvironment } from '../../../utils/configPersistence';
|
||||
import { openUrl } from '../../../utils/openUrl';
|
||||
@@ -20,6 +22,7 @@ import { useSettingsNavigation } from '../hooks/useSettingsNavigation';
|
||||
|
||||
const AboutPanel = () => {
|
||||
const { t } = useT();
|
||||
const dispatch = useAppDispatch();
|
||||
const { navigateBack, breadcrumbs } = useSettingsNavigation();
|
||||
// The auto-cadence is already running via the global <AppUpdatePrompt />;
|
||||
// disable it here so opening the panel doesn't double-trigger probes.
|
||||
@@ -27,6 +30,12 @@ const AboutPanel = () => {
|
||||
const [lastCheckedAt, setLastCheckedAt] = useState<Date | null>(null);
|
||||
const coreMode = useAppSelector(state => state.coreMode.mode);
|
||||
const [rpcUrl, setRpcUrl] = useState<string | null>(null);
|
||||
// Persisted developer mode preference (not the combined IS_DEV || developerMode).
|
||||
// We read the raw preference here so the toggle reflects only the user's choice,
|
||||
// not whether the build is a dev build.
|
||||
const developerModePref = useAppSelector(selectDeveloperMode);
|
||||
// Combined gate — true when IS_DEV or the pref is on. Used for the helper text.
|
||||
const developerModeActive = useDeveloperMode();
|
||||
|
||||
// Local mode picks a dynamic port at app launch, so the authoritative
|
||||
// value lives in the Tauri shell (`core_rpc_url` command) rather than the
|
||||
@@ -148,6 +157,43 @@ const AboutPanel = () => {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Developer Mode toggle — always visible so users can enable it
|
||||
without needing it to be on first (chicken-and-egg avoidance). */}
|
||||
<div
|
||||
className="rounded-xl border border-stone-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 p-4"
|
||||
data-testid="developer-mode-section">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm font-medium text-stone-900 dark:text-neutral-100">
|
||||
{t('settings.developerMode.title')}
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-stone-500 dark:text-neutral-400 leading-relaxed">
|
||||
{developerModeActive && !developerModePref
|
||||
? t('settings.developerMode.enabledByBuild')
|
||||
: t('settings.developerMode.description')}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={developerModePref}
|
||||
aria-label={t('settings.developerMode.title')}
|
||||
onClick={() => {
|
||||
console.debug('[developer-mode] toggled to', !developerModePref);
|
||||
dispatch(setDeveloperMode(!developerModePref));
|
||||
}}
|
||||
className={`relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 ${
|
||||
developerModePref ? 'bg-primary-600' : 'bg-stone-300 dark:bg-neutral-600'
|
||||
}`}>
|
||||
<span
|
||||
className={`pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ${
|
||||
developerModePref ? 'translate-x-5' : 'translate-x-0'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-stone-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 p-4">
|
||||
<div className="text-sm font-medium text-stone-900 dark:text-neutral-100">
|
||||
{t('settings.about.releases')}
|
||||
|
||||
@@ -2,13 +2,10 @@ import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { useT } from '../../../lib/i18n/I18nContext';
|
||||
import {
|
||||
type AgentPaths,
|
||||
type AutonomyLevel,
|
||||
isTauri,
|
||||
openhumanGetAgentPaths,
|
||||
openhumanGetAgentSettings,
|
||||
openhumanGetAutonomySettings,
|
||||
openhumanUpdateAgentPaths,
|
||||
openhumanUpdateAgentSettings,
|
||||
openhumanUpdateAutonomySettings,
|
||||
type TrustedAccess,
|
||||
@@ -18,41 +15,19 @@ import SettingsHeader from '../components/SettingsHeader';
|
||||
import { useSettingsNavigation } from '../hooks/useSettingsNavigation';
|
||||
|
||||
// Installs are always *available* but never silent: every `install_tool` call
|
||||
// is routed through the approval gate, so the user is asked to Approve/Deny each
|
||||
// install in chat. There is therefore no per-user "disable installs" knob here —
|
||||
// the consent is captured per-install by the gate, not by a static config flag.
|
||||
// is routed through the approval gate, so the user is asked to Approve/Deny
|
||||
// each install in chat. There is therefore no per-user "disable installs" knob
|
||||
// here — the consent is captured per-install by the gate, not by a static
|
||||
// config flag.
|
||||
const ALLOW_TOOL_INSTALL = true;
|
||||
|
||||
interface PresetOption {
|
||||
id: AutonomyLevel;
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const AgentAccessPanel = () => {
|
||||
const { t } = useT();
|
||||
const { navigateBack, navigateToSettings, breadcrumbs } = useSettingsNavigation();
|
||||
|
||||
// Tier presets — built inside the component so titles/descriptions resolve
|
||||
// through `t()` (i18n). Order matters: it's the display order.
|
||||
const presets: PresetOption[] = [
|
||||
{
|
||||
id: 'readonly',
|
||||
title: t('settings.agentAccess.tier.readonly.title'),
|
||||
description: t('settings.agentAccess.tier.readonly.desc'),
|
||||
},
|
||||
{
|
||||
id: 'supervised',
|
||||
title: t('settings.agentAccess.tier.supervised.title'),
|
||||
description: t('settings.agentAccess.tier.supervised.desc'),
|
||||
},
|
||||
{
|
||||
id: 'full',
|
||||
title: t('settings.agentAccess.tier.full.title'),
|
||||
description: t('settings.agentAccess.tier.full.desc'),
|
||||
},
|
||||
];
|
||||
|
||||
// Load `level` so we can carry it through when writing other fields, but
|
||||
// the tier-selection UI lives in PermissionsPanel. Never render tier radios
|
||||
// here — that would create two sources of truth.
|
||||
const [level, setLevel] = useState<AutonomyLevel>('supervised');
|
||||
const [workspaceOnly, setWorkspaceOnly] = useState(false);
|
||||
const [requireTaskPlanApproval, setRequireTaskPlanApproval] = useState(true);
|
||||
@@ -80,15 +55,6 @@ const AgentAccessPanel = () => {
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [savedNote, setSavedNote] = useState<string | null>(null);
|
||||
// Live agent filesystem roots fetched from the core. `null` while the
|
||||
// RPC is pending or when not running under Tauri — the JSX falls back to
|
||||
// the documented defaults so the section never renders empty.
|
||||
const [agentPaths, setAgentPaths] = useState<AgentPaths | null>(null);
|
||||
const [actionDirEditing, setActionDirEditing] = useState(false);
|
||||
const [actionDirInput, setActionDirInput] = useState('');
|
||||
const [actionDirError, setActionDirError] = useState<string | null>(null);
|
||||
const [actionDirSaved, setActionDirSaved] = useState<string | null>(null);
|
||||
const [actionDirSaving, setActionDirSaving] = useState(false);
|
||||
// Monotonic guard so out-of-order auto-save responses can't clobber UI state
|
||||
// with a stale result (last write wins).
|
||||
const persistSeqRef = useRef(0);
|
||||
@@ -123,15 +89,6 @@ const AgentAccessPanel = () => {
|
||||
} catch {
|
||||
// Non-fatal: autonomy controls still render; timeout section
|
||||
// stays at defaults and the user can try saving manually.
|
||||
}
|
||||
try {
|
||||
const pathsResp = await openhumanGetAgentPaths();
|
||||
if (cancelled) return;
|
||||
setAgentPaths(pathsResp.result);
|
||||
setActionDirInput(pathsResp.result.action_dir);
|
||||
} catch {
|
||||
// Non-fatal: the Directories section falls back to the documented
|
||||
// defaults below. We don't gate the rest of the panel on this.
|
||||
} finally {
|
||||
if (!cancelled) setIsLoading(false);
|
||||
}
|
||||
@@ -144,16 +101,16 @@ const AgentAccessPanel = () => {
|
||||
}, []);
|
||||
|
||||
// Auto-apply: every change persists immediately (no separate Save button).
|
||||
// `allow_tool_install` is fixed; tier, workspace_only and granted folders
|
||||
// vary. Pass explicit `next` values (setState is async).
|
||||
// `allow_tool_install` is fixed; workspace_only, trusted_roots vary.
|
||||
// `level` is carried through from state (its UI lives in PermissionsPanel).
|
||||
// Pass explicit `next` values (setState is async).
|
||||
const persist = async (next: {
|
||||
level: AutonomyLevel;
|
||||
workspaceOnly: boolean;
|
||||
requireTaskPlanApproval: boolean;
|
||||
trustedRoots: TrustedRoot[];
|
||||
// Only sent when the allowlist itself is being changed. Omitting it leaves
|
||||
// the server's `auto_approve` untouched (partial patch) — important so a
|
||||
// tier/folder change here can't clobber a tool the user just added via the
|
||||
// tier/folder change can't clobber a tool the user just added via the
|
||||
// in-chat "Always allow" button.
|
||||
autoApprove?: string[];
|
||||
}) => {
|
||||
@@ -164,7 +121,7 @@ const AgentAccessPanel = () => {
|
||||
setIsSaving(true);
|
||||
try {
|
||||
await openhumanUpdateAutonomySettings({
|
||||
level: next.level,
|
||||
level,
|
||||
workspace_only: next.workspaceOnly,
|
||||
trusted_roots: next.trustedRoots,
|
||||
allow_tool_install: ALLOW_TOOL_INSTALL,
|
||||
@@ -186,52 +143,14 @@ const AgentAccessPanel = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// True when the env var pins action_dir — the input must be disabled.
|
||||
const actionDirEnvLocked = agentPaths?.action_dir_source === 'env';
|
||||
|
||||
const startEditActionDir = () => {
|
||||
setActionDirInput(agentPaths?.action_dir ?? '');
|
||||
setActionDirError(null);
|
||||
setActionDirSaved(null);
|
||||
setActionDirEditing(true);
|
||||
};
|
||||
|
||||
const cancelEditActionDir = () => {
|
||||
setActionDirEditing(false);
|
||||
setActionDirError(null);
|
||||
setActionDirInput('');
|
||||
};
|
||||
|
||||
const saveActionDir = async () => {
|
||||
if (!isTauri()) return;
|
||||
setActionDirSaving(true);
|
||||
setActionDirError(null);
|
||||
setActionDirSaved(null);
|
||||
try {
|
||||
const resp = await openhumanUpdateAgentPaths({ action_dir: actionDirInput.trim() });
|
||||
setAgentPaths(resp.result);
|
||||
setActionDirEditing(false);
|
||||
setActionDirSaved(t('settings.agentAccess.actionDir.saved'));
|
||||
} catch (e) {
|
||||
setActionDirError(e instanceof Error ? e.message : t('settings.agentAccess.saveError'));
|
||||
} finally {
|
||||
setActionDirSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const selectTier = (next: AutonomyLevel) => {
|
||||
setLevel(next);
|
||||
void persist({ level: next, workspaceOnly, requireTaskPlanApproval, trustedRoots });
|
||||
};
|
||||
|
||||
const toggleWorkspaceOnly = (next: boolean) => {
|
||||
setWorkspaceOnly(next);
|
||||
void persist({ level, workspaceOnly: next, requireTaskPlanApproval, trustedRoots });
|
||||
void persist({ workspaceOnly: next, requireTaskPlanApproval, trustedRoots });
|
||||
};
|
||||
|
||||
const toggleTaskPlanApproval = (next: boolean) => {
|
||||
setRequireTaskPlanApproval(next);
|
||||
void persist({ level, workspaceOnly, requireTaskPlanApproval: next, trustedRoots });
|
||||
void persist({ workspaceOnly, requireTaskPlanApproval: next, trustedRoots });
|
||||
};
|
||||
|
||||
const addRoot = () => {
|
||||
@@ -245,25 +164,19 @@ const AgentAccessPanel = () => {
|
||||
setTrustedRoots(nextRoots);
|
||||
setNewRootPath('');
|
||||
setNewRootAccess('read');
|
||||
void persist({ level, workspaceOnly, requireTaskPlanApproval, trustedRoots: nextRoots });
|
||||
void persist({ workspaceOnly, requireTaskPlanApproval, trustedRoots: nextRoots });
|
||||
};
|
||||
|
||||
const removeRoot = (path: string) => {
|
||||
const nextRoots = trustedRoots.filter(r => r.path !== path);
|
||||
setTrustedRoots(nextRoots);
|
||||
void persist({ level, workspaceOnly, requireTaskPlanApproval, trustedRoots: nextRoots });
|
||||
void persist({ workspaceOnly, requireTaskPlanApproval, trustedRoots: nextRoots });
|
||||
};
|
||||
|
||||
const removeAutoApprove = (tool: string) => {
|
||||
const nextList = autoApprove.filter(name => name !== tool);
|
||||
setAutoApprove(nextList);
|
||||
void persist({
|
||||
level,
|
||||
workspaceOnly,
|
||||
requireTaskPlanApproval,
|
||||
trustedRoots,
|
||||
autoApprove: nextList,
|
||||
});
|
||||
void persist({ workspaceOnly, requireTaskPlanApproval, trustedRoots, autoApprove: nextList });
|
||||
};
|
||||
|
||||
// Persist the action timeout on blur / Enter. Validates the integer range
|
||||
@@ -328,163 +241,8 @@ const AgentAccessPanel = () => {
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
<section className="space-y-2">
|
||||
<h2 className="text-sm font-semibold text-stone-900 dark:text-neutral-100">
|
||||
{t('settings.agentAccess.accessMode')}
|
||||
</h2>
|
||||
<div className="grid gap-2">
|
||||
{presets.map(p => (
|
||||
<button
|
||||
key={p.id}
|
||||
type="button"
|
||||
onClick={() => selectTier(p.id)}
|
||||
className={`text-left rounded-lg border p-3 transition ${
|
||||
level === p.id
|
||||
? 'border-primary-500 bg-primary-50 dark:bg-primary-500/10'
|
||||
: 'border-stone-200 dark:border-neutral-800 hover:border-primary-300 dark:hover:border-primary-500'
|
||||
}`}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className={`inline-block w-3 h-3 rounded-full border ${
|
||||
level === p.id
|
||||
? 'bg-primary-500 border-primary-500'
|
||||
: 'border-stone-300 dark:border-neutral-700'
|
||||
}`}
|
||||
/>
|
||||
<span className="font-medium text-stone-900 dark:text-neutral-100">
|
||||
{p.title}
|
||||
</span>
|
||||
{p.id === 'supervised' && (
|
||||
<span className="text-xs text-stone-600 dark:text-neutral-400">
|
||||
{t('settings.agentAccess.defaultTag')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-stone-600 dark:text-neutral-400">
|
||||
{p.description}
|
||||
</p>
|
||||
</button>
|
||||
))}
|
||||
{level === 'full' && (
|
||||
<p className="rounded border border-coral/40 bg-coral/5 dark:bg-coral/10 p-2 text-xs text-coral-600 dark:text-coral-300">
|
||||
{t('settings.agentAccess.fullWarning')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Directory model — action sandbox vs internal state. */}
|
||||
<section className="space-y-2">
|
||||
<h2 className="text-sm font-semibold text-stone-900 dark:text-neutral-100">
|
||||
{t('settings.agentAccess.directories')}
|
||||
</h2>
|
||||
<div className="rounded-lg border border-stone-200 dark:border-neutral-800 divide-y divide-stone-200 dark:divide-neutral-800">
|
||||
<div className="px-3 py-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="inline-block w-2 h-2 rounded-full bg-sage-500" />
|
||||
<span className="text-xs font-medium text-stone-900 dark:text-neutral-100">
|
||||
{t('settings.agentAccess.actionSandbox')}
|
||||
</span>
|
||||
<span className="text-xs text-sage-600 dark:text-sage-400">
|
||||
{t('settings.agentAccess.readWriteAccess')}
|
||||
</span>
|
||||
</div>
|
||||
{actionDirEditing ? (
|
||||
<div className="mt-1 space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
className="flex-1 rounded border border-stone-300 dark:border-neutral-700 bg-white dark:bg-neutral-900 px-2 py-1 text-xs font-mono text-stone-900 dark:text-neutral-100"
|
||||
value={actionDirInput}
|
||||
onChange={e => setActionDirInput(e.target.value)}
|
||||
placeholder={t('settings.agentAccess.actionDir.placeholder')}
|
||||
disabled={actionDirSaving}
|
||||
data-testid="agent-access-action-dir-input"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="rounded bg-ocean px-2 py-1 text-xs font-medium text-white disabled:opacity-50"
|
||||
onClick={() => void saveActionDir()}
|
||||
disabled={actionDirSaving}
|
||||
data-testid="agent-access-action-dir-save">
|
||||
{t('settings.agentAccess.actionDir.save')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="rounded border border-stone-300 dark:border-neutral-700 px-2 py-1 text-xs font-medium text-stone-700 dark:text-neutral-300 disabled:opacity-50"
|
||||
onClick={cancelEditActionDir}
|
||||
disabled={actionDirSaving}
|
||||
data-testid="agent-access-action-dir-cancel">
|
||||
{t('settings.agentAccess.actionDir.cancel')}
|
||||
</button>
|
||||
</div>
|
||||
{actionDirError && (
|
||||
<p
|
||||
className="text-xs text-coral-600 dark:text-coral-400"
|
||||
data-testid="agent-access-action-dir-error">
|
||||
{actionDirError}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-0.5 flex items-center gap-2">
|
||||
<p
|
||||
className="text-xs text-stone-600 dark:text-neutral-400 font-mono"
|
||||
data-testid="agent-access-action-dir">
|
||||
{agentPaths?.action_dir ?? '~/OpenHuman/projects'}
|
||||
</p>
|
||||
{!actionDirEnvLocked && (
|
||||
<button
|
||||
type="button"
|
||||
className="text-xs font-medium text-ocean hover:underline"
|
||||
onClick={startEditActionDir}
|
||||
data-testid="agent-access-action-dir-edit">
|
||||
{t('settings.agentAccess.actionDir.edit')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{actionDirEnvLocked && (
|
||||
<p
|
||||
className="text-xs text-amber-600 dark:text-amber-400"
|
||||
data-testid="agent-access-action-dir-env-locked">
|
||||
{t('settings.agentAccess.actionDir.envLocked')}
|
||||
</p>
|
||||
)}
|
||||
{actionDirSaved && !actionDirEditing && (
|
||||
<p
|
||||
className="text-xs text-sage-600 dark:text-sage-400"
|
||||
data-testid="agent-access-action-dir-saved">
|
||||
{actionDirSaved}
|
||||
</p>
|
||||
)}
|
||||
<p className="text-xs text-stone-500 dark:text-neutral-500">
|
||||
{t('settings.agentAccess.actionSandboxDesc')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="px-3 py-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="inline-block w-2 h-2 rounded-full bg-coral-500" />
|
||||
<span className="text-xs font-medium text-stone-900 dark:text-neutral-100">
|
||||
{t('settings.agentAccess.internalState')}
|
||||
</span>
|
||||
<span className="text-xs text-coral-600 dark:text-coral-400">
|
||||
{t('settings.agentAccess.agentBlocked')}
|
||||
</span>
|
||||
</div>
|
||||
<p
|
||||
className="mt-0.5 text-xs text-stone-600 dark:text-neutral-400 font-mono"
|
||||
data-testid="agent-access-workspace-dir">
|
||||
{agentPaths?.workspace_dir ?? '~/.openhuman/workspace'}
|
||||
</p>
|
||||
<p className="text-xs text-stone-500 dark:text-neutral-500">
|
||||
{t('settings.agentAccess.internalStateDesc')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Workspace confinement — orthogonal to the tier; applies in all modes. */}
|
||||
{/* Workspace confinement — orthogonal to the tier; applies in all
|
||||
modes. Tier selection moved to PermissionsPanel. */}
|
||||
<section className="space-y-1">
|
||||
<label className="flex items-start gap-2 cursor-pointer">
|
||||
<input
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { useT } from '../../../lib/i18n/I18nContext';
|
||||
import ConnectionPathTab from '../../intelligence/ConnectionPathTab';
|
||||
import DiagramViewerTab from '../../intelligence/DiagramViewerTab';
|
||||
import EntityAssociationsTab from '../../intelligence/EntityAssociationsTab';
|
||||
import GraphCentralityTab from '../../intelligence/GraphCentralityTab';
|
||||
import GraphCohesionTab from '../../intelligence/GraphCohesionTab';
|
||||
import MemoryFreshnessTab from '../../intelligence/MemoryFreshnessTab';
|
||||
import MemoryTimelineTab from '../../intelligence/MemoryTimelineTab';
|
||||
import NamespaceOverviewTab from '../../intelligence/NamespaceOverviewTab';
|
||||
import PillTabBar from '../../PillTabBar';
|
||||
import SettingsHeader from '../components/SettingsHeader';
|
||||
import { useSettingsNavigation } from '../hooks/useSettingsNavigation';
|
||||
|
||||
/**
|
||||
* Analysis views — the 8 parked memory-graph analysis surfaces.
|
||||
*
|
||||
* These were stripped from the layman Memory tab (#3397) but retained on disk
|
||||
* (and still unit-tested). The IA redesign re-surfaces them here, behind the
|
||||
* Developer & Diagnostics door → Knowledge & Memory → "Analysis views", so power
|
||||
* users can still reach them while laymen never see them.
|
||||
*/
|
||||
type AnalysisView =
|
||||
| 'diagram'
|
||||
| 'centrality'
|
||||
| 'cohesion'
|
||||
| 'associations'
|
||||
| 'freshness'
|
||||
| 'timeline'
|
||||
| 'paths'
|
||||
| 'namespaces';
|
||||
|
||||
const AnalysisViewsPanel = () => {
|
||||
const { t } = useT();
|
||||
const { navigateBack, breadcrumbs } = useSettingsNavigation();
|
||||
const [activeView, setActiveView] = useState<AnalysisView>('diagram');
|
||||
|
||||
const views: { id: AnalysisView; label: string }[] = [
|
||||
{ id: 'diagram', label: t('memory.tab.diagram') },
|
||||
{ id: 'centrality', label: t('memory.tab.centrality') },
|
||||
{ id: 'cohesion', label: t('memory.tab.cohesion') },
|
||||
{ id: 'associations', label: t('memory.tab.associations') },
|
||||
{ id: 'freshness', label: t('memory.tab.freshness') },
|
||||
{ id: 'timeline', label: t('memory.tab.timeline') },
|
||||
{ id: 'paths', label: t('memory.tab.path') },
|
||||
{ id: 'namespaces', label: t('memory.tab.namespaces') },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="z-10 relative">
|
||||
<SettingsHeader
|
||||
title={t('settings.analysisViews.title')}
|
||||
showBackButton={true}
|
||||
onBack={navigateBack}
|
||||
breadcrumbs={breadcrumbs}
|
||||
/>
|
||||
<div className="p-4 space-y-4">
|
||||
<PillTabBar
|
||||
items={views.map(view => ({ label: view.label, value: view.id }))}
|
||||
selected={activeView}
|
||||
onChange={setActiveView}
|
||||
containerClassName="flex flex-wrap gap-2 pb-1"
|
||||
/>
|
||||
|
||||
{activeView === 'diagram' && <DiagramViewerTab />}
|
||||
{activeView === 'centrality' && <GraphCentralityTab />}
|
||||
{activeView === 'cohesion' && <GraphCohesionTab />}
|
||||
{activeView === 'associations' && <EntityAssociationsTab />}
|
||||
{activeView === 'freshness' && <MemoryFreshnessTab />}
|
||||
{activeView === 'timeline' && <MemoryTimelineTab />}
|
||||
{activeView === 'paths' && <ConnectionPathTab />}
|
||||
{activeView === 'namespaces' && <NamespaceOverviewTab />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AnalysisViewsPanel;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,62 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { useT } from '../../../lib/i18n/I18nContext';
|
||||
import type { ToastNotification } from '../../../types/intelligence';
|
||||
import { MemorySourcesRegistry } from '../../intelligence/MemorySourcesRegistry';
|
||||
import { SyncAuditPanel } from '../../intelligence/SyncAuditPanel';
|
||||
import { ToastContainer } from '../../intelligence/Toast';
|
||||
import SettingsHeader from '../components/SettingsHeader';
|
||||
import { useSettingsNavigation } from '../hooks/useSettingsNavigation';
|
||||
|
||||
/**
|
||||
* Data Sync — top-level Settings → Account page (#3301).
|
||||
*
|
||||
* The single, focused home for "what syncs, how much, how fresh, and is it
|
||||
* running right now". Shows the source registry (per-source rows, opt-in
|
||||
* toggles, live "syncing now" indicator, per-source settings, and the global
|
||||
* sync-schedule control) plus the Sync History audit table.
|
||||
*
|
||||
* The "Memory Tree" status tiles / per-integration health and the
|
||||
* force-directed memory graph deliberately stay on the developer Memory page
|
||||
* (Dev & Diagnostics → Memory), not here.
|
||||
*/
|
||||
const MemorySyncPanel = () => {
|
||||
const { t } = useT();
|
||||
const { navigateBack, breadcrumbs } = useSettingsNavigation();
|
||||
const [toasts, setToasts] = useState<ToastNotification[]>([]);
|
||||
|
||||
const addToast = useCallback((toast: Omit<ToastNotification, 'id'>) => {
|
||||
const newToast: ToastNotification = { ...toast, id: `toast-${Date.now()}-${Math.random()}` };
|
||||
setToasts(prev => [...prev, newToast]);
|
||||
}, []);
|
||||
|
||||
const removeToast = (id: string) => {
|
||||
setToasts(prev => prev.filter(toast => toast.id !== id));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="z-10 relative">
|
||||
<SettingsHeader
|
||||
title={t('settings.dataSync.title')}
|
||||
showBackButton={true}
|
||||
onBack={navigateBack}
|
||||
breadcrumbs={breadcrumbs}
|
||||
/>
|
||||
<div className="p-4 space-y-4">
|
||||
<p className="text-sm text-stone-600 dark:text-neutral-300">
|
||||
{t('settings.dataSync.description')}
|
||||
</p>
|
||||
<MemorySourcesRegistry onToast={addToast} />
|
||||
<div className="rounded-lg border border-stone-200 bg-white p-4 dark:border-neutral-800 dark:bg-neutral-900">
|
||||
<h3 className="mb-2 text-sm font-semibold text-stone-700 dark:text-neutral-200">
|
||||
{t('sync.auditTitle', 'Sync History')}
|
||||
</h3>
|
||||
<SyncAuditPanel />
|
||||
</div>
|
||||
</div>
|
||||
<ToastContainer notifications={toasts} onRemove={removeToast} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MemorySyncPanel;
|
||||
@@ -0,0 +1,373 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { useT } from '../../../lib/i18n/I18nContext';
|
||||
import {
|
||||
type AgentPaths,
|
||||
type AutonomyLevel,
|
||||
isTauri,
|
||||
openhumanGetAgentPaths,
|
||||
openhumanGetAutonomySettings,
|
||||
openhumanUpdateAgentPaths,
|
||||
openhumanUpdateAutonomySettings,
|
||||
} from '../../../utils/tauriCommands';
|
||||
import SettingsHeader from '../components/SettingsHeader';
|
||||
import { useSettingsNavigation } from '../hooks/useSettingsNavigation';
|
||||
|
||||
// Installs are always *available* but never silent: every `install_tool` call
|
||||
// is routed through the approval gate, so the user is asked to Approve/Deny
|
||||
// each install in chat. There is no per-user "disable installs" knob here —
|
||||
// the consent is captured per-install by the gate, not by a static config flag.
|
||||
const ALLOW_TOOL_INSTALL = true;
|
||||
|
||||
interface PresetOption {
|
||||
id: AutonomyLevel;
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const PermissionsPanel = () => {
|
||||
const { t } = useT();
|
||||
const { navigateBack, breadcrumbs } = useSettingsNavigation();
|
||||
|
||||
// Tier presets — built inside the component so titles/descriptions resolve
|
||||
// through `t()` (i18n). Order matters: it's the display order.
|
||||
const presets: PresetOption[] = [
|
||||
{
|
||||
id: 'readonly',
|
||||
title: t('settings.permissions.preset.readonly.title'),
|
||||
description: t('settings.permissions.preset.readonly.desc'),
|
||||
},
|
||||
{
|
||||
id: 'supervised',
|
||||
title: t('settings.permissions.preset.supervised.title'),
|
||||
description: t('settings.permissions.preset.supervised.desc'),
|
||||
},
|
||||
{
|
||||
id: 'full',
|
||||
title: t('settings.permissions.preset.full.title'),
|
||||
description: t('settings.permissions.preset.full.desc'),
|
||||
},
|
||||
];
|
||||
|
||||
const [level, setLevel] = useState<AutonomyLevel>('supervised');
|
||||
// We need to carry workspace_only and trusted_roots when saving tier changes
|
||||
// so we don't overwrite them with defaults. Load them but don't expose UI for
|
||||
// them (they live in the advanced panel).
|
||||
const [workspaceOnly, setWorkspaceOnly] = useState(false);
|
||||
const [requireTaskPlanApproval, setRequireTaskPlanApproval] = useState(true);
|
||||
const [trustedRoots, setTrustedRoots] = useState<
|
||||
Array<{ path: string; access: 'read' | 'readwrite' }>
|
||||
>([]);
|
||||
|
||||
const [agentPaths, setAgentPaths] = useState<AgentPaths | null>(null);
|
||||
const [actionDirEditing, setActionDirEditing] = useState(false);
|
||||
const [actionDirInput, setActionDirInput] = useState('');
|
||||
const [actionDirError, setActionDirError] = useState<string | null>(null);
|
||||
const [actionDirSaved, setActionDirSaved] = useState<string | null>(null);
|
||||
const [actionDirSaving, setActionDirSaving] = useState(false);
|
||||
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [savedNote, setSavedNote] = useState<string | null>(null);
|
||||
|
||||
// Monotonic guards so out-of-order async responses don't clobber UI state.
|
||||
const persistSeqRef = useRef(0);
|
||||
const dirSeqRef = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
const load = async () => {
|
||||
if (!isTauri()) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const autonomyResp = await openhumanGetAutonomySettings();
|
||||
if (cancelled) return;
|
||||
setLevel(autonomyResp.result.level);
|
||||
setWorkspaceOnly(autonomyResp.result.workspace_only);
|
||||
setRequireTaskPlanApproval(autonomyResp.result.require_task_plan_approval ?? true);
|
||||
setTrustedRoots(autonomyResp.result.trusted_roots ?? []);
|
||||
} catch (e) {
|
||||
if (!cancelled)
|
||||
setError(e instanceof Error ? e.message : t('settings.agentAccess.loadError'));
|
||||
}
|
||||
try {
|
||||
const pathsResp = await openhumanGetAgentPaths();
|
||||
if (cancelled) return;
|
||||
setAgentPaths(pathsResp.result);
|
||||
setActionDirInput(pathsResp.result.action_dir);
|
||||
} catch {
|
||||
// Non-fatal: folder section falls back to documented defaults.
|
||||
} finally {
|
||||
if (!cancelled) setIsLoading(false);
|
||||
}
|
||||
};
|
||||
void load();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
// Persist tier change. Carries the other autonomy fields through unchanged
|
||||
// so we don't accidentally clobber what the advanced panel may have set.
|
||||
const persistTier = async (nextLevel: AutonomyLevel) => {
|
||||
const seq = ++persistSeqRef.current;
|
||||
if (!isTauri()) return;
|
||||
setError(null);
|
||||
setSavedNote(null);
|
||||
setIsSaving(true);
|
||||
try {
|
||||
await openhumanUpdateAutonomySettings({
|
||||
level: nextLevel,
|
||||
workspace_only: workspaceOnly,
|
||||
trusted_roots: trustedRoots,
|
||||
allow_tool_install: ALLOW_TOOL_INSTALL,
|
||||
require_task_plan_approval: requireTaskPlanApproval,
|
||||
});
|
||||
if (persistSeqRef.current === seq) {
|
||||
setSavedNote(t('settings.agentAccess.saved'));
|
||||
}
|
||||
} catch (e) {
|
||||
if (persistSeqRef.current === seq) {
|
||||
setError(e instanceof Error ? e.message : t('settings.agentAccess.saveError'));
|
||||
}
|
||||
} finally {
|
||||
if (persistSeqRef.current === seq) {
|
||||
setIsSaving(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const selectTier = (next: AutonomyLevel) => {
|
||||
setLevel(next);
|
||||
void persistTier(next);
|
||||
};
|
||||
|
||||
// True when the env var pins action_dir — the edit button must be hidden.
|
||||
const actionDirEnvLocked = agentPaths?.action_dir_source === 'env';
|
||||
|
||||
const startEditActionDir = () => {
|
||||
setActionDirInput(agentPaths?.action_dir ?? '');
|
||||
setActionDirError(null);
|
||||
setActionDirSaved(null);
|
||||
setActionDirEditing(true);
|
||||
};
|
||||
|
||||
const cancelEditActionDir = () => {
|
||||
setActionDirEditing(false);
|
||||
setActionDirError(null);
|
||||
setActionDirInput('');
|
||||
};
|
||||
|
||||
const saveActionDir = async () => {
|
||||
if (!isTauri()) return;
|
||||
const seq = ++dirSeqRef.current;
|
||||
setActionDirSaving(true);
|
||||
setActionDirError(null);
|
||||
setActionDirSaved(null);
|
||||
try {
|
||||
const resp = await openhumanUpdateAgentPaths({ action_dir: actionDirInput.trim() });
|
||||
if (dirSeqRef.current === seq) {
|
||||
setAgentPaths(resp.result);
|
||||
setActionDirEditing(false);
|
||||
setActionDirSaved(t('settings.agentAccess.actionDir.saved'));
|
||||
}
|
||||
} catch (e) {
|
||||
if (dirSeqRef.current === seq) {
|
||||
setActionDirError(e instanceof Error ? e.message : t('settings.agentAccess.saveError'));
|
||||
}
|
||||
} finally {
|
||||
if (dirSeqRef.current === seq) {
|
||||
setActionDirSaving(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SettingsHeader
|
||||
title={t('settings.permissions.title')}
|
||||
showBackButton
|
||||
onBack={navigateBack}
|
||||
breadcrumbs={breadcrumbs}
|
||||
/>
|
||||
|
||||
<div className="p-4 space-y-6">
|
||||
{!isTauri() && (
|
||||
<p className="text-sm text-coral-600 dark:text-coral-300">
|
||||
{t('settings.agentAccess.desktopOnly')}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{isLoading ? (
|
||||
<p className="text-sm text-stone-600 dark:text-neutral-400">
|
||||
{t('settings.agentAccess.loading')}
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
{/* Access mode presets — layman-friendly labels */}
|
||||
<section className="space-y-2">
|
||||
<h2 className="text-sm font-semibold text-stone-900 dark:text-neutral-100">
|
||||
{t('settings.permissions.accessMode')}
|
||||
</h2>
|
||||
<p className="text-xs text-stone-600 dark:text-neutral-400">
|
||||
{t('settings.permissions.accessModeDesc')}
|
||||
</p>
|
||||
<div className="grid gap-2">
|
||||
{presets.map(p => (
|
||||
<button
|
||||
key={p.id}
|
||||
type="button"
|
||||
onClick={() => selectTier(p.id)}
|
||||
data-testid={`permissions-preset-${p.id}`}
|
||||
className={`text-left rounded-lg border p-3 transition ${
|
||||
level === p.id
|
||||
? 'border-primary-500 bg-primary-50 dark:bg-primary-500/10'
|
||||
: 'border-stone-200 dark:border-neutral-800 hover:border-primary-300 dark:hover:border-primary-500'
|
||||
}`}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className={`inline-block w-3 h-3 rounded-full border ${
|
||||
level === p.id
|
||||
? 'bg-primary-500 border-primary-500'
|
||||
: 'border-stone-300 dark:border-neutral-700'
|
||||
}`}
|
||||
/>
|
||||
<span className="font-medium text-stone-900 dark:text-neutral-100">
|
||||
{p.title}
|
||||
</span>
|
||||
{p.id === 'supervised' && (
|
||||
<span className="text-xs text-stone-600 dark:text-neutral-400">
|
||||
{t('settings.agentAccess.defaultTag')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-stone-600 dark:text-neutral-400">
|
||||
{p.description}
|
||||
</p>
|
||||
</button>
|
||||
))}
|
||||
{level === 'full' && (
|
||||
<p className="rounded border border-coral/40 bg-coral/5 dark:bg-coral/10 p-2 text-xs text-coral-600 dark:text-coral-300">
|
||||
{t('settings.agentAccess.fullWarning')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Folders the assistant can use */}
|
||||
<section className="space-y-2">
|
||||
<h2 className="text-sm font-semibold text-stone-900 dark:text-neutral-100">
|
||||
{t('settings.permissions.folders')}
|
||||
</h2>
|
||||
<p className="text-xs text-stone-600 dark:text-neutral-400">
|
||||
{t('settings.permissions.foldersDesc')}
|
||||
</p>
|
||||
<div className="rounded-lg border border-stone-200 dark:border-neutral-800 px-3 py-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="inline-block w-2 h-2 rounded-full bg-sage-500" />
|
||||
<span className="text-xs font-medium text-stone-900 dark:text-neutral-100">
|
||||
{t('settings.agentAccess.actionSandbox')}
|
||||
</span>
|
||||
<span className="text-xs text-sage-600 dark:text-sage-400">
|
||||
{t('settings.agentAccess.readWriteAccess')}
|
||||
</span>
|
||||
</div>
|
||||
{actionDirEditing ? (
|
||||
<div className="mt-1 space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
className="flex-1 rounded border border-stone-300 dark:border-neutral-700 bg-white dark:bg-neutral-900 px-2 py-1 text-xs font-mono text-stone-900 dark:text-neutral-100"
|
||||
value={actionDirInput}
|
||||
onChange={e => setActionDirInput(e.target.value)}
|
||||
placeholder={t('settings.agentAccess.actionDir.placeholder')}
|
||||
disabled={actionDirSaving}
|
||||
data-testid="permissions-action-dir-input"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="rounded bg-ocean px-2 py-1 text-xs font-medium text-white disabled:opacity-50"
|
||||
onClick={() => void saveActionDir()}
|
||||
disabled={actionDirSaving}
|
||||
data-testid="permissions-action-dir-save">
|
||||
{t('settings.agentAccess.actionDir.save')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="rounded border border-stone-300 dark:border-neutral-700 px-2 py-1 text-xs font-medium text-stone-700 dark:text-neutral-300 disabled:opacity-50"
|
||||
onClick={cancelEditActionDir}
|
||||
disabled={actionDirSaving}
|
||||
data-testid="permissions-action-dir-cancel">
|
||||
{t('settings.agentAccess.actionDir.cancel')}
|
||||
</button>
|
||||
</div>
|
||||
{actionDirError && (
|
||||
<p
|
||||
className="text-xs text-coral-600 dark:text-coral-400"
|
||||
data-testid="permissions-action-dir-error">
|
||||
{actionDirError}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-0.5 flex items-center gap-2">
|
||||
<p
|
||||
className="text-xs text-stone-600 dark:text-neutral-400 font-mono"
|
||||
data-testid="permissions-action-dir">
|
||||
{agentPaths?.action_dir ?? '~/OpenHuman/projects'}
|
||||
</p>
|
||||
{!actionDirEnvLocked && (
|
||||
<button
|
||||
type="button"
|
||||
className="text-xs font-medium text-ocean hover:underline"
|
||||
onClick={startEditActionDir}
|
||||
data-testid="permissions-action-dir-edit">
|
||||
{t('settings.agentAccess.actionDir.edit')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{actionDirEnvLocked && (
|
||||
<p
|
||||
className="text-xs text-amber-600 dark:text-amber-400"
|
||||
data-testid="permissions-action-dir-env-locked">
|
||||
{t('settings.agentAccess.actionDir.envLocked')}
|
||||
</p>
|
||||
)}
|
||||
{actionDirSaved && !actionDirEditing && (
|
||||
<p className="text-xs text-sage-600 dark:text-sage-400">{actionDirSaved}</p>
|
||||
)}
|
||||
<p className="text-xs text-stone-500 dark:text-neutral-500 mt-0.5">
|
||||
{t('settings.agentAccess.actionSandboxDesc')}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Auto-save status */}
|
||||
<div className="min-h-[1.25rem] text-sm" aria-live="polite">
|
||||
{error ? (
|
||||
<span className="text-coral-600 dark:text-coral-300">{error}</span>
|
||||
) : isSaving ? (
|
||||
<span className="text-stone-600 dark:text-neutral-400">
|
||||
{t('settings.agentAccess.saving')}
|
||||
</span>
|
||||
) : savedNote ? (
|
||||
<span className="text-sage-700 dark:text-sage-300">✓ {savedNote}</span>
|
||||
) : (
|
||||
<span className="text-stone-600 dark:text-neutral-400">
|
||||
{t('settings.agentAccess.changesApply')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PermissionsPanel;
|
||||
@@ -151,3 +151,54 @@ describe('AboutPanel', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('AboutPanel — Developer Mode toggle', () => {
|
||||
beforeEach(() => {
|
||||
statusListeners.length = 0;
|
||||
mockCheckAppUpdate.mockReset();
|
||||
hoisted.mockIsTauri.mockReturnValue(false); // Simplify — no Tauri IPC for these tests
|
||||
});
|
||||
|
||||
// useT() falls back to English even without I18nProvider (resolveEn() lookup).
|
||||
// The toggle's aria-label is set to t('settings.developerMode.title') which
|
||||
// resolves to the English string "Developer mode".
|
||||
const TOGGLE_ARIA_LABEL = /Developer mode/i;
|
||||
|
||||
it('renders the Developer mode switch section', () => {
|
||||
renderWithProviders(<AboutPanel />);
|
||||
expect(screen.getByRole('switch', { name: TOGGLE_ARIA_LABEL })).toBeInTheDocument();
|
||||
expect(screen.getByTestId('developer-mode-section')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('switch defaults to off (aria-checked=false)', () => {
|
||||
renderWithProviders(<AboutPanel />);
|
||||
const toggle = screen.getByRole('switch', { name: TOGGLE_ARIA_LABEL });
|
||||
expect(toggle).toHaveAttribute('aria-checked', 'false');
|
||||
});
|
||||
|
||||
it('turns on developer mode when toggle is clicked', () => {
|
||||
const { store } = renderWithProviders(<AboutPanel />);
|
||||
const toggle = screen.getByRole('switch', { name: TOGGLE_ARIA_LABEL });
|
||||
fireEvent.click(toggle);
|
||||
expect(store.getState().theme.developerMode).toBe(true);
|
||||
expect(toggle).toHaveAttribute('aria-checked', 'true');
|
||||
});
|
||||
|
||||
it('toggles developer mode off when clicked again', () => {
|
||||
const { store } = renderWithProviders(<AboutPanel />, {
|
||||
preloadedState: {
|
||||
theme: {
|
||||
mode: 'system',
|
||||
tabBarLabels: 'hover',
|
||||
fontSize: 'medium',
|
||||
agentMessageViewMode: 'bubbles',
|
||||
developerMode: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
const toggle = screen.getByRole('switch', { name: TOGGLE_ARIA_LABEL });
|
||||
expect(toggle).toHaveAttribute('aria-checked', 'true');
|
||||
fireEvent.click(toggle);
|
||||
expect(store.getState().theme.developerMode).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,19 +3,24 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { renderWithProviders } from '../../../../test/test-utils';
|
||||
import {
|
||||
type AgentPaths,
|
||||
type AgentSettings,
|
||||
type AutonomySettings,
|
||||
isTauri,
|
||||
openhumanGetAgentPaths,
|
||||
openhumanGetAgentSettings,
|
||||
openhumanGetAutonomySettings,
|
||||
openhumanUpdateAgentPaths,
|
||||
openhumanUpdateAgentSettings,
|
||||
openhumanUpdateAutonomySettings,
|
||||
} from '../../../../utils/tauriCommands';
|
||||
import AgentAccessPanel from '../AgentAccessPanel';
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
// Note: Tier-selection and action-dir editing tests live in
|
||||
// PermissionsPanel.test.tsx (those controls moved to the layman panel).
|
||||
// This file covers the ADVANCED surface: workspace confinement, task-plan
|
||||
// approval, action timeout, granted folders, always-allowed tools, and the
|
||||
// approval-history link.
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
const autonomy = (overrides: Partial<AutonomySettings> = {}): AutonomySettings => ({
|
||||
level: 'supervised',
|
||||
workspace_only: false,
|
||||
@@ -37,14 +42,6 @@ const agentSettings = (overrides: Partial<AgentSettings> = {}): AgentSettings =>
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const agentPaths = (overrides: Partial<AgentPaths> = {}): AgentPaths => ({
|
||||
action_dir: '/home/test/OpenHuman/projects',
|
||||
workspace_dir: '/home/test/.openhuman/users/u1/workspace',
|
||||
projects_dir: '/home/test/OpenHuman/projects',
|
||||
action_dir_source: 'default',
|
||||
...overrides,
|
||||
});
|
||||
|
||||
vi.mock('../../hooks/useSettingsNavigation', () => ({
|
||||
useSettingsNavigation: () => ({
|
||||
navigateBack: vi.fn(),
|
||||
@@ -64,8 +61,8 @@ vi.mock('../../../../utils/tauriCommands', async () => {
|
||||
openhumanUpdateAutonomySettings: vi.fn(),
|
||||
openhumanGetAgentSettings: vi.fn(),
|
||||
openhumanUpdateAgentSettings: vi.fn(),
|
||||
openhumanGetAgentPaths: vi.fn(),
|
||||
openhumanUpdateAgentPaths: vi.fn(),
|
||||
// The advanced panel no longer calls the agent-paths RPCs (action-dir
|
||||
// moved to PermissionsPanel) — no mock needed, but keep the import clean.
|
||||
};
|
||||
});
|
||||
|
||||
@@ -73,10 +70,8 @@ const mockGet = vi.mocked(openhumanGetAutonomySettings);
|
||||
const mockUpdate = vi.mocked(openhumanUpdateAutonomySettings);
|
||||
const mockGetAgent = vi.mocked(openhumanGetAgentSettings);
|
||||
const mockUpdateAgent = vi.mocked(openhumanUpdateAgentSettings);
|
||||
const mockGetAgentPaths = vi.mocked(openhumanGetAgentPaths);
|
||||
const mockUpdateAgentPaths = vi.mocked(openhumanUpdateAgentPaths);
|
||||
|
||||
describe('AgentAccessPanel', () => {
|
||||
describe('AgentAccessPanel (advanced)', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(isTauri).mockReturnValue(true);
|
||||
@@ -84,31 +79,24 @@ describe('AgentAccessPanel', () => {
|
||||
mockUpdate.mockResolvedValue({ result: {} as never, logs: [] });
|
||||
mockGetAgent.mockResolvedValue({ result: agentSettings(), logs: [] });
|
||||
mockUpdateAgent.mockResolvedValue({ result: {} as never, logs: [] });
|
||||
mockGetAgentPaths.mockResolvedValue({ result: agentPaths(), logs: [] });
|
||||
mockUpdateAgentPaths.mockResolvedValue({ result: agentPaths(), logs: [] });
|
||||
});
|
||||
|
||||
it('loads settings on mount and renders the three access tiers', async () => {
|
||||
it('loads settings on mount and renders the advanced controls', async () => {
|
||||
renderWithProviders(<AgentAccessPanel />);
|
||||
await waitFor(() => expect(mockGet).toHaveBeenCalledTimes(1));
|
||||
expect(await screen.findByText('Read-only')).toBeInTheDocument();
|
||||
expect(screen.getByText('Ask before edit')).toBeInTheDocument();
|
||||
expect(screen.getByText('Full access')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('selecting the Full tier persists the new level (and renders the warning)', async () => {
|
||||
renderWithProviders(<AgentAccessPanel />);
|
||||
fireEvent.click(await screen.findByText('Full access'));
|
||||
await waitFor(() =>
|
||||
expect(mockUpdate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ level: 'full', allow_tool_install: true })
|
||||
)
|
||||
);
|
||||
// The tier radio buttons no longer live in this panel.
|
||||
expect(screen.queryByText('Read-only')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Ask before edit')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Full access')).not.toBeInTheDocument();
|
||||
// The advanced controls are present.
|
||||
expect(await screen.findByText('Confine to workspace')).toBeInTheDocument();
|
||||
expect(screen.getByText('Granted folders')).toBeInTheDocument();
|
||||
expect(screen.getByText('Always-allowed tools')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('toggling "confine to workspace" persists workspace_only', async () => {
|
||||
renderWithProviders(<AgentAccessPanel />);
|
||||
await screen.findByText('Read-only');
|
||||
await screen.findByText('Confine to workspace');
|
||||
fireEvent.click(screen.getByRole('checkbox', { name: /confine to workspace/i }));
|
||||
await waitFor(() =>
|
||||
expect(mockUpdate).toHaveBeenCalledWith(expect.objectContaining({ workspace_only: true }))
|
||||
@@ -117,7 +105,7 @@ describe('AgentAccessPanel', () => {
|
||||
|
||||
it('toggling task plan approval persists require_task_plan_approval', async () => {
|
||||
renderWithProviders(<AgentAccessPanel />);
|
||||
await screen.findByText('Read-only');
|
||||
await screen.findByText('Confine to workspace');
|
||||
fireEvent.click(screen.getByRole('checkbox', { name: /require task plan approval/i }));
|
||||
await waitFor(() =>
|
||||
expect(mockUpdate).toHaveBeenCalledWith(
|
||||
@@ -146,7 +134,7 @@ describe('AgentAccessPanel', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('renders the loaded tier from settings and pre-existing granted folders', async () => {
|
||||
it('renders the loaded tier and pre-existing granted folders (loaded but not shown)', async () => {
|
||||
mockGet.mockResolvedValue({
|
||||
result: autonomy({
|
||||
level: 'readonly',
|
||||
@@ -156,10 +144,14 @@ describe('AgentAccessPanel', () => {
|
||||
logs: [],
|
||||
});
|
||||
renderWithProviders(<AgentAccessPanel />);
|
||||
// Folder path is visible.
|
||||
expect(await screen.findByText('/home/u/notes')).toBeInTheDocument();
|
||||
// workspace_only checkbox is checked.
|
||||
expect(
|
||||
(screen.getByRole('checkbox', { name: /confine to workspace/i }) as HTMLInputElement).checked
|
||||
).toBe(true);
|
||||
// But the tier radio UI is NOT here (lives in PermissionsPanel).
|
||||
expect(screen.queryByText('Read-only')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows the empty "always-allow" state when no tools are allow-listed', async () => {
|
||||
@@ -177,8 +169,7 @@ describe('AgentAccessPanel', () => {
|
||||
expect(screen.getByText('curl')).toBeInTheDocument();
|
||||
|
||||
// trusted_roots is empty, so the only Remove buttons belong to the
|
||||
// allowlist. Removing the first entry persists the trimmed list via
|
||||
// update_autonomy_settings (auto_approve only — other fields untouched).
|
||||
// allowlist. Removing the first entry persists the trimmed list.
|
||||
fireEvent.click(screen.getAllByText('Remove')[0]);
|
||||
await waitFor(() =>
|
||||
expect(mockUpdate).toHaveBeenLastCalledWith(
|
||||
@@ -196,7 +187,9 @@ describe('AgentAccessPanel', () => {
|
||||
it('shows the desktop-only notice and skips loading off-Tauri', async () => {
|
||||
vi.mocked(isTauri).mockReturnValue(false);
|
||||
renderWithProviders(<AgentAccessPanel />);
|
||||
expect(await screen.findByText('Access mode')).toBeInTheDocument();
|
||||
expect(
|
||||
await screen.findByText('Access settings are only available in the desktop app.')
|
||||
).toBeInTheDocument();
|
||||
expect(mockGet).not.toHaveBeenCalled();
|
||||
expect(mockGetAgent).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -243,120 +236,4 @@ describe('AgentAccessPanel', () => {
|
||||
expect(input.disabled).toBe(true);
|
||||
expect(screen.getByText(/OPENHUMAN_TOOL_TIMEOUT_SECS/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// ── Directories section (#3237) ───────────────────────────────────────────
|
||||
|
||||
it('renders the live action_dir and workspace_dir returned by the core', async () => {
|
||||
mockGetAgentPaths.mockResolvedValue({
|
||||
result: agentPaths({
|
||||
action_dir: '/Users/sample/OpenHuman/projects',
|
||||
workspace_dir: '/Users/sample/.openhuman/users/u1/workspace',
|
||||
}),
|
||||
logs: [],
|
||||
});
|
||||
renderWithProviders(<AgentAccessPanel />);
|
||||
await waitFor(() => expect(mockGetAgentPaths).toHaveBeenCalledTimes(1));
|
||||
expect(await screen.findByTestId('agent-access-action-dir')).toHaveTextContent(
|
||||
'/Users/sample/OpenHuman/projects'
|
||||
);
|
||||
expect(screen.getByTestId('agent-access-workspace-dir')).toHaveTextContent(
|
||||
'/Users/sample/.openhuman/users/u1/workspace'
|
||||
);
|
||||
});
|
||||
|
||||
it('reflects an OPENHUMAN_ACTION_DIR override in the action sandbox row', async () => {
|
||||
// When the operator sets OPENHUMAN_ACTION_DIR, the core's get_agent_paths
|
||||
// returns the override value as `action_dir`. The panel must render that
|
||||
// verbatim instead of the hard-coded `~/OpenHuman/projects` default —
|
||||
// otherwise Settings actively misleads about where the agent writes.
|
||||
mockGetAgentPaths.mockResolvedValue({
|
||||
result: agentPaths({
|
||||
action_dir: '/tmp/custom-actions',
|
||||
projects_dir: '/Users/sample/OpenHuman/projects',
|
||||
}),
|
||||
logs: [],
|
||||
});
|
||||
renderWithProviders(<AgentAccessPanel />);
|
||||
expect(await screen.findByTestId('agent-access-action-dir')).toHaveTextContent(
|
||||
'/tmp/custom-actions'
|
||||
);
|
||||
});
|
||||
|
||||
it('falls back to documented defaults when the agent paths RPC fails', async () => {
|
||||
// Non-fatal failure path: the rest of the panel must still render and
|
||||
// the Directories rows must show the documented default strings so the
|
||||
// section never appears empty.
|
||||
mockGetAgentPaths.mockRejectedValue(new Error('rpc unavailable'));
|
||||
renderWithProviders(<AgentAccessPanel />);
|
||||
expect(await screen.findByTestId('agent-access-action-dir')).toHaveTextContent(
|
||||
'~/OpenHuman/projects'
|
||||
);
|
||||
expect(screen.getByTestId('agent-access-workspace-dir')).toHaveTextContent(
|
||||
'~/.openhuman/workspace'
|
||||
);
|
||||
});
|
||||
|
||||
it('shows an Edit affordance for action_dir when the source is not env', async () => {
|
||||
mockGetAgentPaths.mockResolvedValue({
|
||||
result: agentPaths({ action_dir: '/Users/sample/projects', action_dir_source: 'default' }),
|
||||
logs: [],
|
||||
});
|
||||
renderWithProviders(<AgentAccessPanel />);
|
||||
expect(await screen.findByTestId('agent-access-action-dir-edit')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('agent-access-action-dir-env-locked')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('saving a new action_dir calls openhumanUpdateAgentPaths and updates the display', async () => {
|
||||
mockGetAgentPaths.mockResolvedValue({
|
||||
result: agentPaths({ action_dir: '/Users/sample/old', action_dir_source: 'default' }),
|
||||
logs: [],
|
||||
});
|
||||
mockUpdateAgentPaths.mockResolvedValue({
|
||||
result: agentPaths({ action_dir: '/Users/sample/new', action_dir_source: 'override' }),
|
||||
logs: [],
|
||||
});
|
||||
renderWithProviders(<AgentAccessPanel />);
|
||||
|
||||
fireEvent.click(await screen.findByTestId('agent-access-action-dir-edit'));
|
||||
const input = await screen.findByTestId('agent-access-action-dir-input');
|
||||
fireEvent.change(input, { target: { value: '/Users/sample/new' } });
|
||||
fireEvent.click(screen.getByTestId('agent-access-action-dir-save'));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockUpdateAgentPaths).toHaveBeenCalledWith({ action_dir: '/Users/sample/new' })
|
||||
);
|
||||
expect(await screen.findByTestId('agent-access-action-dir')).toHaveTextContent(
|
||||
'/Users/sample/new'
|
||||
);
|
||||
});
|
||||
|
||||
it('renders a backend validation error inline without leaving edit mode', async () => {
|
||||
mockGetAgentPaths.mockResolvedValue({
|
||||
result: agentPaths({ action_dir: '/Users/sample/old', action_dir_source: 'default' }),
|
||||
logs: [],
|
||||
});
|
||||
mockUpdateAgentPaths.mockRejectedValue(new Error('action_dir must be an absolute path'));
|
||||
renderWithProviders(<AgentAccessPanel />);
|
||||
|
||||
fireEvent.click(await screen.findByTestId('agent-access-action-dir-edit'));
|
||||
const input = await screen.findByTestId('agent-access-action-dir-input');
|
||||
fireEvent.change(input, { target: { value: 'relative/path' } });
|
||||
fireEvent.click(screen.getByTestId('agent-access-action-dir-save'));
|
||||
|
||||
expect(await screen.findByTestId('agent-access-action-dir-error')).toHaveTextContent(
|
||||
'action_dir must be an absolute path'
|
||||
);
|
||||
expect(screen.getByTestId('agent-access-action-dir-input')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('disables editing and shows the env-locked notice when source is env', async () => {
|
||||
mockGetAgentPaths.mockResolvedValue({
|
||||
result: agentPaths({ action_dir: '/tmp/env-pinned', action_dir_source: 'env' }),
|
||||
logs: [],
|
||||
});
|
||||
renderWithProviders(<AgentAccessPanel />);
|
||||
await screen.findByTestId('agent-access-action-dir');
|
||||
expect(screen.queryByTestId('agent-access-action-dir-edit')).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId('agent-access-action-dir-env-locked')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -124,7 +124,9 @@ describe('DeveloperOptionsPanel — CoreModeBadge', () => {
|
||||
);
|
||||
|
||||
expect(screen.getByText('AI 配置')).toBeInTheDocument();
|
||||
expect(screen.getByText('屏幕感知')).toBeInTheDocument();
|
||||
// Two screen-awareness rows now exist (the moved settings row + the debug
|
||||
// panel), which collapse to the same zh-CN label — assert at least one.
|
||||
expect(screen.getAllByText('屏幕感知').length).toBeGreaterThan(0);
|
||||
// The messaging tile was removed; composio replaced it as a single destination.
|
||||
expect(screen.getByText('Composio')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
import { fireEvent, screen, waitFor } from '@testing-library/react';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { renderWithProviders } from '../../../../test/test-utils';
|
||||
import {
|
||||
type AgentPaths,
|
||||
type AutonomySettings,
|
||||
isTauri,
|
||||
openhumanGetAgentPaths,
|
||||
openhumanGetAutonomySettings,
|
||||
openhumanUpdateAgentPaths,
|
||||
openhumanUpdateAutonomySettings,
|
||||
} from '../../../../utils/tauriCommands';
|
||||
import PermissionsPanel from '../PermissionsPanel';
|
||||
|
||||
const autonomy = (overrides: Partial<AutonomySettings> = {}): AutonomySettings => ({
|
||||
level: 'supervised',
|
||||
workspace_only: false,
|
||||
allowed_commands: [],
|
||||
forbidden_paths: [],
|
||||
trusted_roots: [],
|
||||
allow_tool_install: true,
|
||||
max_actions_per_hour: 0,
|
||||
auto_approve: [],
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const agentPaths = (overrides: Partial<AgentPaths> = {}): AgentPaths => ({
|
||||
action_dir: '/home/test/OpenHuman/projects',
|
||||
workspace_dir: '/home/test/.openhuman/users/u1/workspace',
|
||||
projects_dir: '/home/test/OpenHuman/projects',
|
||||
action_dir_source: 'default',
|
||||
...overrides,
|
||||
});
|
||||
|
||||
vi.mock('../../hooks/useSettingsNavigation', () => ({
|
||||
useSettingsNavigation: () => ({
|
||||
navigateBack: vi.fn(),
|
||||
navigateToSettings: vi.fn(),
|
||||
breadcrumbs: [],
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('../../../../utils/tauriCommands', async () => {
|
||||
const actual = await vi.importActual<typeof import('../../../../utils/tauriCommands')>(
|
||||
'../../../../utils/tauriCommands'
|
||||
);
|
||||
return {
|
||||
...actual,
|
||||
isTauri: vi.fn(() => true),
|
||||
openhumanGetAutonomySettings: vi.fn(),
|
||||
openhumanUpdateAutonomySettings: vi.fn(),
|
||||
openhumanGetAgentPaths: vi.fn(),
|
||||
openhumanUpdateAgentPaths: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
const mockGet = vi.mocked(openhumanGetAutonomySettings);
|
||||
const mockUpdate = vi.mocked(openhumanUpdateAutonomySettings);
|
||||
const mockGetPaths = vi.mocked(openhumanGetAgentPaths);
|
||||
const mockUpdatePaths = vi.mocked(openhumanUpdateAgentPaths);
|
||||
|
||||
describe('PermissionsPanel', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(isTauri).mockReturnValue(true);
|
||||
mockGet.mockResolvedValue({ result: autonomy(), logs: [] });
|
||||
mockUpdate.mockResolvedValue({ result: {} as never, logs: [] });
|
||||
mockGetPaths.mockResolvedValue({ result: agentPaths(), logs: [] });
|
||||
mockUpdatePaths.mockResolvedValue({ result: agentPaths(), logs: [] });
|
||||
});
|
||||
|
||||
it('loads settings on mount and renders all three presets', async () => {
|
||||
renderWithProviders(<PermissionsPanel />);
|
||||
await waitFor(() => expect(mockGet).toHaveBeenCalledTimes(1));
|
||||
expect(await screen.findByText("Look, don't touch")).toBeInTheDocument();
|
||||
expect(screen.getByText('Ask me first')).toBeInTheDocument();
|
||||
expect(screen.getByText('Full control')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('highlights the currently-selected preset on load (supervised by default)', async () => {
|
||||
renderWithProviders(<PermissionsPanel />);
|
||||
const supervisedBtn = await screen.findByTestId('permissions-preset-supervised');
|
||||
// Active preset has bg-primary-50 (selected visual indicator).
|
||||
expect(supervisedBtn.className).toContain('bg-primary-50');
|
||||
const readonlyBtn = screen.getByTestId('permissions-preset-readonly');
|
||||
expect(readonlyBtn.className).not.toContain('bg-primary-50');
|
||||
});
|
||||
|
||||
it('selecting the "Look, don\'t touch" preset persists readonly level', async () => {
|
||||
renderWithProviders(<PermissionsPanel />);
|
||||
fireEvent.click(await screen.findByText("Look, don't touch"));
|
||||
await waitFor(() =>
|
||||
expect(mockUpdate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ level: 'readonly', allow_tool_install: true })
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it('selecting the "Full control" preset persists full level', async () => {
|
||||
renderWithProviders(<PermissionsPanel />);
|
||||
fireEvent.click(await screen.findByText('Full control'));
|
||||
await waitFor(() =>
|
||||
expect(mockUpdate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ level: 'full', allow_tool_install: true })
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it('shows the full-access warning when full control is active', async () => {
|
||||
mockGet.mockResolvedValue({ result: autonomy({ level: 'full' }), logs: [] });
|
||||
renderWithProviders(<PermissionsPanel />);
|
||||
// The fullWarning message from agentAccess i18n key appears.
|
||||
expect(await screen.findByText(/Full access runs commands/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('loads and displays the action_dir from the core', async () => {
|
||||
mockGetPaths.mockResolvedValue({
|
||||
result: agentPaths({ action_dir: '/Users/sample/OpenHuman/projects' }),
|
||||
logs: [],
|
||||
});
|
||||
renderWithProviders(<PermissionsPanel />);
|
||||
await waitFor(() => expect(mockGetPaths).toHaveBeenCalledTimes(1));
|
||||
expect(await screen.findByTestId('permissions-action-dir')).toHaveTextContent(
|
||||
'/Users/sample/OpenHuman/projects'
|
||||
);
|
||||
});
|
||||
|
||||
it('falls back to the documented default when the agent paths RPC fails', async () => {
|
||||
mockGetPaths.mockRejectedValue(new Error('rpc unavailable'));
|
||||
renderWithProviders(<PermissionsPanel />);
|
||||
expect(await screen.findByTestId('permissions-action-dir')).toHaveTextContent(
|
||||
'~/OpenHuman/projects'
|
||||
);
|
||||
});
|
||||
|
||||
it('shows an Edit affordance when action_dir_source is not env', async () => {
|
||||
mockGetPaths.mockResolvedValue({
|
||||
result: agentPaths({ action_dir: '/Users/sample/projects', action_dir_source: 'default' }),
|
||||
logs: [],
|
||||
});
|
||||
renderWithProviders(<PermissionsPanel />);
|
||||
expect(await screen.findByTestId('permissions-action-dir-edit')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('permissions-action-dir-env-locked')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('saving a new action_dir calls openhumanUpdateAgentPaths and updates the display', async () => {
|
||||
mockGetPaths.mockResolvedValue({
|
||||
result: agentPaths({ action_dir: '/Users/sample/old', action_dir_source: 'default' }),
|
||||
logs: [],
|
||||
});
|
||||
mockUpdatePaths.mockResolvedValue({
|
||||
result: agentPaths({ action_dir: '/Users/sample/new', action_dir_source: 'override' }),
|
||||
logs: [],
|
||||
});
|
||||
renderWithProviders(<PermissionsPanel />);
|
||||
|
||||
fireEvent.click(await screen.findByTestId('permissions-action-dir-edit'));
|
||||
const input = await screen.findByTestId('permissions-action-dir-input');
|
||||
fireEvent.change(input, { target: { value: '/Users/sample/new' } });
|
||||
fireEvent.click(screen.getByTestId('permissions-action-dir-save'));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockUpdatePaths).toHaveBeenCalledWith({ action_dir: '/Users/sample/new' })
|
||||
);
|
||||
expect(await screen.findByTestId('permissions-action-dir')).toHaveTextContent(
|
||||
'/Users/sample/new'
|
||||
);
|
||||
});
|
||||
|
||||
it('renders a backend validation error inline without leaving edit mode', async () => {
|
||||
mockGetPaths.mockResolvedValue({
|
||||
result: agentPaths({ action_dir: '/Users/sample/old', action_dir_source: 'default' }),
|
||||
logs: [],
|
||||
});
|
||||
mockUpdatePaths.mockRejectedValue(new Error('action_dir must be an absolute path'));
|
||||
renderWithProviders(<PermissionsPanel />);
|
||||
|
||||
fireEvent.click(await screen.findByTestId('permissions-action-dir-edit'));
|
||||
const input = await screen.findByTestId('permissions-action-dir-input');
|
||||
fireEvent.change(input, { target: { value: 'relative/path' } });
|
||||
fireEvent.click(screen.getByTestId('permissions-action-dir-save'));
|
||||
|
||||
expect(await screen.findByTestId('permissions-action-dir-error')).toHaveTextContent(
|
||||
'action_dir must be an absolute path'
|
||||
);
|
||||
expect(screen.getByTestId('permissions-action-dir-input')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('disables editing and shows the env-locked notice when source is env', async () => {
|
||||
mockGetPaths.mockResolvedValue({
|
||||
result: agentPaths({ action_dir: '/tmp/env-pinned', action_dir_source: 'env' }),
|
||||
logs: [],
|
||||
});
|
||||
renderWithProviders(<PermissionsPanel />);
|
||||
await screen.findByTestId('permissions-action-dir');
|
||||
expect(screen.queryByTestId('permissions-action-dir-edit')).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId('permissions-action-dir-env-locked')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows the desktop-only notice and skips loading off-Tauri', async () => {
|
||||
vi.mocked(isTauri).mockReturnValue(false);
|
||||
renderWithProviders(<PermissionsPanel />);
|
||||
expect(
|
||||
await screen.findByText('Access settings are only available in the desktop app.')
|
||||
).toBeInTheDocument();
|
||||
expect(mockGet).not.toHaveBeenCalled();
|
||||
expect(mockGetPaths).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('surfaces a load error without crashing', async () => {
|
||||
mockGet.mockRejectedValue(new Error('load failed'));
|
||||
renderWithProviders(<PermissionsPanel />);
|
||||
expect(await screen.findByText('load failed')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('carries workspace_only and trusted_roots through when persisting a tier change', async () => {
|
||||
mockGet.mockResolvedValue({
|
||||
result: autonomy({
|
||||
level: 'supervised',
|
||||
workspace_only: true,
|
||||
trusted_roots: [{ path: '/tmp/proj', access: 'read' }],
|
||||
}),
|
||||
logs: [],
|
||||
});
|
||||
renderWithProviders(<PermissionsPanel />);
|
||||
|
||||
fireEvent.click(await screen.findByText("Look, don't touch"));
|
||||
await waitFor(() =>
|
||||
expect(mockUpdate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
level: 'readonly',
|
||||
workspace_only: true,
|
||||
trusted_roots: [{ path: '/tmp/proj', access: 'read' }],
|
||||
})
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -551,7 +551,7 @@ describe('createWalkthroughSteps', () => {
|
||||
const navigate = vi.fn();
|
||||
const steps = createWalkthroughSteps(navigate);
|
||||
|
||||
// Steps: 2=chat, 3=integrations, 4=channels, 5=intelligence, 6=settings, 7=home-return, 9=chat-welcome
|
||||
// Steps: 2=chat, 3=integrations, 4=channels, 5=activity, 6=settings, 7=home-return, 9=chat-welcome
|
||||
const crossPageIndices = [2, 3, 4, 5, 6, 7, 9];
|
||||
for (const idx of crossPageIndices) {
|
||||
expect(typeof steps[idx].before, `step[${idx}] should have a before fn`).toBe('function');
|
||||
@@ -571,9 +571,9 @@ describe('createWalkthroughSteps', () => {
|
||||
|
||||
it.each([
|
||||
{ idx: 2, route: '/chat', target: 'chat-agent-panel' },
|
||||
{ idx: 3, route: '/skills', target: 'skills-grid' },
|
||||
{ idx: 3, route: '/connections', target: 'skills-grid' },
|
||||
{ idx: 4, route: null, target: 'skills-channels' },
|
||||
{ idx: 5, route: '/intelligence', target: 'intelligence-header' },
|
||||
{ idx: 5, route: '/activity', target: 'intelligence-header' },
|
||||
{ idx: 6, route: '/settings', target: 'settings-menu' },
|
||||
{ idx: 7, route: '/home', target: 'tab-chat' },
|
||||
{ idx: 9, route: '/chat', target: 'chat-agent-panel' },
|
||||
|
||||
@@ -87,7 +87,7 @@ export function createWalkthroughSteps(navigate: NavigateFunction): Step[] {
|
||||
},
|
||||
},
|
||||
|
||||
// ── Step 4 — /skills ──────────────────────────────────────────────────
|
||||
// ── Step 4 — /connections (Apps tab) ─────────────────────────────────
|
||||
{
|
||||
target: '[data-walkthrough="skills-grid"]',
|
||||
title: 'Connect your world',
|
||||
@@ -96,12 +96,12 @@ export function createWalkthroughSteps(navigate: NavigateFunction): Step[] {
|
||||
placement: 'top',
|
||||
skipBeacon: true,
|
||||
before: async () => {
|
||||
navigate('/skills');
|
||||
navigate('/connections');
|
||||
await waitForTarget('skills-grid');
|
||||
},
|
||||
},
|
||||
|
||||
// ── Step 5 — /skills (channels) ─────────────────────────────────────
|
||||
// ── Step 5 — /connections (Messaging tab) ────────────────────────────
|
||||
{
|
||||
target: '[data-walkthrough="skills-channels"]',
|
||||
title: 'Chat where you already are',
|
||||
@@ -114,16 +114,16 @@ export function createWalkthroughSteps(navigate: NavigateFunction): Step[] {
|
||||
},
|
||||
},
|
||||
|
||||
// ── Step 6 — /intelligence ────────────────────────────────────────────
|
||||
// ── Step 6 — /activity (Activity) ────────────────────────────────────
|
||||
{
|
||||
target: '[data-walkthrough="intelligence-header"]',
|
||||
title: "Your assistant's brain",
|
||||
title: 'Your activity hub',
|
||||
content:
|
||||
'This is where your assistant learns and remembers. It gets smarter the more you use it.',
|
||||
placement: 'bottom',
|
||||
skipBeacon: true,
|
||||
before: async () => {
|
||||
navigate('/intelligence');
|
||||
navigate('/activity');
|
||||
await waitForTarget('intelligence-header');
|
||||
},
|
||||
},
|
||||
@@ -157,7 +157,7 @@ export function createWalkthroughSteps(navigate: NavigateFunction): Step[] {
|
||||
|
||||
// ── Step 8 — /home (already there) ───────────────────────────────────
|
||||
{
|
||||
target: '[data-walkthrough="tab-notifications"]',
|
||||
target: '[data-walkthrough="tab-activity"]',
|
||||
title: 'Stay in the loop',
|
||||
content: 'Alerts and automations live here — briefings, notifications, background activity.',
|
||||
placement: 'top',
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* Tests for navConfig — verifies the shape, count, and key values of NAV_TABS
|
||||
* and AVATAR_MENU_ITEMS so regressions are caught early.
|
||||
*
|
||||
* Phase 6 update: Human tab removed; Chat tab renamed to "Assistant"
|
||||
* (id stays 'chat', labelKey 'nav.assistant', walkthroughAttr 'tab-chat').
|
||||
* Nav drops from 6 tabs to 5.
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { AVATAR_MENU_ITEMS, NAV_TABS } from '../navConfig';
|
||||
|
||||
describe('NAV_TABS', () => {
|
||||
it('has exactly 5 entries (Phase 6: Human merged into Assistant)', () => {
|
||||
expect(NAV_TABS).toHaveLength(5);
|
||||
});
|
||||
|
||||
it('has the correct ids in order', () => {
|
||||
expect(NAV_TABS.map(t => t.id)).toEqual([
|
||||
'home',
|
||||
'chat',
|
||||
'connections',
|
||||
'activity',
|
||||
'settings',
|
||||
]);
|
||||
});
|
||||
|
||||
it('has the correct paths', () => {
|
||||
expect(NAV_TABS.map(t => t.path)).toEqual([
|
||||
'/home',
|
||||
'/chat',
|
||||
'/connections',
|
||||
'/activity',
|
||||
'/settings',
|
||||
]);
|
||||
});
|
||||
|
||||
it('has the correct labelKeys', () => {
|
||||
expect(NAV_TABS.map(t => t.labelKey)).toEqual([
|
||||
'nav.home',
|
||||
'nav.assistant',
|
||||
'nav.connections',
|
||||
'nav.activity',
|
||||
'nav.settings',
|
||||
]);
|
||||
});
|
||||
|
||||
it('has the correct walkthroughAttrs', () => {
|
||||
expect(NAV_TABS.map(t => t.walkthroughAttr)).toEqual([
|
||||
'tab-home',
|
||||
'tab-chat',
|
||||
'tab-connections',
|
||||
'tab-activity',
|
||||
'tab-settings',
|
||||
]);
|
||||
});
|
||||
|
||||
it('does not contain a Human tab (Phase 6: merged into Assistant)', () => {
|
||||
expect(NAV_TABS.find(t => t.id === 'human')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('does not contain a rewards tab', () => {
|
||||
expect(NAV_TABS.find(t => t.id === 'rewards')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('does not contain an intelligence or skills tab id', () => {
|
||||
expect(NAV_TABS.find(t => t.id === 'intelligence')).toBeUndefined();
|
||||
expect(NAV_TABS.find(t => t.id === 'skills')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('Assistant tab uses nav.assistant label key and tab-chat walkthrough attr', () => {
|
||||
const assistantTab = NAV_TABS.find(t => t.id === 'chat');
|
||||
expect(assistantTab).toBeDefined();
|
||||
expect(assistantTab?.labelKey).toBe('nav.assistant');
|
||||
expect(assistantTab?.walkthroughAttr).toBe('tab-chat');
|
||||
expect(assistantTab?.path).toBe('/chat');
|
||||
});
|
||||
});
|
||||
|
||||
describe('AVATAR_MENU_ITEMS', () => {
|
||||
it('has exactly 5 entries', () => {
|
||||
expect(AVATAR_MENU_ITEMS).toHaveLength(5);
|
||||
});
|
||||
|
||||
it('has the correct ids in order', () => {
|
||||
expect(AVATAR_MENU_ITEMS.map(i => i.id)).toEqual([
|
||||
'account',
|
||||
'billing',
|
||||
'rewards',
|
||||
'invites',
|
||||
'wallet',
|
||||
]);
|
||||
});
|
||||
|
||||
it('has the correct labelKeys', () => {
|
||||
expect(AVATAR_MENU_ITEMS.map(i => i.labelKey)).toEqual([
|
||||
'nav.avatarMenu.account',
|
||||
'nav.avatarMenu.billing',
|
||||
'nav.avatarMenu.rewards',
|
||||
'nav.avatarMenu.invites',
|
||||
'nav.avatarMenu.wallet',
|
||||
]);
|
||||
});
|
||||
|
||||
it('billing, rewards, and invites are cloudOnly; account and wallet are not', () => {
|
||||
const cloudOnly = AVATAR_MENU_ITEMS.filter(i => i.cloudOnly).map(i => i.id);
|
||||
expect(cloudOnly).toEqual(['billing', 'rewards', 'invites']);
|
||||
|
||||
const notCloudOnly = AVATAR_MENU_ITEMS.filter(i => !i.cloudOnly).map(i => i.id);
|
||||
expect(notCloudOnly).toEqual(['account', 'wallet']);
|
||||
});
|
||||
|
||||
it('billing uses openUrl; all others use navigate', () => {
|
||||
const openUrlItems = AVATAR_MENU_ITEMS.filter(i => i.kind === 'openUrl').map(i => i.id);
|
||||
expect(openUrlItems).toEqual(['billing']);
|
||||
|
||||
const navigateItems = AVATAR_MENU_ITEMS.filter(i => i.kind === 'navigate').map(i => i.id);
|
||||
expect(navigateItems).toEqual(['account', 'rewards', 'invites', 'wallet']);
|
||||
});
|
||||
|
||||
it('each item has a non-empty target', () => {
|
||||
for (const item of AVATAR_MENU_ITEMS) {
|
||||
expect(item.target.length).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* Single source of truth for bottom-tab-bar navigation entries and the
|
||||
* avatar-menu items that appear in the agent-profile popover.
|
||||
*
|
||||
* This module is pure data — no JSX, no React imports. Icons are owned by
|
||||
* BottomTabBar.tsx and mapped from tab.id.
|
||||
*/
|
||||
|
||||
// ── Tab bar ──────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface NavTab {
|
||||
/** Stable identifier used for analytics, icon-maps, and walkthrough attrs. */
|
||||
id: string;
|
||||
/** i18n key resolved by `useT()` in the consuming component. */
|
||||
labelKey: string;
|
||||
/** Hash-router path this tab navigates to. */
|
||||
path: string;
|
||||
/** Value of `data-walkthrough` attribute on the rendered button, if any. */
|
||||
walkthroughAttr?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ordered list of bottom-bar tabs. Exactly 5 entries (Phase 6: Human merged
|
||||
* into Assistant):
|
||||
* home → chat (Assistant) → connections → activity → settings
|
||||
*
|
||||
* The tab id stays `chat` and walkthroughAttr stays `tab-chat` for
|
||||
* back-compat with analytics and the walkthrough tour. The Human tab has been
|
||||
* retired; `/human` redirects to `/chat`.
|
||||
*/
|
||||
export const NAV_TABS: NavTab[] = [
|
||||
{ id: 'home', labelKey: 'nav.home', path: '/home', walkthroughAttr: 'tab-home' },
|
||||
{ id: 'chat', labelKey: 'nav.assistant', path: '/chat', walkthroughAttr: 'tab-chat' },
|
||||
{
|
||||
id: 'connections',
|
||||
labelKey: 'nav.connections',
|
||||
path: '/connections',
|
||||
walkthroughAttr: 'tab-connections',
|
||||
},
|
||||
{ id: 'activity', labelKey: 'nav.activity', path: '/activity', walkthroughAttr: 'tab-activity' },
|
||||
{ id: 'settings', labelKey: 'nav.settings', path: '/settings', walkthroughAttr: 'tab-settings' },
|
||||
];
|
||||
|
||||
// ── Avatar / account menu ─────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Determines how the menu item is activated.
|
||||
* - `navigate` — internal `react-router-dom` navigation to `target`.
|
||||
* - `openUrl` — opens `target` in the system browser via `openUrl()`.
|
||||
*/
|
||||
export type AvatarMenuItemKind = 'navigate' | 'openUrl';
|
||||
|
||||
export interface AvatarMenuItem {
|
||||
/** Stable identifier. */
|
||||
id: string;
|
||||
/** i18n key resolved by the consuming component. */
|
||||
labelKey: string;
|
||||
/** Navigation destination or external URL, depending on `kind`. */
|
||||
target: string;
|
||||
/** How the item is activated. */
|
||||
kind: AvatarMenuItemKind;
|
||||
/**
|
||||
* When `true`, the item should only be shown for non-local (cloud) sessions.
|
||||
* Cloud-gated items: billing, rewards, invites.
|
||||
*/
|
||||
cloudOnly?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Avatar dropdown menu items shown beneath the agent-profile list.
|
||||
* Order: Account → Billing → Rewards → Invites → Wallet.
|
||||
*/
|
||||
export const AVATAR_MENU_ITEMS: AvatarMenuItem[] = [
|
||||
{
|
||||
id: 'account',
|
||||
labelKey: 'nav.avatarMenu.account',
|
||||
target: '/settings/account',
|
||||
kind: 'navigate',
|
||||
},
|
||||
{
|
||||
id: 'billing',
|
||||
labelKey: 'nav.avatarMenu.billing',
|
||||
// Resolved at runtime via BILLING_DASHBOARD_URL; placeholder keeps typing clean.
|
||||
target: 'https://tinyhumans.ai/dashboard',
|
||||
kind: 'openUrl',
|
||||
cloudOnly: true,
|
||||
},
|
||||
{
|
||||
id: 'rewards',
|
||||
labelKey: 'nav.avatarMenu.rewards',
|
||||
target: '/rewards',
|
||||
kind: 'navigate',
|
||||
cloudOnly: true,
|
||||
},
|
||||
{
|
||||
id: 'invites',
|
||||
labelKey: 'nav.avatarMenu.invites',
|
||||
target: '/invites',
|
||||
kind: 'navigate',
|
||||
cloudOnly: true,
|
||||
},
|
||||
{
|
||||
id: 'wallet',
|
||||
labelKey: 'nav.avatarMenu.wallet',
|
||||
target: '/settings/wallet-balances',
|
||||
kind: 'navigate',
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* useDeveloperMode — runtime developer-mode gate.
|
||||
*
|
||||
* Returns `true` when developer surfaces should be visible. The gate is open
|
||||
* when EITHER the build is a Vite dev build (`IS_DEV`) OR the user has
|
||||
* enabled Developer Mode in Settings › About.
|
||||
*
|
||||
* Gating is UI-only. The Rust `SecurityPolicy` / autonomy-tier enforcement
|
||||
* in the core is authoritative and is never relaxed by this toggle.
|
||||
*/
|
||||
import { useAppSelector } from '../store/hooks';
|
||||
import { selectDeveloperMode } from '../store/themeSlice';
|
||||
import { IS_DEV } from '../utils/config';
|
||||
|
||||
export function useDeveloperMode(): boolean {
|
||||
const persistedMode = useAppSelector(selectDeveloperMode);
|
||||
return IS_DEV || persistedMode;
|
||||
}
|
||||
@@ -17,11 +17,18 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
describe('registerGlobalActions', () => {
|
||||
it('registers the 5 seed nav actions into the global frame', () => {
|
||||
it('registers the 6 seed nav actions into the global frame', () => {
|
||||
const frame = hotkeyManager.pushFrame('global', 'root');
|
||||
const navigate = vi.fn() as unknown as NavigateFunction;
|
||||
registerGlobalActions(navigate, frame);
|
||||
const ids = ['nav.home', 'nav.chat', 'nav.intelligence', 'nav.skills', 'nav.settings'];
|
||||
const ids = [
|
||||
'nav.home',
|
||||
'nav.chat',
|
||||
'nav.intelligence',
|
||||
'nav.skills',
|
||||
'nav.activity',
|
||||
'nav.settings',
|
||||
];
|
||||
for (const id of ids) expect(registry.getAction(id)?.id).toBe(id);
|
||||
expect(registry.getAction('help.show')).toBeUndefined();
|
||||
});
|
||||
|
||||
@@ -32,19 +32,27 @@ export function registerGlobalActions(
|
||||
},
|
||||
{
|
||||
id: 'nav.intelligence',
|
||||
label: 'Go to Intelligence',
|
||||
label: 'Go to Knowledge & Memory',
|
||||
group: 'Navigation',
|
||||
shortcut: 'mod+3',
|
||||
handler: nav('/settings/intelligence'),
|
||||
keywords: ['memory', 'knowledge'],
|
||||
keywords: ['memory', 'knowledge', 'intelligence'],
|
||||
},
|
||||
{
|
||||
id: 'nav.skills',
|
||||
label: 'Go to Skills',
|
||||
label: 'Go to Connections',
|
||||
group: 'Navigation',
|
||||
shortcut: 'mod+4',
|
||||
handler: nav('/skills'),
|
||||
keywords: ['plugins', 'tools'],
|
||||
handler: nav('/connections'),
|
||||
keywords: ['plugins', 'tools', 'connections', 'apps', 'skills'],
|
||||
},
|
||||
{
|
||||
id: 'nav.activity',
|
||||
label: 'Go to Activity',
|
||||
group: 'Navigation',
|
||||
shortcut: 'mod+5',
|
||||
handler: nav('/activity'),
|
||||
keywords: ['tasks', 'automations', 'alerts', 'background'],
|
||||
},
|
||||
{
|
||||
id: 'nav.settings',
|
||||
|
||||
+111
-5
@@ -6,6 +6,11 @@ const messages: TranslationMap = {
|
||||
'nav.home': 'الرئيسية',
|
||||
'nav.human': 'إنسان',
|
||||
'nav.chat': 'المحادثة',
|
||||
'nav.assistant': 'المساعد',
|
||||
'assistant.faceMode.on': 'يتحدث إلى Tiny',
|
||||
'assistant.faceMode.off': 'تحدث إلى Tiny',
|
||||
'assistant.faceMode.turnOn': 'عرض الوجه',
|
||||
'assistant.faceMode.turnOff': 'إخفاء الوجه',
|
||||
'nav.connections': 'الاتصالات',
|
||||
'nav.memory': 'الذكاء',
|
||||
'nav.alerts': 'التنبيهات',
|
||||
@@ -15,6 +20,12 @@ const messages: TranslationMap = {
|
||||
'nav.switchAgentProfile': 'تبديل ملف الوكيل',
|
||||
'nav.defaultAgentProfile': 'الوكيل الافتراضي',
|
||||
'nav.noAgentProfiles': 'لم يتم العثور على ملفات وكلاء',
|
||||
'nav.activity': 'النشاط',
|
||||
'nav.avatarMenu.account': 'الحساب',
|
||||
'nav.avatarMenu.billing': 'الفواتير',
|
||||
'nav.avatarMenu.rewards': 'المكافآت',
|
||||
'nav.avatarMenu.invites': 'دعوة صديق',
|
||||
'nav.avatarMenu.wallet': 'المحفظة',
|
||||
'common.cancel': 'إلغاء',
|
||||
'common.save': 'حفظ',
|
||||
'common.confirm': 'تأكيد',
|
||||
@@ -59,6 +70,50 @@ const messages: TranslationMap = {
|
||||
'common.comingSoon': 'قريبًا',
|
||||
'common.breadcrumb': 'مسار التنقل',
|
||||
'settings.general': 'عام',
|
||||
// Settings layman groups (Phase 4 IA revamp)
|
||||
'settings.groups.account': 'الحساب',
|
||||
'settings.groups.assistant': 'المساعد',
|
||||
'settings.groups.privacySecurity': 'الخصوصية والأمان',
|
||||
'settings.groups.notifications': 'الإشعارات',
|
||||
'settings.groups.about': 'حول',
|
||||
'settings.assistant.personality': 'الشخصية',
|
||||
'settings.assistant.personalityDesc': 'الاسم والوصف وشخصية SOUL.md',
|
||||
'settings.assistant.voice': 'الصوت',
|
||||
'settings.assistant.voiceDesc': 'إعدادات التحويل من كلام إلى نص ومن نص إلى كلام',
|
||||
'settings.assistant.faceMascot': 'الوجه / الشخصية',
|
||||
'settings.assistant.faceMascotDesc': 'اختر لون الشخصية المستخدم في التطبيق',
|
||||
'settings.assistant.backgroundActivity': 'النشاط في الخلفية',
|
||||
'settings.assistant.backgroundActivityDesc': 'تحكم في مدى نشاط مساعدك في الخلفية',
|
||||
'settings.assistant.screenAwareness': 'الوعي بالشاشة',
|
||||
'settings.assistant.screenAwarenessDesc': 'اسمح للمساعد برؤية النافذة النشطة',
|
||||
'settings.assistant.desktopCompanion': 'المرافق لسطح المكتب',
|
||||
'settings.assistant.desktopCompanionDesc': 'وضع المرافق الدائم مع اختصار شريط النظام',
|
||||
'settings.assistant.permissions': 'الأذونات',
|
||||
'settings.assistant.permissionsDesc': 'اختر ما يمكن للمساعد فعله والمكان الذي يمكنه العمل فيه',
|
||||
'settings.privacySecurity.privacy': 'الخصوصية',
|
||||
'settings.privacySecurity.privacyDesc': 'تحكم في البيانات التي تغادر جهازك',
|
||||
'settings.privacySecurity.security': 'الأمان',
|
||||
'settings.privacySecurity.securityDesc': 'الجلسات وخيارات تسجيل الدخول',
|
||||
'settings.privacySecurity.approvalsHistory': 'الموافقات والسجل',
|
||||
'settings.privacySecurity.approvalsHistoryDesc': 'مراجعة قرارات الموافقة الأخيرة على الأدوات',
|
||||
'settings.notifications.menuTitle': 'الإشعارات',
|
||||
'settings.notifications.menuDesc': 'صندوق التنبيهات وتفضيلات الإشعارات',
|
||||
'settings.devGroups.knowledgeMemory': 'المعرفة والذاكرة',
|
||||
'settings.devGroups.agentsAutonomy': 'الوكلاء والاستقلالية',
|
||||
'settings.devGroups.modelsInference': 'النماذج والاستنتاج',
|
||||
'settings.devGroups.automationIntegrations': 'الأتمتة والتكاملات',
|
||||
'settings.devGroups.toolsCapabilities': 'الأدوات والقدرات',
|
||||
'settings.devGroups.council': 'المجلس',
|
||||
'settings.analysisViews.title': 'طرق العرض التحليلية',
|
||||
'settings.analysisViews.menuDesc':
|
||||
'تحليل رسم الذاكرة البياني — المخطط، والمركزية، والتماسك، والارتباطات، والحداثة، والجدول الزمني، والمسارات، والمساحات',
|
||||
'settings.buildInfo.title': 'معلومات الإصدار / البناء',
|
||||
'settings.buildInfo.menuDesc': 'تفاصيل بناء التطبيق وإصداره واتصال النواة',
|
||||
'settings.dataSync.title': 'مزامنة البيانات',
|
||||
'settings.dataSync.menuDesc': 'ما يقوم مساعدك بمزامنته — المصادر والحداثة والحالة',
|
||||
'settings.dataSync.description':
|
||||
'تحكم في ما تتم مزامنته في ذاكرة مساعدك: كل مصدر متصل مع وقت آخر مزامنة له، ومقدار ما تمت مزامنته، وما إذا كان قيد المزامنة الآن.',
|
||||
'settings.devGroups.diagnosticsLogs': 'التشخيصات والسجلات',
|
||||
'settings.featuresAndAI': 'الميزات والذكاء الاصطناعي',
|
||||
'settings.billingAndRewards': 'الفوترة والمكافآت',
|
||||
'settings.support': 'الدعم',
|
||||
@@ -87,6 +142,12 @@ const messages: TranslationMap = {
|
||||
'settings.developerOptions': 'متقدم',
|
||||
'settings.developerOptionsDesc':
|
||||
'إعداد الذكاء الاصطناعي وقنوات المراسلة والأدوات والتشخيص ولوحات التصحيح',
|
||||
'settings.developerDiagnostics': 'المطور والتشخيص',
|
||||
'settings.developerDiagnosticsDesc':
|
||||
'أدوات المطور المتقدمة والتشخيص والذاكرة والوكلاء ولوحات التصحيح',
|
||||
'settings.developerMode.title': 'وضع المطور',
|
||||
'settings.developerMode.description': 'إظهار أدوات المطور والتشخيص المتقدمة',
|
||||
'settings.developerMode.enabledByBuild': 'مفعّل دائمًا في إصدارات التطوير',
|
||||
'settings.clearAppData': 'مسح بيانات التطبيق',
|
||||
'settings.clearAppDataDesc': 'تسجيل الخروج وحذف جميع البيانات المحلية للتطبيق نهائيًا',
|
||||
'settings.logOut': 'تسجيل الخروج',
|
||||
@@ -98,6 +159,14 @@ const messages: TranslationMap = {
|
||||
'settings.languageDesc': 'لغة عرض واجهة التطبيق',
|
||||
'settings.alerts': 'التنبيهات',
|
||||
'settings.alertsDesc': 'عرض التنبيهات الأخيرة والنشاط في صندوق الوارد',
|
||||
'settings.account.profile': 'الملف الشخصي',
|
||||
'settings.account.profileDesc': 'الاسم والبريد الإلكتروني والصورة الرمزية',
|
||||
'settings.account.devices': 'الأجهزة',
|
||||
'settings.account.devicesDesc': 'إقران الأجهزة المحمولة وإدارتها',
|
||||
'settings.account.teamMembers': 'الفريق والأعضاء',
|
||||
'settings.account.teamMembersDesc': 'إدارة وصول الفريق وأدوار الأعضاء',
|
||||
'settings.account.dataMigration': 'البيانات والترحيل',
|
||||
'settings.account.dataMigrationDesc': 'استيراد الذاكرة من مساعد آخر',
|
||||
'settings.account.recoveryPhrase': 'عبارة الاسترداد',
|
||||
'settings.account.recoveryPhraseDesc': 'عرض عبارة استرداد الحساب ونسخها احتياطيًا',
|
||||
'settings.account.team': 'الفريق',
|
||||
@@ -257,7 +326,7 @@ const messages: TranslationMap = {
|
||||
'skills.connected': 'متصل',
|
||||
'skills.available': 'متاح',
|
||||
'skills.addAccount': 'إضافة حساب',
|
||||
'skills.channels': 'القنوات',
|
||||
'skills.channels': 'المراسلة',
|
||||
'skills.explorer.emptyCta': 'التثبيت من رابط',
|
||||
'skills.explorer.emptyDescription':
|
||||
'ثبّت حزمة SKILL.md أو ضع مجلدات بنمط Hermes داخل ~/.openhuman/skills.',
|
||||
@@ -284,9 +353,9 @@ const messages: TranslationMap = {
|
||||
'skills.explorer.installed': 'مثبت',
|
||||
'skills.explorer.install': 'تثبيت',
|
||||
'skills.explorer.installing': 'جارٍ التثبيت…',
|
||||
'skills.integrations': 'التكاملات',
|
||||
'skills.integrations': 'التطبيقات',
|
||||
'skills.integrationsSubtitle':
|
||||
'اتصالات OAuth السحابية — سجّل الدخول بحسابك ويتولى Composio إدارة الرموز حتى يتمكن الوكلاء من القراءة والتصرف نيابةً عنك. لا حاجة لإدارة مفاتيح API.',
|
||||
'اتصالات OAuth السحابية — سجّل الدخول بحسابك وتُدار الرموز بأمان حتى يتمكن الوكلاء من القراءة والتصرف نيابةً عنك. لا حاجة لإدارة مفاتيح API.',
|
||||
'skills.composio.noApiKeyTitle': 'لم يتم إعداد مفتاح Composio API',
|
||||
'skills.composio.noApiKeyDescription':
|
||||
'يستخدم الوضع المحلي مفتاح Composio API الخاص بك. افتح الإعدادات ← الخيارات المتقدمة ← Composio لإضافته قبل توصيل التكاملات هنا.',
|
||||
@@ -296,6 +365,11 @@ const messages: TranslationMap = {
|
||||
'skills.tabs.explorer': 'المهارات',
|
||||
'skills.tabs.meetings': 'اجتماعات Google Meet',
|
||||
'skills.tabs.mcp': 'MCP الخوادم',
|
||||
'connections.tabs.apps': 'التطبيقات',
|
||||
'connections.tabs.messaging': 'المراسلة',
|
||||
'connections.tabs.tools': 'الأدوات',
|
||||
'connections.tabs.explorer': 'المستكشف',
|
||||
'connections.tabs.talents': 'المواهب',
|
||||
'memory.title': 'الذاكرة',
|
||||
'memory.search': 'البحث في الذكريات...',
|
||||
'memory.noResults': 'لم يتم العثور على ذكريات',
|
||||
@@ -685,6 +759,7 @@ const messages: TranslationMap = {
|
||||
'team.failedChangeRole': 'فشل تغيير الدور',
|
||||
'team.failedRemoveMember': 'فشلت إزالة العضو',
|
||||
'devOptions.title': 'متقدم',
|
||||
'devOptions.titleDiagnostics': 'المطور والتشخيص',
|
||||
'devOptions.diagnostics': 'التشخيص',
|
||||
'devOptions.diagnosticsDesc': 'صحة النظام والسجلات ومقاييس الأداء',
|
||||
'devOptions.toolPolicyDiagnosticsDesc':
|
||||
@@ -1780,7 +1855,7 @@ const messages: TranslationMap = {
|
||||
'common.enable': 'تفعيل',
|
||||
'chat.safetyTimeout': 'لا استجابة من الوكيل بعد دقيقتين. حاول مرة أخرى أو تحقق من اتصالك.',
|
||||
'chat.filter.general': 'عام',
|
||||
'chat.filter.subconscious': 'اللاوعي',
|
||||
'chat.filter.subconscious': 'نشاط الخلفية',
|
||||
'chat.filter.meetings': 'الاجتماعات',
|
||||
'chat.filter.tasks': 'المهام',
|
||||
'chat.selectThread': 'اختر محادثة',
|
||||
@@ -3757,6 +3832,11 @@ const messages: TranslationMap = {
|
||||
'settings.developerMenu.mcpServer.desc': 'تكوين عملاء MCP الخارجيين للاتصال بـ OpenHuman',
|
||||
'settings.developerMenu.autonomy.title': 'استقلالية الوكيل',
|
||||
'settings.developerMenu.autonomy.desc': 'حدود معدل إجراءات الأدوات وعتبات الأمان',
|
||||
'settings.developerMenu.autocomplete.title': 'الإكمال التلقائي',
|
||||
'settings.developerMenu.autocomplete.desc':
|
||||
'إعدادات الإكمال التلقائي المدعوم بالذكاء الاصطناعي ولوحة التصحيح',
|
||||
'settings.developerMenu.voiceDebug.title': 'الصوت (تصحيح)',
|
||||
'settings.developerMenu.voiceDebug.desc': 'حالة وقت التشغيل لإملاء الصوت وإعدادات التصحيح',
|
||||
'settings.mcpServer.title': 'Xqx0x',
|
||||
'settings.mcpServer.toolsSectionTitle': 'الأدوات المتاحة',
|
||||
'settings.mcpServer.toolsSectionDesc':
|
||||
@@ -3854,6 +3934,26 @@ const messages: TranslationMap = {
|
||||
'settings.agentAccess.approvalHistoryDesc':
|
||||
'مراجعة قرارات الموافقة / الرفض السابقة التي طلبها الوكيل.',
|
||||
'settings.agentAccess.viewApprovalHistory': 'عرض سجل الموافقات',
|
||||
|
||||
// ── لوحة الأذونات ────────────────────────────────────────────────
|
||||
'settings.permissions.title': 'الأذونات',
|
||||
'settings.permissions.menuDesc': 'اختر ما يمكن للمساعد فعله والمكان الذي يمكنه العمل فيه.',
|
||||
'settings.permissions.accessMode': 'ماذا يمكن للمساعد أن يفعل؟',
|
||||
'settings.permissions.accessModeDesc':
|
||||
'اختر مقدار الحرية التي يتمتع بها المساعد عند اتخاذ الإجراءات على جهاز الكمبيوتر الخاص بك.',
|
||||
'settings.permissions.preset.readonly.title': 'انظر ولا تلمس',
|
||||
'settings.permissions.preset.readonly.desc':
|
||||
'يمكن للمساعد قراءة الملفات والاستكشاف — لكنه لن يكتب أو يعدّل أو يُشغّل أي شيء يغير الحالة.',
|
||||
'settings.permissions.preset.supervised.title': 'اسألني أولاً',
|
||||
'settings.permissions.preset.supervised.desc':
|
||||
'يمكنه إنشاء ملفات جديدة بحرية، لكنه يطلب موافقتك دائمًا قبل التعديل أو تشغيل الأوامر أو الوصول إلى الشبكة.',
|
||||
'settings.permissions.preset.full.title': 'تحكم كامل',
|
||||
'settings.permissions.preset.full.desc':
|
||||
'يعمل بصلاحيات حسابك الكاملة. الأوامر المدمرة والوصول إلى الشبكة والتثبيت لا تزال تطلب الموافقة.',
|
||||
'settings.permissions.folders': 'أين يمكنه العمل؟',
|
||||
'settings.permissions.foldersDesc':
|
||||
'المجلد الافتراضي الذي يقرأ ويكتب فيه المساعد. يمكنك إضافة مجلدات أخرى في الإعدادات المتقدمة.',
|
||||
|
||||
'settings.sandbox.title': 'تنفيذ Sandbox',
|
||||
'settings.sandbox.menuDesc': 'تكوين خلفيات Sandbox لعزل أدوات الوكيل.',
|
||||
'settings.sandbox.loading': 'جارٍ التحميل…',
|
||||
@@ -4067,7 +4167,7 @@ const messages: TranslationMap = {
|
||||
'skills.channelIcon.telegram': 'Telegram',
|
||||
'skills.channelIcon.web': 'الويب',
|
||||
'skills.channelIcon.yuanbao': 'Yuanbao',
|
||||
'skills.composio.poweredBy': 'مدعوم من Composio',
|
||||
'skills.composio.poweredBy': 'OAuth',
|
||||
'skills.composio.staleStatusTitle': 'تظهر الاتصالات حالة قديمة',
|
||||
'skills.create.allowedTools': 'الأدوات المسموح بها',
|
||||
'skills.create.allowedToolsHelp': 'تم عرضها في المادة الأمامية SKILL.md كـ',
|
||||
@@ -4685,6 +4785,12 @@ const messages: TranslationMap = {
|
||||
'autocomplete.maxChars': 'أقصى عدد لأحرف السياق',
|
||||
'autocomplete.overlayTtlMs': 'مهلة الطبقة (مللي ثانية)',
|
||||
'memory.tab.council': 'Council',
|
||||
'activity.tabs.automations': 'أتمتة',
|
||||
'activity.tabs.automationsDescription':
|
||||
'إجراءات قابلة لإعادة الاستخدام والتشغيل — هدف مع الخطوات للوصول إليه.',
|
||||
'activity.tabs.backgroundActivity': 'نشاط الخلفية',
|
||||
'activity.tabs.alerts': 'التنبيهات',
|
||||
|
||||
'intelligence.agents.title': 'مكتبة الوكلاء',
|
||||
'intelligence.agents.subtitle':
|
||||
'استعرض المتخصصين القابلين للتشغيل وأرسل مهمة واحدة إلى وكيل محدد.',
|
||||
|
||||
+113
-5
@@ -6,6 +6,11 @@ const messages: TranslationMap = {
|
||||
'nav.home': 'হোম',
|
||||
'nav.human': 'হিউম্যান',
|
||||
'nav.chat': 'চ্যাট',
|
||||
'nav.assistant': 'সহকারী',
|
||||
'assistant.faceMode.on': 'Tiny-র সাথে কথা বলছে',
|
||||
'assistant.faceMode.off': 'Tiny-র সাথে কথা বলুন',
|
||||
'assistant.faceMode.turnOn': 'মাসকট দেখান',
|
||||
'assistant.faceMode.turnOff': 'মাসকট লুকান',
|
||||
'nav.connections': 'সংযোগ',
|
||||
'nav.memory': 'ইন্টেলিজেন্স',
|
||||
'nav.alerts': 'সতর্কতা',
|
||||
@@ -15,6 +20,12 @@ const messages: TranslationMap = {
|
||||
'nav.switchAgentProfile': 'এজেন্ট প্রোফাইল বদলান',
|
||||
'nav.defaultAgentProfile': 'ডিফল্ট এজেন্ট',
|
||||
'nav.noAgentProfiles': 'কোনো এজেন্ট প্রোফাইল পাওয়া যায়নি',
|
||||
'nav.activity': 'কার্যকলাপ',
|
||||
'nav.avatarMenu.account': 'অ্যাকাউন্ট',
|
||||
'nav.avatarMenu.billing': 'বিলিং',
|
||||
'nav.avatarMenu.rewards': 'পুরস্কার',
|
||||
'nav.avatarMenu.invites': 'বন্ধুকে আমন্ত্রণ জানান',
|
||||
'nav.avatarMenu.wallet': 'ওয়ালেট',
|
||||
'common.cancel': 'বাতিল',
|
||||
'common.save': 'সংরক্ষণ',
|
||||
'common.confirm': 'নিশ্চিত করুন',
|
||||
@@ -59,6 +70,52 @@ const messages: TranslationMap = {
|
||||
'common.comingSoon': 'শীঘ্রই আসছে',
|
||||
'common.breadcrumb': 'ব্রেডক্রাম্ব',
|
||||
'settings.general': 'সাধারণ',
|
||||
// Settings layman groups (Phase 4 IA revamp)
|
||||
'settings.groups.account': 'অ্যাকাউন্ট',
|
||||
'settings.groups.assistant': 'সহকারী',
|
||||
'settings.groups.privacySecurity': 'গোপনীয়তা ও নিরাপত্তা',
|
||||
'settings.groups.notifications': 'বিজ্ঞপ্তি',
|
||||
'settings.groups.about': 'সম্পর্কে',
|
||||
'settings.assistant.personality': 'ব্যক্তিত্ব',
|
||||
'settings.assistant.personalityDesc': 'নাম, বিবরণ এবং SOUL.md ব্যক্তিত্ব',
|
||||
'settings.assistant.voice': 'ভয়েস',
|
||||
'settings.assistant.voiceDesc': 'স্পিচ-টু-টেক্সট ও টেক্সট-টু-স্পিচ সেটিংস',
|
||||
'settings.assistant.faceMascot': 'মুখ / মাসকট',
|
||||
'settings.assistant.faceMascotDesc': 'অ্যাপ জুড়ে ব্যবহৃত মাসকট রঙ বাছুন',
|
||||
'settings.assistant.backgroundActivity': 'ব্যাকগ্রাউন্ড কার্যকলাপ',
|
||||
'settings.assistant.backgroundActivityDesc':
|
||||
'আপনার সহকারী পটভূমিতে কতটা সক্রিয় তা নিয়ন্ত্রণ করুন',
|
||||
'settings.assistant.screenAwareness': 'স্ক্রিন সচেতনতা',
|
||||
'settings.assistant.screenAwarenessDesc': 'সহকারীকে আপনার সক্রিয় উইন্ডো দেখতে দিন',
|
||||
'settings.assistant.desktopCompanion': 'ডেস্কটপ সঙ্গী',
|
||||
'settings.assistant.desktopCompanionDesc': 'সিস্টেম ট্রে শর্টকাট সহ সর্বদা-চালু সঙ্গী মোড',
|
||||
'settings.assistant.permissions': 'অনুমতি',
|
||||
'settings.assistant.permissionsDesc': 'সহকারী কী করতে পারে এবং কোথায় কাজ করতে পারে তা বেছে নিন',
|
||||
'settings.privacySecurity.privacy': 'গোপনীয়তা',
|
||||
'settings.privacySecurity.privacyDesc': 'আপনার কম্পিউটার ছেড়ে যাওয়া ডেটা নিয়ন্ত্রণ করুন',
|
||||
'settings.privacySecurity.security': 'নিরাপত্তা',
|
||||
'settings.privacySecurity.securityDesc': 'সেশন ও সাইন-ইন বিকল্প',
|
||||
'settings.privacySecurity.approvalsHistory': 'অনুমোদন ও ইতিহাস',
|
||||
'settings.privacySecurity.approvalsHistoryDesc':
|
||||
'সাম্প্রতিক টুল-অনুমোদন সিদ্ধান্ত পর্যালোচনা করুন',
|
||||
'settings.notifications.menuTitle': 'বিজ্ঞপ্তি',
|
||||
'settings.notifications.menuDesc': 'সতর্কতা ইনবক্স ও বিজ্ঞপ্তি পছন্দ',
|
||||
'settings.devGroups.knowledgeMemory': 'জ্ঞান ও স্মৃতি',
|
||||
'settings.devGroups.agentsAutonomy': 'এজেন্ট ও স্বায়ত্তশাসন',
|
||||
'settings.devGroups.modelsInference': 'মডেল ও অনুমান',
|
||||
'settings.devGroups.automationIntegrations': 'অটোমেশন ও ইন্টিগ্রেশন',
|
||||
'settings.devGroups.toolsCapabilities': 'টুলস ও ক্ষমতা',
|
||||
'settings.devGroups.council': 'কাউন্সিল',
|
||||
'settings.analysisViews.title': 'বিশ্লেষণ ভিউ',
|
||||
'settings.analysisViews.menuDesc':
|
||||
'মেমরি গ্রাফ বিশ্লেষণ — ডায়াগ্রাম, কেন্দ্রীয়তা, সংহতি, সংযোগ, সতেজতা, টাইমলাইন, পাথ এবং নেমস্পেস',
|
||||
'settings.buildInfo.title': 'বিল্ড / সংস্করণ তথ্য',
|
||||
'settings.buildInfo.menuDesc': 'অ্যাপ বিল্ড, সংস্করণ এবং কোর সংযোগের বিবরণ',
|
||||
'settings.dataSync.title': 'ডেটা সিঙ্ক',
|
||||
'settings.dataSync.menuDesc': 'আপনার সহকারী যা সিঙ্ক করে — উৎস, সতেজতা এবং স্থিতি',
|
||||
'settings.dataSync.description':
|
||||
'আপনার সহকারীর মেমরিতে কী সিঙ্ক হয় তা পরিচালনা করুন: প্রতিটি সংযুক্ত উৎস তার শেষ সিঙ্কের সময়, কতটা সিঙ্ক হয়েছে এবং এখন সিঙ্ক হচ্ছে কিনা সহ।',
|
||||
'settings.devGroups.diagnosticsLogs': 'ডায়াগনস্টিকস ও লগ',
|
||||
'settings.featuresAndAI': 'ফিচার ও AI',
|
||||
'settings.billingAndRewards': 'বিলিং ও পুরস্কার',
|
||||
'settings.support': 'সহায়তা',
|
||||
@@ -87,6 +144,12 @@ const messages: TranslationMap = {
|
||||
'settings.developerOptions': 'অ্যাডভান্সড',
|
||||
'settings.developerOptionsDesc':
|
||||
'AI কনফিগারেশন, মেসেজিং চ্যানেল, টুলস, ডায়াগনস্টিক্স এবং ডিবাগ প্যানেল',
|
||||
'settings.developerDiagnostics': 'ডেভেলপার ও ডায়াগনস্টিক্স',
|
||||
'settings.developerDiagnosticsDesc':
|
||||
'উন্নত ডেভেলপার টুলস, ডায়াগনস্টিক্স, মেমোরি, এজেন্ট এবং ডিবাগ প্যানেল',
|
||||
'settings.developerMode.title': 'ডেভেলপার মোড',
|
||||
'settings.developerMode.description': 'উন্নত ডেভেলপার ও ডায়াগনস্টিক টুলস দেখান',
|
||||
'settings.developerMode.enabledByBuild': 'ডেভেলপমেন্ট বিল্ডে সবসময় চালু',
|
||||
'settings.clearAppData': 'অ্যাপ ডেটা মুছুন',
|
||||
'settings.clearAppDataDesc': 'সাইন আউট করুন এবং সব লোকাল ডেটা স্থায়ীভাবে মুছুন',
|
||||
'settings.logOut': 'লগ আউট',
|
||||
@@ -98,6 +161,14 @@ const messages: TranslationMap = {
|
||||
'settings.languageDesc': 'অ্যাপ ইন্টারফেসের প্রদর্শন ভাষা',
|
||||
'settings.alerts': 'সতর্কতা',
|
||||
'settings.alertsDesc': 'ইনবক্সে সাম্প্রতিক সতর্কতা ও কার্যক্রম দেখুন',
|
||||
'settings.account.profile': 'প্রোফাইল',
|
||||
'settings.account.profileDesc': 'নাম, ইমেইল এবং অ্যাভাটার',
|
||||
'settings.account.devices': 'ডিভাইস',
|
||||
'settings.account.devicesDesc': 'মোবাইল ডিভাইস পেয়ার করুন ও পরিচালনা করুন',
|
||||
'settings.account.teamMembers': 'টিম ও সদস্য',
|
||||
'settings.account.teamMembersDesc': 'টিম অ্যাক্সেস ও সদস্য ভূমিকা পরিচালনা করুন',
|
||||
'settings.account.dataMigration': 'ডেটা ও মাইগ্রেশন',
|
||||
'settings.account.dataMigrationDesc': 'অন্য অ্যাসিস্ট্যান্ট থেকে মেমরি আমদানি করুন',
|
||||
'settings.account.recoveryPhrase': 'রিকভারি ফ্রেজ',
|
||||
'settings.account.recoveryPhraseDesc': 'আপনার অ্যাকাউন্ট রিকভারি ফ্রেজ দেখুন ও ব্যাকআপ করুন',
|
||||
'settings.account.team': 'টিম',
|
||||
@@ -259,7 +330,7 @@ const messages: TranslationMap = {
|
||||
'skills.connected': 'সংযুক্ত',
|
||||
'skills.available': 'পাওয়া যাচ্ছে',
|
||||
'skills.addAccount': 'অ্যাকাউন্ট যোগ করুন',
|
||||
'skills.channels': 'চ্যানেল',
|
||||
'skills.channels': 'মেসেজিং',
|
||||
'skills.explorer.emptyCta': 'URL থেকে ইনস্টল করুন',
|
||||
'skills.explorer.emptyDescription':
|
||||
'একটি SKILL.md প্যাকেজ ইনস্টল করুন বা Hermes-ধাঁচের ফোল্ডার ~/.openhuman/skills-এ রাখুন।',
|
||||
@@ -287,9 +358,9 @@ const messages: TranslationMap = {
|
||||
'skills.explorer.installed': 'ইনস্টল করা হয়েছে',
|
||||
'skills.explorer.install': 'ইনস্টল করুন',
|
||||
'skills.explorer.installing': 'ইনস্টল হচ্ছে…',
|
||||
'skills.integrations': 'ইন্টিগ্রেশন',
|
||||
'skills.integrations': 'অ্যাপস',
|
||||
'skills.integrationsSubtitle':
|
||||
'ক্লাউড-ভিত্তিক OAuth সংযোগ — আপনার অ্যাকাউন্ট দিয়ে সাইন ইন করুন এবং Composio টোকেন পরিচালনা করে যাতে এজেন্টরা আপনার পক্ষে পড়তে এবং কাজ করতে পারে। কোনো API কী পরিচালনা করতে হবে না।',
|
||||
'ক্লাউড-ভিত্তিক OAuth সংযোগ — আপনার অ্যাকাউন্ট দিয়ে সাইন ইন করুন এবং টোকেন নিরাপদে পরিচালিত হয় যাতে এজেন্টরা আপনার পক্ষে পড়তে এবং কাজ করতে পারে। কোনো API কী পরিচালনা করতে হবে না।',
|
||||
'skills.composio.noApiKeyTitle': 'কোনো Composio API Key কনফিগার করা নেই',
|
||||
'skills.composio.noApiKeyDescription':
|
||||
'লোকাল মোডে আপনার নিজের Composio API key ব্যবহার হয়। এখানে ইন্টিগ্রেশন যুক্ত করার আগে Settings → Advanced → Composio খুলে একটি key যোগ করুন।',
|
||||
@@ -299,6 +370,11 @@ const messages: TranslationMap = {
|
||||
'skills.tabs.explorer': 'স্কিল',
|
||||
'skills.tabs.meetings': 'Google Meet মিটিং',
|
||||
'skills.tabs.mcp': 'MCP সার্ভার',
|
||||
'connections.tabs.apps': 'অ্যাপস',
|
||||
'connections.tabs.messaging': 'বার্তা পাঠানো',
|
||||
'connections.tabs.tools': 'সরঞ্জাম',
|
||||
'connections.tabs.explorer': 'এক্সপ্লোরার',
|
||||
'connections.tabs.talents': 'ট্যালেন্ট',
|
||||
'memory.title': 'মেমোরি',
|
||||
'memory.search': 'মেমোরি খুঁজুন...',
|
||||
'memory.noResults': 'কোনো মেমোরি পাওয়া যায়নি',
|
||||
@@ -702,6 +778,7 @@ const messages: TranslationMap = {
|
||||
'team.failedChangeRole': 'ভূমিকা পরিবর্তন করতে ব্যর্থ হয়েছে',
|
||||
'team.failedRemoveMember': 'সদস্য সরাতে ব্যর্থ হয়েছে',
|
||||
'devOptions.title': 'অ্যাডভান্সড',
|
||||
'devOptions.titleDiagnostics': 'ডেভেলপার ও ডায়াগনস্টিক্স',
|
||||
'devOptions.diagnostics': 'ডায়াগনস্টিক্স',
|
||||
'devOptions.diagnosticsDesc': 'সিস্টেম স্বাস্থ্য, লগ এবং পারফরম্যান্স মেট্রিক্স',
|
||||
'devOptions.toolPolicyDiagnosticsDesc': 'টুল- মেকআপ, নীতি পোস্ট, xqxqxs, এবং সম্প্রতি ব্যবহৃত',
|
||||
@@ -1821,7 +1898,7 @@ const messages: TranslationMap = {
|
||||
'chat.safetyTimeout':
|
||||
'২ মিনিট পরেও এজেন্টের কোনো সাড়া নেই। আবার চেষ্টা করুন বা সংযোগ পরীক্ষা করুন।',
|
||||
'chat.filter.general': 'সাধারণ',
|
||||
'chat.filter.subconscious': 'সাবকনশাস',
|
||||
'chat.filter.subconscious': 'ব্যাকগ্রাউন্ড কার্যকলাপ',
|
||||
'chat.filter.meetings': 'মিটিং',
|
||||
'chat.filter.tasks': 'টাস্ক',
|
||||
'chat.selectThread': 'একটি থ্রেড বেছে নিন',
|
||||
@@ -3833,6 +3910,10 @@ const messages: TranslationMap = {
|
||||
'বাহ্যিক MCP ক্লায়েন্টগুলিকে OpenHuman-এ সংযুক্ত করতে কনফিগার করুন',
|
||||
'settings.developerMenu.autonomy.title': 'এজেন্ট স্বায়ত্তশাসন',
|
||||
'settings.developerMenu.autonomy.desc': 'টুল অ্যাকশনের রেট সীমা এবং নিরাপত্তা থ্রেশহোল্ড',
|
||||
'settings.developerMenu.autocomplete.title': 'স্বয়ংসম্পূর্ণ',
|
||||
'settings.developerMenu.autocomplete.desc': 'AI ইনলাইন স্বয়ংসম্পূর্ণ সেটিংস এবং ডিবাগ প্যানেল',
|
||||
'settings.developerMenu.voiceDebug.title': 'ভয়েস (ডিবাগ)',
|
||||
'settings.developerMenu.voiceDebug.desc': 'ভয়েস ডিক্টেশন রানটাইম স্ট্যাটাস এবং ডিবাগ সেটিংস',
|
||||
'settings.mcpServer.title':
|
||||
'MCP সার্ভারের সাথে সংযোগ করতে বহিরাগত __BR1__ ক্লায়েন্ট কনফিগার করুন',
|
||||
'settings.mcpServer.toolsSectionTitle': 'উপলভ্য টুল',
|
||||
@@ -3934,6 +4015,27 @@ const messages: TranslationMap = {
|
||||
'settings.agentAccess.approvalHistoryDesc':
|
||||
'এজেন্টের অনুরোধ করা পূর্ববর্তী অনুমোদন/প্রত্যাখ্যান সিদ্ধান্ত পর্যালোচনা করুন।',
|
||||
'settings.agentAccess.viewApprovalHistory': 'অনুমোদনের ইতিহাস দেখুন',
|
||||
|
||||
// ── অনুমতি প্যানেল ───────────────────────────────────────────────
|
||||
'settings.permissions.title': 'অনুমতি',
|
||||
'settings.permissions.menuDesc':
|
||||
'আপনার সহকারী কী করতে পারে এবং কোথায় কাজ করতে পারে তা বেছে নিন।',
|
||||
'settings.permissions.accessMode': 'সহকারী কী করতে পারে?',
|
||||
'settings.permissions.accessModeDesc':
|
||||
'আপনার কম্পিউটারে পদক্ষেপ নেওয়ার সময় সহকারীর কতটুকু স্বাধীনতা থাকবে তা বেছে নিন।',
|
||||
'settings.permissions.preset.readonly.title': 'দেখো, স্পর্শ করো না',
|
||||
'settings.permissions.preset.readonly.desc':
|
||||
'সহকারী ফাইল পড়তে এবং অন্বেষণ করতে পারে — কিন্তু কখনো লিখতে, সম্পাদনা করতে বা এমন কিছু চালাতে পারবে না যা অবস্থা পরিবর্তন করে।',
|
||||
'settings.permissions.preset.supervised.title': 'আগে আমাকে জিজ্ঞেস করো',
|
||||
'settings.permissions.preset.supervised.desc':
|
||||
'নতুন ফাইল তৈরি করতে পারে, কিন্তু সম্পাদনা, কমান্ড চালানো বা নেটওয়ার্ক অ্যাক্সেসের আগে সবসময় আপনার অনুমতি চাইবে।',
|
||||
'settings.permissions.preset.full.title': 'পূর্ণ নিয়ন্ত্রণ',
|
||||
'settings.permissions.preset.full.desc':
|
||||
'আপনার পূর্ণ অ্যাকাউন্ট অ্যাক্সেস দিয়ে কাজ করে। ধ্বংসাত্মক কমান্ড, নেটওয়ার্ক অ্যাক্সেস এবং ইনস্টলেশন এখনও অনুমতি চাইবে।',
|
||||
'settings.permissions.folders': 'এটি কোথায় কাজ করতে পারে?',
|
||||
'settings.permissions.foldersDesc':
|
||||
'সহকারী যে ডিফল্ট ফোল্ডার পড়ে এবং লেখে। উন্নত সেটিংসে আপনি আরও ফোল্ডার যোগ করতে পারেন।',
|
||||
|
||||
'settings.sandbox.title': 'Sandbox কার্যকরী',
|
||||
'settings.sandbox.menuDesc': 'এজেন্ট টুল আইসোলেশনের জন্য Sandbox ব্যাকএন্ড কনফিগার করুন।',
|
||||
'settings.sandbox.loading': 'লোড হচ্ছে…',
|
||||
@@ -4149,7 +4251,7 @@ const messages: TranslationMap = {
|
||||
'skills.channelIcon.telegram': 'Telegram',
|
||||
'skills.channelIcon.web': 'ওয়েব',
|
||||
'skills.channelIcon.yuanbao': 'Yuanbao',
|
||||
'skills.composio.poweredBy': 'Composio দ্বারা চালিত',
|
||||
'skills.composio.poweredBy': 'OAuth',
|
||||
'skills.composio.staleStatusTitle': 'সংযোগগুলি পুরানো অবস্থা দেখাচ্ছে',
|
||||
'skills.create.allowedTools': 'অনুমোদিত টুল',
|
||||
'skills.create.allowedToolsHelp': 'হিসাবে SKILL.md ফ্রন্টম্যাটারে রেন্ডার করা হয়েছে',
|
||||
@@ -4777,6 +4879,12 @@ const messages: TranslationMap = {
|
||||
'autocomplete.maxChars': 'সর্বোচ্চ প্রসঙ্গ অক্ষর',
|
||||
'autocomplete.overlayTtlMs': 'ওভারলে টাইমআউট (মিলিসেকেন্ড)',
|
||||
'memory.tab.council': 'Council',
|
||||
'activity.tabs.automations': 'অটোমেশন',
|
||||
'activity.tabs.automationsDescription':
|
||||
'পুনর্ব্যবহারযোগ্য, রানযোগ্য পদ্ধতি — একটি লক্ষ্য এবং সেখানে পৌঁছানোর ধাপগুলি।',
|
||||
'activity.tabs.backgroundActivity': 'ব্যাকগ্রাউন্ড কার্যকলাপ',
|
||||
'activity.tabs.alerts': 'সতর্কতা',
|
||||
|
||||
'intelligence.agents.title': 'এজেন্ট লাইব্রেরি',
|
||||
'intelligence.agents.subtitle':
|
||||
'চালানো যায় এমন বিশেষজ্ঞদের দেখুন এবং নির্দিষ্ট এজেন্টকে একটি কাজ পাঠান।',
|
||||
|
||||
+116
-5
@@ -6,6 +6,11 @@ const messages: TranslationMap = {
|
||||
'nav.home': 'Start',
|
||||
'nav.human': 'Mensch',
|
||||
'nav.chat': 'Chat',
|
||||
'nav.assistant': 'Assistent',
|
||||
'assistant.faceMode.on': 'Spricht mit Tiny',
|
||||
'assistant.faceMode.off': 'Mit Tiny sprechen',
|
||||
'assistant.faceMode.turnOn': 'Maskottchen anzeigen',
|
||||
'assistant.faceMode.turnOff': 'Maskottchen ausblenden',
|
||||
'nav.connections': 'Verbindungen',
|
||||
'nav.memory': 'Intelligenz',
|
||||
'nav.alerts': 'Benachrichtigungen',
|
||||
@@ -15,6 +20,12 @@ const messages: TranslationMap = {
|
||||
'nav.switchAgentProfile': 'Agentenprofil wechseln',
|
||||
'nav.defaultAgentProfile': 'Standardagent',
|
||||
'nav.noAgentProfiles': 'Keine Agentenprofile gefunden',
|
||||
'nav.activity': 'Aktivität',
|
||||
'nav.avatarMenu.account': 'Konto',
|
||||
'nav.avatarMenu.billing': 'Abrechnung',
|
||||
'nav.avatarMenu.rewards': 'Prämien',
|
||||
'nav.avatarMenu.invites': 'Freund einladen',
|
||||
'nav.avatarMenu.wallet': 'Wallet',
|
||||
'common.cancel': 'Abbrechen',
|
||||
'common.save': 'Speichern',
|
||||
'common.confirm': 'Bestätigen',
|
||||
@@ -59,6 +70,54 @@ const messages: TranslationMap = {
|
||||
'common.comingSoon': 'Demnächst verfügbar',
|
||||
'common.breadcrumb': 'Breadcrumb',
|
||||
'settings.general': 'Allgemein',
|
||||
// Settings layman groups (Phase 4 IA revamp)
|
||||
'settings.groups.account': 'Konto',
|
||||
'settings.groups.assistant': 'Assistent',
|
||||
'settings.groups.privacySecurity': 'Datenschutz & Sicherheit',
|
||||
'settings.groups.notifications': 'Benachrichtigungen',
|
||||
'settings.groups.about': 'Über',
|
||||
'settings.assistant.personality': 'Persönlichkeit',
|
||||
'settings.assistant.personalityDesc': 'Name, Beschreibung und SOUL.md-Persona',
|
||||
'settings.assistant.voice': 'Stimme',
|
||||
'settings.assistant.voiceDesc': 'Sprache-zu-Text und Text-zu-Sprache Einstellungen',
|
||||
'settings.assistant.faceMascot': 'Gesicht / Maskottchen',
|
||||
'settings.assistant.faceMascotDesc': 'Maskottchen-Farbe in der App auswählen',
|
||||
'settings.assistant.backgroundActivity': 'Hintergrundaktivität',
|
||||
'settings.assistant.backgroundActivityDesc':
|
||||
'Steuern, wie aktiv Ihr Assistent im Hintergrund arbeitet',
|
||||
'settings.assistant.screenAwareness': 'Bildschirmbewusstsein',
|
||||
'settings.assistant.screenAwarenessDesc': 'Dem Assistenten das aktive Fenster zeigen',
|
||||
'settings.assistant.desktopCompanion': 'Desktop-Begleiter',
|
||||
'settings.assistant.desktopCompanionDesc':
|
||||
'Immer aktiver Begleiter-Modus mit System-Tray-Shortcut',
|
||||
'settings.assistant.permissions': 'Berechtigungen',
|
||||
'settings.assistant.permissionsDesc': 'Wähle, was der Assistent tun darf und wo er arbeiten kann',
|
||||
'settings.privacySecurity.privacy': 'Datenschutz',
|
||||
'settings.privacySecurity.privacyDesc': 'Kontrollieren, welche Daten Ihren Computer verlassen',
|
||||
'settings.privacySecurity.security': 'Sicherheit',
|
||||
'settings.privacySecurity.securityDesc': 'Sitzungen und Anmeldeoptionen',
|
||||
'settings.privacySecurity.approvalsHistory': 'Genehmigungen & Verlauf',
|
||||
'settings.privacySecurity.approvalsHistoryDesc':
|
||||
'Aktuelle Tool-Genehmigungsentscheidungen überprüfen',
|
||||
'settings.notifications.menuTitle': 'Benachrichtigungen',
|
||||
'settings.notifications.menuDesc': 'Benachrichtigungs-Posteingang und -Einstellungen',
|
||||
'settings.devGroups.knowledgeMemory': 'Wissen & Gedächtnis',
|
||||
'settings.devGroups.agentsAutonomy': 'Agenten & Autonomie',
|
||||
'settings.devGroups.modelsInference': 'Modelle & Inferenz',
|
||||
'settings.devGroups.automationIntegrations': 'Automatisierung & Integrationen',
|
||||
'settings.devGroups.toolsCapabilities': 'Tools & Fähigkeiten',
|
||||
'settings.devGroups.council': 'Rat',
|
||||
'settings.analysisViews.title': 'Analyseansichten',
|
||||
'settings.analysisViews.menuDesc':
|
||||
'Speichergraph-Analyse — Diagramm, Zentralität, Kohäsion, Verknüpfungen, Aktualität, Zeitachse, Pfade und Namensräume',
|
||||
'settings.buildInfo.title': 'Build-/Versionsinfo',
|
||||
'settings.buildInfo.menuDesc': 'App-Build, Version und Details zur Core-Verbindung',
|
||||
'settings.dataSync.title': 'Datensynchronisierung',
|
||||
'settings.dataSync.menuDesc':
|
||||
'Was dein Assistent synchronisiert — Quellen, Aktualität und Status',
|
||||
'settings.dataSync.description':
|
||||
'Verwalte, was in den Speicher deines Assistenten synchronisiert wird: jede verbundene Quelle mit ihrer letzten Synchronisierungszeit, wie viel synchronisiert ist und ob gerade synchronisiert wird.',
|
||||
'settings.devGroups.diagnosticsLogs': 'Diagnose & Protokolle',
|
||||
'settings.featuresAndAI': 'Funktionen und KI',
|
||||
'settings.billingAndRewards': 'Abrechnung und Prämien',
|
||||
'settings.support': 'Unterstützung',
|
||||
@@ -87,6 +146,12 @@ const messages: TranslationMap = {
|
||||
'settings.developerOptions': 'Fortgeschritten',
|
||||
'settings.developerOptionsDesc':
|
||||
'KI-Konfiguration, Nachrichtenkanäle, Tools, Diagnose und Debug-Panels',
|
||||
'settings.developerDiagnostics': 'Entwickler & Diagnose',
|
||||
'settings.developerDiagnosticsDesc':
|
||||
'Erweiterte Entwickler-Tools, Diagnose, Speicher, Agents und Debug-Panels',
|
||||
'settings.developerMode.title': 'Entwicklermodus',
|
||||
'settings.developerMode.description': 'Erweiterte Entwickler- und Diagnose-Tools anzeigen',
|
||||
'settings.developerMode.enabledByBuild': 'In Entwicklungs-Builds immer aktiviert',
|
||||
'settings.clearAppData': 'App-Daten löschen',
|
||||
'settings.clearAppDataDesc': 'Melde dich ab und lösche alle lokalen App-Daten dauerhaft',
|
||||
'settings.logOut': 'Abmelden',
|
||||
@@ -99,6 +164,14 @@ const messages: TranslationMap = {
|
||||
'settings.alerts': 'Warnungen',
|
||||
'settings.alertsDesc':
|
||||
'Sieh dir aktuelle Benachrichtigungen und Aktivitäten in deinem Posteingang an',
|
||||
'settings.account.profile': 'Profil',
|
||||
'settings.account.profileDesc': 'Name, E-Mail und Avatar',
|
||||
'settings.account.devices': 'Geräte',
|
||||
'settings.account.devicesDesc': 'Mobile Geräte koppeln und verwalten',
|
||||
'settings.account.teamMembers': 'Team & Mitglieder',
|
||||
'settings.account.teamMembersDesc': 'Teamzugang und Mitgliederrollen verwalten',
|
||||
'settings.account.dataMigration': 'Daten & Migration',
|
||||
'settings.account.dataMigrationDesc': 'Erinnerungen aus einem anderen Assistenten importieren',
|
||||
'settings.account.recoveryPhrase': 'Wiederherstellungssatz',
|
||||
'settings.account.recoveryPhraseDesc':
|
||||
'Sieh dir deinen Kontowiederherstellungssatz an und sichere ihn',
|
||||
@@ -268,7 +341,7 @@ const messages: TranslationMap = {
|
||||
'skills.connected': 'Verbunden',
|
||||
'skills.available': 'Verfügbar',
|
||||
'skills.addAccount': 'Konto hinzufügen',
|
||||
'skills.channels': 'Kanäle',
|
||||
'skills.channels': 'Messaging',
|
||||
'skills.explorer.emptyCta': 'Von URL installieren',
|
||||
'skills.explorer.emptyDescription':
|
||||
'Installiere ein SKILL.md-Paket oder lege Hermes-artige Ordner unter ~/.openhuman/skills ab.',
|
||||
@@ -296,9 +369,9 @@ const messages: TranslationMap = {
|
||||
'skills.explorer.installed': 'Installiert',
|
||||
'skills.explorer.install': 'Installieren',
|
||||
'skills.explorer.installing': 'Wird installiert…',
|
||||
'skills.integrations': 'Integrationen',
|
||||
'skills.integrations': 'Apps',
|
||||
'skills.integrationsSubtitle':
|
||||
'Cloud-basierte OAuth-Verbindungen – mit deinem Konto anmelden und Composio verwaltet die Tokens, damit Agenten in deinem Namen lesen und handeln können. Keine API-Schlüssel notwendig.',
|
||||
'Cloud-basierte OAuth-Verbindungen – mit deinem Konto anmelden und Tokens werden sicher verwaltet, damit Agenten in deinem Namen lesen und handeln können. Keine API-Schlüssel notwendig.',
|
||||
'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.',
|
||||
@@ -308,6 +381,11 @@ const messages: TranslationMap = {
|
||||
'skills.tabs.explorer': 'Skills',
|
||||
'skills.tabs.meetings': 'Google Meet-Besprechungen',
|
||||
'skills.tabs.mcp': 'MCP Server',
|
||||
'connections.tabs.apps': 'Apps',
|
||||
'connections.tabs.messaging': 'Nachrichten',
|
||||
'connections.tabs.tools': 'Werkzeuge',
|
||||
'connections.tabs.explorer': 'Erkunden',
|
||||
'connections.tabs.talents': 'Talente',
|
||||
'memory.title': 'Erinnerung',
|
||||
'memory.search': 'Erinnerungen suchen...',
|
||||
'memory.noResults': 'Keine Erinnerungen gefunden',
|
||||
@@ -719,6 +797,7 @@ const messages: TranslationMap = {
|
||||
'team.failedChangeRole': 'Rolle konnte nicht geändert werden.',
|
||||
'team.failedRemoveMember': 'Mitglied konnte nicht entfernt werden.',
|
||||
'devOptions.title': 'Fortgeschritten',
|
||||
'devOptions.titleDiagnostics': 'Entwickler & Diagnose',
|
||||
'devOptions.diagnostics': 'Diagnose',
|
||||
'devOptions.diagnosticsDesc': 'Systemzustand, Protokolle und Leistungsmetriken',
|
||||
'devOptions.toolPolicyDiagnosticsDesc':
|
||||
@@ -1868,7 +1947,7 @@ const messages: TranslationMap = {
|
||||
'chat.safetyTimeout':
|
||||
'Keine Antwort vom Agenten nach 2 Minuten. Versuche es erneut oder prüfe deine Verbindung.',
|
||||
'chat.filter.general': 'Allgemein',
|
||||
'chat.filter.subconscious': 'Unterbewusstsein',
|
||||
'chat.filter.subconscious': 'Hintergrundaktivität',
|
||||
'chat.filter.meetings': 'Meetings',
|
||||
'chat.filter.tasks': 'Aufgaben',
|
||||
'chat.selectThread': 'Wähle einen Thread aus',
|
||||
@@ -3932,6 +4011,12 @@ const messages: TranslationMap = {
|
||||
'settings.developerMenu.autonomy.title': 'Agent-Autonomie',
|
||||
'settings.developerMenu.autonomy.desc':
|
||||
'Aktionsraten-Limits und Sicherheitsschwellen für Werkzeuge',
|
||||
'settings.developerMenu.autocomplete.title': 'Autovervollständigung',
|
||||
'settings.developerMenu.autocomplete.desc':
|
||||
'KI-Inline-Autovervollständigung Einstellungen und Debug-Panel',
|
||||
'settings.developerMenu.voiceDebug.title': 'Sprache (Debug)',
|
||||
'settings.developerMenu.voiceDebug.desc':
|
||||
'Laufzeitstatus und Debug-Einstellungen für die Sprachdiktierung',
|
||||
'settings.mcpServer.title': 'MCP-Server',
|
||||
'settings.mcpServer.toolsSectionTitle': 'Verfügbare Werkzeuge',
|
||||
'settings.mcpServer.toolsSectionDesc':
|
||||
@@ -4035,6 +4120,26 @@ const messages: TranslationMap = {
|
||||
'settings.agentAccess.approvalHistoryDesc':
|
||||
'Vergangene Genehmigen-/Ablehnen-Entscheidungen des Agenten überprüfen.',
|
||||
'settings.agentAccess.viewApprovalHistory': 'Genehmigungsverlauf anzeigen',
|
||||
|
||||
// ── Berechtigungen-Panel ──────────────────────────────────────────
|
||||
'settings.permissions.title': 'Berechtigungen',
|
||||
'settings.permissions.menuDesc': 'Wähle, was dein Assistent tun darf und wo er arbeiten kann.',
|
||||
'settings.permissions.accessMode': 'Was darf der Assistent tun?',
|
||||
'settings.permissions.accessModeDesc':
|
||||
'Wähle, wie viel Freiheit der Assistent hat, wenn er Aktionen auf deinem Computer ausführt.',
|
||||
'settings.permissions.preset.readonly.title': 'Schauen, nicht anfassen',
|
||||
'settings.permissions.preset.readonly.desc':
|
||||
'Der Assistent kann Dateien lesen und erkunden – aber niemals schreiben, bearbeiten oder etwas ausführen, das den Zustand ändert.',
|
||||
'settings.permissions.preset.supervised.title': 'Erst fragen',
|
||||
'settings.permissions.preset.supervised.desc':
|
||||
'Kann neue Dateien frei erstellen, fragt aber immer um Erlaubnis, bevor er bearbeitet, Befehle ausführt oder auf das Netzwerk zugreift.',
|
||||
'settings.permissions.preset.full.title': 'Volle Kontrolle',
|
||||
'settings.permissions.preset.full.desc':
|
||||
'Läuft mit deinem vollen Kontozugriff. Destruktive Befehle, Netzwerkzugriff und Installationen erfordern weiterhin eine Genehmigung.',
|
||||
'settings.permissions.folders': 'Wo darf er arbeiten?',
|
||||
'settings.permissions.foldersDesc':
|
||||
'Der Standardordner, den der Assistent liest und beschreibt. In den erweiterten Einstellungen kannst du weitere Ordner hinzufügen.',
|
||||
|
||||
'settings.sandbox.title': 'Sandbox-Ausführung',
|
||||
'settings.sandbox.menuDesc':
|
||||
'Sandbox-Backends für die Isolation von Agentenwerkzeugen konfigurieren.',
|
||||
@@ -4259,7 +4364,7 @@ const messages: TranslationMap = {
|
||||
'skills.channelIcon.telegram': 'Telegram',
|
||||
'skills.channelIcon.web': 'Web',
|
||||
'skills.channelIcon.yuanbao': 'Yuanbao',
|
||||
'skills.composio.poweredBy': 'Unterstützt von Composio',
|
||||
'skills.composio.poweredBy': 'OAuth',
|
||||
'skills.composio.staleStatusTitle': 'Verbindungen zeigen den veralteten Status an',
|
||||
'skills.create.allowedTools': 'Erlaubte Werkzeuge',
|
||||
'skills.create.allowedToolsHelp': 'Im SKILL.md-Frontmatter gerendert als',
|
||||
@@ -4904,6 +5009,12 @@ const messages: TranslationMap = {
|
||||
'autocomplete.maxChars': 'Maximale Kontextzeichen',
|
||||
'autocomplete.overlayTtlMs': 'Overlay-Timeout (ms)',
|
||||
'memory.tab.council': 'Council',
|
||||
'activity.tabs.automations': 'Automatisierungen',
|
||||
'activity.tabs.automationsDescription':
|
||||
'Wiederverwendbare, ausführbare Abläufe — ein Ziel und die Schritte dorthin.',
|
||||
'activity.tabs.backgroundActivity': 'Hintergrundaktivität',
|
||||
'activity.tabs.alerts': 'Benachrichtigungen',
|
||||
|
||||
'intelligence.agents.title': 'Agentenbibliothek',
|
||||
'intelligence.agents.subtitle':
|
||||
'Prüfe ausführbare Spezialisten und sende eine Aufgabe an einen benannten Agenten.',
|
||||
|
||||
+130
-6
@@ -5,6 +5,13 @@ const en: TranslationMap = {
|
||||
'nav.home': 'Home',
|
||||
'nav.human': 'Human',
|
||||
'nav.chat': 'Chat',
|
||||
'nav.assistant': 'Assistant',
|
||||
|
||||
// Assistant surface — face mode toggle (Phase 6)
|
||||
'assistant.faceMode.on': 'Talking to Tiny',
|
||||
'assistant.faceMode.off': 'Talk to Tiny',
|
||||
'assistant.faceMode.turnOn': 'Show mascot face',
|
||||
'assistant.faceMode.turnOff': 'Hide mascot face',
|
||||
'nav.connections': 'Connections',
|
||||
'nav.memory': 'Intelligence',
|
||||
'nav.alerts': 'Alerts',
|
||||
@@ -14,6 +21,12 @@ const en: TranslationMap = {
|
||||
'nav.switchAgentProfile': 'Switch agent profile',
|
||||
'nav.defaultAgentProfile': 'Default agent',
|
||||
'nav.noAgentProfiles': 'No agent profiles found',
|
||||
'nav.activity': 'Activity',
|
||||
'nav.avatarMenu.account': 'Account',
|
||||
'nav.avatarMenu.billing': 'Billing',
|
||||
'nav.avatarMenu.rewards': 'Rewards',
|
||||
'nav.avatarMenu.invites': 'Invite a friend',
|
||||
'nav.avatarMenu.wallet': 'Wallet',
|
||||
|
||||
// Common
|
||||
'common.cancel': 'Cancel',
|
||||
@@ -69,6 +82,60 @@ const en: TranslationMap = {
|
||||
'settings.dangerZone': 'Danger Zone',
|
||||
'settings.account': 'Account',
|
||||
'settings.accountDesc': 'Recovery phrase, team, connections, and privacy',
|
||||
|
||||
// Settings layman groups (Phase 4 IA revamp)
|
||||
'settings.groups.account': 'Account',
|
||||
'settings.groups.assistant': 'Assistant',
|
||||
'settings.groups.privacySecurity': 'Privacy & Security',
|
||||
'settings.groups.notifications': 'Notifications',
|
||||
'settings.groups.about': 'About',
|
||||
|
||||
// Settings — assistant group items
|
||||
'settings.assistant.personality': 'Personality',
|
||||
'settings.assistant.personalityDesc': 'Name, description, and SOUL.md persona',
|
||||
'settings.assistant.voice': 'Voice',
|
||||
'settings.assistant.voiceDesc': 'Speech-to-text and text-to-speech settings',
|
||||
'settings.assistant.faceMascot': 'Face / Mascot',
|
||||
'settings.assistant.faceMascotDesc': 'Pick the mascot color used across the app',
|
||||
'settings.assistant.backgroundActivity': 'Background activity',
|
||||
'settings.assistant.backgroundActivityDesc':
|
||||
'Control how actively your assistant works in the background',
|
||||
'settings.assistant.screenAwareness': 'Screen awareness',
|
||||
'settings.assistant.screenAwarenessDesc': 'Let the assistant see your active window',
|
||||
'settings.assistant.desktopCompanion': 'Desktop companion',
|
||||
'settings.assistant.desktopCompanionDesc': 'Always-on companion mode with a system-tray shortcut',
|
||||
'settings.assistant.permissions': 'Permissions',
|
||||
'settings.assistant.permissionsDesc': 'Choose what the assistant can do and where it can work',
|
||||
|
||||
// Settings — privacy & security group items
|
||||
'settings.privacySecurity.privacy': 'Privacy',
|
||||
'settings.privacySecurity.privacyDesc': 'Control what data leaves your computer',
|
||||
'settings.privacySecurity.security': 'Security',
|
||||
'settings.privacySecurity.securityDesc': 'Sessions and sign-in options',
|
||||
'settings.privacySecurity.approvalsHistory': 'Approvals & history',
|
||||
'settings.privacySecurity.approvalsHistoryDesc': 'Review recent tool-approval decisions',
|
||||
|
||||
// Settings — notifications group items
|
||||
'settings.notifications.menuTitle': 'Notifications',
|
||||
'settings.notifications.menuDesc': 'Alerts inbox and notification preferences',
|
||||
|
||||
// Developer & Diagnostics — 7 sub-section group labels
|
||||
'settings.devGroups.knowledgeMemory': 'Knowledge & Memory',
|
||||
'settings.devGroups.agentsAutonomy': 'Agents & Autonomy',
|
||||
'settings.devGroups.modelsInference': 'Models & Inference',
|
||||
'settings.devGroups.automationIntegrations': 'Automation & Integrations',
|
||||
'settings.devGroups.toolsCapabilities': 'Tools & Capabilities',
|
||||
'settings.devGroups.council': 'Council',
|
||||
'settings.analysisViews.title': 'Analysis views',
|
||||
'settings.analysisViews.menuDesc':
|
||||
'Memory graph analysis — diagram, centrality, cohesion, associations, freshness, timeline, paths, and namespaces',
|
||||
'settings.buildInfo.title': 'Build / version info',
|
||||
'settings.buildInfo.menuDesc': 'App build, version, and core connection details',
|
||||
'settings.dataSync.title': 'Data Sync',
|
||||
'settings.dataSync.menuDesc': 'What your assistant syncs — sources, freshness, and status',
|
||||
'settings.dataSync.description':
|
||||
"Manage what gets synced into your assistant's memory: every connected source with its last-synced time, how much is synced, and whether it's syncing right now.",
|
||||
'settings.devGroups.diagnosticsLogs': 'Diagnostics & Logs',
|
||||
'settings.notifications': 'Notifications',
|
||||
'settings.notificationsDesc': 'Do Not Disturb and per-account notification controls',
|
||||
'settings.notifications.tabs.preferences': 'Preferences',
|
||||
@@ -90,6 +157,14 @@ const en: TranslationMap = {
|
||||
'settings.developerOptions': 'Advanced',
|
||||
'settings.developerOptionsDesc':
|
||||
'AI configuration, messaging channels, tools, diagnostics, and debug panels',
|
||||
// Developer & Diagnostics (renamed from "Developer Options")
|
||||
'settings.developerDiagnostics': 'Developer & Diagnostics',
|
||||
'settings.developerDiagnosticsDesc':
|
||||
'Advanced developer tools, diagnostics, memory, agents, and debug panels',
|
||||
// Runtime Developer Mode toggle (in About panel)
|
||||
'settings.developerMode.title': 'Developer mode',
|
||||
'settings.developerMode.description': 'Show advanced developer & diagnostic tools',
|
||||
'settings.developerMode.enabledByBuild': 'Always on in development builds',
|
||||
'settings.clearAppData': 'Clear App Data',
|
||||
'settings.clearAppDataDesc': 'Sign out and permanently clear all local app data',
|
||||
'settings.logOut': 'Log out',
|
||||
@@ -102,6 +177,16 @@ const en: TranslationMap = {
|
||||
'settings.alerts': 'Alerts',
|
||||
'settings.alertsDesc': 'View recent alerts and activity in your inbox',
|
||||
|
||||
// Settings: Account — IA revamp group items (SettingsHome.tsx)
|
||||
'settings.account.profile': 'Profile',
|
||||
'settings.account.profileDesc': 'Name, email, and avatar',
|
||||
'settings.account.devices': 'Devices',
|
||||
'settings.account.devicesDesc': 'Pair and manage mobile devices',
|
||||
'settings.account.teamMembers': 'Team & members',
|
||||
'settings.account.teamMembersDesc': 'Manage team access and member roles',
|
||||
'settings.account.dataMigration': 'Data & migration',
|
||||
'settings.account.dataMigrationDesc': 'Import memory from another assistant',
|
||||
|
||||
// Settings: Account
|
||||
'settings.account.recoveryPhrase': 'Recovery Phrase',
|
||||
'settings.account.recoveryPhraseDesc': 'View and back up your account recovery phrase',
|
||||
@@ -284,7 +369,7 @@ const en: TranslationMap = {
|
||||
'skills.connected': 'Connected',
|
||||
'skills.available': 'Available',
|
||||
'skills.addAccount': 'Add Account',
|
||||
'skills.channels': 'Channels',
|
||||
'skills.channels': 'Messaging',
|
||||
'skills.explorer.emptyCta': 'Install from URL',
|
||||
'skills.explorer.emptyDescription':
|
||||
'Install a SKILL.md package or place Hermes-style folders under ~/.openhuman/skills.',
|
||||
@@ -312,9 +397,9 @@ const en: TranslationMap = {
|
||||
'skills.explorer.installed': 'Installed',
|
||||
'skills.explorer.install': 'Install',
|
||||
'skills.explorer.installing': 'Installing…',
|
||||
'skills.integrations': 'Composio Integrations',
|
||||
'skills.integrations': 'Apps',
|
||||
'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.',
|
||||
'Cloud-based OAuth connections — sign in with your account and tokens are brokered securely 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.',
|
||||
@@ -324,6 +409,12 @@ const en: TranslationMap = {
|
||||
'skills.tabs.explorer': 'Skills',
|
||||
'skills.tabs.meetings': 'Google Meet',
|
||||
'skills.tabs.mcp': 'MCP Servers',
|
||||
// Connections page tabs (Phase 2 rename)
|
||||
'connections.tabs.apps': 'Apps',
|
||||
'connections.tabs.messaging': 'Messaging',
|
||||
'connections.tabs.tools': 'Tools',
|
||||
'connections.tabs.explorer': 'Explorer',
|
||||
'connections.tabs.talents': 'Talents',
|
||||
// Intelligence / Memory
|
||||
'memory.title': 'Memory',
|
||||
'memory.search': 'Search memories...',
|
||||
@@ -353,6 +444,14 @@ const en: TranslationMap = {
|
||||
'memory.tab.cohesion': 'Cohesion',
|
||||
'memory.tab.settings': 'Settings',
|
||||
'memory.tab.council': 'Council',
|
||||
|
||||
// Activity surface — Phase 3 renamed tabs
|
||||
'activity.tabs.automations': 'Automations',
|
||||
'activity.tabs.automationsDescription':
|
||||
'Reusable, runnable procedures — a goal plus the steps to reach it. Create one, install from a URL, or open a workflow to run it.',
|
||||
'activity.tabs.backgroundActivity': 'Background activity',
|
||||
'activity.tabs.alerts': 'Alerts',
|
||||
|
||||
'intelligence.agents.title': 'Agents Library',
|
||||
'intelligence.agents.subtitle':
|
||||
'Inspect runnable specialists and send one task to a named agent.',
|
||||
@@ -1014,8 +1113,9 @@ const en: TranslationMap = {
|
||||
'team.failedChangeRole': 'Failed to change role',
|
||||
'team.failedRemoveMember': 'Failed to remove member',
|
||||
|
||||
// Developer Options
|
||||
// Developer Options / Developer & Diagnostics
|
||||
'devOptions.title': 'Advanced',
|
||||
'devOptions.titleDiagnostics': 'Developer & Diagnostics',
|
||||
'devOptions.diagnostics': 'Diagnostics',
|
||||
'devOptions.diagnosticsDesc': 'System health, logs, and performance metrics',
|
||||
'devOptions.toolPolicyDiagnosticsDesc':
|
||||
@@ -2196,7 +2296,7 @@ const en: TranslationMap = {
|
||||
'chat.safetyTimeout':
|
||||
'No response from the agent after 2 minutes. Try again or check your connection.',
|
||||
'chat.filter.general': 'General',
|
||||
'chat.filter.subconscious': 'Subconscious',
|
||||
'chat.filter.subconscious': 'Background activity',
|
||||
'chat.filter.meetings': 'Meetings',
|
||||
'chat.filter.tasks': 'Tasks',
|
||||
'chat.selectThread': 'Select a thread',
|
||||
@@ -4351,6 +4451,11 @@ const en: TranslationMap = {
|
||||
'settings.developerMenu.mcpServer.desc': 'Configure external MCP clients to connect to OpenHuman',
|
||||
'settings.developerMenu.autonomy.title': 'Agent autonomy',
|
||||
'settings.developerMenu.autonomy.desc': 'Tool action rate limits and safety thresholds',
|
||||
// Tools & Capabilities group — autocomplete and voice debug (doc § Tools & Capabilities)
|
||||
'settings.developerMenu.autocomplete.title': 'Autocomplete',
|
||||
'settings.developerMenu.autocomplete.desc': 'AI inline autocomplete settings and debug panel',
|
||||
'settings.developerMenu.voiceDebug.title': 'Voice (debug)',
|
||||
'settings.developerMenu.voiceDebug.desc': 'Voice dictation runtime status and debug settings',
|
||||
'settings.mcpServer.title': 'MCP Server',
|
||||
'settings.mcpServer.toolsSectionTitle': 'Available Tools',
|
||||
'settings.mcpServer.toolsSectionDesc':
|
||||
@@ -4453,6 +4558,25 @@ const en: TranslationMap = {
|
||||
'Review past Approve / Deny decisions the agent requested.',
|
||||
'settings.agentAccess.viewApprovalHistory': 'View approval history',
|
||||
|
||||
// ── Permissions panel (layman split from Agent Access, Deferred-3) ────────
|
||||
'settings.permissions.title': 'Permissions',
|
||||
'settings.permissions.menuDesc': 'Choose what your assistant can do and where it can work.',
|
||||
'settings.permissions.accessMode': 'What can the assistant do?',
|
||||
'settings.permissions.accessModeDesc':
|
||||
'Choose how much freedom the assistant has when it takes actions on your computer.',
|
||||
'settings.permissions.preset.readonly.title': "Look, don't touch",
|
||||
'settings.permissions.preset.readonly.desc':
|
||||
'The assistant can read files and explore — but never write, edit, or run anything that changes state.',
|
||||
'settings.permissions.preset.supervised.title': 'Ask me first',
|
||||
'settings.permissions.preset.supervised.desc':
|
||||
'Can create new files freely, but always asks your approval before editing, running commands, or accessing the network.',
|
||||
'settings.permissions.preset.full.title': 'Full control',
|
||||
'settings.permissions.preset.full.desc':
|
||||
'Runs with your full account access. Destructive commands, network access, and installs still ask for approval.',
|
||||
'settings.permissions.folders': 'Where can it work?',
|
||||
'settings.permissions.foldersDesc':
|
||||
'The default folder the assistant reads and writes. You can add more folders in Advanced settings.',
|
||||
|
||||
// ── Sandbox execution backend ─────────────────────────────────────
|
||||
'settings.sandbox.title': 'Sandbox execution',
|
||||
'settings.sandbox.menuDesc': 'Configure sandbox backends for agent tool isolation.',
|
||||
@@ -4672,7 +4796,7 @@ const en: TranslationMap = {
|
||||
'skills.channelIcon.telegram': 'Telegram',
|
||||
'skills.channelIcon.web': 'Web',
|
||||
'skills.channelIcon.yuanbao': 'Yuanbao',
|
||||
'skills.composio.poweredBy': 'Powered by Composio',
|
||||
'skills.composio.poweredBy': 'OAuth',
|
||||
'skills.composio.staleStatusTitle': 'Connections are showing stale status',
|
||||
'skills.create.allowedTools': 'Allowed tools',
|
||||
'skills.create.allowedToolsHelp': 'Rendered into the SKILL.md frontmatter as',
|
||||
|
||||
+116
-5
@@ -6,6 +6,11 @@ const messages: TranslationMap = {
|
||||
'nav.home': 'Inicio',
|
||||
'nav.human': 'Humano',
|
||||
'nav.chat': 'Charla',
|
||||
'nav.assistant': 'Asistente',
|
||||
'assistant.faceMode.on': 'Hablando con Tiny',
|
||||
'assistant.faceMode.off': 'Habla con Tiny',
|
||||
'assistant.faceMode.turnOn': 'Mostrar mascota',
|
||||
'assistant.faceMode.turnOff': 'Ocultar mascota',
|
||||
'nav.connections': 'Conexiones',
|
||||
'nav.memory': 'Inteligencia',
|
||||
'nav.alerts': 'Alertas',
|
||||
@@ -15,6 +20,12 @@ const messages: TranslationMap = {
|
||||
'nav.switchAgentProfile': 'Cambiar perfil de agente',
|
||||
'nav.defaultAgentProfile': 'Agente predeterminado',
|
||||
'nav.noAgentProfiles': 'No se encontraron perfiles de agente',
|
||||
'nav.activity': 'Actividad',
|
||||
'nav.avatarMenu.account': 'Cuenta',
|
||||
'nav.avatarMenu.billing': 'Facturación',
|
||||
'nav.avatarMenu.rewards': 'Recompensas',
|
||||
'nav.avatarMenu.invites': 'Invitar a un amigo',
|
||||
'nav.avatarMenu.wallet': 'Cartera',
|
||||
'common.cancel': 'Cancelar',
|
||||
'common.save': 'Guardar',
|
||||
'common.confirm': 'Confirmar',
|
||||
@@ -59,6 +70,53 @@ const messages: TranslationMap = {
|
||||
'common.comingSoon': 'Próximamente',
|
||||
'common.breadcrumb': 'Miga de pan',
|
||||
'settings.general': 'generales',
|
||||
// Settings layman groups (Phase 4 IA revamp)
|
||||
'settings.groups.account': 'Cuenta',
|
||||
'settings.groups.assistant': 'Asistente',
|
||||
'settings.groups.privacySecurity': 'Privacidad y seguridad',
|
||||
'settings.groups.notifications': 'Notificaciones',
|
||||
'settings.groups.about': 'Acerca de',
|
||||
'settings.assistant.personality': 'Personalidad',
|
||||
'settings.assistant.personalityDesc': 'Nombre, descripción y persona SOUL.md',
|
||||
'settings.assistant.voice': 'Voz',
|
||||
'settings.assistant.voiceDesc': 'Configuración de voz a texto y texto a voz',
|
||||
'settings.assistant.faceMascot': 'Cara / Mascota',
|
||||
'settings.assistant.faceMascotDesc': 'Elige el color de la mascota en la aplicación',
|
||||
'settings.assistant.backgroundActivity': 'Actividad en segundo plano',
|
||||
'settings.assistant.backgroundActivityDesc':
|
||||
'Controla qué tan activo trabaja tu asistente en segundo plano',
|
||||
'settings.assistant.screenAwareness': 'Conciencia de pantalla',
|
||||
'settings.assistant.screenAwarenessDesc': 'Permite al asistente ver tu ventana activa',
|
||||
'settings.assistant.desktopCompanion': 'Compañero de escritorio',
|
||||
'settings.assistant.desktopCompanionDesc':
|
||||
'Modo compañero siempre activo con acceso directo en la bandeja del sistema',
|
||||
'settings.assistant.permissions': 'Permisos',
|
||||
'settings.assistant.permissionsDesc': 'Elige qué puede hacer el asistente y dónde puede trabajar',
|
||||
'settings.privacySecurity.privacy': 'Privacidad',
|
||||
'settings.privacySecurity.privacyDesc': 'Controla qué datos salen de tu ordenador',
|
||||
'settings.privacySecurity.security': 'Seguridad',
|
||||
'settings.privacySecurity.securityDesc': 'Sesiones y opciones de inicio de sesión',
|
||||
'settings.privacySecurity.approvalsHistory': 'Aprobaciones e historial',
|
||||
'settings.privacySecurity.approvalsHistoryDesc':
|
||||
'Revisa las decisiones de aprobación de herramientas recientes',
|
||||
'settings.notifications.menuTitle': 'Notificaciones',
|
||||
'settings.notifications.menuDesc': 'Bandeja de alertas y preferencias de notificaciones',
|
||||
'settings.devGroups.knowledgeMemory': 'Conocimiento y memoria',
|
||||
'settings.devGroups.agentsAutonomy': 'Agentes y autonomía',
|
||||
'settings.devGroups.modelsInference': 'Modelos e inferencia',
|
||||
'settings.devGroups.automationIntegrations': 'Automatización e integraciones',
|
||||
'settings.devGroups.toolsCapabilities': 'Herramientas y capacidades',
|
||||
'settings.devGroups.council': 'Consejo',
|
||||
'settings.analysisViews.title': 'Vistas de análisis',
|
||||
'settings.analysisViews.menuDesc':
|
||||
'Análisis del grafo de memoria: diagrama, centralidad, cohesión, asociaciones, frescura, línea de tiempo, rutas y espacios de nombres',
|
||||
'settings.buildInfo.title': 'Información de compilación/versión',
|
||||
'settings.buildInfo.menuDesc': 'Compilación de la app, versión y detalles de conexión del núcleo',
|
||||
'settings.dataSync.title': 'Sincronización de datos',
|
||||
'settings.dataSync.menuDesc': 'Lo que sincroniza tu asistente: fuentes, actualidad y estado',
|
||||
'settings.dataSync.description':
|
||||
'Gestiona qué se sincroniza en la memoria de tu asistente: cada fuente conectada con su última sincronización, cuánto se ha sincronizado y si se está sincronizando ahora.',
|
||||
'settings.devGroups.diagnosticsLogs': 'Diagnósticos y registros',
|
||||
'settings.featuresAndAI': 'Funciones e IA',
|
||||
'settings.billingAndRewards': 'Facturación y recompensas',
|
||||
'settings.support': 'Soporte',
|
||||
@@ -88,6 +146,13 @@ const messages: TranslationMap = {
|
||||
'settings.developerOptions': 'Avanzado',
|
||||
'settings.developerOptionsDesc':
|
||||
'Configuración de IA, canales de mensajería, herramientas, diagnósticos y paneles de depuración',
|
||||
'settings.developerDiagnostics': 'Desarrollador y Diagnóstico',
|
||||
'settings.developerDiagnosticsDesc':
|
||||
'Herramientas avanzadas de desarrollador, diagnóstico, memoria, agentes y paneles de depuración',
|
||||
'settings.developerMode.title': 'Modo desarrollador',
|
||||
'settings.developerMode.description':
|
||||
'Mostrar herramientas avanzadas de desarrollador y diagnóstico',
|
||||
'settings.developerMode.enabledByBuild': 'Siempre activo en compilaciones de desarrollo',
|
||||
'settings.clearAppData': 'Borrar datos de la app',
|
||||
'settings.clearAppDataDesc':
|
||||
'Cerrar sesión y eliminar permanentemente todos los datos locales de la app',
|
||||
@@ -100,6 +165,14 @@ const messages: TranslationMap = {
|
||||
'settings.languageDesc': 'Idioma de visualización de la interfaz de la app',
|
||||
'settings.alerts': 'Alertas',
|
||||
'settings.alertsDesc': 'Ver alertas recientes y actividad en tu bandeja',
|
||||
'settings.account.profile': 'Perfil',
|
||||
'settings.account.profileDesc': 'Nombre, correo electrónico y avatar',
|
||||
'settings.account.devices': 'Dispositivos',
|
||||
'settings.account.devicesDesc': 'Vincular y gestionar dispositivos móviles',
|
||||
'settings.account.teamMembers': 'Equipo y miembros',
|
||||
'settings.account.teamMembersDesc': 'Gestionar acceso al equipo y roles de miembros',
|
||||
'settings.account.dataMigration': 'Datos y migración',
|
||||
'settings.account.dataMigrationDesc': 'Importar memoria desde otro asistente',
|
||||
'settings.account.recoveryPhrase': 'Frase de recuperación',
|
||||
'settings.account.recoveryPhraseDesc': 'Ver y respaldar tu frase de recuperación de cuenta',
|
||||
'settings.account.team': 'Equipo',
|
||||
@@ -269,7 +342,7 @@ const messages: TranslationMap = {
|
||||
'skills.connected': 'Conectado',
|
||||
'skills.available': 'Disponible',
|
||||
'skills.addAccount': 'Agregar cuenta',
|
||||
'skills.channels': 'Canales',
|
||||
'skills.channels': 'Mensajería',
|
||||
'skills.explorer.emptyCta': 'Instalar desde URL',
|
||||
'skills.explorer.emptyDescription':
|
||||
'Instala un paquete SKILL.md o coloca carpetas estilo Hermes en ~/.openhuman/skills.',
|
||||
@@ -297,9 +370,9 @@ const messages: TranslationMap = {
|
||||
'skills.explorer.installed': 'Instalada',
|
||||
'skills.explorer.install': 'Instalar',
|
||||
'skills.explorer.installing': 'Instalando…',
|
||||
'skills.integrations': 'Integraciones',
|
||||
'skills.integrations': 'Aplicaciones',
|
||||
'skills.integrationsSubtitle':
|
||||
'Conexiones OAuth en la nube: inicia sesión con tu cuenta y Composio gestiona los tokens para que los agentes puedan leer y actuar en tu nombre. Sin claves de API que administrar.',
|
||||
'Conexiones OAuth en la nube: inicia sesión con tu cuenta y los tokens se gestionan de forma segura para que los agentes puedan leer y actuar en tu nombre. Sin claves de API que administrar.',
|
||||
'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í.',
|
||||
@@ -309,6 +382,11 @@ const messages: TranslationMap = {
|
||||
'skills.tabs.explorer': 'Skills',
|
||||
'skills.tabs.meetings': 'Reuniones de Google Meet',
|
||||
'skills.tabs.mcp': 'MCP Servidores',
|
||||
'connections.tabs.apps': 'Aplicaciones',
|
||||
'connections.tabs.messaging': 'Mensajería',
|
||||
'connections.tabs.tools': 'Herramientas',
|
||||
'connections.tabs.explorer': 'Explorar',
|
||||
'connections.tabs.talents': 'Talentos',
|
||||
'memory.title': 'Memoria',
|
||||
'memory.search': 'Buscar recuerdos...',
|
||||
'memory.noResults': 'No se encontraron recuerdos',
|
||||
@@ -717,6 +795,7 @@ const messages: TranslationMap = {
|
||||
'team.failedChangeRole': 'No se pudo cambiar el rol',
|
||||
'team.failedRemoveMember': 'No se pudo eliminar el miembro',
|
||||
'devOptions.title': 'Avanzado',
|
||||
'devOptions.titleDiagnostics': 'Desarrollador y Diagnóstico',
|
||||
'devOptions.diagnostics': 'Diagnósticos',
|
||||
'devOptions.diagnosticsDesc': 'Estado del sistema, registros y métricas de rendimiento',
|
||||
'devOptions.toolPolicyDiagnosticsDesc':
|
||||
@@ -1860,7 +1939,7 @@ const messages: TranslationMap = {
|
||||
'chat.safetyTimeout':
|
||||
'Sin respuesta del agente después de 2 minutos. Intenta de nuevo o verifica tu conexión.',
|
||||
'chat.filter.general': 'General',
|
||||
'chat.filter.subconscious': 'Subconsciente',
|
||||
'chat.filter.subconscious': 'Actividad en segundo plano',
|
||||
'chat.filter.meetings': 'Reuniones',
|
||||
'chat.filter.tasks': 'Tareas',
|
||||
'chat.selectThread': 'Selecciona un hilo',
|
||||
@@ -3905,6 +3984,12 @@ const messages: TranslationMap = {
|
||||
'settings.developerMenu.autonomy.title': 'Autonomía del agente',
|
||||
'settings.developerMenu.autonomy.desc':
|
||||
'Límites de frecuencia de acciones de herramientas y umbrales de seguridad',
|
||||
'settings.developerMenu.autocomplete.title': 'Autocompletar',
|
||||
'settings.developerMenu.autocomplete.desc':
|
||||
'Configuración de autocompletado en línea con IA y panel de depuración',
|
||||
'settings.developerMenu.voiceDebug.title': 'Voz (depuración)',
|
||||
'settings.developerMenu.voiceDebug.desc':
|
||||
'Estado del tiempo de ejecución del dictado de voz y configuración de depuración',
|
||||
'settings.mcpServer.title': 'MCP Servidor',
|
||||
'settings.mcpServer.toolsSectionTitle': 'Herramientas disponibles',
|
||||
'settings.mcpServer.toolsSectionDesc':
|
||||
@@ -4007,6 +4092,26 @@ const messages: TranslationMap = {
|
||||
'settings.agentAccess.approvalHistoryDesc':
|
||||
'Revisa las decisiones anteriores de Aprobar / Denegar solicitadas por el agente.',
|
||||
'settings.agentAccess.viewApprovalHistory': 'Ver historial de aprobaciones',
|
||||
|
||||
// ── Panel de Permisos ─────────────────────────────────────────────
|
||||
'settings.permissions.title': 'Permisos',
|
||||
'settings.permissions.menuDesc': 'Elige qué puede hacer tu asistente y dónde puede trabajar.',
|
||||
'settings.permissions.accessMode': '¿Qué puede hacer el asistente?',
|
||||
'settings.permissions.accessModeDesc':
|
||||
'Elige cuánta libertad tiene el asistente al realizar acciones en tu ordenador.',
|
||||
'settings.permissions.preset.readonly.title': 'Solo mirar',
|
||||
'settings.permissions.preset.readonly.desc':
|
||||
'El asistente puede leer archivos y explorar, pero nunca escribir, editar ni ejecutar nada que cambie el estado.',
|
||||
'settings.permissions.preset.supervised.title': 'Preguntar primero',
|
||||
'settings.permissions.preset.supervised.desc':
|
||||
'Puede crear archivos nuevos libremente, pero siempre pedirá tu aprobación antes de editar, ejecutar comandos o acceder a la red.',
|
||||
'settings.permissions.preset.full.title': 'Control total',
|
||||
'settings.permissions.preset.full.desc':
|
||||
'Opera con tu acceso de cuenta completo. Los comandos destructivos, el acceso a la red y las instalaciones siguen pidiendo aprobación.',
|
||||
'settings.permissions.folders': '¿Dónde puede trabajar?',
|
||||
'settings.permissions.foldersDesc':
|
||||
'La carpeta predeterminada que el asistente lee y escribe. Puedes añadir más carpetas en los ajustes avanzados.',
|
||||
|
||||
'settings.sandbox.title': 'Ejecución en sandbox',
|
||||
'settings.sandbox.menuDesc':
|
||||
'Configurar backends de sandbox para el aislamiento de herramientas del agente.',
|
||||
@@ -4231,7 +4336,7 @@ const messages: TranslationMap = {
|
||||
'skills.channelIcon.telegram': 'Telegram',
|
||||
'skills.channelIcon.web': 'Web',
|
||||
'skills.channelIcon.yuanbao': 'Yuanbao',
|
||||
'skills.composio.poweredBy': 'Desarrollado por Composio',
|
||||
'skills.composio.poweredBy': 'OAuth',
|
||||
'skills.composio.staleStatusTitle': 'Las conexiones muestran un estado obsoleto',
|
||||
'skills.create.allowedTools': 'Herramientas permitidas',
|
||||
'skills.create.allowedToolsHelp': 'Representado en el frontmatter de SKILL.md como',
|
||||
@@ -4876,6 +4981,12 @@ const messages: TranslationMap = {
|
||||
'autocomplete.maxChars': 'Máximo de caracteres de contexto',
|
||||
'autocomplete.overlayTtlMs': 'Tiempo de espera de superposición (ms)',
|
||||
'memory.tab.council': 'Council',
|
||||
'activity.tabs.automations': 'Automatizaciones',
|
||||
'activity.tabs.automationsDescription':
|
||||
'Procedimientos reutilizables y ejecutables — un objetivo y los pasos para alcanzarlo.',
|
||||
'activity.tabs.backgroundActivity': 'Actividad en segundo plano',
|
||||
'activity.tabs.alerts': 'Alertas',
|
||||
|
||||
'intelligence.agents.title': 'Biblioteca de agentes',
|
||||
'intelligence.agents.subtitle':
|
||||
'Revisa especialistas ejecutables y envía una tarea a un agente concreto.',
|
||||
|
||||
+118
-5
@@ -6,6 +6,11 @@ const messages: TranslationMap = {
|
||||
'nav.home': 'Accueil',
|
||||
'nav.human': 'Humain',
|
||||
'nav.chat': 'Chat',
|
||||
'nav.assistant': 'Assistant',
|
||||
'assistant.faceMode.on': 'Parle avec Tiny',
|
||||
'assistant.faceMode.off': 'Parler à Tiny',
|
||||
'assistant.faceMode.turnOn': 'Afficher la mascotte',
|
||||
'assistant.faceMode.turnOff': 'Masquer la mascotte',
|
||||
'nav.connections': 'Connexions',
|
||||
'nav.memory': 'Intelligence',
|
||||
'nav.alerts': 'Alertes',
|
||||
@@ -15,6 +20,12 @@ const messages: TranslationMap = {
|
||||
'nav.switchAgentProfile': "Changer de profil d'agent",
|
||||
'nav.defaultAgentProfile': 'Agent par défaut',
|
||||
'nav.noAgentProfiles': "Aucun profil d'agent trouvé",
|
||||
'nav.activity': 'Activité',
|
||||
'nav.avatarMenu.account': 'Compte',
|
||||
'nav.avatarMenu.billing': 'Facturation',
|
||||
'nav.avatarMenu.rewards': 'Récompenses',
|
||||
'nav.avatarMenu.invites': 'Inviter un ami',
|
||||
'nav.avatarMenu.wallet': 'Portefeuille',
|
||||
'common.cancel': 'Annuler',
|
||||
'common.save': 'Enregistrer',
|
||||
'common.confirm': 'Confirmer',
|
||||
@@ -59,6 +70,54 @@ const messages: TranslationMap = {
|
||||
'common.comingSoon': 'Bientôt disponible',
|
||||
'common.breadcrumb': "Fil d'Ariane",
|
||||
'settings.general': 'Général',
|
||||
// Settings layman groups (Phase 4 IA revamp)
|
||||
'settings.groups.account': 'Compte',
|
||||
'settings.groups.assistant': 'Assistant',
|
||||
'settings.groups.privacySecurity': 'Confidentialité et sécurité',
|
||||
'settings.groups.notifications': 'Notifications',
|
||||
'settings.groups.about': 'À propos',
|
||||
'settings.assistant.personality': 'Personnalité',
|
||||
'settings.assistant.personalityDesc': 'Nom, description et persona SOUL.md',
|
||||
'settings.assistant.voice': 'Voix',
|
||||
'settings.assistant.voiceDesc': 'Paramètres de synthèse vocale et de reconnaissance vocale',
|
||||
'settings.assistant.faceMascot': 'Visage / Mascotte',
|
||||
'settings.assistant.faceMascotDesc': "Choisir la couleur de la mascotte dans l'application",
|
||||
'settings.assistant.backgroundActivity': 'Activité en arrière-plan',
|
||||
'settings.assistant.backgroundActivityDesc':
|
||||
"Contrôler l'activité en arrière-plan de votre assistant",
|
||||
'settings.assistant.screenAwareness': "Conscience de l'écran",
|
||||
'settings.assistant.screenAwarenessDesc': "Permettre à l'assistant de voir votre fenêtre active",
|
||||
'settings.assistant.desktopCompanion': 'Compagnon de bureau',
|
||||
'settings.assistant.desktopCompanionDesc':
|
||||
'Mode compagnon toujours actif avec raccourci dans la barre système',
|
||||
'settings.assistant.permissions': 'Autorisations',
|
||||
'settings.assistant.permissionsDesc':
|
||||
"Choisissez ce que l'assistant peut faire et où il peut travailler",
|
||||
'settings.privacySecurity.privacy': 'Confidentialité',
|
||||
'settings.privacySecurity.privacyDesc': 'Contrôler quelles données quittent votre ordinateur',
|
||||
'settings.privacySecurity.security': 'Sécurité',
|
||||
'settings.privacySecurity.securityDesc': 'Sessions et options de connexion',
|
||||
'settings.privacySecurity.approvalsHistory': 'Approbations et historique',
|
||||
'settings.privacySecurity.approvalsHistoryDesc':
|
||||
"Examiner les décisions d'approbation récentes des outils",
|
||||
'settings.notifications.menuTitle': 'Notifications',
|
||||
'settings.notifications.menuDesc': "Boîte de réception d'alertes et préférences de notifications",
|
||||
'settings.devGroups.knowledgeMemory': 'Connaissance et mémoire',
|
||||
'settings.devGroups.agentsAutonomy': 'Agents et autonomie',
|
||||
'settings.devGroups.modelsInference': 'Modèles et inférence',
|
||||
'settings.devGroups.automationIntegrations': 'Automatisation et intégrations',
|
||||
'settings.devGroups.toolsCapabilities': 'Outils et capacités',
|
||||
'settings.devGroups.council': 'Conseil',
|
||||
'settings.analysisViews.title': 'Vues d’analyse',
|
||||
'settings.analysisViews.menuDesc':
|
||||
'Analyse du graphe mémoire — diagramme, centralité, cohésion, associations, fraîcheur, chronologie, chemins et espaces de noms',
|
||||
'settings.buildInfo.title': 'Infos de build/version',
|
||||
'settings.buildInfo.menuDesc': 'Build de l’application, version et détails de connexion du cœur',
|
||||
'settings.dataSync.title': 'Synchronisation des données',
|
||||
'settings.dataSync.menuDesc': 'Ce que votre assistant synchronise — sources, fraîcheur et état',
|
||||
'settings.dataSync.description':
|
||||
'Gérez ce qui est synchronisé dans la mémoire de votre assistant : chaque source connectée avec sa dernière synchronisation, la quantité synchronisée et si une synchronisation est en cours.',
|
||||
'settings.devGroups.diagnosticsLogs': 'Diagnostics et journaux',
|
||||
'settings.featuresAndAI': 'Fonctionnalités & IA',
|
||||
'settings.billingAndRewards': 'Facturation & Récompenses',
|
||||
'settings.support': 'Assistance',
|
||||
@@ -88,6 +147,13 @@ const messages: TranslationMap = {
|
||||
'settings.developerOptions': 'Avancé',
|
||||
'settings.developerOptionsDesc':
|
||||
'Configuration IA, canaux de messagerie, outils, diagnostics et panneaux de débogage',
|
||||
'settings.developerDiagnostics': 'Développeur et Diagnostics',
|
||||
'settings.developerDiagnosticsDesc':
|
||||
'Outils avancés de développeur, diagnostics, mémoire, agents et panneaux de débogage',
|
||||
'settings.developerMode.title': 'Mode développeur',
|
||||
'settings.developerMode.description':
|
||||
'Afficher les outils avancés de développeur et de diagnostic',
|
||||
'settings.developerMode.enabledByBuild': 'Toujours activé dans les builds de développement',
|
||||
'settings.clearAppData': "Effacer les données de l'app",
|
||||
'settings.clearAppDataDesc':
|
||||
'Se déconnecter et supprimer définitivement toutes les données locales',
|
||||
@@ -100,6 +166,14 @@ const messages: TranslationMap = {
|
||||
'settings.languageDesc': "Langue d'affichage de l'interface",
|
||||
'settings.alerts': 'Alertes',
|
||||
'settings.alertsDesc': "Voir les alertes récentes et l'activité dans ta boîte de réception",
|
||||
'settings.account.profile': 'Profil',
|
||||
'settings.account.profileDesc': 'Nom, e-mail et avatar',
|
||||
'settings.account.devices': 'Appareils',
|
||||
'settings.account.devicesDesc': 'Associer et gérer les appareils mobiles',
|
||||
'settings.account.teamMembers': 'Équipe & membres',
|
||||
'settings.account.teamMembersDesc': "Gérer l'accès à l'équipe et les rôles des membres",
|
||||
'settings.account.dataMigration': 'Données & migration',
|
||||
'settings.account.dataMigrationDesc': 'Importer la mémoire depuis un autre assistant',
|
||||
'settings.account.recoveryPhrase': 'Phrase de récupération',
|
||||
'settings.account.recoveryPhraseDesc': 'Voir et sauvegarder ta phrase de récupération',
|
||||
'settings.account.team': 'Équipe',
|
||||
@@ -267,7 +341,7 @@ const messages: TranslationMap = {
|
||||
'skills.connected': 'Connecté',
|
||||
'skills.available': 'Disponible',
|
||||
'skills.addAccount': 'Ajouter un compte',
|
||||
'skills.channels': 'Canaux',
|
||||
'skills.channels': 'Messagerie',
|
||||
'skills.explorer.emptyCta': 'Installer depuis une URL',
|
||||
'skills.explorer.emptyDescription':
|
||||
'Installez un paquet SKILL.md ou placez des dossiers de style Hermes dans ~/.openhuman/skills.',
|
||||
@@ -295,9 +369,9 @@ const messages: TranslationMap = {
|
||||
'skills.explorer.installed': 'Installée',
|
||||
'skills.explorer.install': 'Installer',
|
||||
'skills.explorer.installing': 'Installation…',
|
||||
'skills.integrations': 'Intégrations',
|
||||
'skills.integrations': 'Applications',
|
||||
'skills.integrationsSubtitle':
|
||||
'Connexions OAuth cloud — connectez-vous avec votre compte et Composio gère les jetons pour que les agents puissent lire et agir en votre nom. Aucune clé API à gérer.',
|
||||
'Connexions OAuth cloud — connectez-vous avec votre compte et les jetons sont gérés en toute sécurité pour que les agents puissent lire et agir en votre nom. Aucune clé API à gérer.',
|
||||
'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.',
|
||||
@@ -307,6 +381,11 @@ const messages: TranslationMap = {
|
||||
'skills.tabs.explorer': 'Skills',
|
||||
'skills.tabs.meetings': 'Réunions Google Meet',
|
||||
'skills.tabs.mcp': 'MCP Serveurs',
|
||||
'connections.tabs.apps': 'Applications',
|
||||
'connections.tabs.messaging': 'Messagerie',
|
||||
'connections.tabs.tools': 'Outils',
|
||||
'connections.tabs.explorer': 'Explorateur',
|
||||
'connections.tabs.talents': 'Talents',
|
||||
'memory.title': 'Mémoire',
|
||||
'memory.search': 'Rechercher dans la mémoire…',
|
||||
'memory.noResults': 'Aucun souvenir trouvé',
|
||||
@@ -717,6 +796,7 @@ const messages: TranslationMap = {
|
||||
'team.failedChangeRole': 'Échec du changement de rôle',
|
||||
'team.failedRemoveMember': 'Échec de la suppression du membre',
|
||||
'devOptions.title': 'Avancé',
|
||||
'devOptions.titleDiagnostics': 'Développeur et Diagnostics',
|
||||
'devOptions.diagnostics': 'Diagnostics',
|
||||
'devOptions.diagnosticsDesc': 'Santé du système, journaux et métriques de performance',
|
||||
'devOptions.toolPolicyDiagnosticsDesc':
|
||||
@@ -1868,7 +1948,7 @@ const messages: TranslationMap = {
|
||||
'chat.safetyTimeout':
|
||||
"Aucune réponse de l'agent après 2 minutes. Réessaie ou vérifie ta connexion.",
|
||||
'chat.filter.general': 'Général',
|
||||
'chat.filter.subconscious': 'Subconscient',
|
||||
'chat.filter.subconscious': 'Activité en arrière-plan',
|
||||
'chat.filter.meetings': 'Réunions',
|
||||
'chat.filter.tasks': 'Tâches',
|
||||
'chat.selectThread': 'Sélectionne un fil',
|
||||
@@ -3920,6 +4000,12 @@ const messages: TranslationMap = {
|
||||
'settings.developerMenu.autonomy.title': 'Autonomie de l’agent',
|
||||
'settings.developerMenu.autonomy.desc':
|
||||
'Limites de fréquence des actions des outils et seuils de sécurité',
|
||||
'settings.developerMenu.autocomplete.title': 'Autocomplétion',
|
||||
'settings.developerMenu.autocomplete.desc':
|
||||
'Paramètres d’autocomplétion IA en ligne et panneau de débogage',
|
||||
'settings.developerMenu.voiceDebug.title': 'Voix (débogage)',
|
||||
'settings.developerMenu.voiceDebug.desc':
|
||||
'État d’exécution de la dictée vocale et paramètres de débogage',
|
||||
'settings.mcpServer.title': 'Serveur MCP',
|
||||
'settings.mcpServer.toolsSectionTitle': 'Outils disponibles',
|
||||
'settings.mcpServer.toolsSectionDesc':
|
||||
@@ -4023,6 +4109,27 @@ const messages: TranslationMap = {
|
||||
'settings.agentAccess.approvalHistoryDesc':
|
||||
"Consultez les décisions Approuver / Refuser passées demandées par l'agent.",
|
||||
'settings.agentAccess.viewApprovalHistory': "Voir l'historique des approbations",
|
||||
|
||||
// ── Panneau des autorisations ─────────────────────────────────────
|
||||
'settings.permissions.title': 'Autorisations',
|
||||
'settings.permissions.menuDesc':
|
||||
'Choisissez ce que votre assistant peut faire et où il peut travailler.',
|
||||
'settings.permissions.accessMode': "Que peut faire l'assistant ?",
|
||||
'settings.permissions.accessModeDesc':
|
||||
"Choisissez la liberté accordée à l'assistant lorsqu'il effectue des actions sur votre ordinateur.",
|
||||
'settings.permissions.preset.readonly.title': 'Regarder sans toucher',
|
||||
'settings.permissions.preset.readonly.desc':
|
||||
"L'assistant peut lire des fichiers et explorer, mais ne peut jamais écrire, modifier ou exécuter quoi que ce soit qui change l'état.",
|
||||
'settings.permissions.preset.supervised.title': "M'interroger d'abord",
|
||||
'settings.permissions.preset.supervised.desc':
|
||||
"Peut créer de nouveaux fichiers librement, mais demande toujours votre approbation avant de modifier, d'exécuter des commandes ou d'accéder au réseau.",
|
||||
'settings.permissions.preset.full.title': 'Contrôle total',
|
||||
'settings.permissions.preset.full.desc':
|
||||
"Fonctionne avec votre accès de compte complet. Les commandes destructives, l'accès réseau et les installations demandent toujours une approbation.",
|
||||
'settings.permissions.folders': 'Où peut-il travailler ?',
|
||||
'settings.permissions.foldersDesc':
|
||||
"Le dossier par défaut que l'assistant lit et écrit. Vous pouvez ajouter d'autres dossiers dans les paramètres avancés.",
|
||||
|
||||
'settings.sandbox.title': 'Exécution en sandbox',
|
||||
'settings.sandbox.menuDesc':
|
||||
"Configurer les backends sandbox pour l'isolation des outils de l'agent.",
|
||||
@@ -4247,7 +4354,7 @@ const messages: TranslationMap = {
|
||||
'skills.channelIcon.telegram': 'Telegram',
|
||||
'skills.channelIcon.web': 'Web',
|
||||
'skills.channelIcon.yuanbao': 'Yuanbao',
|
||||
'skills.composio.poweredBy': 'Propulsé par Composio',
|
||||
'skills.composio.poweredBy': 'OAuth',
|
||||
'skills.composio.staleStatusTitle': 'Les connexions affichent un état obsolète',
|
||||
'skills.create.allowedTools': 'Outils autorisés',
|
||||
'skills.create.allowedToolsHelp': 'Rendu dans le frontmatter SKILL.md comme',
|
||||
@@ -4893,6 +5000,12 @@ const messages: TranslationMap = {
|
||||
'autocomplete.maxChars': 'Caractères de contexte maximum',
|
||||
'autocomplete.overlayTtlMs': "Délai d'affichage (ms)",
|
||||
'memory.tab.council': 'Council',
|
||||
'activity.tabs.automations': 'Automatisations',
|
||||
'activity.tabs.automationsDescription':
|
||||
"Procédures réutilisables et exécutables — un objectif et les étapes pour l'atteindre.",
|
||||
'activity.tabs.backgroundActivity': 'Activité en arrière-plan',
|
||||
'activity.tabs.alerts': 'Alertes',
|
||||
|
||||
'intelligence.agents.title': "Bibliothèque d'agents",
|
||||
'intelligence.agents.subtitle':
|
||||
'Inspectez les spécialistes exécutables et envoyez une tâche à un agent nommé.',
|
||||
|
||||
+111
-5
@@ -6,6 +6,11 @@ const messages: TranslationMap = {
|
||||
'nav.home': 'होम',
|
||||
'nav.human': 'मानव',
|
||||
'nav.chat': 'चैट',
|
||||
'nav.assistant': 'सहायक',
|
||||
'assistant.faceMode.on': 'Tiny से बात हो रही है',
|
||||
'assistant.faceMode.off': 'Tiny से बात करें',
|
||||
'assistant.faceMode.turnOn': 'मैस्कट दिखाएं',
|
||||
'assistant.faceMode.turnOff': 'मैस्कट छुपाएं',
|
||||
'nav.connections': 'कनेक्शन',
|
||||
'nav.memory': 'इंटेलिजेंस',
|
||||
'nav.alerts': 'अलर्ट',
|
||||
@@ -15,6 +20,12 @@ const messages: TranslationMap = {
|
||||
'nav.switchAgentProfile': 'एजेंट प्रोफाइल बदलें',
|
||||
'nav.defaultAgentProfile': 'डिफॉल्ट एजेंट',
|
||||
'nav.noAgentProfiles': 'कोई एजेंट प्रोफाइल नहीं मिला',
|
||||
'nav.activity': 'गतिविधि',
|
||||
'nav.avatarMenu.account': 'खाता',
|
||||
'nav.avatarMenu.billing': 'बिलिंग',
|
||||
'nav.avatarMenu.rewards': 'रिवॉर्ड',
|
||||
'nav.avatarMenu.invites': 'दोस्त को आमंत्रित करें',
|
||||
'nav.avatarMenu.wallet': 'वॉलेट',
|
||||
'common.cancel': 'रद्द करें',
|
||||
'common.save': 'सेव करें',
|
||||
'common.confirm': 'कन्फर्म करें',
|
||||
@@ -59,6 +70,51 @@ const messages: TranslationMap = {
|
||||
'common.comingSoon': 'जल्द आ रहा है',
|
||||
'common.breadcrumb': 'ब्रेडक्रंब',
|
||||
'settings.general': 'सामान्य',
|
||||
// Settings layman groups (Phase 4 IA revamp)
|
||||
'settings.groups.account': 'खाता',
|
||||
'settings.groups.assistant': 'सहायक',
|
||||
'settings.groups.privacySecurity': 'गोपनीयता और सुरक्षा',
|
||||
'settings.groups.notifications': 'सूचनाएं',
|
||||
'settings.groups.about': 'के बारे में',
|
||||
'settings.assistant.personality': 'व्यक्तित्व',
|
||||
'settings.assistant.personalityDesc': 'नाम, विवरण और SOUL.md व्यक्तित्व',
|
||||
'settings.assistant.voice': 'आवाज़',
|
||||
'settings.assistant.voiceDesc': 'स्पीच-टू-टेक्स्ट और टेक्स्ट-टू-स्पीच सेटिंग्स',
|
||||
'settings.assistant.faceMascot': 'चेहरा / शुभंकर',
|
||||
'settings.assistant.faceMascotDesc': 'ऐप में उपयोग किया जाने वाला शुभंकर रंग चुनें',
|
||||
'settings.assistant.backgroundActivity': 'पृष्ठभूमि गतिविधि',
|
||||
'settings.assistant.backgroundActivityDesc':
|
||||
'नियंत्रित करें कि आपका सहायक पृष्ठभूमि में कितना सक्रिय काम करे',
|
||||
'settings.assistant.screenAwareness': 'स्क्रीन जागरूकता',
|
||||
'settings.assistant.screenAwarenessDesc': 'सहायक को आपकी सक्रिय विंडो देखने दें',
|
||||
'settings.assistant.desktopCompanion': 'डेस्कटॉप साथी',
|
||||
'settings.assistant.desktopCompanionDesc': 'सिस्टम ट्रे शॉर्टकट के साथ हमेशा-चालू साथी मोड',
|
||||
'settings.assistant.permissions': 'अनुमतियाँ',
|
||||
'settings.assistant.permissionsDesc': 'चुनें कि सहायक क्या कर सकता है और कहाँ काम कर सकता है',
|
||||
'settings.privacySecurity.privacy': 'गोपनीयता',
|
||||
'settings.privacySecurity.privacyDesc': 'नियंत्रित करें कि आपके कंप्यूटर से कौन सा डेटा जाता है',
|
||||
'settings.privacySecurity.security': 'सुरक्षा',
|
||||
'settings.privacySecurity.securityDesc': 'सत्र और साइन-इन विकल्प',
|
||||
'settings.privacySecurity.approvalsHistory': 'अनुमोदन और इतिहास',
|
||||
'settings.privacySecurity.approvalsHistoryDesc': 'हाल के टूल-अनुमोदन निर्णय देखें',
|
||||
'settings.notifications.menuTitle': 'सूचनाएं',
|
||||
'settings.notifications.menuDesc': 'अलर्ट इनबॉक्स और सूचना प्राथमिकताएं',
|
||||
'settings.devGroups.knowledgeMemory': 'ज्ञान और स्मृति',
|
||||
'settings.devGroups.agentsAutonomy': 'एजेंट और स्वायत्तता',
|
||||
'settings.devGroups.modelsInference': 'मॉडल और अनुमान',
|
||||
'settings.devGroups.automationIntegrations': 'स्वचालन और एकीकरण',
|
||||
'settings.devGroups.toolsCapabilities': 'उपकरण और क्षमताएं',
|
||||
'settings.devGroups.council': 'परिषद',
|
||||
'settings.analysisViews.title': 'विश्लेषण दृश्य',
|
||||
'settings.analysisViews.menuDesc':
|
||||
'मेमोरी ग्राफ़ विश्लेषण — डायग्राम, केंद्रीयता, संसक्ति, संबंध, ताज़गी, टाइमलाइन, पथ और नेमस्पेस',
|
||||
'settings.buildInfo.title': 'बिल्ड / संस्करण जानकारी',
|
||||
'settings.buildInfo.menuDesc': 'ऐप बिल्ड, संस्करण और कोर कनेक्शन विवरण',
|
||||
'settings.dataSync.title': 'डेटा सिंक',
|
||||
'settings.dataSync.menuDesc': 'आपका सहायक क्या सिंक करता है — स्रोत, ताज़गी और स्थिति',
|
||||
'settings.dataSync.description':
|
||||
'प्रबंधित करें कि आपके सहायक की मेमोरी में क्या सिंक होता है: हर कनेक्ट किया गया स्रोत उसके अंतिम सिंक समय, कितना सिंक हुआ है और अभी सिंक हो रहा है या नहीं के साथ।',
|
||||
'settings.devGroups.diagnosticsLogs': 'निदान और लॉग',
|
||||
'settings.featuresAndAI': 'फीचर्स और AI',
|
||||
'settings.billingAndRewards': 'बिलिंग और रिवॉर्ड',
|
||||
'settings.support': 'सपोर्ट',
|
||||
@@ -86,6 +142,12 @@ const messages: TranslationMap = {
|
||||
'settings.aboutDesc': 'ऐप वर्जन और सॉफ्टवेयर अपडेट',
|
||||
'settings.developerOptions': 'एडवांस्ड',
|
||||
'settings.developerOptionsDesc': 'AI कॉन्फिग, मैसेजिंग चैनल, टूल्स, डायग्नोस्टिक्स और डिबग पैनल',
|
||||
'settings.developerDiagnostics': 'डेवलपर और डायग्नोस्टिक्स',
|
||||
'settings.developerDiagnosticsDesc':
|
||||
'उन्नत डेवलपर टूल्स, डायग्नोस्टिक्स, मेमोरी, एजेंट और डिबग पैनल',
|
||||
'settings.developerMode.title': 'डेवलपर मोड',
|
||||
'settings.developerMode.description': 'उन्नत डेवलपर और डायग्नोस्टिक टूल्स दिखाएं',
|
||||
'settings.developerMode.enabledByBuild': 'डेवलपमेंट बिल्ड में हमेशा चालू',
|
||||
'settings.clearAppData': 'ऐप डेटा क्लियर करें',
|
||||
'settings.clearAppDataDesc': 'साइन आउट करें और सारा लोकल ऐप डेटा हमेशा के लिए मिटाएं',
|
||||
'settings.logOut': 'लॉग आउट',
|
||||
@@ -97,6 +159,14 @@ const messages: TranslationMap = {
|
||||
'settings.languageDesc': 'ऐप इंटरफेस की डिस्प्ले भाषा',
|
||||
'settings.alerts': 'अलर्ट',
|
||||
'settings.alertsDesc': 'हाल के अलर्ट और इनबॉक्स एक्टिविटी देखें',
|
||||
'settings.account.profile': 'प्रोफ़ाइल',
|
||||
'settings.account.profileDesc': 'नाम, ईमेल और अवतार',
|
||||
'settings.account.devices': 'डिवाइस',
|
||||
'settings.account.devicesDesc': 'मोबाइल डिवाइस पेयर करें और प्रबंधित करें',
|
||||
'settings.account.teamMembers': 'टीम और सदस्य',
|
||||
'settings.account.teamMembersDesc': 'टीम एक्सेस और सदस्य भूमिकाएं प्रबंधित करें',
|
||||
'settings.account.dataMigration': 'डेटा और माइग्रेशन',
|
||||
'settings.account.dataMigrationDesc': 'दूसरे असिस्टेंट से मेमरी आयात करें',
|
||||
'settings.account.recoveryPhrase': 'रिकवरी फ्रेज़',
|
||||
'settings.account.recoveryPhraseDesc': 'अपना अकाउंट रिकवरी फ्रेज़ देखें और बैकअप लें',
|
||||
'settings.account.team': 'टीम',
|
||||
@@ -259,7 +329,7 @@ const messages: TranslationMap = {
|
||||
'skills.connected': 'कनेक्टेड',
|
||||
'skills.available': 'उपलब्ध',
|
||||
'skills.addAccount': 'अकाउंट जोड़ें',
|
||||
'skills.channels': 'चैनल',
|
||||
'skills.channels': 'मैसेजिंग',
|
||||
'skills.explorer.emptyCta': 'URL से इंस्टॉल करें',
|
||||
'skills.explorer.emptyDescription':
|
||||
'SKILL.md पैकेज इंस्टॉल करें या Hermes-शैली फ़ोल्डर ~/.openhuman/skills में रखें।',
|
||||
@@ -287,9 +357,9 @@ const messages: TranslationMap = {
|
||||
'skills.explorer.installed': 'इंस्टॉल किया गया',
|
||||
'skills.explorer.install': 'इंस्टॉल करें',
|
||||
'skills.explorer.installing': 'इंस्टॉल हो रहा है…',
|
||||
'skills.integrations': 'इंटीग्रेशन',
|
||||
'skills.integrations': 'ऐप्स',
|
||||
'skills.integrationsSubtitle':
|
||||
'क्लाउड-आधारित OAuth कनेक्शन — अपने अकाउंट से साइन इन करें और Composio टोकन ब्रोकर करता है ताकि एजेंट आपकी ओर से पढ़ और कार्य कर सकें। कोई API कुंजी प्रबंधित नहीं करनी।',
|
||||
'क्लाउड-आधारित OAuth कनेक्शन — अपने अकाउंट से साइन इन करें और टोकन सुरक्षित रूप से प्रबंधित होते हैं ताकि एजेंट आपकी ओर से पढ़ और कार्य कर सकें। कोई API कुंजी प्रबंधित नहीं करनी।',
|
||||
'skills.composio.noApiKeyTitle': 'कोई Composio API key कॉन्फ़िगर नहीं है',
|
||||
'skills.composio.noApiKeyDescription':
|
||||
'लोकल मोड आपकी अपनी Composio API key का उपयोग करता है। यहाँ integrations जोड़ने से पहले Settings → Advanced → Composio खोलकर key जोड़ें।',
|
||||
@@ -299,6 +369,11 @@ const messages: TranslationMap = {
|
||||
'skills.tabs.explorer': 'स्किल',
|
||||
'skills.tabs.meetings': 'Google Meet बैठकें',
|
||||
'skills.tabs.mcp': 'MCP सर्वर',
|
||||
'connections.tabs.apps': 'ऐप्स',
|
||||
'connections.tabs.messaging': 'मैसेजिंग',
|
||||
'connections.tabs.tools': 'टूल्स',
|
||||
'connections.tabs.explorer': 'एक्सप्लोरर',
|
||||
'connections.tabs.talents': 'टैलेंट',
|
||||
'memory.title': 'मेमोरी',
|
||||
'memory.search': 'मेमोरी सर्च करें...',
|
||||
'memory.noResults': 'कोई मेमोरी नहीं मिली',
|
||||
@@ -699,6 +774,7 @@ const messages: TranslationMap = {
|
||||
'team.failedChangeRole': 'भूमिका बदलने में विफल',
|
||||
'team.failedRemoveMember': 'सदस्य को हटाने में विफल',
|
||||
'devOptions.title': 'एडवांस्ड',
|
||||
'devOptions.titleDiagnostics': 'डेवलपर और डायग्नोस्टिक्स',
|
||||
'devOptions.diagnostics': 'डायग्नोस्टिक्स',
|
||||
'devOptions.diagnosticsDesc': 'सिस्टम हेल्थ, लॉग्स और परफॉर्मेंस मेट्रिक्स',
|
||||
'devOptions.toolPolicyDiagnosticsDesc':
|
||||
@@ -1821,7 +1897,7 @@ const messages: TranslationMap = {
|
||||
'chat.safetyTimeout':
|
||||
'2 मिनट बाद भी एजेंट से कोई जवाब नहीं मिला। दोबारा कोशिश करें या अपना कनेक्शन चेक करें।',
|
||||
'chat.filter.general': 'सामान्य',
|
||||
'chat.filter.subconscious': 'सबकॉन्शस',
|
||||
'chat.filter.subconscious': 'पृष्ठभूमि गतिविधि',
|
||||
'chat.filter.meetings': 'मीटिंग',
|
||||
'chat.filter.tasks': 'टास्क',
|
||||
'chat.selectThread': 'एक थ्रेड चुनें',
|
||||
@@ -3841,6 +3917,10 @@ const messages: TranslationMap = {
|
||||
'OpenHuman से कनेक्ट करने के लिए बाहरी MCP क्लाइंट को कॉन्फ़िगर करें',
|
||||
'settings.developerMenu.autonomy.title': 'एजेंट स्वायत्तता',
|
||||
'settings.developerMenu.autonomy.desc': 'टूल क्रिया दर सीमाएँ और सुरक्षा सीमाएँ',
|
||||
'settings.developerMenu.autocomplete.title': 'स्वतः पूर्ण',
|
||||
'settings.developerMenu.autocomplete.desc': 'AI इनलाइन स्वतः-पूर्ण सेटिंग्स और डिबग पैनल',
|
||||
'settings.developerMenu.voiceDebug.title': 'वॉइस (डिबग)',
|
||||
'settings.developerMenu.voiceDebug.desc': 'वॉइस डिक्टेशन रनटाइम स्थिति और डिबग सेटिंग्स',
|
||||
'settings.mcpServer.title': 'MCP सर्वर',
|
||||
'settings.mcpServer.toolsSectionTitle': 'उपलब्ध उपकरण',
|
||||
'settings.mcpServer.toolsSectionDesc':
|
||||
@@ -3941,6 +4021,26 @@ const messages: TranslationMap = {
|
||||
'settings.agentAccess.approvalHistoryDesc':
|
||||
'एजेंट द्वारा अनुरोधित पिछले स्वीकृति/अस्वीकृति निर्णयों की समीक्षा करें।',
|
||||
'settings.agentAccess.viewApprovalHistory': 'अनुमोदन इतिहास देखें',
|
||||
|
||||
// ── अनुमति पैनल ──────────────────────────────────────────────────
|
||||
'settings.permissions.title': 'अनुमतियाँ',
|
||||
'settings.permissions.menuDesc': 'चुनें कि आपका सहायक क्या कर सकता है और कहाँ काम कर सकता है।',
|
||||
'settings.permissions.accessMode': 'सहायक क्या कर सकता है?',
|
||||
'settings.permissions.accessModeDesc':
|
||||
'चुनें कि आपके कंप्यूटर पर कार्रवाई करते समय सहायक को कितनी स्वतंत्रता है।',
|
||||
'settings.permissions.preset.readonly.title': 'देखो, छुओ मत',
|
||||
'settings.permissions.preset.readonly.desc':
|
||||
'सहायक फ़ाइलें पढ़ और खोज सकता है — लेकिन कभी लिखेगा, संपादित करेगा या ऐसा कुछ नहीं चलाएगा जो स्थिति बदले।',
|
||||
'settings.permissions.preset.supervised.title': 'पहले पूछो',
|
||||
'settings.permissions.preset.supervised.desc':
|
||||
'नई फ़ाइलें स्वतंत्र रूप से बना सकता है, लेकिन संपादन, कमांड चलाने या नेटवर्क तक पहुँचने से पहले हमेशा आपकी अनुमति माँगेगा।',
|
||||
'settings.permissions.preset.full.title': 'पूर्ण नियंत्रण',
|
||||
'settings.permissions.preset.full.desc':
|
||||
'आपके पूर्ण खाता पहुँच के साथ काम करता है। विनाशकारी कमांड, नेटवर्क एक्सेस और इंस्टॉलेशन फिर भी अनुमोदन माँगेंगे।',
|
||||
'settings.permissions.folders': 'यह कहाँ काम कर सकता है?',
|
||||
'settings.permissions.foldersDesc':
|
||||
'डिफ़ॉल्ट फ़ोल्डर जिसे सहायक पढ़ता और लिखता है। आप उन्नत सेटिंग्स में और फ़ोल्डर जोड़ सकते हैं।',
|
||||
|
||||
'settings.sandbox.title': 'Sandbox निष्पादन',
|
||||
'settings.sandbox.menuDesc': 'एजेंट टूल आइसोलेशन के लिए Sandbox बैकएंड कॉन्फ़िगर करें।',
|
||||
'settings.sandbox.loading': 'लोड हो रहा है…',
|
||||
@@ -4156,7 +4256,7 @@ const messages: TranslationMap = {
|
||||
'skills.channelIcon.telegram': 'Telegram',
|
||||
'skills.channelIcon.web': 'वेब',
|
||||
'skills.channelIcon.yuanbao': 'Yuanbao',
|
||||
'skills.composio.poweredBy': 'Composio द्वारा संचालित',
|
||||
'skills.composio.poweredBy': 'OAuth',
|
||||
'skills.composio.staleStatusTitle': 'कनेक्शन पुरानी स्थिति दिखा रहे हैं',
|
||||
'skills.create.allowedTools': 'अनुमत टूल्स',
|
||||
'skills.create.allowedToolsHelp': 'SKILL.md फ्रंटमैटर में प्रस्तुत किया गया',
|
||||
@@ -4786,6 +4886,12 @@ const messages: TranslationMap = {
|
||||
'autocomplete.maxChars': 'अधिकतम संदर्भ वर्ण',
|
||||
'autocomplete.overlayTtlMs': 'ओवरले समय-समाप्ति (ms)',
|
||||
'memory.tab.council': 'Council',
|
||||
'activity.tabs.automations': 'स्वचालन',
|
||||
'activity.tabs.automationsDescription':
|
||||
'पुन: उपयोग योग्य, चलाने योग्य प्रक्रियाएँ — एक लक्ष्य और उसे प्राप्त करने के चरण।',
|
||||
'activity.tabs.backgroundActivity': 'पृष्ठभूमि गतिविधि',
|
||||
'activity.tabs.alerts': 'अलर्ट',
|
||||
|
||||
'intelligence.agents.title': 'एजेंट लाइब्रेरी',
|
||||
'intelligence.agents.subtitle':
|
||||
'चलाए जा सकने वाले विशेषज्ञ देखें और किसी नामित एजेंट को एक काम भेजें।',
|
||||
|
||||
+115
-5
@@ -6,6 +6,11 @@ const messages: TranslationMap = {
|
||||
'nav.home': 'Beranda',
|
||||
'nav.human': 'Manusia',
|
||||
'nav.chat': 'Obrolan',
|
||||
'nav.assistant': 'Asisten',
|
||||
'assistant.faceMode.on': 'Berbicara dengan Tiny',
|
||||
'assistant.faceMode.off': 'Bicara dengan Tiny',
|
||||
'assistant.faceMode.turnOn': 'Tampilkan maskot',
|
||||
'assistant.faceMode.turnOff': 'Sembunyikan maskot',
|
||||
'nav.connections': 'Koneksi',
|
||||
'nav.memory': 'Memori',
|
||||
'nav.alerts': 'Peringatan',
|
||||
@@ -15,6 +20,12 @@ const messages: TranslationMap = {
|
||||
'nav.switchAgentProfile': 'Ganti profil agen',
|
||||
'nav.defaultAgentProfile': 'Agen default',
|
||||
'nav.noAgentProfiles': 'Profil agen tidak ditemukan',
|
||||
'nav.activity': 'Aktivitas',
|
||||
'nav.avatarMenu.account': 'Akun',
|
||||
'nav.avatarMenu.billing': 'Tagihan',
|
||||
'nav.avatarMenu.rewards': 'Hadiah',
|
||||
'nav.avatarMenu.invites': 'Undang teman',
|
||||
'nav.avatarMenu.wallet': 'Dompet',
|
||||
'common.cancel': 'Batal',
|
||||
'common.save': 'Simpan',
|
||||
'common.confirm': 'Konfirmasi',
|
||||
@@ -59,6 +70,53 @@ const messages: TranslationMap = {
|
||||
'common.comingSoon': 'Segera Hadir',
|
||||
'common.breadcrumb': 'Breadcrumb',
|
||||
'settings.general': 'Umum',
|
||||
// Settings layman groups (Phase 4 IA revamp)
|
||||
'settings.groups.account': 'Akun',
|
||||
'settings.groups.assistant': 'Asisten',
|
||||
'settings.groups.privacySecurity': 'Privasi & Keamanan',
|
||||
'settings.groups.notifications': 'Notifikasi',
|
||||
'settings.groups.about': 'Tentang',
|
||||
'settings.assistant.personality': 'Kepribadian',
|
||||
'settings.assistant.personalityDesc': 'Nama, deskripsi, dan persona SOUL.md',
|
||||
'settings.assistant.voice': 'Suara',
|
||||
'settings.assistant.voiceDesc': 'Pengaturan ucapan-ke-teks dan teks-ke-ucapan',
|
||||
'settings.assistant.faceMascot': 'Wajah / Maskot',
|
||||
'settings.assistant.faceMascotDesc': 'Pilih warna maskot yang digunakan di seluruh aplikasi',
|
||||
'settings.assistant.backgroundActivity': 'Aktivitas latar belakang',
|
||||
'settings.assistant.backgroundActivityDesc':
|
||||
'Kontrol seberapa aktif asisten Anda bekerja di latar belakang',
|
||||
'settings.assistant.screenAwareness': 'Kesadaran layar',
|
||||
'settings.assistant.screenAwarenessDesc': 'Biarkan asisten melihat jendela aktif Anda',
|
||||
'settings.assistant.desktopCompanion': 'Pendamping desktop',
|
||||
'settings.assistant.desktopCompanionDesc':
|
||||
'Mode pendamping selalu aktif dengan pintasan baki sistem',
|
||||
'settings.assistant.permissions': 'Izin',
|
||||
'settings.assistant.permissionsDesc':
|
||||
'Pilih apa yang dapat dilakukan asisten dan di mana ia dapat bekerja',
|
||||
'settings.privacySecurity.privacy': 'Privasi',
|
||||
'settings.privacySecurity.privacyDesc': 'Kontrol data apa yang meninggalkan komputer Anda',
|
||||
'settings.privacySecurity.security': 'Keamanan',
|
||||
'settings.privacySecurity.securityDesc': 'Sesi dan opsi masuk',
|
||||
'settings.privacySecurity.approvalsHistory': 'Persetujuan & riwayat',
|
||||
'settings.privacySecurity.approvalsHistoryDesc': 'Tinjau keputusan persetujuan alat terbaru',
|
||||
'settings.notifications.menuTitle': 'Notifikasi',
|
||||
'settings.notifications.menuDesc': 'Kotak masuk peringatan dan preferensi notifikasi',
|
||||
'settings.devGroups.knowledgeMemory': 'Pengetahuan & Memori',
|
||||
'settings.devGroups.agentsAutonomy': 'Agen & Otonomi',
|
||||
'settings.devGroups.modelsInference': 'Model & Inferensi',
|
||||
'settings.devGroups.automationIntegrations': 'Otomasi & Integrasi',
|
||||
'settings.devGroups.toolsCapabilities': 'Alat & Kemampuan',
|
||||
'settings.devGroups.council': 'Dewan',
|
||||
'settings.analysisViews.title': 'Tampilan analisis',
|
||||
'settings.analysisViews.menuDesc':
|
||||
'Analisis grafik memori — diagram, sentralitas, kohesi, asosiasi, kesegaran, lini masa, jalur, dan namespace',
|
||||
'settings.buildInfo.title': 'Info build / versi',
|
||||
'settings.buildInfo.menuDesc': 'Build aplikasi, versi, dan detail koneksi core',
|
||||
'settings.dataSync.title': 'Sinkronisasi Data',
|
||||
'settings.dataSync.menuDesc': 'Yang disinkronkan asisten Anda — sumber, kesegaran, dan status',
|
||||
'settings.dataSync.description':
|
||||
'Kelola apa yang disinkronkan ke memori asisten Anda: setiap sumber terhubung dengan waktu sinkronisasi terakhirnya, seberapa banyak yang disinkronkan, dan apakah sedang menyinkronkan sekarang.',
|
||||
'settings.devGroups.diagnosticsLogs': 'Diagnostik & Log',
|
||||
'settings.featuresAndAI': 'Fitur & AI',
|
||||
'settings.billingAndRewards': 'Tagihan & Hadiah',
|
||||
'settings.support': 'Dukungan',
|
||||
@@ -86,6 +144,12 @@ const messages: TranslationMap = {
|
||||
'settings.aboutDesc': 'Versi aplikasi dan pembaruan perangkat lunak',
|
||||
'settings.developerOptions': 'Opsi Developer',
|
||||
'settings.developerOptionsDesc': 'Diagnostik, panel debug, webhook, dan inspeksi memori',
|
||||
'settings.developerDiagnostics': 'Developer & Diagnostik',
|
||||
'settings.developerDiagnosticsDesc':
|
||||
'Alat developer lanjutan, diagnostik, memori, agen, dan panel debug',
|
||||
'settings.developerMode.title': 'Mode developer',
|
||||
'settings.developerMode.description': 'Tampilkan alat developer & diagnostik lanjutan',
|
||||
'settings.developerMode.enabledByBuild': 'Selalu aktif di build pengembangan',
|
||||
'settings.clearAppData': 'Bersihkan Data Aplikasi',
|
||||
'settings.clearAppDataDesc': 'Keluar dan hapus permanen semua data aplikasi lokal',
|
||||
'settings.logOut': 'Keluar',
|
||||
@@ -97,6 +161,14 @@ const messages: TranslationMap = {
|
||||
'settings.languageDesc': 'Bahasa tampilan untuk antarmuka aplikasi',
|
||||
'settings.alerts': 'Peringatan',
|
||||
'settings.alertsDesc': 'Lihat peringatan terbaru dan aktivitas di kotak masuk Anda',
|
||||
'settings.account.profile': 'Profil',
|
||||
'settings.account.profileDesc': 'Nama, email, dan avatar',
|
||||
'settings.account.devices': 'Perangkat',
|
||||
'settings.account.devicesDesc': 'Pasangkan dan kelola perangkat seluler',
|
||||
'settings.account.teamMembers': 'Tim & anggota',
|
||||
'settings.account.teamMembersDesc': 'Kelola akses tim dan peran anggota',
|
||||
'settings.account.dataMigration': 'Data & migrasi',
|
||||
'settings.account.dataMigrationDesc': 'Impor memori dari asisten lain',
|
||||
'settings.account.recoveryPhrase': 'Frasa Pemulihan',
|
||||
'settings.account.recoveryPhraseDesc': 'Lihat dan cadangkan frasa pemulihan akun Anda',
|
||||
'settings.account.team': 'Tim',
|
||||
@@ -261,7 +333,7 @@ const messages: TranslationMap = {
|
||||
'skills.connected': 'Terhubung',
|
||||
'skills.available': 'Tersedia',
|
||||
'skills.addAccount': 'Tambah Akun',
|
||||
'skills.channels': 'Kanal',
|
||||
'skills.channels': 'Pesan',
|
||||
'skills.explorer.emptyCta': 'Instal dari URL',
|
||||
'skills.explorer.emptyDescription':
|
||||
'Instal paket SKILL.md atau letakkan folder bergaya Hermes di ~/.openhuman/skills.',
|
||||
@@ -289,9 +361,9 @@ const messages: TranslationMap = {
|
||||
'skills.explorer.installed': 'Terpasang',
|
||||
'skills.explorer.install': 'Pasang',
|
||||
'skills.explorer.installing': 'Memasang…',
|
||||
'skills.integrations': 'Integrasi',
|
||||
'skills.integrations': 'Aplikasi',
|
||||
'skills.integrationsSubtitle':
|
||||
'Koneksi OAuth berbasis cloud — masuk dengan akun Anda dan Composio mengelola token agar agen dapat membaca dan bertindak atas nama Anda. Tidak perlu mengelola API key.',
|
||||
'Koneksi OAuth berbasis cloud — masuk dengan akun Anda dan token dikelola dengan aman agar agen dapat membaca dan bertindak atas nama Anda. Tidak perlu mengelola API key.',
|
||||
'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.',
|
||||
@@ -301,6 +373,11 @@ const messages: TranslationMap = {
|
||||
'skills.tabs.explorer': 'Skill',
|
||||
'skills.tabs.meetings': 'Rapat Google Meet',
|
||||
'skills.tabs.mcp': 'MCP Server',
|
||||
'connections.tabs.apps': 'Aplikasi',
|
||||
'connections.tabs.messaging': 'Pesan',
|
||||
'connections.tabs.tools': 'Alat',
|
||||
'connections.tabs.explorer': 'Penjelajah',
|
||||
'connections.tabs.talents': 'Talenta',
|
||||
'memory.title': 'Memori',
|
||||
'memory.search': 'Cari memori...',
|
||||
'memory.noResults': 'Memori tidak ditemukan',
|
||||
@@ -705,6 +782,7 @@ const messages: TranslationMap = {
|
||||
'team.failedChangeRole': 'Gagal mengubah peran',
|
||||
'team.failedRemoveMember': 'Gagal menghapus anggota',
|
||||
'devOptions.title': 'Opsi Developer',
|
||||
'devOptions.titleDiagnostics': 'Developer & Diagnostik',
|
||||
'devOptions.diagnostics': 'Diagnostik',
|
||||
'devOptions.diagnosticsDesc': 'Kesehatan sistem, log, dan metrik performa',
|
||||
'devOptions.toolPolicyDiagnosticsDesc':
|
||||
@@ -1824,7 +1902,7 @@ const messages: TranslationMap = {
|
||||
'common.enable': 'Aktifkan',
|
||||
'chat.safetyTimeout': 'Tidak ada respons dari agen setelah 2 menit. Coba lagi atau cek koneksi.',
|
||||
'chat.filter.general': 'Umum',
|
||||
'chat.filter.subconscious': 'Bawah sadar',
|
||||
'chat.filter.subconscious': 'Aktivitas latar belakang',
|
||||
'chat.filter.meetings': 'Rapat',
|
||||
'chat.filter.tasks': 'Tugas',
|
||||
'chat.selectThread': 'Pilih thread',
|
||||
@@ -3849,6 +3927,11 @@ const messages: TranslationMap = {
|
||||
'Konfigurasikan klien MCP eksternal untuk terhubung ke OpenHuman',
|
||||
'settings.developerMenu.autonomy.title': 'Otonomi agen',
|
||||
'settings.developerMenu.autonomy.desc': 'Batas laju aksi alat dan ambang keamanan',
|
||||
'settings.developerMenu.autocomplete.title': 'Pelengkap otomatis',
|
||||
'settings.developerMenu.autocomplete.desc':
|
||||
'Pengaturan pelengkap otomatis AI inline dan panel debug',
|
||||
'settings.developerMenu.voiceDebug.title': 'Suara (debug)',
|
||||
'settings.developerMenu.voiceDebug.desc': 'Status runtime dikte suara dan pengaturan debug',
|
||||
'settings.mcpServer.title': 'Server MCP',
|
||||
'settings.mcpServer.toolsSectionTitle': 'Alat yang tersedia',
|
||||
'settings.mcpServer.toolsSectionDesc':
|
||||
@@ -3950,6 +4033,27 @@ const messages: TranslationMap = {
|
||||
'settings.agentAccess.approvalHistoryDesc':
|
||||
'Tinjau keputusan Setuju / Tolak yang diminta oleh agen sebelumnya.',
|
||||
'settings.agentAccess.viewApprovalHistory': 'Lihat riwayat persetujuan',
|
||||
|
||||
// ── Panel Izin ────────────────────────────────────────────────────
|
||||
'settings.permissions.title': 'Izin',
|
||||
'settings.permissions.menuDesc':
|
||||
'Pilih apa yang dapat dilakukan asisten Anda dan di mana ia dapat bekerja.',
|
||||
'settings.permissions.accessMode': 'Apa yang bisa dilakukan asisten?',
|
||||
'settings.permissions.accessModeDesc':
|
||||
'Pilih seberapa banyak kebebasan yang dimiliki asisten saat mengambil tindakan di komputer Anda.',
|
||||
'settings.permissions.preset.readonly.title': 'Lihat, jangan sentuh',
|
||||
'settings.permissions.preset.readonly.desc':
|
||||
'Asisten dapat membaca file dan menjelajah — tetapi tidak pernah menulis, mengedit, atau menjalankan apa pun yang mengubah status.',
|
||||
'settings.permissions.preset.supervised.title': 'Tanya saya dulu',
|
||||
'settings.permissions.preset.supervised.desc':
|
||||
'Dapat membuat file baru secara bebas, tetapi selalu meminta persetujuan Anda sebelum mengedit, menjalankan perintah, atau mengakses jaringan.',
|
||||
'settings.permissions.preset.full.title': 'Kontrol penuh',
|
||||
'settings.permissions.preset.full.desc':
|
||||
'Berjalan dengan akses akun penuh Anda. Perintah destruktif, akses jaringan, dan instalasi masih meminta persetujuan.',
|
||||
'settings.permissions.folders': 'Di mana bisa bekerja?',
|
||||
'settings.permissions.foldersDesc':
|
||||
'Folder default yang dibaca dan ditulis asisten. Anda dapat menambahkan folder lain di pengaturan Lanjutan.',
|
||||
|
||||
'settings.sandbox.title': 'Eksekusi sandbox',
|
||||
'settings.sandbox.menuDesc': 'Konfigurasikan backend sandbox untuk isolasi alat agen.',
|
||||
'settings.sandbox.loading': 'Memuat…',
|
||||
@@ -4166,7 +4270,7 @@ const messages: TranslationMap = {
|
||||
'skills.channelIcon.telegram': 'Telegram',
|
||||
'skills.channelIcon.web': 'Web',
|
||||
'skills.channelIcon.yuanbao': 'Yuanbao',
|
||||
'skills.composio.poweredBy': 'Didukung oleh Composio',
|
||||
'skills.composio.poweredBy': 'OAuth',
|
||||
'skills.composio.staleStatusTitle': 'Koneksi menunjukkan status basi',
|
||||
'skills.create.allowedTools': 'Tool yang diizinkan',
|
||||
'skills.create.allowedToolsHelp': 'Dirender ke frontmatter SKILL.md sebagai',
|
||||
@@ -4796,6 +4900,12 @@ const messages: TranslationMap = {
|
||||
'autocomplete.maxChars': 'Karakter konteks maks',
|
||||
'autocomplete.overlayTtlMs': 'Batas waktu overlay (md)',
|
||||
'memory.tab.council': 'Council',
|
||||
'activity.tabs.automations': 'Otomatisasi',
|
||||
'activity.tabs.automationsDescription':
|
||||
'Prosedur yang dapat digunakan kembali dan dijalankan — tujuan beserta langkah-langkah untuk mencapainya.',
|
||||
'activity.tabs.backgroundActivity': 'Aktivitas latar belakang',
|
||||
'activity.tabs.alerts': 'Peringatan',
|
||||
|
||||
'intelligence.agents.title': 'Pustaka Agen',
|
||||
'intelligence.agents.subtitle':
|
||||
'Periksa spesialis yang dapat dijalankan dan kirim satu tugas ke agen tertentu.',
|
||||
|
||||
+115
-5
@@ -6,6 +6,11 @@ const messages: TranslationMap = {
|
||||
'nav.home': 'Home',
|
||||
'nav.human': 'Umano',
|
||||
'nav.chat': 'Chat',
|
||||
'nav.assistant': 'Assistente',
|
||||
'assistant.faceMode.on': 'Sta parlando con Tiny',
|
||||
'assistant.faceMode.off': 'Parla con Tiny',
|
||||
'assistant.faceMode.turnOn': 'Mostra mascotte',
|
||||
'assistant.faceMode.turnOff': 'Nascondi mascotte',
|
||||
'nav.connections': 'Connessioni',
|
||||
'nav.memory': 'Intelligenza',
|
||||
'nav.alerts': 'Avvisi',
|
||||
@@ -15,6 +20,12 @@ const messages: TranslationMap = {
|
||||
'nav.switchAgentProfile': 'Cambia profilo agente',
|
||||
'nav.defaultAgentProfile': 'Agente predefinito',
|
||||
'nav.noAgentProfiles': 'Nessun profilo agente trovato',
|
||||
'nav.activity': 'Attività',
|
||||
'nav.avatarMenu.account': 'Account',
|
||||
'nav.avatarMenu.billing': 'Fatturazione',
|
||||
'nav.avatarMenu.rewards': 'Premi',
|
||||
'nav.avatarMenu.invites': 'Invita un amico',
|
||||
'nav.avatarMenu.wallet': 'Portafoglio',
|
||||
'common.cancel': 'Annulla',
|
||||
'common.save': 'Salva',
|
||||
'common.confirm': 'Conferma',
|
||||
@@ -59,6 +70,53 @@ const messages: TranslationMap = {
|
||||
'common.comingSoon': 'Prossimamente',
|
||||
'common.breadcrumb': 'breadcrumb',
|
||||
'settings.general': 'Generale',
|
||||
// Settings layman groups (Phase 4 IA revamp)
|
||||
'settings.groups.account': 'Account',
|
||||
'settings.groups.assistant': 'Assistente',
|
||||
'settings.groups.privacySecurity': 'Privacy e sicurezza',
|
||||
'settings.groups.notifications': 'Notifiche',
|
||||
'settings.groups.about': 'Informazioni',
|
||||
'settings.assistant.personality': 'Personalità',
|
||||
'settings.assistant.personalityDesc': 'Nome, descrizione e persona SOUL.md',
|
||||
'settings.assistant.voice': 'Voce',
|
||||
'settings.assistant.voiceDesc': 'Impostazioni di sintesi vocale e riconoscimento vocale',
|
||||
'settings.assistant.faceMascot': 'Faccia / Mascotte',
|
||||
'settings.assistant.faceMascotDesc': "Scegli il colore della mascotte usato nell'app",
|
||||
'settings.assistant.backgroundActivity': 'Attività in background',
|
||||
'settings.assistant.backgroundActivityDesc':
|
||||
'Controlla quanto attivamente il tuo assistente lavora in background',
|
||||
'settings.assistant.screenAwareness': 'Consapevolezza dello schermo',
|
||||
'settings.assistant.screenAwarenessDesc': "Consenti all'assistente di vedere la finestra attiva",
|
||||
'settings.assistant.desktopCompanion': 'Compagno desktop',
|
||||
'settings.assistant.desktopCompanionDesc':
|
||||
'Modalità compagno sempre attiva con scorciatoia nella barra di sistema',
|
||||
'settings.assistant.permissions': 'Autorizzazioni',
|
||||
'settings.assistant.permissionsDesc': "Scegli cosa può fare l'assistente e dove può lavorare",
|
||||
'settings.privacySecurity.privacy': 'Privacy',
|
||||
'settings.privacySecurity.privacyDesc': 'Controlla quali dati lasciano il tuo computer',
|
||||
'settings.privacySecurity.security': 'Sicurezza',
|
||||
'settings.privacySecurity.securityDesc': 'Sessioni e opzioni di accesso',
|
||||
'settings.privacySecurity.approvalsHistory': 'Approvazioni e cronologia',
|
||||
'settings.privacySecurity.approvalsHistoryDesc':
|
||||
'Esamina le decisioni di approvazione degli strumenti recenti',
|
||||
'settings.notifications.menuTitle': 'Notifiche',
|
||||
'settings.notifications.menuDesc': 'Posta in arrivo degli avvisi e preferenze di notifica',
|
||||
'settings.devGroups.knowledgeMemory': 'Conoscenza e memoria',
|
||||
'settings.devGroups.agentsAutonomy': 'Agenti e autonomia',
|
||||
'settings.devGroups.modelsInference': 'Modelli e inferenza',
|
||||
'settings.devGroups.automationIntegrations': 'Automazione e integrazioni',
|
||||
'settings.devGroups.toolsCapabilities': 'Strumenti e capacità',
|
||||
'settings.devGroups.council': 'Consiglio',
|
||||
'settings.analysisViews.title': 'Viste di analisi',
|
||||
'settings.analysisViews.menuDesc':
|
||||
'Analisi del grafo di memoria — diagramma, centralità, coesione, associazioni, freschezza, cronologia, percorsi e namespace',
|
||||
'settings.buildInfo.title': 'Info build/versione',
|
||||
'settings.buildInfo.menuDesc': 'Build dell’app, versione e dettagli di connessione del core',
|
||||
'settings.dataSync.title': 'Sincronizzazione dati',
|
||||
'settings.dataSync.menuDesc': 'Ciò che il tuo assistente sincronizza — fonti, freschezza e stato',
|
||||
'settings.dataSync.description':
|
||||
"Gestisci ciò che viene sincronizzato nella memoria del tuo assistente: ogni fonte connessa con l'ora dell'ultima sincronizzazione, quanto è sincronizzato e se sta sincronizzando adesso.",
|
||||
'settings.devGroups.diagnosticsLogs': 'Diagnostica e registri',
|
||||
'settings.featuresAndAI': 'Funzionalità e AI',
|
||||
'settings.billingAndRewards': 'Fatturazione e premi',
|
||||
'settings.support': 'Supporto',
|
||||
@@ -87,6 +145,12 @@ const messages: TranslationMap = {
|
||||
'settings.developerOptions': 'Avanzate',
|
||||
'settings.developerOptionsDesc':
|
||||
'Configurazione AI, canali di messaggistica, strumenti, diagnostica e pannelli di debug',
|
||||
'settings.developerDiagnostics': 'Sviluppatore e Diagnostica',
|
||||
'settings.developerDiagnosticsDesc':
|
||||
'Strumenti avanzati per sviluppatori, diagnostica, memoria, agenti e pannelli di debug',
|
||||
'settings.developerMode.title': 'Modalità sviluppatore',
|
||||
'settings.developerMode.description': 'Mostra strumenti avanzati per sviluppatori e diagnostica',
|
||||
'settings.developerMode.enabledByBuild': 'Sempre attiva nelle build di sviluppo',
|
||||
'settings.clearAppData': 'Cancella dati app',
|
||||
'settings.clearAppDataDesc':
|
||||
"Disconnetti e cancella permanentemente tutti i dati locali dell'app",
|
||||
@@ -99,6 +163,14 @@ const messages: TranslationMap = {
|
||||
'settings.languageDesc': "Lingua di visualizzazione dell'interfaccia dell'app",
|
||||
'settings.alerts': 'Avvisi',
|
||||
'settings.alertsDesc': 'Visualizza avvisi recenti e attività nella tua posta',
|
||||
'settings.account.profile': 'Profilo',
|
||||
'settings.account.profileDesc': 'Nome, email e avatar',
|
||||
'settings.account.devices': 'Dispositivi',
|
||||
'settings.account.devicesDesc': 'Associa e gestisci dispositivi mobili',
|
||||
'settings.account.teamMembers': 'Team & membri',
|
||||
'settings.account.teamMembersDesc': 'Gestisci accesso al team e ruoli dei membri',
|
||||
'settings.account.dataMigration': 'Dati & migrazione',
|
||||
'settings.account.dataMigrationDesc': 'Importa memoria da un altro assistente',
|
||||
'settings.account.recoveryPhrase': 'Frase di recupero',
|
||||
'settings.account.recoveryPhraseDesc': 'Visualizza e fai il backup della frase di recupero',
|
||||
'settings.account.team': 'Squadra',
|
||||
@@ -265,7 +337,7 @@ const messages: TranslationMap = {
|
||||
'skills.connected': 'Connesso',
|
||||
'skills.available': 'Disponibile',
|
||||
'skills.addAccount': 'Aggiungi account',
|
||||
'skills.channels': 'Canali',
|
||||
'skills.channels': 'Messaggistica',
|
||||
'skills.explorer.emptyCta': 'Installa da URL',
|
||||
'skills.explorer.emptyDescription':
|
||||
'Installa un pacchetto SKILL.md o inserisci cartelle in stile Hermes in ~/.openhuman/skills.',
|
||||
@@ -293,9 +365,9 @@ const messages: TranslationMap = {
|
||||
'skills.explorer.installed': 'Installata',
|
||||
'skills.explorer.install': 'Installa',
|
||||
'skills.explorer.installing': 'Installazione…',
|
||||
'skills.integrations': 'Integrazioni',
|
||||
'skills.integrations': 'App',
|
||||
'skills.integrationsSubtitle':
|
||||
'Connessioni OAuth basate su cloud — accedi con il tuo account e Composio gestisce i token affinché gli agenti possano leggere e agire per tuo conto. Nessuna chiave API da gestire.',
|
||||
'Connessioni OAuth basate su cloud — accedi con il tuo account e i token sono gestiti in modo sicuro affinché gli agenti possano leggere e agire per tuo conto. Nessuna chiave API da gestire.',
|
||||
'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.',
|
||||
@@ -305,6 +377,11 @@ const messages: TranslationMap = {
|
||||
'skills.tabs.explorer': 'Skill',
|
||||
'skills.tabs.meetings': 'Riunioni Google Meet',
|
||||
'skills.tabs.mcp': 'MCP Server',
|
||||
'connections.tabs.apps': 'App',
|
||||
'connections.tabs.messaging': 'Messaggistica',
|
||||
'connections.tabs.tools': 'Strumenti',
|
||||
'connections.tabs.explorer': 'Esplora',
|
||||
'connections.tabs.talents': 'Talenti',
|
||||
'memory.title': 'Memoria',
|
||||
'memory.search': 'Cerca memorie...',
|
||||
'memory.noResults': 'Nessuna memoria trovata',
|
||||
@@ -713,6 +790,7 @@ const messages: TranslationMap = {
|
||||
'team.failedChangeRole': 'Impossibile modificare il ruolo',
|
||||
'team.failedRemoveMember': 'Impossibile rimuovere il membro',
|
||||
'devOptions.title': 'Avanzate',
|
||||
'devOptions.titleDiagnostics': 'Sviluppatore e Diagnostica',
|
||||
'devOptions.diagnostics': 'Diagnostica',
|
||||
'devOptions.diagnosticsDesc': 'Stato del sistema, log e metriche di performance',
|
||||
'devOptions.toolPolicyDiagnosticsDesc':
|
||||
@@ -1852,7 +1930,7 @@ const messages: TranslationMap = {
|
||||
'chat.safetyTimeout':
|
||||
"Nessuna risposta dall'agente dopo 2 minuti. Riprova o controlla la connessione.",
|
||||
'chat.filter.general': 'Generale',
|
||||
'chat.filter.subconscious': 'Subconscio',
|
||||
'chat.filter.subconscious': 'Attività in background',
|
||||
'chat.filter.meetings': 'Riunioni',
|
||||
'chat.filter.tasks': 'Attività',
|
||||
'chat.selectThread': 'Seleziona un thread',
|
||||
@@ -3900,6 +3978,12 @@ const messages: TranslationMap = {
|
||||
'settings.developerMenu.autonomy.title': 'Autonomia agente',
|
||||
'settings.developerMenu.autonomy.desc':
|
||||
'Limiti di frequenza delle azioni degli strumenti e soglie di sicurezza',
|
||||
'settings.developerMenu.autocomplete.title': 'Completamento automatico',
|
||||
'settings.developerMenu.autocomplete.desc':
|
||||
'Impostazioni del completamento automatico AI in linea e pannello di debug',
|
||||
'settings.developerMenu.voiceDebug.title': 'Voce (debug)',
|
||||
'settings.developerMenu.voiceDebug.desc':
|
||||
'Stato di runtime della dettatura vocale e impostazioni di debug',
|
||||
'settings.mcpServer.title': 'Server MCP',
|
||||
'settings.mcpServer.toolsSectionTitle': 'Strumenti disponibili',
|
||||
'settings.mcpServer.toolsSectionDesc':
|
||||
@@ -4003,6 +4087,26 @@ const messages: TranslationMap = {
|
||||
'settings.agentAccess.approvalHistoryDesc':
|
||||
"Rivedi le decisioni Approva / Nega precedenti richieste dall'agente.",
|
||||
'settings.agentAccess.viewApprovalHistory': 'Visualizza cronologia approvazioni',
|
||||
|
||||
// ── Pannello Autorizzazioni ───────────────────────────────────────
|
||||
'settings.permissions.title': 'Autorizzazioni',
|
||||
'settings.permissions.menuDesc': 'Scegli cosa può fare il tuo assistente e dove può lavorare.',
|
||||
'settings.permissions.accessMode': "Cosa può fare l'assistente?",
|
||||
'settings.permissions.accessModeDesc':
|
||||
"Scegli quanta libertà ha l'assistente quando esegue azioni sul tuo computer.",
|
||||
'settings.permissions.preset.readonly.title': 'Guarda, non toccare',
|
||||
'settings.permissions.preset.readonly.desc':
|
||||
"L'assistente può leggere file ed esplorare, ma non può mai scrivere, modificare o eseguire nulla che cambi lo stato.",
|
||||
'settings.permissions.preset.supervised.title': 'Chiedimi prima',
|
||||
'settings.permissions.preset.supervised.desc':
|
||||
'Può creare nuovi file liberamente, ma chiede sempre la tua approvazione prima di modificare, eseguire comandi o accedere alla rete.',
|
||||
'settings.permissions.preset.full.title': 'Controllo completo',
|
||||
'settings.permissions.preset.full.desc':
|
||||
"Opera con il tuo pieno accesso all'account. Comandi distruttivi, accesso alla rete e installazioni richiedono ancora approvazione.",
|
||||
'settings.permissions.folders': 'Dove può lavorare?',
|
||||
'settings.permissions.foldersDesc':
|
||||
"La cartella predefinita che l'assistente legge e scrive. Puoi aggiungere altre cartelle nelle impostazioni avanzate.",
|
||||
|
||||
'settings.sandbox.title': 'Esecuzione in sandbox',
|
||||
'settings.sandbox.menuDesc':
|
||||
"Configura i backend sandbox per l'isolamento degli strumenti agente.",
|
||||
@@ -4225,7 +4329,7 @@ const messages: TranslationMap = {
|
||||
'skills.channelIcon.telegram': 'Telegram',
|
||||
'skills.channelIcon.web': 'Web',
|
||||
'skills.channelIcon.yuanbao': 'Yuanbao',
|
||||
'skills.composio.poweredBy': 'Offerto da Composio',
|
||||
'skills.composio.poweredBy': 'OAuth',
|
||||
'skills.composio.staleStatusTitle': 'Le connessioni mostrano uno stato obsoleto',
|
||||
'skills.create.allowedTools': 'Strumenti consentiti',
|
||||
'skills.create.allowedToolsHelp': 'Resi nel frontmatter SKILL.md come',
|
||||
@@ -4869,6 +4973,12 @@ const messages: TranslationMap = {
|
||||
'autocomplete.maxChars': 'Caratteri massimi di contesto',
|
||||
'autocomplete.overlayTtlMs': 'Timeout overlay (ms)',
|
||||
'memory.tab.council': 'Council',
|
||||
'activity.tabs.automations': 'Automazioni',
|
||||
'activity.tabs.automationsDescription':
|
||||
'Procedure riutilizzabili ed eseguibili — un obiettivo e i passi per raggiungerlo.',
|
||||
'activity.tabs.backgroundActivity': 'Attività in background',
|
||||
'activity.tabs.alerts': 'Avvisi',
|
||||
|
||||
'intelligence.agents.title': 'Libreria agenti',
|
||||
'intelligence.agents.subtitle':
|
||||
'Ispeziona specialisti eseguibili e invia un compito a un agente specifico.',
|
||||
|
||||
+113
-5
@@ -6,6 +6,11 @@ const messages: TranslationMap = {
|
||||
'nav.home': '홈',
|
||||
'nav.human': '휴먼',
|
||||
'nav.chat': '채팅',
|
||||
'nav.assistant': '어시스턴트',
|
||||
'assistant.faceMode.on': 'Tiny와 대화 중',
|
||||
'assistant.faceMode.off': 'Tiny와 대화하기',
|
||||
'assistant.faceMode.turnOn': '마스코트 표시',
|
||||
'assistant.faceMode.turnOff': '마스코트 숨기기',
|
||||
'nav.connections': '연결',
|
||||
'nav.memory': '인텔리전스',
|
||||
'nav.alerts': '알림',
|
||||
@@ -15,6 +20,12 @@ const messages: TranslationMap = {
|
||||
'nav.switchAgentProfile': '에이전트 프로필 전환',
|
||||
'nav.defaultAgentProfile': '기본 에이전트',
|
||||
'nav.noAgentProfiles': '에이전트 프로필을 찾을 수 없습니다',
|
||||
'nav.activity': '활동',
|
||||
'nav.avatarMenu.account': '계정',
|
||||
'nav.avatarMenu.billing': '결제',
|
||||
'nav.avatarMenu.rewards': '보상',
|
||||
'nav.avatarMenu.invites': '친구 초대',
|
||||
'nav.avatarMenu.wallet': '지갑',
|
||||
'common.cancel': '취소',
|
||||
'common.save': '저장',
|
||||
'common.confirm': '확인',
|
||||
@@ -59,6 +70,53 @@ const messages: TranslationMap = {
|
||||
'common.comingSoon': '출시 예정',
|
||||
'common.breadcrumb': '탐색경로',
|
||||
'settings.general': '일반',
|
||||
// Settings layman groups (Phase 4 IA revamp)
|
||||
'settings.groups.account': '계정',
|
||||
'settings.groups.assistant': '어시스턴트',
|
||||
'settings.groups.privacySecurity': '개인정보 및 보안',
|
||||
'settings.groups.notifications': '알림',
|
||||
'settings.groups.about': '정보',
|
||||
'settings.assistant.personality': '개성',
|
||||
'settings.assistant.personalityDesc': '이름, 설명 및 SOUL.md 페르소나',
|
||||
'settings.assistant.voice': '음성',
|
||||
'settings.assistant.voiceDesc': '음성-텍스트 및 텍스트-음성 설정',
|
||||
'settings.assistant.faceMascot': '얼굴 / 마스코트',
|
||||
'settings.assistant.faceMascotDesc': '앱 전체에서 사용되는 마스코트 색상 선택',
|
||||
'settings.assistant.backgroundActivity': '백그라운드 활동',
|
||||
'settings.assistant.backgroundActivityDesc':
|
||||
'어시스턴트가 백그라운드에서 얼마나 활발히 작동하는지 제어',
|
||||
'settings.assistant.screenAwareness': '화면 인식',
|
||||
'settings.assistant.screenAwarenessDesc': '어시스턴트가 활성 창을 볼 수 있도록 허용',
|
||||
'settings.assistant.desktopCompanion': '데스크탑 동반자',
|
||||
'settings.assistant.desktopCompanionDesc':
|
||||
'시스템 트레이 단축키가 있는 항상 활성화된 동반자 모드',
|
||||
'settings.assistant.permissions': '권한',
|
||||
'settings.assistant.permissionsDesc':
|
||||
'어시스턴트가 무엇을 할 수 있고 어디서 작업할 수 있는지 선택하세요',
|
||||
'settings.privacySecurity.privacy': '개인정보',
|
||||
'settings.privacySecurity.privacyDesc': '컴퓨터를 떠나는 데이터 제어',
|
||||
'settings.privacySecurity.security': '보안',
|
||||
'settings.privacySecurity.securityDesc': '세션 및 로그인 옵션',
|
||||
'settings.privacySecurity.approvalsHistory': '승인 및 기록',
|
||||
'settings.privacySecurity.approvalsHistoryDesc': '최근 도구 승인 결정 검토',
|
||||
'settings.notifications.menuTitle': '알림',
|
||||
'settings.notifications.menuDesc': '알림 받은편지함 및 알림 기본설정',
|
||||
'settings.devGroups.knowledgeMemory': '지식 및 메모리',
|
||||
'settings.devGroups.agentsAutonomy': '에이전트 및 자율성',
|
||||
'settings.devGroups.modelsInference': '모델 및 추론',
|
||||
'settings.devGroups.automationIntegrations': '자동화 및 통합',
|
||||
'settings.devGroups.toolsCapabilities': '도구 및 기능',
|
||||
'settings.devGroups.council': '위원회',
|
||||
'settings.analysisViews.title': '분석 보기',
|
||||
'settings.analysisViews.menuDesc':
|
||||
'메모리 그래프 분석 — 다이어그램, 중심성, 응집도, 연관, 최신성, 타임라인, 경로 및 네임스페이스',
|
||||
'settings.buildInfo.title': '빌드 / 버전 정보',
|
||||
'settings.buildInfo.menuDesc': '앱 빌드, 버전 및 코어 연결 세부 정보',
|
||||
'settings.dataSync.title': '데이터 동기화',
|
||||
'settings.dataSync.menuDesc': '어시스턴트가 동기화하는 항목 — 소스, 최신성 및 상태',
|
||||
'settings.dataSync.description':
|
||||
'어시스턴트의 메모리에 무엇이 동기화되는지 관리하세요: 연결된 각 소스의 마지막 동기화 시간, 동기화된 양, 지금 동기화 중인지 여부를 확인할 수 있습니다.',
|
||||
'settings.devGroups.diagnosticsLogs': '진단 및 로그',
|
||||
'settings.featuresAndAI': '기능 및 AI',
|
||||
'settings.billingAndRewards': '결제 및 보상',
|
||||
'settings.support': '지원',
|
||||
@@ -86,6 +144,11 @@ const messages: TranslationMap = {
|
||||
'settings.aboutDesc': '앱 버전 및 소프트웨어 업데이트',
|
||||
'settings.developerOptions': '고급',
|
||||
'settings.developerOptionsDesc': 'AI 구성, 메시징 채널, 도구, 진단 및 디버그 패널',
|
||||
'settings.developerDiagnostics': '개발자 및 진단',
|
||||
'settings.developerDiagnosticsDesc': '고급 개발자 도구, 진단, 메모리, 에이전트 및 디버그 패널',
|
||||
'settings.developerMode.title': '개발자 모드',
|
||||
'settings.developerMode.description': '고급 개발자 및 진단 도구 표시',
|
||||
'settings.developerMode.enabledByBuild': '개발 빌드에서 항상 활성화',
|
||||
'settings.clearAppData': '앱 데이터 삭제',
|
||||
'settings.clearAppDataDesc': '로그아웃하고 모든 로컬 앱 데이터를 영구적으로 삭제',
|
||||
'settings.logOut': '로그아웃',
|
||||
@@ -97,6 +160,14 @@ const messages: TranslationMap = {
|
||||
'settings.languageDesc': '앱 인터페이스 표시 언어',
|
||||
'settings.alerts': '알림',
|
||||
'settings.alertsDesc': '받은 편지함에서 최근 알림 및 활동 보기',
|
||||
'settings.account.profile': '프로필',
|
||||
'settings.account.profileDesc': '이름, 이메일, 아바타',
|
||||
'settings.account.devices': '기기',
|
||||
'settings.account.devicesDesc': '모바일 기기 연결 및 관리',
|
||||
'settings.account.teamMembers': '팀 & 멤버',
|
||||
'settings.account.teamMembersDesc': '팀 접근 및 멤버 역할 관리',
|
||||
'settings.account.dataMigration': '데이터 & 마이그레이션',
|
||||
'settings.account.dataMigrationDesc': '다른 어시스턴트에서 메모리 가져오기',
|
||||
'settings.account.recoveryPhrase': '복구 문구',
|
||||
'settings.account.recoveryPhraseDesc': '계정 복구 문구를 확인하고 백업합니다',
|
||||
'settings.account.team': '팀',
|
||||
@@ -259,7 +330,7 @@ const messages: TranslationMap = {
|
||||
'skills.connected': '연결됨',
|
||||
'skills.available': '사용 가능',
|
||||
'skills.addAccount': '계정 추가',
|
||||
'skills.channels': '채널',
|
||||
'skills.channels': '메시징',
|
||||
'skills.explorer.emptyCta': 'URL에서 설치',
|
||||
'skills.explorer.emptyDescription':
|
||||
'SKILL.md 패키지를 설치하거나 Hermes 스타일 폴더를 ~/.openhuman/skills에 넣으세요.',
|
||||
@@ -287,9 +358,9 @@ const messages: TranslationMap = {
|
||||
'skills.explorer.installed': '설치됨',
|
||||
'skills.explorer.install': '설치',
|
||||
'skills.explorer.installing': '설치 중…',
|
||||
'skills.integrations': '통합',
|
||||
'skills.integrations': '앱',
|
||||
'skills.integrationsSubtitle':
|
||||
'클라우드 기반 OAuth 연결 — 계정으로 로그인하면 Composio가 토큰을 관리하여 에이전트가 사용자를 대신해 읽고 작동할 수 있습니다. API 키 관리가 필요 없습니다.',
|
||||
'클라우드 기반 OAuth 연결 — 계정으로 로그인하면 토큰이 안전하게 관리되어 에이전트가 사용자를 대신해 읽고 작동할 수 있습니다. API 키 관리가 필요 없습니다.',
|
||||
'skills.composio.noApiKeyTitle': '아니요 Composio API 키가 구성됨',
|
||||
'skills.composio.noApiKeyDescription':
|
||||
'로컬 모드에서는 자체 Composio API 키를 사용합니다. 설정 → 고급 → Composio에서 키를 추가한 후 여기서 통합을 연결하세요.',
|
||||
@@ -299,6 +370,11 @@ const messages: TranslationMap = {
|
||||
'skills.tabs.explorer': '스킬',
|
||||
'skills.tabs.meetings': 'Google Meet 회의',
|
||||
'skills.tabs.mcp': 'MCP 서버',
|
||||
'connections.tabs.apps': '앱',
|
||||
'connections.tabs.messaging': '메시징',
|
||||
'connections.tabs.tools': '도구',
|
||||
'connections.tabs.explorer': '탐색기',
|
||||
'connections.tabs.talents': '재능',
|
||||
'memory.title': '메모리',
|
||||
'memory.search': '메모리 검색...',
|
||||
'memory.noResults': '메모리를 찾을 수 없습니다',
|
||||
@@ -696,6 +772,7 @@ const messages: TranslationMap = {
|
||||
'team.failedChangeRole': '역할 변경 실패',
|
||||
'team.failedRemoveMember': '구성원 제거 실패',
|
||||
'devOptions.title': '고급',
|
||||
'devOptions.titleDiagnostics': '개발자 및 진단',
|
||||
'devOptions.diagnostics': '진단',
|
||||
'devOptions.diagnosticsDesc': '시스템 상태, 로그 및 성능 지표',
|
||||
'devOptions.toolPolicyDiagnosticsDesc': '도구 인벤토리, 정책 상태, MCP 허용 목록 및 최근 차단',
|
||||
@@ -1800,7 +1877,7 @@ const messages: TranslationMap = {
|
||||
'common.enable': '활성화',
|
||||
'chat.safetyTimeout': '2분 후에도 에이전트의 응답이 없습니다. 다시 시도하거나 연결을 확인하세요.',
|
||||
'chat.filter.general': '일반',
|
||||
'chat.filter.subconscious': '잠재의식',
|
||||
'chat.filter.subconscious': '백그라운드 활동',
|
||||
'chat.filter.meetings': '회의',
|
||||
'chat.filter.tasks': '작업',
|
||||
'chat.selectThread': '스레드 선택',
|
||||
@@ -3795,6 +3872,10 @@ const messages: TranslationMap = {
|
||||
'settings.developerMenu.mcpServer.desc': 'OpenHuman에 연결하도록 외부 MCP 클라이언트 구성',
|
||||
'settings.developerMenu.autonomy.title': '에이전트 자율성',
|
||||
'settings.developerMenu.autonomy.desc': '도구 작업 속도 제한 및 안전 임계값',
|
||||
'settings.developerMenu.autocomplete.title': '자동 완성',
|
||||
'settings.developerMenu.autocomplete.desc': 'AI 인라인 자동 완성 설정 및 디버그 패널',
|
||||
'settings.developerMenu.voiceDebug.title': '음성 (디버그)',
|
||||
'settings.developerMenu.voiceDebug.desc': '음성 받아쓰기 런타임 상태 및 디버그 설정',
|
||||
'settings.mcpServer.title': 'MCP 서버',
|
||||
'settings.mcpServer.toolsSectionTitle': '사용 가능한 도구',
|
||||
'settings.mcpServer.toolsSectionDesc':
|
||||
@@ -3893,6 +3974,27 @@ const messages: TranslationMap = {
|
||||
'settings.agentAccess.approvalHistory': '승인 이력',
|
||||
'settings.agentAccess.approvalHistoryDesc': '에이전트가 요청한 이전 승인/거부 결정을 검토합니다.',
|
||||
'settings.agentAccess.viewApprovalHistory': '승인 기록 보기',
|
||||
|
||||
// ── 권한 패널 ─────────────────────────────────────────────────────
|
||||
'settings.permissions.title': '권한',
|
||||
'settings.permissions.menuDesc':
|
||||
'어시스턴트가 무엇을 할 수 있고 어디서 작업할 수 있는지 선택하세요.',
|
||||
'settings.permissions.accessMode': '어시스턴트가 무엇을 할 수 있나요?',
|
||||
'settings.permissions.accessModeDesc':
|
||||
'어시스턴트가 컴퓨터에서 작업을 수행할 때 얼마나 많은 자유를 허용할지 선택하세요.',
|
||||
'settings.permissions.preset.readonly.title': '보기만 가능',
|
||||
'settings.permissions.preset.readonly.desc':
|
||||
'어시스턴트는 파일을 읽고 탐색할 수 있지만, 상태를 변경하는 쓰기, 편집 또는 실행은 절대 할 수 없습니다.',
|
||||
'settings.permissions.preset.supervised.title': '먼저 물어보기',
|
||||
'settings.permissions.preset.supervised.desc':
|
||||
'새 파일을 자유롭게 만들 수 있지만, 편집, 명령 실행 또는 네트워크 접근 전에 항상 승인을 요청합니다.',
|
||||
'settings.permissions.preset.full.title': '전체 제어',
|
||||
'settings.permissions.preset.full.desc':
|
||||
'전체 계정 접근 권한으로 실행됩니다. 파괴적 명령, 네트워크 접근 및 설치는 여전히 승인이 필요합니다.',
|
||||
'settings.permissions.folders': '어디서 작업할 수 있나요?',
|
||||
'settings.permissions.foldersDesc':
|
||||
'어시스턴트가 읽고 쓰는 기본 폴더입니다. 고급 설정에서 추가 폴더를 추가할 수 있습니다.',
|
||||
|
||||
'settings.sandbox.title': '샌드박스 실행',
|
||||
'settings.sandbox.menuDesc': '에이전트 도구 격리를 위한 샌드박스 백엔드를 구성합니다.',
|
||||
'settings.sandbox.loading': '로딩 중…',
|
||||
@@ -4108,7 +4210,7 @@ const messages: TranslationMap = {
|
||||
'skills.channelIcon.telegram': 'Telegram',
|
||||
'skills.channelIcon.web': '웹',
|
||||
'skills.channelIcon.yuanbao': 'Yuanbao',
|
||||
'skills.composio.poweredBy': 'Composio 제공',
|
||||
'skills.composio.poweredBy': 'OAuth',
|
||||
'skills.composio.staleStatusTitle': '연결이 오래된 상태를 표시합니다.',
|
||||
'skills.create.allowedTools': '허용된 도구',
|
||||
'skills.create.allowedToolsHelp': 'SKILL.md 앞부분에 다음과 같이 렌더링됩니다.',
|
||||
@@ -4733,6 +4835,12 @@ const messages: TranslationMap = {
|
||||
'autocomplete.maxChars': '최대 컨텍스트 문자 수',
|
||||
'autocomplete.overlayTtlMs': '오버레이 시간 초과 (ms)',
|
||||
'memory.tab.council': 'Council',
|
||||
'activity.tabs.automations': '자동화',
|
||||
'activity.tabs.automationsDescription':
|
||||
'재사용 가능하고 실행 가능한 절차 — 목표와 그것을 달성하기 위한 단계.',
|
||||
'activity.tabs.backgroundActivity': '백그라운드 활동',
|
||||
'activity.tabs.alerts': '알림',
|
||||
|
||||
'intelligence.agents.title': '에이전트 라이브러리',
|
||||
'intelligence.agents.subtitle':
|
||||
'실행 가능한 전문가를 살펴보고 지정한 에이전트에 작업을 보냅니다.',
|
||||
|
||||
+114
-5
@@ -6,6 +6,11 @@ const messages: TranslationMap = {
|
||||
'nav.home': 'Start',
|
||||
'nav.human': 'Człowiek',
|
||||
'nav.chat': 'Czat',
|
||||
'nav.assistant': 'Asystent',
|
||||
'assistant.faceMode.on': 'Rozmawia z Tiny',
|
||||
'assistant.faceMode.off': 'Porozmawiaj z Tiny',
|
||||
'assistant.faceMode.turnOn': 'Pokaż maskotka',
|
||||
'assistant.faceMode.turnOff': 'Ukryj maskotka',
|
||||
'nav.connections': 'Połączenia',
|
||||
'nav.memory': 'Inteligencja',
|
||||
'nav.alerts': 'Alerty',
|
||||
@@ -15,6 +20,12 @@ const messages: TranslationMap = {
|
||||
'nav.switchAgentProfile': 'Przełącz profil agenta',
|
||||
'nav.defaultAgentProfile': 'Domyślny agent',
|
||||
'nav.noAgentProfiles': 'Nie znaleziono profili agentów',
|
||||
'nav.activity': 'Aktywność',
|
||||
'nav.avatarMenu.account': 'Konto',
|
||||
'nav.avatarMenu.billing': 'Rozliczenia',
|
||||
'nav.avatarMenu.rewards': 'Nagrody',
|
||||
'nav.avatarMenu.invites': 'Zaproś znajomego',
|
||||
'nav.avatarMenu.wallet': 'Portfel',
|
||||
'common.cancel': 'Anuluj',
|
||||
'common.save': 'Zapisz',
|
||||
'common.confirm': 'Potwierdź',
|
||||
@@ -59,6 +70,52 @@ const messages: TranslationMap = {
|
||||
'common.comingSoon': 'Wkrótce',
|
||||
'common.breadcrumb': 'Ścieżka nawigacji',
|
||||
'settings.general': 'Ogólne',
|
||||
// Settings layman groups (Phase 4 IA revamp)
|
||||
'settings.groups.account': 'Konto',
|
||||
'settings.groups.assistant': 'Asystent',
|
||||
'settings.groups.privacySecurity': 'Prywatność i bezpieczeństwo',
|
||||
'settings.groups.notifications': 'Powiadomienia',
|
||||
'settings.groups.about': 'O aplikacji',
|
||||
'settings.assistant.personality': 'Osobowość',
|
||||
'settings.assistant.personalityDesc': 'Nazwa, opis i persona SOUL.md',
|
||||
'settings.assistant.voice': 'Głos',
|
||||
'settings.assistant.voiceDesc': 'Ustawienia mowy na tekst i tekstu na mowę',
|
||||
'settings.assistant.faceMascot': 'Twarz / Maskotka',
|
||||
'settings.assistant.faceMascotDesc': 'Wybierz kolor maskotki używany w aplikacji',
|
||||
'settings.assistant.backgroundActivity': 'Aktywność w tle',
|
||||
'settings.assistant.backgroundActivityDesc': 'Kontroluj, jak aktywnie asystent pracuje w tle',
|
||||
'settings.assistant.screenAwareness': 'Świadomość ekranu',
|
||||
'settings.assistant.screenAwarenessDesc': 'Pozwól asystentowi widzieć aktywne okno',
|
||||
'settings.assistant.desktopCompanion': 'Towarzysz pulpitu',
|
||||
'settings.assistant.desktopCompanionDesc':
|
||||
'Tryb towarzyski zawsze aktywny ze skrótem w zasobniku systemowym',
|
||||
'settings.assistant.permissions': 'Uprawnienia',
|
||||
'settings.assistant.permissionsDesc': 'Wybierz, co może robić asystent i gdzie może pracować',
|
||||
'settings.privacySecurity.privacy': 'Prywatność',
|
||||
'settings.privacySecurity.privacyDesc': 'Kontroluj, jakie dane opuszczają Twój komputer',
|
||||
'settings.privacySecurity.security': 'Bezpieczeństwo',
|
||||
'settings.privacySecurity.securityDesc': 'Sesje i opcje logowania',
|
||||
'settings.privacySecurity.approvalsHistory': 'Zatwierdzenia i historia',
|
||||
'settings.privacySecurity.approvalsHistoryDesc':
|
||||
'Przejrzyj ostatnie decyzje zatwierdzenia narzędzi',
|
||||
'settings.notifications.menuTitle': 'Powiadomienia',
|
||||
'settings.notifications.menuDesc': 'Skrzynka odbiorcza alertów i preferencje powiadomień',
|
||||
'settings.devGroups.knowledgeMemory': 'Wiedza i pamięć',
|
||||
'settings.devGroups.agentsAutonomy': 'Agenci i autonomia',
|
||||
'settings.devGroups.modelsInference': 'Modele i wnioskowanie',
|
||||
'settings.devGroups.automationIntegrations': 'Automatyzacja i integracje',
|
||||
'settings.devGroups.toolsCapabilities': 'Narzędzia i możliwości',
|
||||
'settings.devGroups.council': 'Rada',
|
||||
'settings.analysisViews.title': 'Widoki analizy',
|
||||
'settings.analysisViews.menuDesc':
|
||||
'Analiza grafu pamięci — diagram, centralność, spójność, powiązania, świeżość, oś czasu, ścieżki i przestrzenie nazw',
|
||||
'settings.buildInfo.title': 'Informacje o kompilacji/wersji',
|
||||
'settings.buildInfo.menuDesc': 'Kompilacja aplikacji, wersja i szczegóły połączenia z rdzeniem',
|
||||
'settings.dataSync.title': 'Synchronizacja danych',
|
||||
'settings.dataSync.menuDesc': 'Co synchronizuje Twój asystent — źródła, świeżość i status',
|
||||
'settings.dataSync.description':
|
||||
'Zarządzaj tym, co jest synchronizowane do pamięci asystenta: każde połączone źródło z czasem ostatniej synchronizacji, ile zsynchronizowano i czy synchronizacja trwa teraz.',
|
||||
'settings.devGroups.diagnosticsLogs': 'Diagnostyka i logi',
|
||||
'settings.featuresAndAI': 'Funkcje i AI',
|
||||
'settings.billingAndRewards': 'Rozliczenia i nagrody',
|
||||
'settings.support': 'Wsparcie',
|
||||
@@ -87,6 +144,13 @@ const messages: TranslationMap = {
|
||||
'settings.developerOptions': 'Zaawansowane',
|
||||
'settings.developerOptionsDesc':
|
||||
'Konfiguracja AI, kanały wiadomości, narzędzia, diagnostyka i panele debugowania',
|
||||
'settings.developerDiagnostics': 'Deweloper i Diagnostyka',
|
||||
'settings.developerDiagnosticsDesc':
|
||||
'Zaawansowane narzędzia deweloperskie, diagnostyka, pamięć, agenci i panele debugowania',
|
||||
'settings.developerMode.title': 'Tryb deweloperski',
|
||||
'settings.developerMode.description':
|
||||
'Pokaż zaawansowane narzędzia deweloperskie i diagnostyczne',
|
||||
'settings.developerMode.enabledByBuild': 'Zawsze włączony w buildach deweloperskich',
|
||||
'settings.clearAppData': 'Wyczyść dane aplikacji',
|
||||
'settings.clearAppDataDesc': 'Wyloguj się i trwale wyczyść wszystkie lokalne dane aplikacji',
|
||||
'settings.logOut': 'Wyloguj się',
|
||||
@@ -98,6 +162,14 @@ const messages: TranslationMap = {
|
||||
'settings.languageDesc': 'Język wyświetlania interfejsu aplikacji',
|
||||
'settings.alerts': 'Alerty',
|
||||
'settings.alertsDesc': 'Zobacz ostatnie alerty i aktywność w skrzynce odbiorczej',
|
||||
'settings.account.profile': 'Profil',
|
||||
'settings.account.profileDesc': 'Imię, e-mail i awatar',
|
||||
'settings.account.devices': 'Urządzenia',
|
||||
'settings.account.devicesDesc': 'Paruj urządzenia mobilne i zarządzaj nimi',
|
||||
'settings.account.teamMembers': 'Zespół i członkowie',
|
||||
'settings.account.teamMembersDesc': 'Zarządzaj dostępem do zespołu i rolami członków',
|
||||
'settings.account.dataMigration': 'Dane i migracja',
|
||||
'settings.account.dataMigrationDesc': 'Importuj pamięć z innego asystenta',
|
||||
'settings.account.recoveryPhrase': 'Fraza odzyskiwania',
|
||||
'settings.account.recoveryPhraseDesc': 'Wyświetl i zabezpiecz swoją frazę odzyskiwania',
|
||||
'settings.account.team': 'Zespół',
|
||||
@@ -263,7 +335,7 @@ const messages: TranslationMap = {
|
||||
'skills.connected': 'Połączone',
|
||||
'skills.available': 'Dostępne',
|
||||
'skills.addAccount': 'Dodaj konto',
|
||||
'skills.channels': 'Kanały',
|
||||
'skills.channels': 'Wiadomości',
|
||||
'skills.explorer.emptyCta': 'Zainstaluj z URL',
|
||||
'skills.explorer.emptyDescription':
|
||||
'Zainstaluj pakiet SKILL.md albo umieść foldery w stylu Hermes w ~/.openhuman/skills.',
|
||||
@@ -291,9 +363,9 @@ const messages: TranslationMap = {
|
||||
'skills.explorer.installed': 'Zainstalowano',
|
||||
'skills.explorer.install': 'Zainstaluj',
|
||||
'skills.explorer.installing': 'Instalowanie…',
|
||||
'skills.integrations': 'Integracje Composio',
|
||||
'skills.integrations': 'Aplikacje',
|
||||
'skills.integrationsSubtitle':
|
||||
'Chmurowe połączenia OAuth — zaloguj się swoim kontem, a Composio pośredniczy w tokenach, dzięki czemu agenci mogą czytać i działać w Twoim imieniu. Bez kluczy API do utrzymywania.',
|
||||
'Chmurowe połączenia OAuth — zaloguj się swoim kontem, a tokeny są bezpiecznie zarządzane, dzięki czemu agenci mogą czytać i działać w Twoim imieniu. Bez kluczy API do utrzymywania.',
|
||||
'skills.composio.noApiKeyTitle': 'Brak skonfigurowanego klucza API Composio',
|
||||
'skills.composio.noApiKeyDescription':
|
||||
'W trybie lokalnym używany jest Twój własny klucz API Composio. Otwórz Ustawienia → Zaawansowane → Composio, aby dodać klucz, zanim podłączysz tutaj integracje.',
|
||||
@@ -303,6 +375,11 @@ const messages: TranslationMap = {
|
||||
'skills.tabs.explorer': 'Skille',
|
||||
'skills.tabs.meetings': 'Spotkania Google Meet',
|
||||
'skills.tabs.mcp': 'Serwery MCP',
|
||||
'connections.tabs.apps': 'Aplikacje',
|
||||
'connections.tabs.messaging': 'Wiadomości',
|
||||
'connections.tabs.tools': 'Narzędzia',
|
||||
'connections.tabs.explorer': 'Eksplorator',
|
||||
'connections.tabs.talents': 'Talenty',
|
||||
'memory.title': 'Pamięć',
|
||||
'memory.search': 'Szukaj w pamięci...',
|
||||
'memory.noResults': 'Nie znaleziono wspomnień',
|
||||
@@ -711,6 +788,7 @@ const messages: TranslationMap = {
|
||||
'team.failedChangeRole': 'Nie udało się zmienić roli',
|
||||
'team.failedRemoveMember': 'Nie udało się usunąć członka',
|
||||
'devOptions.title': 'Zaawansowane',
|
||||
'devOptions.titleDiagnostics': 'Deweloper i Diagnostyka',
|
||||
'devOptions.diagnostics': 'Diagnostyka',
|
||||
'devOptions.diagnosticsDesc': 'Zdrowie systemu, logi i metryki wydajności',
|
||||
'devOptions.toolPolicyDiagnosticsDesc':
|
||||
@@ -1842,7 +1920,7 @@ const messages: TranslationMap = {
|
||||
'chat.safetyTimeout':
|
||||
'Brak odpowiedzi agenta po 2 minutach. Spróbuj ponownie lub sprawdź połączenie.',
|
||||
'chat.filter.general': 'Ogólne',
|
||||
'chat.filter.subconscious': 'Podświadomość',
|
||||
'chat.filter.subconscious': 'Aktywność w tle',
|
||||
'chat.filter.meetings': 'Spotkania',
|
||||
'chat.filter.tasks': 'Zadania',
|
||||
'chat.selectThread': 'Wybierz wątek',
|
||||
@@ -3899,6 +3977,11 @@ const messages: TranslationMap = {
|
||||
'Skonfiguruj zewnętrznych klientów MCP do połączenia z OpenHumanem',
|
||||
'settings.developerMenu.autonomy.title': 'Autonomia agenta',
|
||||
'settings.developerMenu.autonomy.desc': 'Limity szybkości akcji narzędzi i progi bezpieczeństwa',
|
||||
'settings.developerMenu.autocomplete.title': 'Autouzupełnianie',
|
||||
'settings.developerMenu.autocomplete.desc': 'Ustawienia autouzupełniania AI i panel debugowania',
|
||||
'settings.developerMenu.voiceDebug.title': 'Głos (debugowanie)',
|
||||
'settings.developerMenu.voiceDebug.desc':
|
||||
'Status środowiska uruchomieniowego dyktowania głosowego i ustawienia debugowania',
|
||||
'settings.mcpServer.title': 'Serwer MCP',
|
||||
'settings.mcpServer.toolsSectionTitle': 'Dostępne narzędzia',
|
||||
'settings.mcpServer.toolsSectionDesc':
|
||||
@@ -4000,6 +4083,26 @@ const messages: TranslationMap = {
|
||||
'settings.agentAccess.approvalHistoryDesc':
|
||||
'Przegląd wcześniejszych decyzji Zatwierdź / Odrzuć żądanych przez agenta.',
|
||||
'settings.agentAccess.viewApprovalHistory': 'Wyświetl historię zatwierdzeń',
|
||||
|
||||
// ── Panel Uprawnień ───────────────────────────────────────────────
|
||||
'settings.permissions.title': 'Uprawnienia',
|
||||
'settings.permissions.menuDesc': 'Wybierz, co może robić Twój asystent i gdzie może pracować.',
|
||||
'settings.permissions.accessMode': 'Co może robić asystent?',
|
||||
'settings.permissions.accessModeDesc':
|
||||
'Wybierz, ile swobody ma asystent podczas wykonywania działań na Twoim komputerze.',
|
||||
'settings.permissions.preset.readonly.title': 'Patrzeć, nie dotykać',
|
||||
'settings.permissions.preset.readonly.desc':
|
||||
'Asystent może czytać pliki i eksplorować — ale nigdy nie pisze, edytuje ani nie uruchamia niczego zmieniającego stan.',
|
||||
'settings.permissions.preset.supervised.title': 'Najpierw zapytaj',
|
||||
'settings.permissions.preset.supervised.desc':
|
||||
'Może swobodnie tworzyć nowe pliki, ale zawsze prosi o Twoją zgodę przed edycją, uruchamianiem poleceń lub dostępem do sieci.',
|
||||
'settings.permissions.preset.full.title': 'Pełna kontrola',
|
||||
'settings.permissions.preset.full.desc':
|
||||
'Działa z pełnym dostępem do Twojego konta. Destrukcyjne polecenia, dostęp do sieci i instalacje nadal wymagają zatwierdzenia.',
|
||||
'settings.permissions.folders': 'Gdzie może pracować?',
|
||||
'settings.permissions.foldersDesc':
|
||||
'Domyślny folder, który asystent odczytuje i zapisuje. Możesz dodać więcej folderów w ustawieniach zaawansowanych.',
|
||||
|
||||
'settings.sandbox.title': 'Wykonanie w piaskownicy',
|
||||
'settings.sandbox.menuDesc': 'Konfiguruj backendy piaskownicy do izolacji narzędzi agenta.',
|
||||
'settings.sandbox.loading': 'Ładowanie…',
|
||||
@@ -4220,7 +4323,7 @@ const messages: TranslationMap = {
|
||||
'skills.channelIcon.telegram': 'Telegram',
|
||||
'skills.channelIcon.web': 'Sieć',
|
||||
'skills.channelIcon.yuanbao': 'Yuanbao',
|
||||
'skills.composio.poweredBy': 'Napędzane przez Composio',
|
||||
'skills.composio.poweredBy': 'OAuth',
|
||||
'skills.composio.staleStatusTitle': 'Połączenia pokazują nieaktualny status',
|
||||
'skills.create.allowedTools': 'Dozwolone narzędzia',
|
||||
'skills.create.allowedToolsHelp': 'Renderowane do nagłówka frontmatter SKILL.md jako',
|
||||
@@ -4827,6 +4930,12 @@ const messages: TranslationMap = {
|
||||
'autocomplete.maxChars': 'Maksymalna liczba znaków kontekstu',
|
||||
'autocomplete.overlayTtlMs': 'Limit czasu nakładki (ms)',
|
||||
'memory.tab.council': 'Council',
|
||||
'activity.tabs.automations': 'Automatyzacje',
|
||||
'activity.tabs.automationsDescription':
|
||||
'Wielokrotnego użytku, uruchamialne procedury — cel i kroki do jego osiągnięcia.',
|
||||
'activity.tabs.backgroundActivity': 'Aktywność w tle',
|
||||
'activity.tabs.alerts': 'Alerty',
|
||||
|
||||
'intelligence.agents.title': 'Biblioteka agentów',
|
||||
'intelligence.agents.subtitle':
|
||||
'Przeglądaj uruchamialnych specjalistów i wyślij zadanie do wskazanego agenta.',
|
||||
|
||||
+118
-5
@@ -6,6 +6,11 @@ const messages: TranslationMap = {
|
||||
'nav.home': 'Início',
|
||||
'nav.human': 'Humano',
|
||||
'nav.chat': 'Bate-papo',
|
||||
'nav.assistant': 'Assistente',
|
||||
'assistant.faceMode.on': 'Falando com o Tiny',
|
||||
'assistant.faceMode.off': 'Falar com o Tiny',
|
||||
'assistant.faceMode.turnOn': 'Mostrar mascote',
|
||||
'assistant.faceMode.turnOff': 'Ocultar mascote',
|
||||
'nav.connections': 'Conexões',
|
||||
'nav.memory': 'Inteligência',
|
||||
'nav.alerts': 'Alertas',
|
||||
@@ -15,6 +20,12 @@ const messages: TranslationMap = {
|
||||
'nav.switchAgentProfile': 'Trocar perfil de agente',
|
||||
'nav.defaultAgentProfile': 'Agente padrão',
|
||||
'nav.noAgentProfiles': 'Nenhum perfil de agente encontrado',
|
||||
'nav.activity': 'Atividade',
|
||||
'nav.avatarMenu.account': 'Conta',
|
||||
'nav.avatarMenu.billing': 'Faturamento',
|
||||
'nav.avatarMenu.rewards': 'Recompensas',
|
||||
'nav.avatarMenu.invites': 'Convidar um amigo',
|
||||
'nav.avatarMenu.wallet': 'Carteira',
|
||||
'common.cancel': 'Cancelar',
|
||||
'common.save': 'Salvar',
|
||||
'common.confirm': 'Confirmar',
|
||||
@@ -59,6 +70,54 @@ const messages: TranslationMap = {
|
||||
'common.comingSoon': 'Em breve',
|
||||
'common.breadcrumb': 'Breadcrumb',
|
||||
'settings.general': 'Geral',
|
||||
// Settings layman groups (Phase 4 IA revamp)
|
||||
'settings.groups.account': 'Conta',
|
||||
'settings.groups.assistant': 'Assistente',
|
||||
'settings.groups.privacySecurity': 'Privacidade e segurança',
|
||||
'settings.groups.notifications': 'Notificações',
|
||||
'settings.groups.about': 'Sobre',
|
||||
'settings.assistant.personality': 'Personalidade',
|
||||
'settings.assistant.personalityDesc': 'Nome, descrição e persona SOUL.md',
|
||||
'settings.assistant.voice': 'Voz',
|
||||
'settings.assistant.voiceDesc': 'Configurações de fala para texto e texto para fala',
|
||||
'settings.assistant.faceMascot': 'Rosto / Mascote',
|
||||
'settings.assistant.faceMascotDesc': 'Escolha a cor do mascote usada no aplicativo',
|
||||
'settings.assistant.backgroundActivity': 'Atividade em segundo plano',
|
||||
'settings.assistant.backgroundActivityDesc':
|
||||
'Controle o quão ativamente seu assistente trabalha em segundo plano',
|
||||
'settings.assistant.screenAwareness': 'Consciência de tela',
|
||||
'settings.assistant.screenAwarenessDesc': 'Permitir que o assistente veja sua janela ativa',
|
||||
'settings.assistant.desktopCompanion': 'Companheiro de área de trabalho',
|
||||
'settings.assistant.desktopCompanionDesc':
|
||||
'Modo companheiro sempre ativo com atalho na bandeja do sistema',
|
||||
'settings.assistant.permissions': 'Permissões',
|
||||
'settings.assistant.permissionsDesc':
|
||||
'Escolha o que o assistente pode fazer e onde pode trabalhar',
|
||||
'settings.privacySecurity.privacy': 'Privacidade',
|
||||
'settings.privacySecurity.privacyDesc': 'Controlar quais dados saem do seu computador',
|
||||
'settings.privacySecurity.security': 'Segurança',
|
||||
'settings.privacySecurity.securityDesc': 'Sessões e opções de login',
|
||||
'settings.privacySecurity.approvalsHistory': 'Aprovações e histórico',
|
||||
'settings.privacySecurity.approvalsHistoryDesc':
|
||||
'Revisar decisões recentes de aprovação de ferramentas',
|
||||
'settings.notifications.menuTitle': 'Notificações',
|
||||
'settings.notifications.menuDesc': 'Caixa de entrada de alertas e preferências de notificações',
|
||||
'settings.devGroups.knowledgeMemory': 'Conhecimento e memória',
|
||||
'settings.devGroups.agentsAutonomy': 'Agentes e autonomia',
|
||||
'settings.devGroups.modelsInference': 'Modelos e inferência',
|
||||
'settings.devGroups.automationIntegrations': 'Automação e integrações',
|
||||
'settings.devGroups.toolsCapabilities': 'Ferramentas e capacidades',
|
||||
'settings.devGroups.council': 'Conselho',
|
||||
'settings.analysisViews.title': 'Visões de análise',
|
||||
'settings.analysisViews.menuDesc':
|
||||
'Análise do grafo de memória — diagrama, centralidade, coesão, associações, atualidade, linha do tempo, caminhos e namespaces',
|
||||
'settings.buildInfo.title': 'Informações de build/versão',
|
||||
'settings.buildInfo.menuDesc': 'Build do app, versão e detalhes de conexão do núcleo',
|
||||
'settings.dataSync.title': 'Sincronização de dados',
|
||||
'settings.dataSync.menuDesc': 'O que o seu assistente sincroniza — fontes, atualidade e status',
|
||||
'settings.dataSync.description':
|
||||
'Gerencie o que é sincronizado na memória do seu assistente: cada fonte conectada com seu horário da última sincronização, quanto foi sincronizado e se está sincronizando agora.',
|
||||
'settings.devGroups.diagnosticsLogs': 'Diagnósticos e registros',
|
||||
'settings.featuresAndAI': 'Recursos e IA',
|
||||
'settings.billingAndRewards': 'Cobrança e Recompensas',
|
||||
'settings.support': 'Suporte',
|
||||
@@ -88,6 +147,13 @@ const messages: TranslationMap = {
|
||||
'settings.developerOptions': 'Avançado',
|
||||
'settings.developerOptionsDesc':
|
||||
'Configuração de IA, canais de mensagens, ferramentas, diagnósticos e painéis de depuração',
|
||||
'settings.developerDiagnostics': 'Desenvolvedor e Diagnósticos',
|
||||
'settings.developerDiagnosticsDesc':
|
||||
'Ferramentas avançadas de desenvolvedor, diagnósticos, memória, agentes e painéis de depuração',
|
||||
'settings.developerMode.title': 'Modo desenvolvedor',
|
||||
'settings.developerMode.description':
|
||||
'Mostrar ferramentas avançadas de desenvolvedor e diagnóstico',
|
||||
'settings.developerMode.enabledByBuild': 'Sempre ativado em builds de desenvolvimento',
|
||||
'settings.clearAppData': 'Limpar Dados do App',
|
||||
'settings.clearAppDataDesc': 'Sair e excluir permanentemente todos os dados locais do app',
|
||||
'settings.logOut': 'Sair',
|
||||
@@ -99,6 +165,14 @@ const messages: TranslationMap = {
|
||||
'settings.languageDesc': 'Idioma de exibição da interface do app',
|
||||
'settings.alerts': 'Alertas',
|
||||
'settings.alertsDesc': 'Ver alertas recentes e atividades na sua caixa de entrada',
|
||||
'settings.account.profile': 'Perfil',
|
||||
'settings.account.profileDesc': 'Nome, e-mail e avatar',
|
||||
'settings.account.devices': 'Dispositivos',
|
||||
'settings.account.devicesDesc': 'Vincular e gerenciar dispositivos móveis',
|
||||
'settings.account.teamMembers': 'Equipe & membros',
|
||||
'settings.account.teamMembersDesc': 'Gerenciar acesso à equipe e funções dos membros',
|
||||
'settings.account.dataMigration': 'Dados & migração',
|
||||
'settings.account.dataMigrationDesc': 'Importar memória de outro assistente',
|
||||
'settings.account.recoveryPhrase': 'Frase de Recuperação',
|
||||
'settings.account.recoveryPhraseDesc': 'Ver e fazer backup da sua frase de recuperação de conta',
|
||||
'settings.account.team': 'Equipe',
|
||||
@@ -267,7 +341,7 @@ const messages: TranslationMap = {
|
||||
'skills.connected': 'Conectado',
|
||||
'skills.available': 'Disponível',
|
||||
'skills.addAccount': 'Adicionar Conta',
|
||||
'skills.channels': 'Canais',
|
||||
'skills.channels': 'Mensagens',
|
||||
'skills.explorer.emptyCta': 'Instalar por URL',
|
||||
'skills.explorer.emptyDescription':
|
||||
'Instale um pacote SKILL.md ou coloque pastas no estilo Hermes em ~/.openhuman/skills.',
|
||||
@@ -295,9 +369,9 @@ const messages: TranslationMap = {
|
||||
'skills.explorer.installed': 'Instalada',
|
||||
'skills.explorer.install': 'Instalar',
|
||||
'skills.explorer.installing': 'Instalando…',
|
||||
'skills.integrations': 'Integrações',
|
||||
'skills.integrations': 'Aplicativos',
|
||||
'skills.integrationsSubtitle':
|
||||
'Conexões OAuth baseadas em nuvem — faça login com sua conta e o Composio gerencia os tokens para que os agentes possam ler e agir em seu nome. Sem chaves de API para gerenciar.',
|
||||
'Conexões OAuth baseadas em nuvem — faça login com sua conta e os tokens são gerenciados com segurança para que os agentes possam ler e agir em seu nome. Sem chaves de API para gerenciar.',
|
||||
'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.',
|
||||
@@ -307,6 +381,11 @@ const messages: TranslationMap = {
|
||||
'skills.tabs.explorer': 'Skills',
|
||||
'skills.tabs.meetings': 'Reuniões do Google Meet',
|
||||
'skills.tabs.mcp': 'MCP Servidores',
|
||||
'connections.tabs.apps': 'Aplicativos',
|
||||
'connections.tabs.messaging': 'Mensagens',
|
||||
'connections.tabs.tools': 'Ferramentas',
|
||||
'connections.tabs.explorer': 'Explorar',
|
||||
'connections.tabs.talents': 'Talentos',
|
||||
'memory.title': 'Memória',
|
||||
'memory.search': 'Pesquisar memórias...',
|
||||
'memory.noResults': 'Nenhuma memória encontrada',
|
||||
@@ -717,6 +796,7 @@ const messages: TranslationMap = {
|
||||
'team.failedChangeRole': 'Falha ao alterar função',
|
||||
'team.failedRemoveMember': 'Falha ao remover membro',
|
||||
'devOptions.title': 'Avançado',
|
||||
'devOptions.titleDiagnostics': 'Desenvolvedor e Diagnósticos',
|
||||
'devOptions.diagnostics': 'Diagnósticos',
|
||||
'devOptions.diagnosticsDesc': 'Saúde do sistema, logs e métricas de desempenho',
|
||||
'devOptions.toolPolicyDiagnosticsDesc':
|
||||
@@ -1859,7 +1939,7 @@ const messages: TranslationMap = {
|
||||
'chat.safetyTimeout':
|
||||
'Nenhuma resposta do agente após 2 minutos. Tente novamente ou verifique sua conexão.',
|
||||
'chat.filter.general': 'Geral',
|
||||
'chat.filter.subconscious': 'Subconsciente',
|
||||
'chat.filter.subconscious': 'Atividade em segundo plano',
|
||||
'chat.filter.meetings': 'Reuniões',
|
||||
'chat.filter.tasks': 'Tarefas',
|
||||
'chat.selectThread': 'Selecione uma conversa',
|
||||
@@ -3899,6 +3979,12 @@ const messages: TranslationMap = {
|
||||
'settings.developerMenu.autonomy.title': 'Autonomia do agente',
|
||||
'settings.developerMenu.autonomy.desc':
|
||||
'Limites de taxa de ações de ferramentas e limites de segurança',
|
||||
'settings.developerMenu.autocomplete.title': 'Preenchimento automático',
|
||||
'settings.developerMenu.autocomplete.desc':
|
||||
'Configurações de preenchimento automático de IA embutido e painel de depuração',
|
||||
'settings.developerMenu.voiceDebug.title': 'Voz (depuração)',
|
||||
'settings.developerMenu.voiceDebug.desc':
|
||||
'Status de tempo de execução do ditado de voz e configurações de depuração',
|
||||
'settings.mcpServer.title': 'Servidor MCP',
|
||||
'settings.mcpServer.toolsSectionTitle': 'Ferramentas disponíveis',
|
||||
'settings.mcpServer.toolsSectionDesc':
|
||||
@@ -4001,6 +4087,27 @@ const messages: TranslationMap = {
|
||||
'settings.agentAccess.approvalHistoryDesc':
|
||||
'Revise as decisões anteriores de Aprovar / Negar solicitadas pelo agente.',
|
||||
'settings.agentAccess.viewApprovalHistory': 'Ver histórico de aprovações',
|
||||
|
||||
// ── Painel de Permissões ──────────────────────────────────────────
|
||||
'settings.permissions.title': 'Permissões',
|
||||
'settings.permissions.menuDesc':
|
||||
'Escolha o que o seu assistente pode fazer e onde pode trabalhar.',
|
||||
'settings.permissions.accessMode': 'O que o assistente pode fazer?',
|
||||
'settings.permissions.accessModeDesc':
|
||||
'Escolha quanta liberdade o assistente tem ao realizar ações no seu computador.',
|
||||
'settings.permissions.preset.readonly.title': 'Olhar, não tocar',
|
||||
'settings.permissions.preset.readonly.desc':
|
||||
'O assistente pode ler ficheiros e explorar — mas nunca escrever, editar ou executar qualquer coisa que mude o estado.',
|
||||
'settings.permissions.preset.supervised.title': 'Perguntar primeiro',
|
||||
'settings.permissions.preset.supervised.desc':
|
||||
'Pode criar novos ficheiros livremente, mas pede sempre a sua aprovação antes de editar, executar comandos ou aceder à rede.',
|
||||
'settings.permissions.preset.full.title': 'Controlo total',
|
||||
'settings.permissions.preset.full.desc':
|
||||
'Opera com o seu acesso completo à conta. Comandos destrutivos, acesso à rede e instalações ainda pedem aprovação.',
|
||||
'settings.permissions.folders': 'Onde pode trabalhar?',
|
||||
'settings.permissions.foldersDesc':
|
||||
'A pasta predefinida que o assistente lê e escreve. Pode adicionar mais pastas nas definições Avançadas.',
|
||||
|
||||
'settings.sandbox.title': 'Execução em sandbox',
|
||||
'settings.sandbox.menuDesc':
|
||||
'Configure backends de sandbox para isolamento das ferramentas do agente.',
|
||||
@@ -4224,7 +4331,7 @@ const messages: TranslationMap = {
|
||||
'skills.channelIcon.telegram': 'Telegram',
|
||||
'skills.channelIcon.web': 'Web',
|
||||
'skills.channelIcon.yuanbao': 'Yuanbao',
|
||||
'skills.composio.poweredBy': 'Desenvolvido por Composio',
|
||||
'skills.composio.poweredBy': 'OAuth',
|
||||
'skills.composio.staleStatusTitle': 'As conexões estão mostrando status obsoleto',
|
||||
'skills.create.allowedTools': 'Ferramentas permitidas',
|
||||
'skills.create.allowedToolsHelp': 'Renderizado no frontmatter SKILL.md como',
|
||||
@@ -4861,6 +4968,12 @@ const messages: TranslationMap = {
|
||||
'autocomplete.maxChars': 'Máximo de caracteres de contexto',
|
||||
'autocomplete.overlayTtlMs': 'Tempo limite da sobreposição (ms)',
|
||||
'memory.tab.council': 'Council',
|
||||
'activity.tabs.automations': 'Automações',
|
||||
'activity.tabs.automationsDescription':
|
||||
'Procedimentos reutilizáveis e executáveis — um objetivo e os passos para alcançá-lo.',
|
||||
'activity.tabs.backgroundActivity': 'Atividade em segundo plano',
|
||||
'activity.tabs.alerts': 'Alertas',
|
||||
|
||||
'intelligence.agents.title': 'Biblioteca de agentes',
|
||||
'intelligence.agents.subtitle':
|
||||
'Inspecione especialistas executáveis e envie uma tarefa para um agente nomeado.',
|
||||
|
||||
+118
-5
@@ -6,6 +6,11 @@ const messages: TranslationMap = {
|
||||
'nav.home': 'Главная',
|
||||
'nav.human': 'Человек',
|
||||
'nav.chat': 'Чат',
|
||||
'nav.assistant': 'Ассистент',
|
||||
'assistant.faceMode.on': 'Говорит с Tiny',
|
||||
'assistant.faceMode.off': 'Поговорить с Tiny',
|
||||
'assistant.faceMode.turnOn': 'Показать маскота',
|
||||
'assistant.faceMode.turnOff': 'Скрыть маскота',
|
||||
'nav.connections': 'Подключения',
|
||||
'nav.memory': 'Интеллект',
|
||||
'nav.alerts': 'Оповещения',
|
||||
@@ -15,6 +20,12 @@ const messages: TranslationMap = {
|
||||
'nav.switchAgentProfile': 'Сменить профиль агента',
|
||||
'nav.defaultAgentProfile': 'Агент по умолчанию',
|
||||
'nav.noAgentProfiles': 'Профили агентов не найдены',
|
||||
'nav.activity': 'Активность',
|
||||
'nav.avatarMenu.account': 'Аккаунт',
|
||||
'nav.avatarMenu.billing': 'Оплата',
|
||||
'nav.avatarMenu.rewards': 'Награды',
|
||||
'nav.avatarMenu.invites': 'Пригласить друга',
|
||||
'nav.avatarMenu.wallet': 'Кошелёк',
|
||||
'common.cancel': 'Отмена',
|
||||
'common.save': 'Сохранить',
|
||||
'common.confirm': 'Подтвердить',
|
||||
@@ -59,6 +70,54 @@ const messages: TranslationMap = {
|
||||
'common.comingSoon': 'Скоро',
|
||||
'common.breadcrumb': 'Breadcrumb',
|
||||
'settings.general': 'Общие',
|
||||
// Settings layman groups (Phase 4 IA revamp)
|
||||
'settings.groups.account': 'Аккаунт',
|
||||
'settings.groups.assistant': 'Ассистент',
|
||||
'settings.groups.privacySecurity': 'Конфиденциальность и безопасность',
|
||||
'settings.groups.notifications': 'Уведомления',
|
||||
'settings.groups.about': 'О приложении',
|
||||
'settings.assistant.personality': 'Личность',
|
||||
'settings.assistant.personalityDesc': 'Имя, описание и персона SOUL.md',
|
||||
'settings.assistant.voice': 'Голос',
|
||||
'settings.assistant.voiceDesc': 'Настройки распознавания и синтеза речи',
|
||||
'settings.assistant.faceMascot': 'Лицо / Маскот',
|
||||
'settings.assistant.faceMascotDesc': 'Выберите цвет маскота в приложении',
|
||||
'settings.assistant.backgroundActivity': 'Фоновая активность',
|
||||
'settings.assistant.backgroundActivityDesc':
|
||||
'Управление тем, насколько активно ассистент работает в фоне',
|
||||
'settings.assistant.screenAwareness': 'Осведомлённость об экране',
|
||||
'settings.assistant.screenAwarenessDesc': 'Разрешить ассистенту видеть активное окно',
|
||||
'settings.assistant.desktopCompanion': 'Компаньон рабочего стола',
|
||||
'settings.assistant.desktopCompanionDesc':
|
||||
'Режим постоянного компаньона с ярлыком в системном лотке',
|
||||
'settings.assistant.permissions': 'Разрешения',
|
||||
'settings.assistant.permissionsDesc':
|
||||
'Выберите, что может делать помощник и где он может работать',
|
||||
'settings.privacySecurity.privacy': 'Конфиденциальность',
|
||||
'settings.privacySecurity.privacyDesc': 'Контроль данных, покидающих ваш компьютер',
|
||||
'settings.privacySecurity.security': 'Безопасность',
|
||||
'settings.privacySecurity.securityDesc': 'Сеансы и параметры входа',
|
||||
'settings.privacySecurity.approvalsHistory': 'Подтверждения и история',
|
||||
'settings.privacySecurity.approvalsHistoryDesc':
|
||||
'Просмотр недавних решений об одобрении инструментов',
|
||||
'settings.notifications.menuTitle': 'Уведомления',
|
||||
'settings.notifications.menuDesc': 'Входящие оповещения и настройки уведомлений',
|
||||
'settings.devGroups.knowledgeMemory': 'Знания и память',
|
||||
'settings.devGroups.agentsAutonomy': 'Агенты и автономия',
|
||||
'settings.devGroups.modelsInference': 'Модели и вывод',
|
||||
'settings.devGroups.automationIntegrations': 'Автоматизация и интеграции',
|
||||
'settings.devGroups.toolsCapabilities': 'Инструменты и возможности',
|
||||
'settings.devGroups.council': 'Совет',
|
||||
'settings.analysisViews.title': 'Аналитические виды',
|
||||
'settings.analysisViews.menuDesc':
|
||||
'Анализ графа памяти — диаграмма, центральность, связность, ассоциации, свежесть, временная шкала, пути и пространства имён',
|
||||
'settings.buildInfo.title': 'Сведения о сборке/версии',
|
||||
'settings.buildInfo.menuDesc': 'Сборка приложения, версия и сведения о подключении ядра',
|
||||
'settings.dataSync.title': 'Синхронизация данных',
|
||||
'settings.dataSync.menuDesc': 'Что синхронизирует ваш ассистент — источники, свежесть и статус',
|
||||
'settings.dataSync.description':
|
||||
'Управляйте тем, что синхронизируется в память ассистента: каждый подключённый источник с временем последней синхронизации, объёмом синхронизированных данных и тем, идёт ли синхронизация сейчас.',
|
||||
'settings.devGroups.diagnosticsLogs': 'Диагностика и логи',
|
||||
'settings.featuresAndAI': 'Функции и AI',
|
||||
'settings.billingAndRewards': 'Оплата и награды',
|
||||
'settings.support': 'Поддержка',
|
||||
@@ -88,6 +147,13 @@ const messages: TranslationMap = {
|
||||
'settings.developerOptions': 'Дополнительно',
|
||||
'settings.developerOptionsDesc':
|
||||
'Настройки AI, каналы связи, инструменты, диагностика и панели отладки',
|
||||
'settings.developerDiagnostics': 'Разработчик и Диагностика',
|
||||
'settings.developerDiagnosticsDesc':
|
||||
'Расширенные инструменты разработчика, диагностика, память, агенты и панели отладки',
|
||||
'settings.developerMode.title': 'Режим разработчика',
|
||||
'settings.developerMode.description':
|
||||
'Показывать расширенные инструменты разработчика и диагностики',
|
||||
'settings.developerMode.enabledByBuild': 'Всегда включён в сборках разработки',
|
||||
'settings.clearAppData': 'Очистить данные приложения',
|
||||
'settings.clearAppDataDesc': 'Выйти из аккаунта и удалить все локальные данные приложения',
|
||||
'settings.logOut': 'Выйти',
|
||||
@@ -99,6 +165,14 @@ const messages: TranslationMap = {
|
||||
'settings.languageDesc': 'Язык отображения интерфейса',
|
||||
'settings.alerts': 'Оповещения',
|
||||
'settings.alertsDesc': 'Смотри последние оповещения и активность во входящих',
|
||||
'settings.account.profile': 'Профиль',
|
||||
'settings.account.profileDesc': 'Имя, email и аватар',
|
||||
'settings.account.devices': 'Устройства',
|
||||
'settings.account.devicesDesc': 'Сопрягайте мобильные устройства и управляйте ими',
|
||||
'settings.account.teamMembers': 'Команда и участники',
|
||||
'settings.account.teamMembersDesc': 'Управляйте доступом команды и ролями участников',
|
||||
'settings.account.dataMigration': 'Данные и миграция',
|
||||
'settings.account.dataMigrationDesc': 'Импорт памяти из другого ассистента',
|
||||
'settings.account.recoveryPhrase': 'Фраза восстановления',
|
||||
'settings.account.recoveryPhraseDesc': 'Просмотр и резервное копирование фразы восстановления',
|
||||
'settings.account.team': 'Команда',
|
||||
@@ -261,7 +335,7 @@ const messages: TranslationMap = {
|
||||
'skills.connected': 'Подключено',
|
||||
'skills.available': 'Доступно',
|
||||
'skills.addAccount': 'Добавить аккаунт',
|
||||
'skills.channels': 'Каналы',
|
||||
'skills.channels': 'Сообщения',
|
||||
'skills.explorer.emptyCta': 'Установить по URL',
|
||||
'skills.explorer.emptyDescription':
|
||||
'Установите пакет SKILL.md или поместите папки в стиле Hermes в ~/.openhuman/skills.',
|
||||
@@ -289,9 +363,9 @@ const messages: TranslationMap = {
|
||||
'skills.explorer.installed': 'Установлено',
|
||||
'skills.explorer.install': 'Установить',
|
||||
'skills.explorer.installing': 'Установка…',
|
||||
'skills.integrations': 'Интеграции',
|
||||
'skills.integrations': 'Приложения',
|
||||
'skills.integrationsSubtitle':
|
||||
'Облачные OAuth-подключения — войдите в свой аккаунт, и Composio управляет токенами, чтобы агенты могли читать и действовать от вашего имени. Никаких API-ключей для управления.',
|
||||
'Облачные OAuth-подключения — войдите в свой аккаунт, и токены управляются безопасно, чтобы агенты могли читать и действовать от вашего имени. Никаких API-ключей для управления.',
|
||||
'skills.composio.noApiKeyTitle': 'Ключ API Composio не настроен',
|
||||
'skills.composio.noApiKeyDescription':
|
||||
'Локальный режим использует ваш собственный ключ API Composio. Откройте Настройки → Дополнительно → Composio, чтобы добавить ключ перед подключением интеграций здесь.',
|
||||
@@ -301,6 +375,11 @@ const messages: TranslationMap = {
|
||||
'skills.tabs.explorer': 'Навыки',
|
||||
'skills.tabs.meetings': 'Встречи Google Meet',
|
||||
'skills.tabs.mcp': 'MCP Серверы',
|
||||
'connections.tabs.apps': 'Приложения',
|
||||
'connections.tabs.messaging': 'Сообщения',
|
||||
'connections.tabs.tools': 'Инструменты',
|
||||
'connections.tabs.explorer': 'Обозреватель',
|
||||
'connections.tabs.talents': 'Таланты',
|
||||
'memory.title': 'Память',
|
||||
'memory.search': 'Поиск воспоминаний...',
|
||||
'memory.noResults': 'Воспоминания не найдены',
|
||||
@@ -705,6 +784,7 @@ const messages: TranslationMap = {
|
||||
'team.failedChangeRole': 'Не удалось изменить роль.',
|
||||
'team.failedRemoveMember': 'Не удалось удалить участника.',
|
||||
'devOptions.title': 'Дополнительно',
|
||||
'devOptions.titleDiagnostics': 'Разработчик и Диагностика',
|
||||
'devOptions.diagnostics': 'Диагностика',
|
||||
'devOptions.diagnosticsDesc': 'Состояние системы, логи и метрики производительности',
|
||||
'devOptions.toolPolicyDiagnosticsDesc':
|
||||
@@ -1835,7 +1915,7 @@ const messages: TranslationMap = {
|
||||
'chat.safetyTimeout':
|
||||
'Агент не ответил в течение 2 минут. Попробуй снова или проверь соединение.',
|
||||
'chat.filter.general': 'Общее',
|
||||
'chat.filter.subconscious': 'Подсознание',
|
||||
'chat.filter.subconscious': 'Фоновая активность',
|
||||
'chat.filter.meetings': 'Встречи',
|
||||
'chat.filter.tasks': 'Задачи',
|
||||
'chat.selectThread': 'Выбери чат',
|
||||
@@ -3868,6 +3948,12 @@ const messages: TranslationMap = {
|
||||
'settings.developerMenu.autonomy.title': 'Автономия агента',
|
||||
'settings.developerMenu.autonomy.desc':
|
||||
'Ограничения частоты действий инструментов и пороги безопасности',
|
||||
'settings.developerMenu.autocomplete.title': 'Автодополнение',
|
||||
'settings.developerMenu.autocomplete.desc':
|
||||
'Настройки встроенного автодополнения ИИ и панель отладки',
|
||||
'settings.developerMenu.voiceDebug.title': 'Голос (отладка)',
|
||||
'settings.developerMenu.voiceDebug.desc':
|
||||
'Состояние среды выполнения голосового диктовки и настройки отладки',
|
||||
'settings.mcpServer.title': 'MCP',
|
||||
'settings.mcpServer.toolsSectionTitle': 'Доступные инструменты',
|
||||
'settings.mcpServer.toolsSectionDesc':
|
||||
@@ -3969,6 +4055,27 @@ const messages: TranslationMap = {
|
||||
'settings.agentAccess.approvalHistoryDesc':
|
||||
'Просмотр прошлых решений Одобрить / Отклонить, запрошенных агентом.',
|
||||
'settings.agentAccess.viewApprovalHistory': 'Просмотреть историю утверждений',
|
||||
|
||||
// ── Панель разрешений ─────────────────────────────────────────────
|
||||
'settings.permissions.title': 'Разрешения',
|
||||
'settings.permissions.menuDesc':
|
||||
'Выберите, что может делать ваш помощник и где он может работать.',
|
||||
'settings.permissions.accessMode': 'Что может делать помощник?',
|
||||
'settings.permissions.accessModeDesc':
|
||||
'Выберите, насколько свободен помощник при выполнении действий на вашем компьютере.',
|
||||
'settings.permissions.preset.readonly.title': 'Смотреть, не трогать',
|
||||
'settings.permissions.preset.readonly.desc':
|
||||
'Помощник может читать файлы и исследовать систему — но никогда не пишет, не редактирует и не запускает ничего, что изменяет состояние.',
|
||||
'settings.permissions.preset.supervised.title': 'Сначала спросить',
|
||||
'settings.permissions.preset.supervised.desc':
|
||||
'Может свободно создавать новые файлы, но всегда запрашивает ваше одобрение перед редактированием, выполнением команд или доступом к сети.',
|
||||
'settings.permissions.preset.full.title': 'Полный контроль',
|
||||
'settings.permissions.preset.full.desc':
|
||||
'Работает с полным доступом к вашей учётной записи. Деструктивные команды, доступ к сети и установка всё равно требуют одобрения.',
|
||||
'settings.permissions.folders': 'Где он может работать?',
|
||||
'settings.permissions.foldersDesc':
|
||||
'Папка по умолчанию, которую помощник читает и в которую пишет. В расширенных настройках можно добавить другие папки.',
|
||||
|
||||
'settings.sandbox.title': 'Выполнение в песочнице',
|
||||
'settings.sandbox.menuDesc': 'Настройте бэкенды песочницы для изоляции инструментов агента.',
|
||||
'settings.sandbox.loading': 'Загрузка…',
|
||||
@@ -4188,7 +4295,7 @@ const messages: TranslationMap = {
|
||||
'skills.channelIcon.telegram': 'Telegram',
|
||||
'skills.channelIcon.web': 'Интернет',
|
||||
'skills.channelIcon.yuanbao': 'Yuanbao',
|
||||
'skills.composio.poweredBy': 'Работает на Composio',
|
||||
'skills.composio.poweredBy': 'OAuth',
|
||||
'skills.composio.staleStatusTitle': 'Соединения показывают устаревший статус',
|
||||
'skills.create.allowedTools': 'Разрешённые инструменты',
|
||||
'skills.create.allowedToolsHelp': 'Отображается в SKILL.md как',
|
||||
@@ -4826,6 +4933,12 @@ const messages: TranslationMap = {
|
||||
'autocomplete.maxChars': 'Макс. символов контекста',
|
||||
'autocomplete.overlayTtlMs': 'Тайм-аут наложения (мс)',
|
||||
'memory.tab.council': 'Council',
|
||||
'activity.tabs.automations': 'Автоматизации',
|
||||
'activity.tabs.automationsDescription':
|
||||
'Многократно используемые, запускаемые процедуры — цель и шаги для её достижения.',
|
||||
'activity.tabs.backgroundActivity': 'Фоновая активность',
|
||||
'activity.tabs.alerts': 'Уведомления',
|
||||
|
||||
'intelligence.agents.title': 'Библиотека агентов',
|
||||
'intelligence.agents.subtitle':
|
||||
'Просматривайте доступных специалистов и отправляйте задачу выбранному агенту.',
|
||||
|
||||
+106
-5
@@ -6,6 +6,11 @@ const messages: TranslationMap = {
|
||||
'nav.home': '首页',
|
||||
'nav.human': '助手',
|
||||
'nav.chat': '对话',
|
||||
'nav.assistant': '助手',
|
||||
'assistant.faceMode.on': '正在与 Tiny 对话',
|
||||
'assistant.faceMode.off': '与 Tiny 对话',
|
||||
'assistant.faceMode.turnOn': '显示形象',
|
||||
'assistant.faceMode.turnOff': '隐藏形象',
|
||||
'nav.connections': '连接',
|
||||
'nav.memory': '记忆',
|
||||
'nav.alerts': '通知',
|
||||
@@ -15,6 +20,12 @@ const messages: TranslationMap = {
|
||||
'nav.switchAgentProfile': '切换代理档案',
|
||||
'nav.defaultAgentProfile': '默认代理',
|
||||
'nav.noAgentProfiles': '未找到代理档案',
|
||||
'nav.activity': '动态',
|
||||
'nav.avatarMenu.account': '账户',
|
||||
'nav.avatarMenu.billing': '账单',
|
||||
'nav.avatarMenu.rewards': '奖励',
|
||||
'nav.avatarMenu.invites': '邀请好友',
|
||||
'nav.avatarMenu.wallet': '钱包',
|
||||
'common.cancel': '取消',
|
||||
'common.save': '保存',
|
||||
'common.confirm': '确认',
|
||||
@@ -59,6 +70,50 @@ const messages: TranslationMap = {
|
||||
'common.comingSoon': '即将推出',
|
||||
'common.breadcrumb': '面包屑',
|
||||
'settings.general': '通用',
|
||||
// Settings layman groups (Phase 4 IA revamp)
|
||||
'settings.groups.account': '账户',
|
||||
'settings.groups.assistant': '助手',
|
||||
'settings.groups.privacySecurity': '隐私与安全',
|
||||
'settings.groups.notifications': '通知',
|
||||
'settings.groups.about': '关于',
|
||||
'settings.assistant.personality': '个性',
|
||||
'settings.assistant.personalityDesc': '名称、描述和 SOUL.md 人设',
|
||||
'settings.assistant.voice': '语音',
|
||||
'settings.assistant.voiceDesc': '语音转文字和文字转语音设置',
|
||||
'settings.assistant.faceMascot': '面孔 / 吉祥物',
|
||||
'settings.assistant.faceMascotDesc': '选择应用中使用的吉祥物颜色',
|
||||
'settings.assistant.backgroundActivity': '后台活动',
|
||||
'settings.assistant.backgroundActivityDesc': '控制助手在后台工作的活跃程度',
|
||||
'settings.assistant.screenAwareness': '屏幕感知',
|
||||
'settings.assistant.screenAwarenessDesc': '允许助手查看您的活动窗口',
|
||||
'settings.assistant.desktopCompanion': '桌面伴侣',
|
||||
'settings.assistant.desktopCompanionDesc': '带有系统托盘快捷方式的常驻伴侣模式',
|
||||
'settings.assistant.permissions': '权限',
|
||||
'settings.assistant.permissionsDesc': '选择助手可以做什么以及可以在哪里工作',
|
||||
'settings.privacySecurity.privacy': '隐私',
|
||||
'settings.privacySecurity.privacyDesc': '控制哪些数据离开您的计算机',
|
||||
'settings.privacySecurity.security': '安全',
|
||||
'settings.privacySecurity.securityDesc': '会话和登录选项',
|
||||
'settings.privacySecurity.approvalsHistory': '审批与历史',
|
||||
'settings.privacySecurity.approvalsHistoryDesc': '查看最近的工具审批决定',
|
||||
'settings.notifications.menuTitle': '通知',
|
||||
'settings.notifications.menuDesc': '提醒收件箱和通知偏好设置',
|
||||
'settings.devGroups.knowledgeMemory': '知识与记忆',
|
||||
'settings.devGroups.agentsAutonomy': '智能体与自主性',
|
||||
'settings.devGroups.modelsInference': '模型与推理',
|
||||
'settings.devGroups.automationIntegrations': '自动化与集成',
|
||||
'settings.devGroups.toolsCapabilities': '工具与能力',
|
||||
'settings.devGroups.council': '委员会',
|
||||
'settings.analysisViews.title': '分析视图',
|
||||
'settings.analysisViews.menuDesc':
|
||||
'内存图分析 — 关系图、中心性、内聚性、关联、新鲜度、时间线、路径和命名空间',
|
||||
'settings.buildInfo.title': '构建/版本信息',
|
||||
'settings.buildInfo.menuDesc': '应用构建、版本和核心连接详情',
|
||||
'settings.dataSync.title': '数据同步',
|
||||
'settings.dataSync.menuDesc': '助手同步的内容 — 来源、新鲜度和状态',
|
||||
'settings.dataSync.description':
|
||||
'管理同步到助手记忆中的内容:每个已连接的来源及其上次同步时间、已同步的数量,以及当前是否正在同步。',
|
||||
'settings.devGroups.diagnosticsLogs': '诊断与日志',
|
||||
'settings.featuresAndAI': '功能与 AI',
|
||||
'settings.billingAndRewards': '账单与奖励',
|
||||
'settings.support': '支持',
|
||||
@@ -86,6 +141,11 @@ const messages: TranslationMap = {
|
||||
'settings.aboutDesc': '应用版本与软件更新',
|
||||
'settings.developerOptions': '开发者选项',
|
||||
'settings.developerOptionsDesc': '诊断、调试面板、Webhook 与记忆检查',
|
||||
'settings.developerDiagnostics': '开发者与诊断',
|
||||
'settings.developerDiagnosticsDesc': '高级开发者工具、诊断、记忆、代理和调试面板',
|
||||
'settings.developerMode.title': '开发者模式',
|
||||
'settings.developerMode.description': '显示高级开发者和诊断工具',
|
||||
'settings.developerMode.enabledByBuild': '开发版本中始终启用',
|
||||
'settings.clearAppData': '清除应用数据',
|
||||
'settings.clearAppDataDesc': '退出登录并永久清除所有本地应用数据',
|
||||
'settings.logOut': '退出登录',
|
||||
@@ -97,6 +157,14 @@ const messages: TranslationMap = {
|
||||
'settings.languageDesc': '应用界面显示语言',
|
||||
'settings.alerts': '通知',
|
||||
'settings.alertsDesc': '查看收件箱中的最新通知和活动',
|
||||
'settings.account.profile': '个人资料',
|
||||
'settings.account.profileDesc': '姓名、邮箱和头像',
|
||||
'settings.account.devices': '设备',
|
||||
'settings.account.devicesDesc': '配对和管理移动设备',
|
||||
'settings.account.teamMembers': '团队与成员',
|
||||
'settings.account.teamMembersDesc': '管理团队访问权限和成员角色',
|
||||
'settings.account.dataMigration': '数据与迁移',
|
||||
'settings.account.dataMigrationDesc': '从其他助手导入记忆',
|
||||
'settings.account.recoveryPhrase': '恢复短语',
|
||||
'settings.account.recoveryPhraseDesc': '查看并备份你的账户恢复短语',
|
||||
'settings.account.team': '团队',
|
||||
@@ -248,7 +316,7 @@ const messages: TranslationMap = {
|
||||
'skills.connected': '已连接',
|
||||
'skills.available': '可用',
|
||||
'skills.addAccount': '添加账户',
|
||||
'skills.channels': '渠道',
|
||||
'skills.channels': '消息',
|
||||
'skills.explorer.emptyCta': '通过 URL 安装',
|
||||
'skills.explorer.emptyDescription':
|
||||
'安装 SKILL.md 包,或将 Hermes 风格的文件夹放到 ~/.openhuman/skills 下。',
|
||||
@@ -274,9 +342,9 @@ const messages: TranslationMap = {
|
||||
'skills.explorer.installed': '已安装',
|
||||
'skills.explorer.install': '安装',
|
||||
'skills.explorer.installing': '安装中…',
|
||||
'skills.integrations': '集成',
|
||||
'skills.integrations': '应用',
|
||||
'skills.integrationsSubtitle':
|
||||
'基于云端的 OAuth 连接——使用您的账户登录,Composio 代管令牌,让智能体能以您的名义读取数据并执行操作,无需管理 API 密钥。',
|
||||
'基于云端的 OAuth 连接——使用您的账户登录,令牌将被安全管理,让智能体能以您的名义读取数据并执行操作,无需管理 API 密钥。',
|
||||
'skills.composio.noApiKeyTitle': '尚未配置 Composio API 密钥',
|
||||
'skills.composio.noApiKeyDescription':
|
||||
'本地模式使用你自己的 Composio API 密钥。在此连接集成之前,请打开 设置 → 高级 → Composio 添加一个密钥。',
|
||||
@@ -286,6 +354,11 @@ const messages: TranslationMap = {
|
||||
'skills.tabs.explorer': '技能',
|
||||
'skills.tabs.meetings': 'Google Meet 会议',
|
||||
'skills.tabs.mcp': 'MCP 服务器',
|
||||
'connections.tabs.apps': '应用',
|
||||
'connections.tabs.messaging': '消息',
|
||||
'connections.tabs.tools': '工具',
|
||||
'connections.tabs.explorer': '探索',
|
||||
'connections.tabs.talents': '才能',
|
||||
'memory.title': '记忆',
|
||||
'memory.search': '搜索记忆...',
|
||||
'memory.noResults': '未找到记忆',
|
||||
@@ -662,6 +735,7 @@ const messages: TranslationMap = {
|
||||
'team.failedChangeRole': '角色变更失败',
|
||||
'team.failedRemoveMember': '删除会员失败',
|
||||
'devOptions.title': '开发者选项',
|
||||
'devOptions.titleDiagnostics': '开发者与诊断',
|
||||
'devOptions.diagnostics': '诊断',
|
||||
'devOptions.diagnosticsDesc': '系统健康、日志与性能指标',
|
||||
'devOptions.toolPolicyDiagnosticsDesc': '工具清单、策略态势、MCP 允许列表和近期拦截',
|
||||
@@ -1718,7 +1792,7 @@ const messages: TranslationMap = {
|
||||
'common.enable': '启用',
|
||||
'chat.safetyTimeout': '助手 2 分钟内未响应。请重试或检查你的连接。',
|
||||
'chat.filter.general': '常规',
|
||||
'chat.filter.subconscious': '潜意识',
|
||||
'chat.filter.subconscious': '后台活动',
|
||||
'chat.filter.meetings': '会议',
|
||||
'chat.filter.tasks': '任务',
|
||||
'chat.selectThread': '选择一个对话',
|
||||
@@ -3645,6 +3719,10 @@ const messages: TranslationMap = {
|
||||
'settings.developerMenu.mcpServer.desc': '配置外部 MCP 客户端以连接到 OpenHuman',
|
||||
'settings.developerMenu.autonomy.title': '智能体自主权',
|
||||
'settings.developerMenu.autonomy.desc': '工具操作速率限制和安全阈值',
|
||||
'settings.developerMenu.autocomplete.title': '自动补全',
|
||||
'settings.developerMenu.autocomplete.desc': 'AI 内联自动补全设置和调试面板',
|
||||
'settings.developerMenu.voiceDebug.title': '语音(调试)',
|
||||
'settings.developerMenu.voiceDebug.desc': '语音听写运行时状态和调试设置',
|
||||
'settings.mcpServer.title': 'MCP 服务器',
|
||||
'settings.mcpServer.toolsSectionTitle': '可用工具',
|
||||
'settings.mcpServer.toolsSectionDesc':
|
||||
@@ -3741,6 +3819,24 @@ const messages: TranslationMap = {
|
||||
'settings.agentAccess.approvalHistory': '审批历史',
|
||||
'settings.agentAccess.approvalHistoryDesc': '查看智能体请求的历次批准/拒绝决定。',
|
||||
'settings.agentAccess.viewApprovalHistory': '查看审批历史',
|
||||
|
||||
// ── 权限面板 ──────────────────────────────────────────────────────
|
||||
'settings.permissions.title': '权限',
|
||||
'settings.permissions.menuDesc': '选择您的助手可以做什么以及可以在哪里工作。',
|
||||
'settings.permissions.accessMode': '助手可以做什么?',
|
||||
'settings.permissions.accessModeDesc': '选择助手在您的电脑上执行操作时拥有多大的自由度。',
|
||||
'settings.permissions.preset.readonly.title': '只看不动',
|
||||
'settings.permissions.preset.readonly.desc':
|
||||
'助手可以读取文件并进行探索 — 但绝不会写入、编辑或运行任何改变状态的内容。',
|
||||
'settings.permissions.preset.supervised.title': '先征求意见',
|
||||
'settings.permissions.preset.supervised.desc':
|
||||
'可以自由创建新文件,但在编辑、运行命令或访问网络之前,始终会请求您的批准。',
|
||||
'settings.permissions.preset.full.title': '完全控制',
|
||||
'settings.permissions.preset.full.desc':
|
||||
'以您的完整账户权限运行。破坏性命令、网络访问和安装仍需批准。',
|
||||
'settings.permissions.folders': '它可以在哪里工作?',
|
||||
'settings.permissions.foldersDesc': '助手读写的默认文件夹。您可以在高级设置中添加更多文件夹。',
|
||||
|
||||
'settings.sandbox.title': '沙盒执行',
|
||||
'settings.sandbox.menuDesc': '配置沙盒后端以隔离代理工具。',
|
||||
'settings.sandbox.loading': '加载中…',
|
||||
@@ -3947,7 +4043,7 @@ const messages: TranslationMap = {
|
||||
'skills.channelIcon.telegram': 'Telegram',
|
||||
'skills.channelIcon.web': '网络',
|
||||
'skills.channelIcon.yuanbao': '元宝',
|
||||
'skills.composio.poweredBy': '由 Composio 提供支持',
|
||||
'skills.composio.poweredBy': 'OAuth',
|
||||
'skills.composio.staleStatusTitle': '连接显示陈旧状态',
|
||||
'skills.create.allowedTools': '允许的工具',
|
||||
'skills.create.allowedToolsHelp': '渲染到 SKILL.md frontmatter 中为',
|
||||
@@ -4544,6 +4640,11 @@ const messages: TranslationMap = {
|
||||
'autocomplete.maxChars': '最大上下文字符数',
|
||||
'autocomplete.overlayTtlMs': '覆盖层超时 (ms)',
|
||||
'memory.tab.council': 'Council',
|
||||
'activity.tabs.automations': '自动化',
|
||||
'activity.tabs.automationsDescription': '可复用、可运行的流程 — 目标及达成目标的步骤。',
|
||||
'activity.tabs.backgroundActivity': '后台活动',
|
||||
'activity.tabs.alerts': '提醒',
|
||||
|
||||
'intelligence.agents.title': '智能体库',
|
||||
'intelligence.agents.subtitle': '查看可运行的专家,并把一个任务交给指定智能体。',
|
||||
'intelligence.agents.refresh': '刷新',
|
||||
|
||||
@@ -78,8 +78,8 @@ describe('resolveSystemRoute', () => {
|
||||
expect(resolveSystemRoute(makeSystem({ category: 'agents' }))).toBe('/chat');
|
||||
});
|
||||
|
||||
it('routes skills category to /skills', () => {
|
||||
expect(resolveSystemRoute(makeSystem({ category: 'skills' }))).toBe('/skills');
|
||||
it('routes skills category to /connections (Phase 2 rename)', () => {
|
||||
expect(resolveSystemRoute(makeSystem({ category: 'skills' }))).toBe('/connections');
|
||||
});
|
||||
|
||||
it('routes system category to /home', () => {
|
||||
|
||||
@@ -11,7 +11,7 @@ const log = debug('notifications:router');
|
||||
|
||||
const ROUTES = {
|
||||
chat: '/chat',
|
||||
skills: '/skills',
|
||||
skills: '/connections',
|
||||
home: '/home',
|
||||
notifications: '/notifications',
|
||||
} as const;
|
||||
@@ -83,7 +83,7 @@ export function resolveSystemRoute(item: NotificationItem): string {
|
||||
log('[notification-router] system id=%s category=agents → /chat', item.id);
|
||||
return ROUTES.chat;
|
||||
case 'skills':
|
||||
log('[notification-router] system id=%s category=skills → /skills', item.id);
|
||||
log('[notification-router] system id=%s category=skills → /connections', item.id);
|
||||
return ROUTES.skills;
|
||||
case 'system':
|
||||
log('[notification-router] system id=%s category=system → /home', item.id);
|
||||
|
||||
+168
-4
@@ -3,6 +3,13 @@ import { useEffect, useMemo, useState } from 'react';
|
||||
import AddAccountModal from '../components/accounts/AddAccountModal';
|
||||
import { AgentIcon, ProviderIcon } from '../components/accounts/providerIcons';
|
||||
import WebviewHost from '../components/accounts/WebviewHost';
|
||||
import {
|
||||
CustomGifMascot,
|
||||
getMascotPalette,
|
||||
hexToArgbInt,
|
||||
RiveMascot,
|
||||
} from '../features/human/Mascot';
|
||||
import { useHumanMascot } from '../features/human/useHumanMascot';
|
||||
import { usePrewarmMostRecentAccount } from '../hooks/usePrewarmMostRecentAccount';
|
||||
import { useT } from '../lib/i18n/I18nContext';
|
||||
import { trackEvent } from '../services/analytics';
|
||||
@@ -19,9 +26,18 @@ import {
|
||||
setLastActiveAccount,
|
||||
} from '../store/accountsSlice';
|
||||
import { useAppDispatch, useAppSelector } from '../store/hooks';
|
||||
import {
|
||||
selectCustomMascotGifUrl,
|
||||
selectCustomPrimaryColor,
|
||||
selectCustomSecondaryColor,
|
||||
selectMascotColor,
|
||||
} from '../store/mascotSlice';
|
||||
import type { Account, AccountProvider, ProviderDescriptor } from '../types/accounts';
|
||||
import { AGENT_ACCOUNT_ID as AGENT_ID } from '../utils/accountsFullscreen';
|
||||
import { AgentChatPanel } from './Conversations';
|
||||
import Conversations, { AgentChatPanel } from './Conversations';
|
||||
|
||||
// Persistence key for face-toggle state across sessions.
|
||||
const FACE_MODE_KEY = 'chat.faceMode';
|
||||
|
||||
function makeAccountId(): string {
|
||||
const c = globalThis.crypto;
|
||||
@@ -99,6 +115,83 @@ interface ContextMenuState {
|
||||
y: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mascot + TTS panel rendered in face mode (right column of the Assistant
|
||||
* surface). Extracted as a separate component so its hooks only run when
|
||||
* face mode is on — keeps the main Accounts component lean when the toggle
|
||||
* is off.
|
||||
*
|
||||
* Phase 6 — reuses the exact same mascot subcomponents and useHumanMascot
|
||||
* hook from features/human/ rather than duplicating any logic.
|
||||
*/
|
||||
const FaceModePanel = () => {
|
||||
const { t } = useT();
|
||||
const [speakReplies, setSpeakReplies] = useState<boolean>(() => {
|
||||
try {
|
||||
const raw = window.localStorage.getItem('human.speakReplies');
|
||||
return raw === null ? true : raw === '1';
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
window.localStorage.setItem('human.speakReplies', speakReplies ? '1' : '0');
|
||||
} catch {
|
||||
// localStorage may be unavailable in sandboxed contexts.
|
||||
}
|
||||
}, [speakReplies]);
|
||||
|
||||
const { face, visemeCode } = useHumanMascot({ speakReplies });
|
||||
const mascotColor = useAppSelector(selectMascotColor);
|
||||
const customPrimary = useAppSelector(selectCustomPrimaryColor);
|
||||
const customSecondary = useAppSelector(selectCustomSecondaryColor);
|
||||
const customMascotGifUrl = useAppSelector(selectCustomMascotGifUrl);
|
||||
|
||||
const palette = getMascotPalette(mascotColor);
|
||||
const primaryColor = useMemo(
|
||||
() => hexToArgbInt(mascotColor === 'custom' ? customPrimary : palette.bodyFill),
|
||||
[mascotColor, customPrimary, palette]
|
||||
);
|
||||
const secondaryColor = useMemo(
|
||||
() => hexToArgbInt(mascotColor === 'custom' ? customSecondary : palette.neckShadowColor),
|
||||
[mascotColor, customSecondary, palette]
|
||||
);
|
||||
|
||||
return (
|
||||
<aside
|
||||
className="flex min-w-0 flex-1 flex-col items-center justify-center gap-4 bg-stone-50 dark:bg-neutral-900/60 rounded-2xl border border-stone-200/70 dark:border-neutral-800/70 my-3 mr-0 py-4 px-3 overflow-hidden"
|
||||
data-testid="face-mode-panel">
|
||||
{/* Mascot stage — the dominant element of the "Talk to Tiny" surface */}
|
||||
<div className="relative w-full max-w-[460px] aspect-square">
|
||||
{customMascotGifUrl ? (
|
||||
<CustomGifMascot src={customMascotGifUrl} face={face} />
|
||||
) : (
|
||||
<RiveMascot
|
||||
face={face}
|
||||
primaryColor={primaryColor}
|
||||
secondaryColor={secondaryColor}
|
||||
visemeCode={visemeCode}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* TTS / speak-replies toggle */}
|
||||
<label className="inline-flex cursor-pointer select-none items-center gap-2 rounded-full border border-stone-300 dark:border-neutral-700 bg-white/80 dark:bg-neutral-900/80 px-3 py-1.5 text-xs text-stone-700 dark:text-neutral-200 shadow-soft backdrop-blur-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={speakReplies}
|
||||
onChange={e => setSpeakReplies(e.target.checked)}
|
||||
className="cursor-pointer"
|
||||
data-testid="speak-replies-toggle"
|
||||
/>
|
||||
{t('voice.pushToTalk')}
|
||||
</label>
|
||||
</aside>
|
||||
);
|
||||
};
|
||||
|
||||
const Accounts = () => {
|
||||
const { t } = useT();
|
||||
const dispatch = useAppDispatch();
|
||||
@@ -109,6 +202,28 @@ const Accounts = () => {
|
||||
const [addOpen, setAddOpen] = useState(false);
|
||||
const [ctxMenu, setCtxMenu] = useState<ContextMenuState | null>(null);
|
||||
|
||||
// Face-mode toggle — persists across sessions. Face mode only affects the
|
||||
// agent-chat surface (external webview accounts ignore it).
|
||||
const [faceMode, setFaceMode] = useState<boolean>(() => {
|
||||
try {
|
||||
return window.localStorage.getItem(FACE_MODE_KEY) === '1';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
const toggleFaceMode = () => {
|
||||
setFaceMode(prev => {
|
||||
const next = !prev;
|
||||
try {
|
||||
window.localStorage.setItem(FACE_MODE_KEY, next ? '1' : '0');
|
||||
} catch {
|
||||
// Swallow storage errors.
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
startWebviewAccountService();
|
||||
}, []);
|
||||
@@ -231,6 +346,7 @@ const Accounts = () => {
|
||||
|
||||
return (
|
||||
<div
|
||||
// `h-full` makes this page fill the shell's content box, which bypasses
|
||||
className="relative flex h-full gap-3 overflow-hidden"
|
||||
data-testid="accounts-page"
|
||||
data-analytics-id="chat-right-sidebar">
|
||||
@@ -282,10 +398,58 @@ const Accounts = () => {
|
||||
</button>
|
||||
</aside>
|
||||
|
||||
{/* Main pane */}
|
||||
<main className="flex min-w-0 flex-1 flex-col">
|
||||
{/* Floating "Talk to Tiny" face-mode toggle. Kept out of the layout flow
|
||||
(absolute) so it never steals vertical space from the chat composer —
|
||||
the previous in-flow header strip pushed the input below the viewport. */}
|
||||
{isAgentSelected && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleFaceMode}
|
||||
data-testid="face-toggle-button"
|
||||
aria-pressed={faceMode}
|
||||
className={`absolute right-4 top-4 z-40 inline-flex items-center gap-1.5 rounded-full border px-3 py-1.5 text-xs font-medium shadow-soft backdrop-blur-sm transition-colors ${
|
||||
faceMode
|
||||
? 'border-primary-300 bg-primary-50/90 text-primary-700 dark:bg-primary-900/40 dark:text-primary-200'
|
||||
: 'border-stone-300/80 bg-white/90 text-stone-600 hover:border-primary-300 hover:text-primary-600 dark:border-neutral-700/80 dark:bg-neutral-900/90 dark:text-neutral-300 dark:hover:text-primary-300'
|
||||
}`}
|
||||
aria-label={faceMode ? t('assistant.faceMode.turnOff') : t('assistant.faceMode.turnOn')}>
|
||||
<span aria-hidden="true">🙂</span>
|
||||
{faceMode ? t('assistant.faceMode.on') : t('assistant.faceMode.off')}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Main pane
|
||||
In face mode (agent selected), the layout is a horizontal split:
|
||||
the chat panel on the left and the mascot panel on the right.
|
||||
Face mode is ignored when an external webview account is active. */}
|
||||
<main
|
||||
className={`flex min-w-0 flex-1 gap-3 ${isAgentSelected && faceMode ? 'flex-row' : 'flex-col'}`}>
|
||||
{isAgentSelected ? (
|
||||
<AgentChatPanel />
|
||||
<>
|
||||
{/* Agent chat — face mode uses sidebar variant to avoid a second
|
||||
thread list; normal mode uses the full-page variant (AgentChatPanel). */}
|
||||
<div
|
||||
className={`flex min-h-0 min-w-0 flex-col ${faceMode ? 'w-[360px] flex-none' : 'flex-1'}`}>
|
||||
{faceMode ? (
|
||||
// Face mode: mascot sidebar chat. The toggle floats on the page
|
||||
// root (see below) so it never steals height from the composer.
|
||||
// `min-h-0` lets the inner message list scroll instead of growing
|
||||
// and pushing the composer off-screen.
|
||||
<div className="flex min-h-0 flex-1 flex-col overflow-hidden rounded-2xl border border-stone-200/70 dark:border-neutral-800/70 my-3 mr-0">
|
||||
<Conversations variant="sidebar" />
|
||||
</div>
|
||||
) : (
|
||||
// `min-h-0` is required so the chat's internal message list owns
|
||||
// the overflow (scrolls) rather than expanding and shoving the
|
||||
// composer below the viewport on long threads.
|
||||
<div className="min-h-0 flex-1 overflow-hidden">
|
||||
<AgentChatPanel />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* Mascot + TTS panel — only visible in face mode */}
|
||||
{faceMode && <FaceModePanel />}
|
||||
</>
|
||||
) : active ? (
|
||||
<div className="flex-1 py-3 pr-3">
|
||||
<WebviewHost accountId={active.id} provider={active.provider} />
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
import { ConfirmationModal } from '../components/intelligence/ConfirmationModal';
|
||||
import IntelligenceSubconsciousTab from '../components/intelligence/IntelligenceSubconsciousTab';
|
||||
import IntelligenceTasksTab from '../components/intelligence/IntelligenceTasksTab';
|
||||
import { ToastContainer } from '../components/intelligence/Toast';
|
||||
import WorkflowsTab from '../components/intelligence/WorkflowsTab';
|
||||
import PillTabBar from '../components/PillTabBar';
|
||||
import {
|
||||
useIntelligenceSocket,
|
||||
useIntelligenceSocketManager,
|
||||
} from '../hooks/useIntelligenceSocket';
|
||||
import { useSubconscious } from '../hooks/useSubconscious';
|
||||
import { useT } from '../lib/i18n/I18nContext';
|
||||
import type {
|
||||
ConfirmationModal as ConfirmationModalType,
|
||||
ToastNotification,
|
||||
} from '../types/intelligence';
|
||||
import Notifications from './Notifications';
|
||||
|
||||
// Visible tab IDs for the Activity surface.
|
||||
// memory, agents, and council have moved to Settings → Developer & Diagnostics
|
||||
// (routes: /settings/intelligence, /settings/agents).
|
||||
// Back-compat: ?tab=memory / ?tab=agents / ?tab=council are unknown to the
|
||||
// visible set and therefore fall back to 'tasks' (see makeIsVisibleTab below).
|
||||
type ActivityTab = 'tasks' | 'automations' | 'backgroundActivity' | 'alerts';
|
||||
|
||||
const ACTIVITY_TABS: ActivityTab[] = ['tasks', 'automations', 'backgroundActivity', 'alerts'];
|
||||
|
||||
/**
|
||||
* Returns a type-guard predicate for the currently visible tabs.
|
||||
* Unknown values (including old deep-link tabs like ?tab=memory) fall back to
|
||||
* the default tab rather than erroring.
|
||||
*/
|
||||
const isVisibleTab = (tab: string | null | undefined): tab is ActivityTab =>
|
||||
(ACTIVITY_TABS as string[]).includes(tab ?? '');
|
||||
|
||||
export default function Activity() {
|
||||
const { t } = useT();
|
||||
|
||||
// Tab is URL-backed (/activity?tab=…) so navigating away and coming back
|
||||
// restores the same tab. `replace` so switching tabs doesn't stack history.
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const tabParam = searchParams.get('tab');
|
||||
const activeTab: ActivityTab = isVisibleTab(tabParam) ? tabParam : 'tasks';
|
||||
const setActiveTab = useCallback(
|
||||
(tab: ActivityTab) => {
|
||||
setSearchParams(
|
||||
prev => {
|
||||
prev.set('tab', tab);
|
||||
return prev;
|
||||
},
|
||||
{ replace: true }
|
||||
);
|
||||
},
|
||||
[setSearchParams]
|
||||
);
|
||||
|
||||
// Subconscious engine data (used by the Background Activity tab).
|
||||
const {
|
||||
status: subconsciousEngineStatus,
|
||||
mode: subconsciousMode,
|
||||
intervalMinutes: subconsciousInterval,
|
||||
triggering: subconsciousTriggering,
|
||||
settingMode: subconsciousSettingMode,
|
||||
triggerTick,
|
||||
setMode: setSubconsciousMode,
|
||||
setIntervalMinutes: setSubconsciousInterval,
|
||||
} = useSubconscious();
|
||||
|
||||
// Socket integration
|
||||
const socketManager = useIntelligenceSocketManager();
|
||||
const { isConnected: socketConnected } = useIntelligenceSocket();
|
||||
|
||||
// Local state for UI
|
||||
const [toasts, setToasts] = useState<ToastNotification[]>([]);
|
||||
const [confirmationModal, setConfirmationModal] = useState<ConfirmationModalType>({
|
||||
isOpen: false,
|
||||
title: '',
|
||||
message: '',
|
||||
onConfirm: () => {},
|
||||
onCancel: () => {},
|
||||
});
|
||||
|
||||
const removeToast = useCallback((id: string) => {
|
||||
setToasts(prev => prev.filter(toast => toast.id !== id));
|
||||
}, []);
|
||||
|
||||
// Initialize socket connection
|
||||
useEffect(() => {
|
||||
if (!socketConnected) {
|
||||
socketManager.connect();
|
||||
}
|
||||
}, [socketConnected, socketManager]);
|
||||
|
||||
const tabs: { id: ActivityTab; label: string; description?: string; comingSoon?: boolean }[] = [
|
||||
{ id: 'tasks', label: t('memory.tab.tasks'), description: t('memory.tab.tasksDescription') },
|
||||
{
|
||||
id: 'automations',
|
||||
label: t('activity.tabs.automations'),
|
||||
description: t('activity.tabs.automationsDescription'),
|
||||
},
|
||||
{ id: 'backgroundActivity', label: t('activity.tabs.backgroundActivity') },
|
||||
{ id: 'alerts', label: t('activity.tabs.alerts') },
|
||||
];
|
||||
const activeTabDef = tabs.find(tab => tab.id === activeTab);
|
||||
|
||||
return (
|
||||
<div className="min-h-full p-4 pt-6">
|
||||
<div className="max-w-4xl mx-auto space-y-4">
|
||||
<PillTabBar
|
||||
items={tabs.map(tab => ({ label: tab.label, value: tab.id }))}
|
||||
selected={activeTab}
|
||||
onChange={setActiveTab}
|
||||
activeClassName="border-primary-600 bg-primary-600 text-white"
|
||||
renderItem={(item, active) => {
|
||||
const tab = tabs.find(entry => entry.id === item.value);
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<span>{item.label}</span>
|
||||
{tab?.comingSoon && (
|
||||
<span
|
||||
className={`rounded-full border px-1.5 py-0.5 text-[10px] ${
|
||||
active
|
||||
? 'border-white/30 bg-white/15 text-white'
|
||||
: 'border-stone-200 dark:border-neutral-800 bg-stone-50 dark:bg-neutral-800/60 text-stone-500 dark:text-neutral-400'
|
||||
}`}>
|
||||
{t('misc.beta')}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Alerts tab renders outside the card so Notifications can use its own
|
||||
full-width layout with multiple sections. */}
|
||||
{activeTab === 'alerts' ? (
|
||||
<Notifications />
|
||||
) : (
|
||||
<div className="bg-white dark:bg-neutral-900 rounded-2xl shadow-soft border border-stone-200 dark:border-neutral-800 p-6">
|
||||
<div>
|
||||
{/* Header — reflects the active tab so the panel title matches
|
||||
what's shown below it, rather than a static "Activity". */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="min-w-0">
|
||||
<h1
|
||||
className="text-xl font-bold text-stone-900 dark:text-neutral-100"
|
||||
data-walkthrough="intelligence-header">
|
||||
{activeTabDef?.label ?? t('nav.activity')}
|
||||
</h1>
|
||||
{activeTabDef?.description && (
|
||||
<p className="mt-1 text-sm text-stone-500 dark:text-neutral-400">
|
||||
{activeTabDef.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tab content */}
|
||||
{activeTab === 'tasks' && <IntelligenceTasksTab />}
|
||||
|
||||
{activeTab === 'automations' && <WorkflowsTab />}
|
||||
|
||||
{activeTab === 'backgroundActivity' && (
|
||||
<IntelligenceSubconsciousTab
|
||||
status={subconsciousEngineStatus}
|
||||
mode={subconsciousMode}
|
||||
intervalMinutes={subconsciousInterval}
|
||||
triggerTick={triggerTick}
|
||||
triggering={subconsciousTriggering}
|
||||
settingMode={subconsciousSettingMode}
|
||||
setMode={setSubconsciousMode}
|
||||
setIntervalMinutes={setSubconsciousInterval}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Toast notifications */}
|
||||
<ToastContainer notifications={toasts} onRemove={removeToast} />
|
||||
|
||||
{/* Confirmation modal */}
|
||||
<ConfirmationModal
|
||||
modal={confirmationModal}
|
||||
onClose={() => setConfirmationModal(prev => ({ ...prev, isOpen: false }))}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+4
-23
@@ -16,29 +16,10 @@ import { selectBlockingState } from '../store/connectivitySelectors';
|
||||
import { useAppDispatch, useAppSelector } from '../store/hooks';
|
||||
import { resolveTheme, setThemeMode, type ThemeMode } from '../store/themeSlice';
|
||||
import { APP_VERSION } from '../utils/config';
|
||||
import { resolveUserName } from '../utils/userName';
|
||||
|
||||
export function resolveHomeUserName(user: unknown): string {
|
||||
if (!user || typeof user !== 'object') return 'User';
|
||||
|
||||
const record = user as Record<string, unknown>;
|
||||
const firstName =
|
||||
(typeof record.firstName === 'string' && record.firstName.trim()) ||
|
||||
(typeof record.first_name === 'string' && record.first_name.trim()) ||
|
||||
'';
|
||||
const lastName =
|
||||
(typeof record.lastName === 'string' && record.lastName.trim()) ||
|
||||
(typeof record.last_name === 'string' && record.last_name.trim()) ||
|
||||
'';
|
||||
const username = typeof record.username === 'string' ? record.username.trim() : '';
|
||||
const email = typeof record.email === 'string' ? record.email.trim() : '';
|
||||
|
||||
const fullName = [firstName, lastName].filter(Boolean).join(' ').trim();
|
||||
if (fullName) return fullName;
|
||||
if (firstName) return firstName;
|
||||
if (username) return username.startsWith('@') ? username : `@${username}`;
|
||||
if (email) return email.split('@')[0] || 'User';
|
||||
return 'User';
|
||||
}
|
||||
/** @deprecated Use `resolveUserName` from `utils/userName`. Kept for back-compat. */
|
||||
export const resolveHomeUserName = resolveUserName;
|
||||
|
||||
const Home = () => {
|
||||
const { t } = useT();
|
||||
@@ -279,7 +260,7 @@ const Home = () => {
|
||||
<div className="text-[11px] uppercase tracking-wide text-stone-400 mb-2">Next steps</div>
|
||||
<div className="divide-y divide-stone-100">
|
||||
<button
|
||||
onClick={() => navigate('/skills')}
|
||||
onClick={() => navigate('/connections')}
|
||||
className="w-full flex items-center justify-between py-2.5 text-left hover:bg-stone-50 rounded-md px-2 -mx-2 transition-colors">
|
||||
<div>
|
||||
<div className="text-sm font-medium text-stone-900">Connect your services</div>
|
||||
|
||||
@@ -6,7 +6,8 @@ import Intelligence from './Intelligence';
|
||||
|
||||
vi.mock('../lib/i18n/I18nContext', () => ({ useT: () => ({ t: (k: string) => k }) }));
|
||||
|
||||
// IS_DEV gates the dev-only "council" tab; default to a non-dev build.
|
||||
// IS_DEV / useDeveloperMode gate the dev-only "council" tab; default to
|
||||
// a non-dev build so the gate is closed unless the test overrides it.
|
||||
const isDev = vi.hoisted(() => ({ value: false }));
|
||||
vi.mock('../utils/config', async () => {
|
||||
const actual = await vi.importActual<typeof import('../utils/config')>('../utils/config');
|
||||
@@ -17,6 +18,9 @@ vi.mock('../utils/config', async () => {
|
||||
},
|
||||
};
|
||||
});
|
||||
// useDeveloperMode combines IS_DEV with the persisted Redux preference.
|
||||
// Mock it here so tests don't need a Redux Provider — just respect isDev.value.
|
||||
vi.mock('../hooks/useDeveloperMode', () => ({ useDeveloperMode: () => isDev.value }));
|
||||
|
||||
// Heavy hooks → minimal stubs.
|
||||
vi.mock('../hooks/useIntelligenceSocket', () => ({
|
||||
|
||||
@@ -11,6 +11,7 @@ import ModelCouncilTab from '../components/intelligence/ModelCouncilTab';
|
||||
import { ToastContainer } from '../components/intelligence/Toast';
|
||||
import WorkflowsTab from '../components/intelligence/WorkflowsTab';
|
||||
import PillTabBar from '../components/PillTabBar';
|
||||
import { useDeveloperMode } from '../hooks/useDeveloperMode';
|
||||
import {
|
||||
useIntelligenceSocket,
|
||||
useIntelligenceSocketManager,
|
||||
@@ -21,7 +22,6 @@ import type {
|
||||
ConfirmationModal as ConfirmationModalType,
|
||||
ToastNotification,
|
||||
} from '../types/intelligence';
|
||||
import { IS_DEV } from '../utils/config';
|
||||
|
||||
type IntelligenceTab =
|
||||
| 'memory'
|
||||
@@ -42,17 +42,21 @@ const INTELLIGENCE_TABS: IntelligenceTab[] = [
|
||||
'council',
|
||||
];
|
||||
|
||||
// Tabs gated to dev builds (mirrors the `devOnly` flags on `allTabs` below).
|
||||
// A `?tab=` deep link must be validated against the *visible* set, not the raw
|
||||
// enum, so `?tab=council` can't force-open a hidden dev-only tab in prod.
|
||||
// Tabs gated to dev builds or runtime developer mode (mirrors the `devOnly`
|
||||
// flags on `allTabs` below). A `?tab=` deep link must be validated against the
|
||||
// *visible* set, not the raw enum, so a user cannot force-open a hidden tab.
|
||||
const DEV_ONLY_TABS: IntelligenceTab[] = ['council'];
|
||||
|
||||
const isVisibleTab = (tab: string | null | undefined): tab is IntelligenceTab =>
|
||||
(INTELLIGENCE_TABS as string[]).includes(tab ?? '') &&
|
||||
(IS_DEV || !(DEV_ONLY_TABS as string[]).includes(tab ?? ''));
|
||||
const makeIsVisibleTab =
|
||||
(developerModeEnabled: boolean) =>
|
||||
(tab: string | null | undefined): tab is IntelligenceTab =>
|
||||
(INTELLIGENCE_TABS as string[]).includes(tab ?? '') &&
|
||||
(developerModeEnabled || !(DEV_ONLY_TABS as string[]).includes(tab ?? ''));
|
||||
|
||||
export default function Intelligence() {
|
||||
const { t } = useT();
|
||||
const developerMode = useDeveloperMode();
|
||||
const isVisibleTab = makeIsVisibleTab(developerMode);
|
||||
|
||||
// Tab is URL-backed (`/intelligence?tab=…`) so navigating away — e.g. to
|
||||
// Settings → Task Sources from the Agent Tasks tab — and coming back via
|
||||
@@ -152,7 +156,7 @@ export default function Intelligence() {
|
||||
{ id: 'council', label: t('memory.tab.council'), devOnly: true },
|
||||
{ id: 'agents', label: t('memory.tab.agents'), description: t('memory.tab.agentsDescription') },
|
||||
];
|
||||
const tabs = allTabs.filter(tab => !tab.devOnly || IS_DEV);
|
||||
const tabs = allTabs.filter(tab => !tab.devOnly || developerMode);
|
||||
const activeTabDef = tabs.find(tab => tab.id === activeTab);
|
||||
|
||||
return (
|
||||
|
||||
+17
-18
@@ -10,6 +10,7 @@ import AgentChatPanel from '../components/settings/panels/AgentChatPanel';
|
||||
import AgentEditorPage from '../components/settings/panels/AgentEditorPage';
|
||||
import AgentsPanel from '../components/settings/panels/AgentsPanel';
|
||||
import AIPanel from '../components/settings/panels/AIPanel';
|
||||
import AnalysisViewsPanel from '../components/settings/panels/AnalysisViewsPanel';
|
||||
import AppearancePanel from '../components/settings/panels/AppearancePanel';
|
||||
import ApprovalHistoryPanel from '../components/settings/panels/ApprovalHistoryPanel';
|
||||
import AutocompleteDebugPanel from '../components/settings/panels/AutocompleteDebugPanel';
|
||||
@@ -32,9 +33,11 @@ import MascotPanel from '../components/settings/panels/MascotPanel';
|
||||
import McpServerPanel from '../components/settings/panels/McpServerPanel';
|
||||
import MemoryDataPanel from '../components/settings/panels/MemoryDataPanel';
|
||||
import MemoryDebugPanel from '../components/settings/panels/MemoryDebugPanel';
|
||||
import MemorySyncPanel from '../components/settings/panels/MemorySyncPanel';
|
||||
import MigrationPanel from '../components/settings/panels/MigrationPanel';
|
||||
import ModelHealthPanel from '../components/settings/panels/ModelHealthPanel';
|
||||
import NotificationsTabbedPanel from '../components/settings/panels/NotificationsTabbedPanel';
|
||||
import PermissionsPanel from '../components/settings/panels/PermissionsPanel';
|
||||
import PersonaPanel from '../components/settings/panels/PersonaPanel';
|
||||
import PrivacyPanel from '../components/settings/panels/PrivacyPanel';
|
||||
import RecoveryPhrasePanel from '../components/settings/panels/RecoveryPhrasePanel';
|
||||
@@ -123,16 +126,6 @@ const ScreenIcon = (
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
const MessagingIcon = (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M8 10h.01M12 10h.01M16 10h.01M21 11c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 19l1.395-3.72C3.512 14.042 3 12.574 3 11c0-4.418 4.03-8 9-8s9 3.582 9 8z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
const NotificationsIcon = (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
@@ -235,7 +228,9 @@ const WalletIcon = (
|
||||
|
||||
const WrappedSettingsPage = ({
|
||||
children,
|
||||
maxWidthClass = 'max-w-lg',
|
||||
// Default widened ~30% (max-w-lg 512px → max-w-2xl 672px) for a roomier
|
||||
// settings list per design feedback.
|
||||
maxWidthClass = 'max-w-2xl',
|
||||
}: {
|
||||
children: ReactNode;
|
||||
maxWidthClass?: string;
|
||||
@@ -341,13 +336,8 @@ const Settings = () => {
|
||||
icon: ScreenIcon,
|
||||
},
|
||||
// Autocomplete + Voice Dictation hidden per #717 (routes retained for re-enable).
|
||||
{
|
||||
id: 'messaging',
|
||||
title: t('pages.settings.features.messagingChannels'),
|
||||
description: t('pages.settings.features.messagingChannelsDesc'),
|
||||
route: 'messaging',
|
||||
icon: MessagingIcon,
|
||||
},
|
||||
// Dead "messaging" menu item removed (Phase 2): the route `messaging` never
|
||||
// existed in Settings — messaging channels live at /connections (Messaging tab).
|
||||
{
|
||||
id: 'notifications',
|
||||
title: t('pages.settings.features.notifications'),
|
||||
@@ -603,6 +593,7 @@ const Settings = () => {
|
||||
<Route path="persona" element={wrapSettingsPage(<PersonaPanel />)} />
|
||||
<Route path="appearance" element={wrapSettingsPage(<AppearancePanel />)} />
|
||||
<Route path="agent-access" element={wrapSettingsPage(<AgentAccessPanel />)} />
|
||||
<Route path="permissions" element={wrapSettingsPage(<PermissionsPanel />)} />
|
||||
<Route path="activity-level" element={wrapSettingsPage(<AgentActivityPanel />)} />
|
||||
<Route path="sandbox-settings" element={wrapSettingsPage(<SandboxSettingsPanel />)} />
|
||||
<Route path="approval-history" element={wrapSettingsPage(<ApprovalHistoryPanel />)} />
|
||||
@@ -659,8 +650,16 @@ const Settings = () => {
|
||||
path="model-health"
|
||||
element={wrapSettingsPage(<ModelHealthPanel />, { maxWidthClass: 'max-w-4xl' })}
|
||||
/>
|
||||
<Route
|
||||
path="memory-sync"
|
||||
element={wrapSettingsPage(<MemorySyncPanel />, { maxWidthClass: 'max-w-4xl' })}
|
||||
/>
|
||||
<Route path="memory-data" element={wrapSettingsPage(<MemoryDataPanel />)} />
|
||||
<Route path="memory-debug" element={wrapSettingsPage(<MemoryDebugPanel />)} />
|
||||
<Route
|
||||
path="analysis-views"
|
||||
element={wrapSettingsPage(<AnalysisViewsPanel />, { maxWidthClass: 'max-w-4xl' })}
|
||||
/>
|
||||
<Route path="intelligence" element={<Intelligence />} />
|
||||
<Route path="webhooks-triggers" element={<Webhooks />} />
|
||||
<Route path="composio-triggers" element={wrapSettingsPage(<ComposioTriagePanel />)} />
|
||||
|
||||
+52
-21
@@ -343,7 +343,19 @@ interface SkillItem {
|
||||
|
||||
// ─── Main Skills Page ──────────────────────────────────────────────────────────
|
||||
|
||||
type ConnectionsTab = 'channels' | 'composio' | 'mcp' | 'skills' | 'meetings';
|
||||
/**
|
||||
* Primary tab values for the Connections page.
|
||||
*
|
||||
* Phase 2 rename mapping (old → new):
|
||||
* composio → apps
|
||||
* channels → messaging
|
||||
* mcp → tools (meetings content folded in under Tools)
|
||||
* skills → explorer (kept secondary)
|
||||
*
|
||||
* Back-compat: the old ?tab= values (composio, channels, mcp, meetings) are
|
||||
* normalised to the new values so existing deep links continue to work.
|
||||
*/
|
||||
type ConnectionsTab = 'apps' | 'messaging' | 'tools' | 'explorer' | 'talents';
|
||||
|
||||
export default function Skills() {
|
||||
const { t } = useT();
|
||||
@@ -351,15 +363,28 @@ export default function Skills() {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const isLocalSession = isLocalSessionToken(getCoreStateSnapshot().snapshot.sessionToken);
|
||||
// Honour `?tab=<composio|channels|mcp|meetings>` so deep links land on the right
|
||||
// sub-tab. (The legacy `runners` tab was removed; running a workflow now
|
||||
// lives on its detail drawer → /skills/run.)
|
||||
// Honour `?tab=<apps|messaging|tools|explorer>` so deep links land on the
|
||||
// right sub-tab. Also normalise legacy tab names from the old /skills route
|
||||
// so that e.g. `/skills?tab=composio` still works after the redirect.
|
||||
const initialTab: ConnectionsTab = (() => {
|
||||
const params = new URLSearchParams(location.search);
|
||||
const t = params.get('tab');
|
||||
if (t === 'composio' || t === 'channels' || t === 'mcp' || t === 'skills' || t === 'meetings')
|
||||
return t;
|
||||
return 'composio';
|
||||
const raw = params.get('tab');
|
||||
// New canonical values
|
||||
if (
|
||||
raw === 'apps' ||
|
||||
raw === 'messaging' ||
|
||||
raw === 'tools' ||
|
||||
raw === 'explorer' ||
|
||||
raw === 'talents'
|
||||
)
|
||||
return raw;
|
||||
// Legacy back-compat aliases
|
||||
if (raw === 'composio') return 'apps';
|
||||
if (raw === 'channels') return 'messaging';
|
||||
if (raw === 'mcp') return 'tools';
|
||||
if (raw === 'meetings') return 'talents';
|
||||
if (raw === 'skills') return 'explorer';
|
||||
return 'apps';
|
||||
})();
|
||||
const [activeTab, setActiveTab] = useState<ConnectionsTab>(initialTab);
|
||||
const dispatch = useAppDispatch();
|
||||
@@ -792,16 +817,16 @@ export default function Skills() {
|
||||
selected={activeTab}
|
||||
onChange={setActiveTab}
|
||||
items={[
|
||||
{ value: 'composio', label: t('skills.tabs.composio') },
|
||||
{ value: 'channels', label: t('skills.tabs.channels') },
|
||||
{ value: 'skills', label: t('skills.tabs.explorer') },
|
||||
{ value: 'meetings', label: t('skills.tabs.meetings') },
|
||||
{ value: 'mcp', label: t('skills.tabs.mcp') },
|
||||
{ value: 'apps', label: t('connections.tabs.apps') },
|
||||
{ value: 'messaging', label: t('connections.tabs.messaging') },
|
||||
{ value: 'tools', label: t('connections.tabs.tools') },
|
||||
{ value: 'explorer', label: t('connections.tabs.explorer') },
|
||||
{ value: 'talents', label: t('connections.tabs.talents') },
|
||||
]}
|
||||
/>
|
||||
{
|
||||
<>
|
||||
{activeTab === 'channels' && channelsGroup && (
|
||||
{activeTab === 'messaging' && channelsGroup && (
|
||||
<div className="rounded-2xl border border-stone-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 p-3 shadow-soft animate-fade-up">
|
||||
<div className="px-1 pb-3 pt-1">
|
||||
<h2
|
||||
@@ -863,9 +888,7 @@ export default function Skills() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'meetings' && <MeetingBotsCard onToast={addToast} />}
|
||||
|
||||
{activeTab === 'composio' && (
|
||||
{activeTab === 'apps' && (
|
||||
<div className="rounded-2xl border border-stone-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 p-3 shadow-soft animate-fade-up">
|
||||
<div className="px-1 pb-3 pt-1">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -941,15 +964,23 @@ export default function Skills() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'composio' && otherGroups.map(group => renderGroup(group))}
|
||||
{activeTab === 'apps' && otherGroups.map(group => renderGroup(group))}
|
||||
|
||||
{activeTab === 'skills' && <SkillsExplorerTab onToast={addToast} />}
|
||||
{activeTab === 'explorer' && <SkillsExplorerTab onToast={addToast} />}
|
||||
|
||||
{activeTab === 'mcp' && (
|
||||
<div className="animate-fade-up">
|
||||
{activeTab === 'tools' && (
|
||||
<div className="space-y-4 animate-fade-up">
|
||||
{/* MCP Servers */}
|
||||
<McpServersTab />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'talents' && (
|
||||
<div className="space-y-4 animate-fade-up">
|
||||
{/* Meeting bots live under Talents (moved out of Tools) */}
|
||||
<MeetingBotsCard onToast={addToast} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -30,7 +30,7 @@ const renderPage = () =>
|
||||
<MemoryRouter initialEntries={['/workflows/new']}>
|
||||
<Routes>
|
||||
<Route path="/workflows/new" element={<WorkflowNew />} />
|
||||
<Route path="/skills" element={<div data-testid="dashboard-landed">dashboard</div>} />
|
||||
<Route path="/connections" element={<div data-testid="dashboard-landed">dashboard</div>} />
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
);
|
||||
@@ -49,10 +49,10 @@ describe('WorkflowNew', () => {
|
||||
expect(screen.getByLabelText(/skills.create.description/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('cancel button navigates back to /skills', () => {
|
||||
it('cancel button navigates back to /connections', async () => {
|
||||
renderPage();
|
||||
fireEvent.click(screen.getByTestId('skill-new-cancel'));
|
||||
expect(screen.getByTestId('dashboard-landed')).toBeInTheDocument();
|
||||
expect(await screen.findByTestId('dashboard-landed')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('submit is disabled until both required fields are filled', () => {
|
||||
@@ -71,7 +71,7 @@ describe('WorkflowNew', () => {
|
||||
expect(submit).not.toBeDisabled();
|
||||
});
|
||||
|
||||
it('navigates to /skills after a successful submit', async () => {
|
||||
it('navigates to /connections after a successful submit', async () => {
|
||||
hoisted.createWorkflow.mockResolvedValue({
|
||||
id: 'new-skill',
|
||||
name: 'New Skill',
|
||||
|
||||
@@ -8,13 +8,14 @@
|
||||
* new SKILL.md drafts.
|
||||
*
|
||||
* Behaviour on submit:
|
||||
* - Success → navigate to /skills (dashboard) so the user lands
|
||||
* somewhere meaningful. We considered /workflows/run?workflow=<new-id>,
|
||||
* but new skills aren't auto-scheduled and the runner picker
|
||||
* pre-select only makes sense once the user has filled in inputs.
|
||||
* Dashboard sends a clearer "skill created, here's what's
|
||||
* scheduled" signal.
|
||||
* - Cancel → /skills.
|
||||
* - Success → navigate to /connections so the user lands somewhere
|
||||
* meaningful. We considered /workflows/run?workflow=<new-id>, but
|
||||
* new skills aren't auto-scheduled and the runner picker pre-select
|
||||
* only makes sense once the user has filled in inputs. The
|
||||
* Connections page (defaulting to Apps tab) provides a clear "here
|
||||
* are your connections" signal. Use ?tab=explorer to deep-link to
|
||||
* the Explorer tab if needed.
|
||||
* - Cancel → /connections.
|
||||
*/
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
@@ -42,7 +43,7 @@ export default function WorkflowNew() {
|
||||
// The dashboard re-fetches the cron list on mount, so any
|
||||
// schedule the user adds for this new skill will appear there
|
||||
// automatically — no need to plumb the new id through state.
|
||||
navigate('/skills?tab=runners');
|
||||
navigate('/connections');
|
||||
},
|
||||
[navigate]
|
||||
);
|
||||
@@ -68,7 +69,7 @@ export default function WorkflowNew() {
|
||||
<button
|
||||
type="button"
|
||||
data-testid="skill-new-cancel"
|
||||
onClick={() => navigate('/skills?tab=runners')}
|
||||
onClick={() => navigate('/connections')}
|
||||
disabled={submitting}
|
||||
className="rounded-lg px-4 py-2 text-sm font-medium text-stone-600 dark:text-neutral-300 transition-colors hover:bg-stone-100 dark:hover:bg-neutral-800 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-1 disabled:opacity-40">
|
||||
{t('common.cancel')}
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
/**
|
||||
* Tests for the Phase 6 face-mode toggle in the Accounts (Assistant) page.
|
||||
*
|
||||
* Verifies:
|
||||
* - Face toggle button is rendered
|
||||
* - Face mode is off by default
|
||||
* - Clicking the toggle shows the face-mode panel (data-testid="face-mode-panel")
|
||||
* - Clicking the toggle again hides the face-mode panel
|
||||
* - Face mode state is persisted to localStorage (chat.faceMode)
|
||||
* - When face mode is on, Conversations is rendered with variant="sidebar"
|
||||
*/
|
||||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import { act, fireEvent, render, screen } from '@testing-library/react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import accountsReducer from '../../store/accountsSlice';
|
||||
import chatRuntimeReducer from '../../store/chatRuntimeSlice';
|
||||
import mascotReducer from '../../store/mascotSlice';
|
||||
import threadReducer from '../../store/threadSlice';
|
||||
// ── Static component import (after mocks are hoisted) ───────────────────────
|
||||
import Accounts from '../Accounts';
|
||||
|
||||
// ── Heavy dependency stubs — all must be declared before the component import ──
|
||||
|
||||
// Stub Conversations so it doesn't pull in the full chat stack.
|
||||
vi.mock('../Conversations', () => ({
|
||||
default: ({ variant }: { variant?: string }) => (
|
||||
<div data-testid="conversations-stub" data-variant={variant ?? 'page'} />
|
||||
),
|
||||
AgentChatPanel: () => <div data-testid="agent-chat-panel-stub" />,
|
||||
}));
|
||||
|
||||
// Stub webview account components.
|
||||
vi.mock('../../components/accounts/WebviewHost', () => ({
|
||||
default: () => <div data-testid="webview-host-stub" />,
|
||||
}));
|
||||
vi.mock('../../components/accounts/AddAccountModal', () => ({ default: () => null }));
|
||||
vi.mock('../../components/accounts/providerIcons', () => ({
|
||||
AgentIcon: ({ className }: { className?: string }) => (
|
||||
<svg data-testid="agent-icon" className={className} />
|
||||
),
|
||||
ProviderIcon: ({ provider, className }: { provider: string; className?: string }) => (
|
||||
<svg data-testid={`provider-icon-${provider}`} className={className} />
|
||||
),
|
||||
}));
|
||||
|
||||
// Stub webview account service.
|
||||
vi.mock('../../services/webviewAccountService', () => ({
|
||||
startWebviewAccountService: vi.fn(),
|
||||
hideWebviewAccount: vi.fn(),
|
||||
showWebviewAccount: vi.fn(),
|
||||
purgeWebviewAccount: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
vi.mock('../../services/analytics', () => ({ trackEvent: vi.fn() }));
|
||||
|
||||
// Stub mascot subcomponents — they pull in a Rive WASM runtime.
|
||||
vi.mock('../../features/human/Mascot', () => ({
|
||||
RiveMascot: () => <div data-testid="rive-mascot-stub" />,
|
||||
CustomGifMascot: ({ src }: { src: string }) => (
|
||||
<img data-testid="custom-gif-mascot-stub" src={src} alt="" />
|
||||
),
|
||||
getMascotPalette: vi.fn(() => ({ bodyFill: '#4A83DD', neckShadowColor: '#2A63BD' })),
|
||||
hexToArgbInt: vi.fn((_hex: string) => 0xff4a83dd),
|
||||
}));
|
||||
|
||||
vi.mock('../../features/human/useHumanMascot', () => ({
|
||||
useHumanMascot: () => ({ face: 'idle', visemeCode: 0 }),
|
||||
}));
|
||||
|
||||
vi.mock('../../hooks/usePrewarmMostRecentAccount', () => ({
|
||||
usePrewarmMostRecentAccount: vi.fn(),
|
||||
}));
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
const FACE_MODE_KEY = 'chat.faceMode';
|
||||
|
||||
function buildStore() {
|
||||
return configureStore({
|
||||
reducer: {
|
||||
accounts: accountsReducer,
|
||||
mascot: mascotReducer,
|
||||
thread: threadReducer,
|
||||
chatRuntime: chatRuntimeReducer,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function renderAccounts(store = buildStore()) {
|
||||
return render(
|
||||
<Provider store={store}>
|
||||
<MemoryRouter>
|
||||
<Accounts />
|
||||
</MemoryRouter>
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Tests ────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('Accounts — face-mode toggle', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
it('renders the face-toggle button', () => {
|
||||
renderAccounts();
|
||||
expect(screen.getByTestId('face-toggle-button')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('face mode is off by default (no face-mode-panel rendered)', () => {
|
||||
renderAccounts();
|
||||
expect(screen.queryByTestId('face-mode-panel')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('clicking the toggle shows the face-mode panel', async () => {
|
||||
renderAccounts();
|
||||
const toggle = screen.getByTestId('face-toggle-button');
|
||||
await act(async () => {
|
||||
fireEvent.click(toggle);
|
||||
});
|
||||
expect(screen.getByTestId('face-mode-panel')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('clicking the toggle again hides the face-mode panel', async () => {
|
||||
renderAccounts();
|
||||
const toggle = screen.getByTestId('face-toggle-button');
|
||||
await act(async () => {
|
||||
fireEvent.click(toggle);
|
||||
});
|
||||
expect(screen.getByTestId('face-mode-panel')).toBeInTheDocument();
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByTestId('face-toggle-button'));
|
||||
});
|
||||
expect(screen.queryByTestId('face-mode-panel')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('persists face-mode ON to localStorage', async () => {
|
||||
renderAccounts();
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByTestId('face-toggle-button'));
|
||||
});
|
||||
expect(localStorage.getItem(FACE_MODE_KEY)).toBe('1');
|
||||
});
|
||||
|
||||
it('persists face-mode OFF to localStorage after toggling twice', async () => {
|
||||
renderAccounts();
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByTestId('face-toggle-button'));
|
||||
});
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByTestId('face-toggle-button'));
|
||||
});
|
||||
expect(localStorage.getItem(FACE_MODE_KEY)).toBe('0');
|
||||
});
|
||||
|
||||
it('reads face-mode ON from localStorage on mount', () => {
|
||||
localStorage.setItem(FACE_MODE_KEY, '1');
|
||||
renderAccounts();
|
||||
expect(screen.getByTestId('face-mode-panel')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('when face mode is on, Conversations is rendered with variant="sidebar"', async () => {
|
||||
renderAccounts();
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByTestId('face-toggle-button'));
|
||||
});
|
||||
const conversations = screen.getByTestId('conversations-stub');
|
||||
expect(conversations).toHaveAttribute('data-variant', 'sidebar');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Phase 3 — Activity route redirect tests.
|
||||
*
|
||||
* Verifies that:
|
||||
* /intelligence → /activity (back-compat, old route rename)
|
||||
* /routines → /activity?tab=automations (orphaned page removal)
|
||||
* /workflows → /activity?tab=automations (old deep link back-compat)
|
||||
*
|
||||
* We render a minimal route tree so we do not need the full provider tree.
|
||||
*/
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { MemoryRouter, Navigate, Route, Routes } from 'react-router-dom';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
/**
|
||||
* Minimal route tree that mirrors only the Phase 3 redirects under test.
|
||||
* Using `Navigate` directly avoids needing to mock the full app.
|
||||
*/
|
||||
function TestRoutes() {
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="/activity" element={<div data-testid="activity-page">activity</div>} />
|
||||
<Route path="/intelligence" element={<Navigate to="/activity" replace />} />
|
||||
<Route path="/routines" element={<Navigate to="/activity?tab=automations" replace />} />
|
||||
<Route path="/workflows" element={<Navigate to="/activity?tab=automations" replace />} />
|
||||
</Routes>
|
||||
);
|
||||
}
|
||||
|
||||
const renderAt = (path: string) =>
|
||||
render(
|
||||
<MemoryRouter initialEntries={[path]}>
|
||||
<TestRoutes />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
describe('Phase 3 route redirects', () => {
|
||||
it('/intelligence redirects to /activity', () => {
|
||||
renderAt('/intelligence');
|
||||
expect(screen.getByTestId('activity-page')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('/routines redirects to /activity (Automations tab)', () => {
|
||||
renderAt('/routines');
|
||||
expect(screen.getByTestId('activity-page')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('/workflows redirects to /activity (Automations tab)', () => {
|
||||
renderAt('/workflows');
|
||||
expect(screen.getByTestId('activity-page')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('/activity renders the activity page directly', () => {
|
||||
renderAt('/activity');
|
||||
expect(screen.getByTestId('activity-page')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,174 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Stubs — keep the test fast by mocking every heavy dependency.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
vi.mock('../../lib/i18n/I18nContext', () => ({ useT: () => ({ t: (key: string) => key }) }));
|
||||
|
||||
vi.mock('../../components/intelligence/IntelligenceSubconsciousTab', () => ({
|
||||
default: () => <div data-testid="tab-backgroundActivity" />,
|
||||
}));
|
||||
vi.mock('../../components/intelligence/IntelligenceTasksTab', () => ({
|
||||
default: () => <div data-testid="tab-tasks" />,
|
||||
}));
|
||||
vi.mock('../../components/intelligence/WorkflowsTab', () => ({
|
||||
default: () => <div data-testid="tab-automations" />,
|
||||
}));
|
||||
vi.mock('../../components/intelligence/Toast', () => ({ ToastContainer: () => null }));
|
||||
vi.mock('../../components/intelligence/ConfirmationModal', () => ({
|
||||
ConfirmationModal: () => null,
|
||||
}));
|
||||
|
||||
// Stub Notifications so the Alerts tab renders a predictable sentinel without
|
||||
// needing a Redux store, router context beyond MemoryRouter, etc.
|
||||
vi.mock('../Notifications', () => ({ default: () => <div data-testid="tab-alerts" /> }));
|
||||
|
||||
vi.mock('../../hooks/useIntelligenceSocket', () => ({
|
||||
useIntelligenceSocket: () => ({ isConnected: true }),
|
||||
useIntelligenceSocketManager: () => ({ connect: vi.fn() }),
|
||||
}));
|
||||
vi.mock('../../hooks/useSubconscious', () => ({
|
||||
useSubconscious: () => ({
|
||||
status: 'idle',
|
||||
mode: 'manual',
|
||||
intervalMinutes: 30,
|
||||
triggering: false,
|
||||
settingMode: false,
|
||||
triggerTick: vi.fn(),
|
||||
setMode: vi.fn(),
|
||||
setIntervalMinutes: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
// Dynamic import AFTER all mocks are in place (same pattern as original test).
|
||||
const Activity = (await import('../Activity')).default;
|
||||
|
||||
function renderAt(path: string) {
|
||||
return render(
|
||||
<MemoryRouter initialEntries={[path]}>
|
||||
<Activity />
|
||||
</MemoryRouter>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Activity URL-backed tab', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('defaults to the tasks tab when no ?tab is present', async () => {
|
||||
renderAt('/activity');
|
||||
await waitFor(() => expect(screen.getByTestId('tab-tasks')).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('honours ?tab=tasks from the URL', async () => {
|
||||
renderAt('/activity?tab=tasks');
|
||||
await waitFor(() => expect(screen.getByTestId('tab-tasks')).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('honours ?tab=automations from the URL', async () => {
|
||||
renderAt('/activity?tab=automations');
|
||||
await waitFor(() => expect(screen.getByTestId('tab-automations')).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('honours ?tab=backgroundActivity from the URL', async () => {
|
||||
renderAt('/activity?tab=backgroundActivity');
|
||||
await waitFor(() => expect(screen.getByTestId('tab-backgroundActivity')).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('honours ?tab=alerts from the URL', async () => {
|
||||
renderAt('/activity?tab=alerts');
|
||||
await waitFor(() => expect(screen.getByTestId('tab-alerts')).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('falls back to tasks for an unknown ?tab value', async () => {
|
||||
renderAt('/activity?tab=bogus');
|
||||
await waitFor(() => expect(screen.getByTestId('tab-tasks')).toBeInTheDocument());
|
||||
});
|
||||
|
||||
// Back-compat: old deep links (?tab=memory|agents|council) are no longer
|
||||
// visible Activity tabs — they should fall back to tasks rather than error.
|
||||
it('falls back to tasks for ?tab=memory (relocated to Settings)', async () => {
|
||||
renderAt('/activity?tab=memory');
|
||||
await waitFor(() => expect(screen.getByTestId('tab-tasks')).toBeInTheDocument());
|
||||
expect(screen.queryByTestId('tab-memory')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('falls back to tasks for ?tab=agents (relocated to Settings)', async () => {
|
||||
renderAt('/activity?tab=agents');
|
||||
await waitFor(() => expect(screen.getByTestId('tab-tasks')).toBeInTheDocument());
|
||||
expect(screen.queryByTestId('tab-agents')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('falls back to tasks for ?tab=council (relocated to Settings)', async () => {
|
||||
renderAt('/activity?tab=council');
|
||||
await waitFor(() => expect(screen.getByTestId('tab-tasks')).toBeInTheDocument());
|
||||
expect(screen.queryByTestId('tab-council')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the intelligence-header data-walkthrough anchor (non-alerts tabs)', async () => {
|
||||
renderAt('/activity');
|
||||
await waitFor(() =>
|
||||
expect(document.querySelector('[data-walkthrough="intelligence-header"]')).not.toBeNull()
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Activity tab — tab set', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders exactly four tab pills: tasks, automations, backgroundActivity, alerts', async () => {
|
||||
renderAt('/activity');
|
||||
await waitFor(() => screen.getByTestId('tab-tasks'));
|
||||
// Each label key is returned as-is by the stub t() function.
|
||||
expect(screen.getAllByText('memory.tab.tasks').length).toBeGreaterThan(0);
|
||||
expect(screen.getAllByText('activity.tabs.automations').length).toBeGreaterThan(0);
|
||||
expect(screen.getAllByText('activity.tabs.backgroundActivity').length).toBeGreaterThan(0);
|
||||
expect(screen.getAllByText('activity.tabs.alerts').length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('does not render memory, agents, or council pills', async () => {
|
||||
renderAt('/activity');
|
||||
await waitFor(() => screen.getByTestId('tab-tasks'));
|
||||
expect(screen.queryByText('memory.tab.memory')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('memory.tab.agents')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('memory.tab.council')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('clicking the automations pill switches to the automations tab', async () => {
|
||||
renderAt('/activity');
|
||||
await waitFor(() => screen.getAllByText('activity.tabs.automations'));
|
||||
fireEvent.click(screen.getAllByText('activity.tabs.automations')[0]);
|
||||
await waitFor(() => expect(screen.getByTestId('tab-automations')).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('clicking the backgroundActivity pill switches to the backgroundActivity tab', async () => {
|
||||
renderAt('/activity');
|
||||
await waitFor(() => screen.getAllByText('activity.tabs.backgroundActivity'));
|
||||
fireEvent.click(screen.getAllByText('activity.tabs.backgroundActivity')[0]);
|
||||
await waitFor(() => expect(screen.getByTestId('tab-backgroundActivity')).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('clicking the alerts pill switches to the alerts tab', async () => {
|
||||
renderAt('/activity');
|
||||
await waitFor(() => screen.getAllByText('activity.tabs.alerts'));
|
||||
fireEvent.click(screen.getAllByText('activity.tabs.alerts')[0]);
|
||||
await waitFor(() => expect(screen.getByTestId('tab-alerts')).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('alerts tab renders the Notifications component', async () => {
|
||||
renderAt('/activity?tab=alerts');
|
||||
await waitFor(() => expect(screen.getByTestId('tab-alerts')).toBeInTheDocument());
|
||||
// The card wrapper is NOT rendered in the alerts tab.
|
||||
expect(screen.queryByTestId('tab-tasks')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Phase 6 — /human → /chat redirect test.
|
||||
*
|
||||
* Verifies that the desktop route for /human redirects to /chat (Assistant
|
||||
* surface). iOS keeps /human as a real route in AppRoutesIOS.tsx — only
|
||||
* the desktop redirect is tested here.
|
||||
*
|
||||
* Uses a minimal route tree so no full provider chain is needed.
|
||||
*/
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { MemoryRouter, Navigate, Route, Routes } from 'react-router-dom';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
function TestRoutes() {
|
||||
return (
|
||||
<Routes>
|
||||
{/* The real /chat route (Assistant surface). */}
|
||||
<Route path="/chat" element={<div data-testid="chat-page">chat</div>} />
|
||||
{/* Phase 6 back-compat redirect. */}
|
||||
<Route path="/human" element={<Navigate to="/chat" replace />} />
|
||||
</Routes>
|
||||
);
|
||||
}
|
||||
|
||||
const renderAt = (path: string) =>
|
||||
render(
|
||||
<MemoryRouter initialEntries={[path]}>
|
||||
<TestRoutes />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
describe('Phase 6 route redirects', () => {
|
||||
it('/human redirects to /chat (Assistant surface)', () => {
|
||||
renderAt('/human');
|
||||
expect(screen.getByTestId('chat-page')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('/chat renders the chat page directly', () => {
|
||||
renderAt('/chat');
|
||||
expect(screen.getByTestId('chat-page')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Phase 2 — route redirect tests.
|
||||
*
|
||||
* Verifies that:
|
||||
* /skills → /connections (back-compat redirect)
|
||||
* /channels → /connections?tab=messaging (orphaned page redirect)
|
||||
*
|
||||
* We render a minimal route tree so we do not need the full provider tree.
|
||||
*/
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { MemoryRouter, Navigate, Route, Routes } from 'react-router-dom';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
/**
|
||||
* Minimal route tree that mirrors only the redirects under test.
|
||||
* Using `Navigate` directly avoids needing to mock the full app.
|
||||
*/
|
||||
function TestRoutes() {
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="/connections" element={<div data-testid="connections-page">connections</div>} />
|
||||
<Route path="/skills" element={<Navigate to="/connections" replace />} />
|
||||
<Route path="/channels" element={<Navigate to="/connections?tab=messaging" replace />} />
|
||||
</Routes>
|
||||
);
|
||||
}
|
||||
|
||||
const renderAt = (path: string) =>
|
||||
render(
|
||||
<MemoryRouter initialEntries={[path]}>
|
||||
<TestRoutes />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
describe('Phase 2 route redirects', () => {
|
||||
it('/skills redirects to /connections', () => {
|
||||
renderAt('/skills');
|
||||
expect(screen.getByTestId('connections-page')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('/channels redirects to /connections (Messaging tab)', () => {
|
||||
renderAt('/channels');
|
||||
expect(screen.getByTestId('connections-page')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('/connections renders the connections page directly', () => {
|
||||
renderAt('/connections');
|
||||
expect(screen.getByTestId('connections-page')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1405,7 +1405,7 @@ describe('Conversations — smoke render (#1123 welcome-lock removal)', () => {
|
||||
|
||||
// Bucket tabs must be present regardless of thread count.
|
||||
expect(screen.getByRole('tab', { name: 'General' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('tab', { name: 'Subconscious' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('tab', { name: 'Background activity' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('tab', { name: 'Tasks' })).toBeInTheDocument();
|
||||
expect(screen.queryByRole('tab', { name: 'All' })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('tab', { name: 'Briefing' })).not.toBeInTheDocument();
|
||||
@@ -1423,7 +1423,7 @@ describe('Conversations — smoke render (#1123 welcome-lock removal)', () => {
|
||||
await openSidebar();
|
||||
|
||||
expect(screen.getByRole('tab', { name: 'General' })).toHaveAttribute('aria-selected', 'true');
|
||||
expect(screen.getByRole('tab', { name: 'Subconscious' })).toHaveAttribute(
|
||||
expect(screen.getByRole('tab', { name: 'Background activity' })).toHaveAttribute(
|
||||
'aria-selected',
|
||||
'false'
|
||||
);
|
||||
|
||||
@@ -40,6 +40,10 @@ vi.mock('../../hooks/useIntelligenceSocket', () => ({
|
||||
useIntelligenceSocket: () => ({ isConnected: true }),
|
||||
useIntelligenceSocketManager: () => ({}),
|
||||
}));
|
||||
// useDeveloperMode needs a Redux Provider; mock it so tests render without one.
|
||||
// All tabs are visible (developer mode on) — the test exercises URL routing,
|
||||
// not the dev gate, so a stable open-gate is fine here.
|
||||
vi.mock('../../hooks/useDeveloperMode', () => ({ useDeveloperMode: () => true }));
|
||||
vi.mock('../../hooks/useSubconscious', () => ({
|
||||
useSubconscious: () => ({
|
||||
status: 'idle',
|
||||
|
||||
@@ -63,16 +63,16 @@ vi.mock('../../lib/composio/hooks', () => ({
|
||||
|
||||
describe('Skills page — Channels grid', () => {
|
||||
beforeEach(() => {
|
||||
// The default tab is 'composio'; click 'Channels' to reveal the Channels card.
|
||||
// The default tab is 'composio'; click 'Messaging' to reveal the Channels card.
|
||||
});
|
||||
|
||||
it('renders configured channels as tiles in a dedicated card and opens the setup modal on click', async () => {
|
||||
renderWithProviders(<Skills />, { initialEntries: ['/skills'] });
|
||||
renderWithProviders(<Skills />, { initialEntries: ['/connections'] });
|
||||
|
||||
// Switch to the Channels tab to make the Channels card visible.
|
||||
fireEvent.click(screen.getByRole('tab', { name: 'Channels' }));
|
||||
// Switch to the Messaging tab to make the Channels card visible.
|
||||
fireEvent.click(screen.getByRole('tab', { name: 'Messaging' }));
|
||||
|
||||
const channelsHeading = screen.getByRole('heading', { name: 'Channels' });
|
||||
const channelsHeading = screen.getByRole('heading', { name: 'Messaging' });
|
||||
expect(channelsHeading).toBeInTheDocument();
|
||||
|
||||
const channelsCard = channelsHeading.closest('.rounded-2xl');
|
||||
@@ -134,11 +134,11 @@ describe('Skills page — Channels grid', () => {
|
||||
},
|
||||
};
|
||||
|
||||
renderWithProviders(<Skills />, { initialEntries: ['/skills'], preloadedState });
|
||||
// Switch to the Channels tab so the Channels card is visible.
|
||||
fireEvent.click(screen.getByRole('tab', { name: 'Channels' }));
|
||||
renderWithProviders(<Skills />, { initialEntries: ['/connections'], preloadedState });
|
||||
// Switch to the Messaging tab so the Channels card is visible.
|
||||
fireEvent.click(screen.getByRole('tab', { name: 'Messaging' }));
|
||||
const channelsCard = screen
|
||||
.getByRole('heading', { name: 'Channels' })
|
||||
.getByRole('heading', { name: 'Messaging' })
|
||||
.closest('.rounded-2xl');
|
||||
const telegramTile = within(channelsCard as HTMLElement).getByRole('button', {
|
||||
name: new RegExp(`Telegram.*${labelPattern.source}`, 'i'),
|
||||
@@ -148,11 +148,11 @@ describe('Skills page — Channels grid', () => {
|
||||
);
|
||||
|
||||
it('does not surface a Channels chip in the category filter inside the Integrations card', () => {
|
||||
renderWithProviders(<Skills />, { initialEntries: ['/skills'] });
|
||||
fireEvent.click(screen.getByRole('tab', { name: 'Composio' }));
|
||||
renderWithProviders(<Skills />, { initialEntries: ['/connections'] });
|
||||
fireEvent.click(screen.getByRole('tab', { name: 'Apps' }));
|
||||
|
||||
// The Composio tab owns the Integrations category filter.
|
||||
const integrationsHeading = screen.getByRole('heading', { name: 'Composio Integrations' });
|
||||
// The Apps tab owns the Integrations category filter.
|
||||
const integrationsHeading = screen.getByRole('heading', { name: 'Apps' });
|
||||
const integrationsCard = integrationsHeading.closest('.rounded-2xl');
|
||||
expect(integrationsCard).not.toBeNull();
|
||||
const filterTabs = within(integrationsCard as HTMLElement)
|
||||
|
||||
@@ -78,15 +78,15 @@ describe('Skills page — Composio catalog fallback', () => {
|
||||
agentReadyState = { agentReady: new Set<string>(), loading: true, error: null };
|
||||
});
|
||||
|
||||
function openComposioTab() {
|
||||
fireEvent.click(screen.getByRole('tab', { name: 'Composio' }));
|
||||
function openAppsTab() {
|
||||
fireEvent.click(screen.getByRole('tab', { name: 'Apps' }));
|
||||
}
|
||||
|
||||
it('shows known composio integrations in the integrations icon grid when the live toolkit list is empty', () => {
|
||||
renderWithProviders(<Skills />, { initialEntries: ['/skills'] });
|
||||
openComposioTab();
|
||||
renderWithProviders(<Skills />, { initialEntries: ['/connections'] });
|
||||
openAppsTab();
|
||||
|
||||
expect(screen.getByRole('heading', { name: 'Composio Integrations' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('heading', { name: 'Apps' })).toBeInTheDocument();
|
||||
expect(screen.getByText('Discord')).toBeInTheDocument();
|
||||
expect(screen.getByText('Google Calendar')).toBeInTheDocument();
|
||||
expect(screen.getByText('Google Drive')).toBeInTheDocument();
|
||||
@@ -103,7 +103,7 @@ describe('Skills page — Composio catalog fallback', () => {
|
||||
// missing Composio Zoom tile even though the Meeting bots card also
|
||||
// renders a "Zoom" entry on the same page.
|
||||
const integrationsSection = screen
|
||||
.getByRole('heading', { name: 'Composio Integrations' })
|
||||
.getByRole('heading', { name: 'Apps' })
|
||||
.closest('.rounded-2xl');
|
||||
expect(integrationsSection).not.toBeNull();
|
||||
expect(within(integrationsSection as HTMLElement).getByText('Zoom')).toBeInTheDocument();
|
||||
@@ -113,14 +113,14 @@ describe('Skills page — Composio catalog fallback', () => {
|
||||
it('shows a stale/error state instead of disconnected toolkits when composio loading fails', () => {
|
||||
composioError = 'Backend unavailable';
|
||||
|
||||
renderWithProviders(<Skills />, { initialEntries: ['/skills'] });
|
||||
openComposioTab();
|
||||
renderWithProviders(<Skills />, { initialEntries: ['/connections'] });
|
||||
openAppsTab();
|
||||
|
||||
expect(screen.getByText('Connections are showing stale status')).toBeInTheDocument();
|
||||
expect(screen.getByText('Backend unavailable')).toBeInTheDocument();
|
||||
|
||||
const integrationsSection = screen
|
||||
.getByRole('heading', { name: 'Composio Integrations' })
|
||||
.getByRole('heading', { name: 'Apps' })
|
||||
.closest('.rounded-2xl');
|
||||
expect(integrationsSection).not.toBeNull();
|
||||
const gmailTile = within(integrationsSection as HTMLElement).getByRole('button', {
|
||||
@@ -139,11 +139,11 @@ describe('Skills page — Composio catalog fallback', () => {
|
||||
['gmail', { id: 'ca_expired', toolkit: 'gmail', status: 'EXPIRED' }],
|
||||
]);
|
||||
|
||||
renderWithProviders(<Skills />, { initialEntries: ['/skills'] });
|
||||
openComposioTab();
|
||||
renderWithProviders(<Skills />, { initialEntries: ['/connections'] });
|
||||
openAppsTab();
|
||||
|
||||
const integrationsSection = screen
|
||||
.getByRole('heading', { name: 'Composio Integrations' })
|
||||
.getByRole('heading', { name: 'Apps' })
|
||||
.closest('.rounded-2xl');
|
||||
expect(integrationsSection).not.toBeNull();
|
||||
const gmailTile = within(integrationsSection as HTMLElement).getByRole('button', {
|
||||
@@ -174,11 +174,11 @@ describe('Skills page — Composio catalog fallback', () => {
|
||||
]);
|
||||
agentReadyState = { agentReady: new Set(['gmail']), loading: false, error: null };
|
||||
|
||||
renderWithProviders(<Skills />, { initialEntries: ['/skills'] });
|
||||
openComposioTab();
|
||||
renderWithProviders(<Skills />, { initialEntries: ['/connections'] });
|
||||
openAppsTab();
|
||||
|
||||
const integrationsSection = screen
|
||||
.getByRole('heading', { name: 'Composio Integrations' })
|
||||
.getByRole('heading', { name: 'Apps' })
|
||||
.closest('.rounded-2xl');
|
||||
expect(integrationsSection).not.toBeNull();
|
||||
expect(within(integrationsSection as HTMLElement).getByText('2')).toBeInTheDocument();
|
||||
@@ -194,11 +194,11 @@ describe('Skills page — Composio catalog fallback', () => {
|
||||
// misrepresenting the agent surface.
|
||||
agentReadyState = { agentReady: new Set<string>(), loading: false, error: 'rpc unavailable' };
|
||||
|
||||
renderWithProviders(<Skills />, { initialEntries: ['/skills'] });
|
||||
openComposioTab();
|
||||
renderWithProviders(<Skills />, { initialEntries: ['/connections'] });
|
||||
openAppsTab();
|
||||
|
||||
const integrationsSection = screen
|
||||
.getByRole('heading', { name: 'Composio Integrations' })
|
||||
.getByRole('heading', { name: 'Apps' })
|
||||
.closest('.rounded-2xl');
|
||||
expect(integrationsSection).not.toBeNull();
|
||||
// No Preview badges anywhere in the integrations grid. The
|
||||
@@ -218,11 +218,11 @@ describe('Skills page — Composio catalog fallback', () => {
|
||||
]);
|
||||
agentReadyState = { agentReady: new Set<string>(['gmail']), loading: false, error: null };
|
||||
|
||||
renderWithProviders(<Skills />, { initialEntries: ['/skills'] });
|
||||
openComposioTab();
|
||||
renderWithProviders(<Skills />, { initialEntries: ['/connections'] });
|
||||
openAppsTab();
|
||||
|
||||
const integrationsSection = screen
|
||||
.getByRole('heading', { name: 'Composio Integrations' })
|
||||
.getByRole('heading', { name: 'Apps' })
|
||||
.closest('.rounded-2xl');
|
||||
expect(integrationsSection).not.toBeNull();
|
||||
const zohoTile = within(integrationsSection as HTMLElement).getByRole('button', {
|
||||
@@ -243,8 +243,8 @@ describe('Skills page — Composio catalog fallback', () => {
|
||||
sessionToken = 'header.payload.local';
|
||||
composioModeStatus = { result: { mode: 'direct', api_key_set: false }, logs: [] };
|
||||
|
||||
renderWithProviders(<Skills />, { initialEntries: ['/skills'] });
|
||||
openComposioTab();
|
||||
renderWithProviders(<Skills />, { initialEntries: ['/connections'] });
|
||||
openAppsTab();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/No Composio API Key Configured/i)).toBeInTheDocument();
|
||||
|
||||
@@ -49,13 +49,13 @@ vi.mock('../../services/api/mcpClientsApi', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
describe('Skills page — MCP tab', () => {
|
||||
it('renders the live MCP servers tab with unified table view (not a coming-soon placeholder)', async () => {
|
||||
renderWithProviders(<Skills />, { initialEntries: ['/skills'] });
|
||||
describe('Skills page — Tools tab (MCP + Meeting bots, Phase 2)', () => {
|
||||
it('renders the MCP servers table in the Tools tab', async () => {
|
||||
renderWithProviders(<Skills />, { initialEntries: ['/connections'] });
|
||||
|
||||
fireEvent.click(screen.getByRole('tab', { name: 'MCP Servers' }));
|
||||
fireEvent.click(screen.getByRole('tab', { name: 'Tools' }));
|
||||
|
||||
// The new tab shows filter chips (All / Installed / Registry) and a search input
|
||||
// The Tools tab shows filter chips (All / Installed / Registry) and a search input
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: 'All' })).toBeInTheDocument();
|
||||
});
|
||||
@@ -63,10 +63,10 @@ describe('Skills page — MCP tab', () => {
|
||||
expect(screen.getByRole('button', { name: 'Registry' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows the table header columns on the MCP tab', async () => {
|
||||
renderWithProviders(<Skills />, { initialEntries: ['/skills'] });
|
||||
it('shows the table header columns on the Tools tab', async () => {
|
||||
renderWithProviders(<Skills />, { initialEntries: ['/connections'] });
|
||||
|
||||
fireEvent.click(screen.getByRole('tab', { name: 'MCP Servers' }));
|
||||
fireEvent.click(screen.getByRole('tab', { name: 'Tools' }));
|
||||
|
||||
// Wait for initial load to complete
|
||||
await waitFor(() => {
|
||||
@@ -79,9 +79,9 @@ describe('Skills page — MCP tab', () => {
|
||||
});
|
||||
|
||||
it('shows empty-installed state when Installed chip is clicked', async () => {
|
||||
renderWithProviders(<Skills />, { initialEntries: ['/skills'] });
|
||||
renderWithProviders(<Skills />, { initialEntries: ['/connections'] });
|
||||
|
||||
fireEvent.click(screen.getByRole('tab', { name: 'MCP Servers' }));
|
||||
fireEvent.click(screen.getByRole('tab', { name: 'Tools' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: /Installed/i })).toBeInTheDocument();
|
||||
@@ -92,4 +92,13 @@ describe('Skills page — MCP tab', () => {
|
||||
expect(screen.getByText('No MCP servers installed yet.')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('supports direct links via legacy ?tab=mcp (normalised to tools)', async () => {
|
||||
renderWithProviders(<Skills />, { initialEntries: ['/connections?tab=mcp'] });
|
||||
|
||||
expect(screen.getByRole('tab', { name: 'Tools' })).toHaveAttribute('aria-selected', 'true');
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: 'All' })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -38,24 +38,33 @@ vi.mock('../../lib/composio/hooks', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('Skills page — Meetings tab', () => {
|
||||
it('keeps the meeting bot CTA in its own Connections tab', () => {
|
||||
renderWithProviders(<Skills />, { initialEntries: ['/skills'] });
|
||||
describe('Skills page — Talents tab (meeting bots)', () => {
|
||||
it('shows the meeting bot CTA inside the Talents tab (not Tools)', () => {
|
||||
renderWithProviders(<Skills />, { initialEntries: ['/connections'] });
|
||||
|
||||
expect(screen.queryByTestId('meeting-bots-card')).not.toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByRole('tab', { name: 'Google Meet' }));
|
||||
// Tools no longer hosts the meeting bot CTA.
|
||||
fireEvent.click(screen.getByRole('tab', { name: 'Tools' }));
|
||||
expect(screen.queryByTestId('meeting-bots-card')).not.toBeInTheDocument();
|
||||
|
||||
// Talents does.
|
||||
fireEvent.click(screen.getByRole('tab', { name: 'Talents' }));
|
||||
expect(screen.getByTestId('meeting-bots-card')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('supports direct links to the Meetings tab', () => {
|
||||
renderWithProviders(<Skills />, { initialEntries: ['/skills?tab=meetings'] });
|
||||
it('supports direct links via legacy ?tab=meetings (normalised to talents)', () => {
|
||||
// The old ?tab=meetings alias now maps to the new "Talents" tab.
|
||||
renderWithProviders(<Skills />, { initialEntries: ['/connections?tab=meetings'] });
|
||||
|
||||
expect(screen.getByRole('tab', { name: 'Google Meet' })).toHaveAttribute(
|
||||
'aria-selected',
|
||||
'true'
|
||||
);
|
||||
expect(screen.getByRole('tab', { name: 'Talents' })).toHaveAttribute('aria-selected', 'true');
|
||||
expect(screen.getByTestId('meeting-bots-card')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('supports direct links via ?tab=talents', () => {
|
||||
renderWithProviders(<Skills />, { initialEntries: ['/connections?tab=talents'] });
|
||||
|
||||
expect(screen.getByRole('tab', { name: 'Talents' })).toHaveAttribute('aria-selected', 'true');
|
||||
expect(screen.getByTestId('meeting-bots-card')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -40,11 +40,11 @@ vi.mock('../../lib/composio/hooks', () => ({
|
||||
|
||||
describe('Skills page — Gmail composio integration', () => {
|
||||
it('renders Gmail as a connected composio integration and opens its management modal', async () => {
|
||||
renderWithProviders(<Skills />, { initialEntries: ['/skills'] });
|
||||
fireEvent.click(screen.getByRole('tab', { name: 'Composio' }));
|
||||
renderWithProviders(<Skills />, { initialEntries: ['/connections'] });
|
||||
fireEvent.click(screen.getByRole('tab', { name: 'Apps' }));
|
||||
|
||||
const integrationsSection = screen
|
||||
.getByRole('heading', { name: 'Composio Integrations' })
|
||||
.getByRole('heading', { name: 'Apps' })
|
||||
.closest('.rounded-2xl');
|
||||
expect(integrationsSection).not.toBeNull();
|
||||
expect(within(integrationsSection as HTMLElement).getByText('Gmail')).toBeInTheDocument();
|
||||
|
||||
@@ -36,10 +36,10 @@ vi.mock('../../lib/composio/hooks', () => ({
|
||||
|
||||
describe('Skills page — Notion composio integration', () => {
|
||||
it('renders Notion as a disconnected composio integration and opens its connect modal', async () => {
|
||||
renderWithProviders(<Skills />, { initialEntries: ['/skills'] });
|
||||
fireEvent.click(screen.getByRole('tab', { name: 'Composio' }));
|
||||
renderWithProviders(<Skills />, { initialEntries: ['/connections'] });
|
||||
fireEvent.click(screen.getByRole('tab', { name: 'Apps' }));
|
||||
|
||||
expect(screen.getByRole('heading', { name: 'Composio Integrations' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('heading', { name: 'Apps' })).toBeInTheDocument();
|
||||
const notionTile = screen.getByRole('button', { name: /Notion.*Connect/i });
|
||||
expect(notionTile).toBeInTheDocument();
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Tests for the `developerMode` field added to themeSlice and the
|
||||
* `selectDeveloperMode` selector.
|
||||
*
|
||||
* The field is persisted via the `theme` redux-persist config (localStorage,
|
||||
* same as other theme preferences). These tests validate the reducer + selector
|
||||
* in isolation without requiring the full persist machinery.
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import themeReducer, { selectDeveloperMode, setDeveloperMode } from './themeSlice';
|
||||
|
||||
const initialState = themeReducer(undefined, { type: '@@INIT' });
|
||||
|
||||
describe('themeSlice — developerMode field', () => {
|
||||
it('defaults developerMode to false', () => {
|
||||
expect(initialState.developerMode).toBe(false);
|
||||
});
|
||||
|
||||
it('enables developerMode via setDeveloperMode(true)', () => {
|
||||
const state = themeReducer(initialState, setDeveloperMode(true));
|
||||
expect(state.developerMode).toBe(true);
|
||||
});
|
||||
|
||||
it('disables developerMode via setDeveloperMode(false)', () => {
|
||||
const withDevMode = themeReducer(initialState, setDeveloperMode(true));
|
||||
const back = themeReducer(withDevMode, setDeveloperMode(false));
|
||||
expect(back.developerMode).toBe(false);
|
||||
});
|
||||
|
||||
it('leaves all other theme fields untouched when toggling developerMode', () => {
|
||||
const { developerMode: _, ...restBefore } = initialState;
|
||||
const withDevMode = themeReducer(initialState, setDeveloperMode(true));
|
||||
const { developerMode: _2, ...restAfter } = withDevMode;
|
||||
expect(restAfter).toEqual(restBefore);
|
||||
});
|
||||
});
|
||||
|
||||
describe('selectDeveloperMode', () => {
|
||||
it('returns false from a fresh store', () => {
|
||||
const result = selectDeveloperMode({ theme: initialState });
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true after setDeveloperMode(true)', () => {
|
||||
const state = themeReducer(initialState, setDeveloperMode(true));
|
||||
expect(selectDeveloperMode({ theme: state })).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false after toggling back to false', () => {
|
||||
const enabled = themeReducer(initialState, setDeveloperMode(true));
|
||||
const disabled = themeReducer(enabled, setDeveloperMode(false));
|
||||
expect(selectDeveloperMode({ theme: disabled })).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -95,7 +95,7 @@ const persistedLocaleReducer = persistReducer(localePersistConfig, localeReducer
|
||||
const themePersistConfig = {
|
||||
key: 'theme',
|
||||
storage: localStorageAdapter,
|
||||
whitelist: ['mode', 'tabBarLabels', 'fontSize', 'agentMessageViewMode'],
|
||||
whitelist: ['mode', 'tabBarLabels', 'fontSize', 'agentMessageViewMode', 'developerMode'],
|
||||
};
|
||||
const persistedThemeReducer = persistReducer(themePersistConfig, themeReducer);
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ describe('themeSlice', () => {
|
||||
tabBarLabels: 'always',
|
||||
fontSize: 'xlarge',
|
||||
agentMessageViewMode: 'bubbles',
|
||||
developerMode: false,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -28,6 +28,14 @@ interface ThemeState {
|
||||
tabBarLabels: TabBarLabels;
|
||||
fontSize: FontSize;
|
||||
agentMessageViewMode: AgentMessageViewMode;
|
||||
/**
|
||||
* Runtime Developer Mode (default OFF).
|
||||
* When true, all developer and diagnostic surfaces become visible.
|
||||
* Combines with the build-time `IS_DEV` flag — either one enables the gate.
|
||||
* Gating is UI-only: the Rust SecurityPolicy / autonomy tier enforcement
|
||||
* is authoritative and is never relaxed by this toggle.
|
||||
*/
|
||||
developerMode: boolean;
|
||||
}
|
||||
|
||||
const initialState: ThemeState = {
|
||||
@@ -35,6 +43,7 @@ const initialState: ThemeState = {
|
||||
tabBarLabels: 'hover',
|
||||
fontSize: 'medium',
|
||||
agentMessageViewMode: 'bubbles',
|
||||
developerMode: false,
|
||||
};
|
||||
|
||||
const themeSlice = createSlice({
|
||||
@@ -53,13 +62,28 @@ const themeSlice = createSlice({
|
||||
setAgentMessageViewMode(state, action: PayloadAction<AgentMessageViewMode>) {
|
||||
state.agentMessageViewMode = action.payload;
|
||||
},
|
||||
setDeveloperMode(state, action: PayloadAction<boolean>) {
|
||||
state.developerMode = action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { setThemeMode, setTabBarLabels, setFontSize, setAgentMessageViewMode } =
|
||||
themeSlice.actions;
|
||||
export const {
|
||||
setThemeMode,
|
||||
setTabBarLabels,
|
||||
setFontSize,
|
||||
setAgentMessageViewMode,
|
||||
setDeveloperMode,
|
||||
} = themeSlice.actions;
|
||||
export default themeSlice.reducer;
|
||||
|
||||
/**
|
||||
* Selector for the persisted `developerMode` preference.
|
||||
* Use {@link useDeveloperMode} in components — it combines this with `IS_DEV`.
|
||||
*/
|
||||
export const selectDeveloperMode = (state: { theme: ThemeState }): boolean =>
|
||||
state.theme.developerMode;
|
||||
|
||||
/**
|
||||
* Resolves a `ThemeMode` to the concrete `light` or `dark` value that should
|
||||
* be applied to `<html>`. `system` consults `prefers-color-scheme`; in non-DOM
|
||||
|
||||
@@ -292,7 +292,7 @@ const handleOAuthDeepLink = async (parsed: URL) => {
|
||||
`[DeepLink] OAuth success for integration=${integrationId}${toolkit ? ` toolkit=${toolkit}` : ''}`
|
||||
);
|
||||
window.dispatchEvent(new CustomEvent('oauth:success', { detail: { integrationId, toolkit } }));
|
||||
window.location.hash = '/skills';
|
||||
window.location.hash = '/connections';
|
||||
} else if (path === 'error') {
|
||||
const provider = sanitizeOAuthDiagnosticValue(
|
||||
parsed.searchParams.get('provider'),
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Resolve a human-friendly display name from a core-owned user snapshot,
|
||||
* tolerating both camelCase and snake_case identity fields. Returns the
|
||||
* literal `'User'` when nothing usable is present.
|
||||
*
|
||||
* Shared by the Home greeting and the bottom-bar avatar so both derive the
|
||||
* same name from the same source.
|
||||
*/
|
||||
export function resolveUserName(user: unknown): string {
|
||||
if (!user || typeof user !== 'object') return 'User';
|
||||
|
||||
const record = user as Record<string, unknown>;
|
||||
const firstName =
|
||||
(typeof record.firstName === 'string' && record.firstName.trim()) ||
|
||||
(typeof record.first_name === 'string' && record.first_name.trim()) ||
|
||||
'';
|
||||
const lastName =
|
||||
(typeof record.lastName === 'string' && record.lastName.trim()) ||
|
||||
(typeof record.last_name === 'string' && record.last_name.trim()) ||
|
||||
'';
|
||||
const username = typeof record.username === 'string' ? record.username.trim() : '';
|
||||
const email = typeof record.email === 'string' ? record.email.trim() : '';
|
||||
|
||||
const fullName = [firstName, lastName].filter(Boolean).join(' ').trim();
|
||||
if (fullName) return fullName;
|
||||
if (firstName) return firstName;
|
||||
if (username) return username.startsWith('@') ? username : `@${username}`;
|
||||
if (email) return email.split('@')[0] || 'User';
|
||||
return 'User';
|
||||
}
|
||||
@@ -120,12 +120,16 @@ export async function clickFirstMatch(candidates, timeout = 5_000) {
|
||||
|
||||
/** Appium Mac2 cannot run W3C Execute Script in WKWebView — use sidebar labels instead. */
|
||||
const HASH_TO_SIDEBAR_LABEL = {
|
||||
'/skills': 'Skills',
|
||||
// Phase 2/3 IA revamp: /skills → /connections, /intelligence → /activity
|
||||
'/connections': 'Connections',
|
||||
'/activity': 'Activity',
|
||||
'/home': 'Home',
|
||||
'/chat': 'Chat',
|
||||
'/chat': 'Assistant',
|
||||
'/notifications': 'Alerts',
|
||||
'/settings': 'Settings',
|
||||
'/intelligence': 'Intelligence',
|
||||
// Back-compat: old routes redirect — keep entries so existing callers still work
|
||||
'/skills': 'Connections',
|
||||
'/intelligence': 'Activity',
|
||||
};
|
||||
|
||||
function normalizeHash(value) {
|
||||
@@ -143,7 +147,10 @@ function routeReadySelector(hash) {
|
||||
'/settings/migration': '[data-testid="migration-form"]',
|
||||
'/settings/voice': '[data-testid="voice-providers-section"]',
|
||||
'/settings/memory-data': '[data-testid="memory-workspace"]',
|
||||
'/intelligence': '[data-testid="memory-workspace"]',
|
||||
// Phase 3: /intelligence → /activity; memory-workspace is dev-gated (tab=memory).
|
||||
// Use a non-dev-gated selector for the activity route instead.
|
||||
'/intelligence': '[data-testid="intelligence-tasks"]',
|
||||
'/activity': '[data-testid="intelligence-tasks"]',
|
||||
};
|
||||
return selectors[path] || null;
|
||||
}
|
||||
@@ -425,12 +432,24 @@ export async function navigateToBilling() {
|
||||
console.log('[E2E] Billing page loaded (after fallback)');
|
||||
}
|
||||
|
||||
/** @deprecated Phase 2: use navigateToConnections() instead. Still works via redirect. */
|
||||
export async function navigateToSkills() {
|
||||
await navigateViaHash('/skills');
|
||||
await navigateViaHash('/connections');
|
||||
}
|
||||
|
||||
/** Navigate to the Connections page (was /skills in Phase 1). */
|
||||
export async function navigateToConnections() {
|
||||
await navigateViaHash('/connections');
|
||||
}
|
||||
|
||||
/** @deprecated Phase 3: use navigateToActivity() instead. Still works via redirect. */
|
||||
export async function navigateToIntelligence() {
|
||||
await navigateViaHash('/intelligence');
|
||||
await navigateViaHash('/activity');
|
||||
}
|
||||
|
||||
/** Navigate to the Activity page (was /intelligence in Phase 2). */
|
||||
export async function navigateToActivity() {
|
||||
await navigateViaHash('/activity');
|
||||
}
|
||||
|
||||
export async function navigateToConversations() {
|
||||
|
||||
@@ -144,7 +144,8 @@ suiteRunner('Conversations web channel flow', () => {
|
||||
expect(sent).toBe(true);
|
||||
|
||||
await waitForText(uniquePayload, 20_000);
|
||||
await navigateViaHash('/skills');
|
||||
// Phase 2: /skills → /connections
|
||||
await navigateViaHash('/connections');
|
||||
await browser.pause(1_500);
|
||||
await navigateToConversations();
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
*
|
||||
* 1. Skills gate: start tour, reach the skills step, confirm skills UI is
|
||||
* present. The tooltip advances via Next — the current implementation
|
||||
* navigates to /skills and highlights the grid via a `before` async hook
|
||||
* navigates to /connections (was /skills) and highlights the grid via a `before` async hook
|
||||
* in walkthroughSteps.ts. The test polls for the hash change rather than
|
||||
* reading it immediately, because the Joyride `before` hook is awaited
|
||||
* asynchronously and the hash may lag by a render cycle.
|
||||
@@ -33,7 +33,7 @@
|
||||
* assertion is skipped and documented as GP-2.
|
||||
*
|
||||
* Product gaps surfaced (skipped):
|
||||
* - GP-1: No skill-connection gate on the /skills tour step.
|
||||
* - GP-1: No skill-connection gate on the /connections tour step (Phase 2: was /skills).
|
||||
* - GP-2: No step-index persistence — tour always restarts from step 0
|
||||
* on reload rather than resuming at the last incomplete step.
|
||||
* - GP-3: No API to jump to an arbitrary Joyride step — the only way to
|
||||
@@ -243,8 +243,8 @@ describe('Guided tour — gates and resume behaviour (#1215)', function () {
|
||||
// appears after dispatchWalkthroughRestart() when Joyride has already
|
||||
// completed a prior run on the same mounted instance. Without the
|
||||
// tooltip, advanceTourSteps() finds no primary button and the hash
|
||||
// stays at #/home instead of advancing to #/skills.
|
||||
it.skip('tour navigates to /skills and highlights skills-grid after 3 Next clicks', async () => {
|
||||
// stays at #/home instead of advancing to #/connections (Phase 2: was #/skills).
|
||||
it.skip('tour navigates to /connections and highlights skills-grid after 3 Next clicks', async () => {
|
||||
// SKIPPED — depends on tooltip appearing at step 1, which is blocked by
|
||||
// the same Joyride run-state issue documented above. Re-enable once
|
||||
// AppWalkthrough forces a step-index reset on walkthrough:restart.
|
||||
@@ -256,7 +256,7 @@ describe('Guided tour — gates and resume behaviour (#1215)', function () {
|
||||
// hold the "Next" button disabled until a `openhuman.workflows_list` RPC
|
||||
// call confirms at least one skill is connected, then re-enable it.
|
||||
it.skip('GP-1 (NOT IMPLEMENTED): tour Next button is disabled until user connects a skill', async () => {
|
||||
// Expected product behaviour: the Next button on the /skills step
|
||||
// Expected product behaviour: the Next button on the /connections step (Phase 2: was /skills)
|
||||
// should remain disabled (`aria-disabled="true"` or `disabled`) while
|
||||
// no skill is connected, and become enabled only after the
|
||||
// `skills.skill_connected` event fires or a polling RPC returns >= 1
|
||||
@@ -391,7 +391,7 @@ describe('Guided tour — gates and resume behaviour (#1215)', function () {
|
||||
// when AppWalkthrough mounts with `run=true`.
|
||||
it.skip('GP-2 (NOT IMPLEMENTED): tour resumes at last incomplete step after reload', async () => {
|
||||
// Expected product behaviour:
|
||||
// 1. User advances to step 4 (/skills).
|
||||
// 1. User advances to step 4 (/connections — was /skills before Phase 2).
|
||||
// 2. App is closed (renderer reloaded) before the tour finishes.
|
||||
// 3. On reopen the tour shows step 4, not step 0.
|
||||
//
|
||||
|
||||
@@ -9,8 +9,8 @@ import { startMockServer, stopMockServer } from '../mock-server';
|
||||
/**
|
||||
* E2E for the global memory-sync schedule control (#3302).
|
||||
*
|
||||
* Drives the real React UI + in-process core: navigate to Intelligence →
|
||||
* Memory, confirm the schedule defaults to "Every 24h", pick the 4h preset,
|
||||
* Drives the real React UI + in-process core: navigate to Settings → Account →
|
||||
* Data Sync, confirm the schedule defaults to "Every 24h", pick the 4h preset,
|
||||
* confirm the summary updates, then re-navigate to prove the choice persisted
|
||||
* through `config_update_memory_sync_settings` / `config_get_memory_sync_settings`.
|
||||
*
|
||||
@@ -27,11 +27,11 @@ function stepLog(message: string, context?: unknown): void {
|
||||
}
|
||||
|
||||
async function gotoMemoryWorkspace(): Promise<void> {
|
||||
// HashRouter + useSearchParams: the Memory tab lives at
|
||||
// `#/intelligence?tab=memory`, whose default sub-tab renders the
|
||||
// MemoryWorkspace (and the MemorySourcesRegistry schedule control).
|
||||
// The memory-sync schedule control lives on the layman Data Sync page
|
||||
// (Settings → Account → Data Sync), which renders MemorySourcesRegistry and
|
||||
// its schedule control without needing developer mode.
|
||||
await browser.execute(() => {
|
||||
window.location.hash = '/intelligence?tab=memory';
|
||||
window.location.hash = '/settings/memory-sync';
|
||||
});
|
||||
await browser.pause(2_000);
|
||||
}
|
||||
@@ -85,7 +85,7 @@ describe('Memory sync schedule', () => {
|
||||
|
||||
// Navigate away and back to prove the value was persisted server-side.
|
||||
await browser.execute(() => {
|
||||
window.location.hash = '/intelligence?tab=tasks';
|
||||
window.location.hash = '/settings';
|
||||
});
|
||||
await browser.pause(1_000);
|
||||
await gotoMemoryWorkspace();
|
||||
|
||||
@@ -22,6 +22,11 @@ const USER_ID = 'e2e-navigation-smoothness';
|
||||
const ROUTE_TIMEOUT = 10_000;
|
||||
|
||||
// Routes to visit, with optional text markers that confirm the panel loaded.
|
||||
// Phase 2/3/6 IA revamp:
|
||||
// /skills → /connections (back-compat redirect); use canonical route here
|
||||
// /intelligence → /activity (back-compat redirect); use canonical route here
|
||||
// /channels → /connections?tab=messaging (redirect)
|
||||
// /human → /chat (Phase 6 redirect)
|
||||
interface RouteCheck {
|
||||
hash: string;
|
||||
markers: string[];
|
||||
@@ -29,36 +34,18 @@ interface RouteCheck {
|
||||
|
||||
const ROUTES: RouteCheck[] = [
|
||||
{ hash: '/chat', markers: ['Threads', 'Chat', 'Message', 'New thread'] },
|
||||
{ hash: '/skills', markers: ['Skills', 'Skill', 'Install', 'Browse'] },
|
||||
{
|
||||
hash: '/home',
|
||||
markers: [
|
||||
'Good morning',
|
||||
'Good afternoon',
|
||||
'Good evening',
|
||||
'Message OpenHuman',
|
||||
'Test',
|
||||
'Upgrade',
|
||||
],
|
||||
},
|
||||
{ hash: '/channels', markers: ['Channels', 'Channel', 'Connect', 'Add', 'Gmail', 'Telegram'] },
|
||||
// Connections page (was /skills) — tabs: Apps, Messaging, Tools, Explorer
|
||||
{ hash: '/connections', markers: ['Apps', 'Messaging', 'Tools', 'Connections'] },
|
||||
{ hash: '/home', markers: ['Ask your assistant anything', 'Your device is connected', 'Home'] },
|
||||
{
|
||||
hash: '/notifications',
|
||||
markers: ['Notifications', 'Alerts', 'Notification', 'No notifications'],
|
||||
},
|
||||
{ hash: '/rewards', markers: ['Rewards', 'Referral', 'Credits', 'Earn', 'Invite'] },
|
||||
{ hash: '/settings', markers: ['Settings', 'Account', 'Billing', 'Advanced'] },
|
||||
{
|
||||
hash: '/home',
|
||||
markers: [
|
||||
'Good morning',
|
||||
'Good afternoon',
|
||||
'Good evening',
|
||||
'Message OpenHuman',
|
||||
'Test',
|
||||
'Upgrade',
|
||||
],
|
||||
},
|
||||
// Activity page (was /intelligence) — tabs: Tasks, Automations, Background activity
|
||||
{ hash: '/activity', markers: ['Tasks', 'Automations', 'Background', 'Activity'] },
|
||||
{ hash: '/home', markers: ['Ask your assistant anything', 'Your device is connected', 'Home'] },
|
||||
];
|
||||
|
||||
async function rootTextLength(): Promise<number> {
|
||||
@@ -109,7 +96,7 @@ describe('Navigation smoothness', () => {
|
||||
console.log(`${LOG_PREFIX} Teardown complete`);
|
||||
});
|
||||
|
||||
it('N1.1 — all 8 major routes render without error within timing budget', async () => {
|
||||
it('N1.1 — all major routes render without error within timing budget', async () => {
|
||||
console.log(`${LOG_PREFIX} N1.1: first pass — normal navigation`);
|
||||
for (const route of ROUTES) {
|
||||
console.log(`${LOG_PREFIX} N1.1: navigating to ${route.hash}`);
|
||||
|
||||
@@ -28,12 +28,15 @@ interface Route {
|
||||
minChars?: number;
|
||||
}
|
||||
|
||||
// Phase 2/3/6 IA revamp:
|
||||
// /human → /chat (Phase 6 — back-compat redirect)
|
||||
// /skills → /connections (Phase 2 — back-compat redirect)
|
||||
// /intelligence → /activity (Phase 3 — back-compat redirect)
|
||||
const ROUTES: Route[] = [
|
||||
{ hash: '/home' },
|
||||
{ hash: '/human' },
|
||||
{ hash: '/chat' },
|
||||
{ hash: '/skills' },
|
||||
{ hash: '/intelligence' },
|
||||
{ hash: '/connections' },
|
||||
{ hash: '/activity' },
|
||||
{ hash: '/rewards' },
|
||||
{ hash: '/settings' },
|
||||
];
|
||||
|
||||
@@ -30,8 +30,9 @@ describe('Settings - Channels & Permissions', () => {
|
||||
});
|
||||
|
||||
it('allows switching default messaging channel (13.2.1)', async () => {
|
||||
// Default Messaging Channel UI moved from /settings/messaging to /skills (channels tab).
|
||||
await navigateViaHash('/skills?tab=channels');
|
||||
// Phase 2: Default Messaging Channel UI is at /connections (Messaging tab).
|
||||
// Old /skills?tab=channels → /connections?tab=messaging.
|
||||
await navigateViaHash('/connections?tab=messaging');
|
||||
|
||||
await waitForText('Default Messaging Channel', 15_000);
|
||||
expect(await textExists('Telegram')).toBe(true);
|
||||
|
||||
@@ -75,15 +75,16 @@ describe('Settings - Feature Preferences', () => {
|
||||
await waitForText('Features', 15_000);
|
||||
// Settings uses t('pages.settings.features.screenAwareness') = 'Screen awareness'
|
||||
await waitForText('Screen awareness', 15_000);
|
||||
// Settings uses t('pages.settings.features.messagingChannels') = 'Messaging channels'
|
||||
await waitForText('Messaging channels', 15_000);
|
||||
// Phase 2: default messaging channel moved to /connections (Messaging tab);
|
||||
// the settings/features panel no longer has a "Messaging channels" entry.
|
||||
await waitForText('Notifications', 15_000);
|
||||
await waitForText('Tools', 15_000);
|
||||
});
|
||||
|
||||
it('persists the default messaging channel through redux state', async () => {
|
||||
// Default Messaging Channel moved from /settings/messaging to /skills (channels tab).
|
||||
await navigateViaHash('/skills?tab=channels');
|
||||
// Phase 2: Default Messaging Channel moved to /connections (Messaging tab).
|
||||
// Old /skills?tab=channels → /connections?tab=messaging.
|
||||
await navigateViaHash('/connections?tab=messaging');
|
||||
|
||||
await waitForText('Default Messaging Channel', 15_000);
|
||||
await clickText('Discord', 10_000);
|
||||
|
||||
@@ -40,12 +40,14 @@ describe('Skill discovery (UI + core RPC)', () => {
|
||||
expect(ping.ok).toBe(true);
|
||||
});
|
||||
|
||||
it('Skills UI surface shows installed tools', async () => {
|
||||
it('Connections UI surface shows installed tools', async () => {
|
||||
// Phase 2: navigateToSkills() now navigates to /connections
|
||||
await navigateToSkills();
|
||||
await browser.pause(2_000);
|
||||
|
||||
const hash = await browser.execute(() => window.location.hash);
|
||||
expect(String(hash)).toContain('/skills');
|
||||
// Phase 2: /skills redirects to /connections
|
||||
expect(String(hash)).toContain('/connections');
|
||||
|
||||
const visible =
|
||||
(await textExists('Skills')) ||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Skill lifecycle smoke (issue #224).
|
||||
*
|
||||
* Drives auth → onboarding → Skills page and asserts:
|
||||
* 1. The route mounts (`#/skills`).
|
||||
* 1. The route mounts (`#/connections` — was `#/skills` before Phase 2).
|
||||
* 2. The Skills shell renders one of the well-known affordances
|
||||
* (Skills/Install/Available header).
|
||||
*
|
||||
@@ -33,21 +33,26 @@ describe('Skill lifecycle smoke', () => {
|
||||
await stopMockServer();
|
||||
});
|
||||
|
||||
it('Skills page mounts and fetched the registry', async () => {
|
||||
it('Connections page mounts and fetched the registry', async () => {
|
||||
// Phase 2: navigateToSkills() now points to /connections
|
||||
await navigateToSkills();
|
||||
await browser.waitUntil(
|
||||
async () => String(await browser.execute(() => window.location.hash)).includes('/skills'),
|
||||
{ timeout: 10_000, interval: 250, timeoutMsg: 'Skills route did not mount in time' }
|
||||
// Phase 2: /skills redirects to /connections
|
||||
async () =>
|
||||
String(await browser.execute(() => window.location.hash)).includes('/connections'),
|
||||
{ timeout: 10_000, interval: 250, timeoutMsg: 'Connections route did not mount in time' }
|
||||
);
|
||||
|
||||
const hash = await browser.execute(() => window.location.hash);
|
||||
expect(String(hash)).toContain('/skills');
|
||||
// Phase 2: /skills → /connections
|
||||
expect(String(hash)).toContain('/connections');
|
||||
|
||||
// Skills page now shows "Connections" title with Composio/Channels/MCP tabs.
|
||||
// Connections page shows tabs: Apps (was Composio), Messaging (was Channels), Tools (was MCP)
|
||||
const visible =
|
||||
(await textExists('Connections')) ||
|
||||
(await textExists('Composio')) ||
|
||||
(await textExists('MCP Servers'));
|
||||
(await textExists('Apps')) ||
|
||||
(await textExists('Messaging')) ||
|
||||
(await textExists('Tools')) ||
|
||||
(await textExists('Connections'));
|
||||
expect(visible).toBe(true);
|
||||
|
||||
// Verify the core RPC route for skills is reachable. The Skills page
|
||||
|
||||
@@ -53,18 +53,21 @@ describe('Skills registry flow', () => {
|
||||
await stopMockServer();
|
||||
});
|
||||
|
||||
it('navigates to /skills and renders catalog content', async () => {
|
||||
it('navigates to /connections and renders catalog content', async () => {
|
||||
clearRequestLog();
|
||||
// Phase 2: navigateToSkills() now navigates to /connections
|
||||
await navigateToSkills();
|
||||
|
||||
const currentHash = await browser.execute(() => window.location.hash);
|
||||
expect(String(currentHash)).toContain('/skills');
|
||||
// Phase 2: /skills → /connections
|
||||
expect(String(currentHash)).toContain('/connections');
|
||||
|
||||
await browser.pause(2_000);
|
||||
// Phase 2: tab labels changed — Apps/Messaging/Tools instead of Composio/Channels/MCP
|
||||
const hasSkillsContent =
|
||||
(await textExists('Install')) ||
|
||||
(await textExists('Available')) ||
|
||||
(await textExists('Skills')) ||
|
||||
(await textExists('Apps')) ||
|
||||
(await textExists('Messaging')) ||
|
||||
(await textExists('Tools')) ||
|
||||
(await textExists('Telegram')) ||
|
||||
(await textExists('Notion'));
|
||||
|
||||
|
||||
@@ -251,19 +251,21 @@ describe.skip('Voice mode — Human tab capture & error mapping (#1610)', () =>
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper: navigate to the Human tab via hash routing.
|
||||
// Helper: navigate to the Assistant (Chat) tab via hash routing.
|
||||
// Phase 6: /human merged into /chat (Assistant surface). /human redirects.
|
||||
// ---------------------------------------------------------------------------
|
||||
async function navigateToHumanTab(): Promise<void> {
|
||||
if (supportsExecuteScript()) {
|
||||
await browser.execute(() => {
|
||||
window.location.hash = '#/human';
|
||||
// Phase 6: /human → /chat (back-compat redirect preserved)
|
||||
window.location.hash = '#/chat';
|
||||
});
|
||||
} else {
|
||||
// Mac2 path: use the shared helper which abstracts the XCUIElementTypeButton
|
||||
// XPath so the selector stays cross-driver and policy-compliant.
|
||||
await clickNativeButton('Human');
|
||||
await clickNativeButton('Assistant');
|
||||
}
|
||||
// Allow React router to settle and the Human page to mount.
|
||||
// Allow React router to settle and the Chat page to mount.
|
||||
await browser.pause(1_500);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,10 +15,10 @@ test.describe('Channels Smoke', () => {
|
||||
await waitForAppReady(page);
|
||||
await dismissWalkthroughIfPresent(page);
|
||||
|
||||
await expect(page.getByText('Channels')).toBeVisible();
|
||||
await expect(page.getByRole('heading', { name: 'Telegram', exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: /Telegram Disconnected/ })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: /Discord Disconnected/ })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Connect' }).first()).toBeVisible();
|
||||
// /channels redirects to /connections?tab=messaging; connectors now render
|
||||
// as ChannelTile buttons named "<Name>, <status>. <cta>." (not headings/
|
||||
// standalone Disconnected buttons).
|
||||
await expect(page.getByRole('button', { name: /Telegram/ }).first()).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: /Discord/ }).first()).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -31,8 +31,8 @@ test.describe('Command Palette', () => {
|
||||
|
||||
await expect(page.getByText('Go Home')).toBeVisible();
|
||||
await expect(page.getByText('Go to Chat')).toBeVisible();
|
||||
await expect(page.getByText('Go to Intelligence')).toBeVisible();
|
||||
await expect(page.getByText('Go to Skills')).toBeVisible();
|
||||
await expect(page.getByText('Go to Knowledge & Memory')).toBeVisible();
|
||||
await expect(page.getByText('Go to Connections')).toBeVisible();
|
||||
await expect(page.getByText('Open Settings')).toBeVisible();
|
||||
|
||||
await page.keyboard.press('Escape');
|
||||
|
||||
@@ -74,24 +74,20 @@ async function bootSkillsPage(page: Page, userId: string) {
|
||||
localStorage.setItem('openhuman:walkthrough_completed', 'true');
|
||||
localStorage.removeItem('openhuman:walkthrough_pending');
|
||||
} catch {}
|
||||
window.location.hash = '/skills';
|
||||
// Phase 2: /skills → /connections
|
||||
window.location.hash = '/connections';
|
||||
});
|
||||
await expect
|
||||
.poll(async () => page.evaluate(() => window.location.hash), { timeout: 10_000 })
|
||||
.toContain('/skills');
|
||||
.toContain('/connections');
|
||||
await waitForAppReady(page);
|
||||
await dismissWalkthroughIfPresent(page);
|
||||
await page.getByRole('tab', { name: 'Composio' }).click();
|
||||
const heading = page.getByRole('heading', { name: 'Composio Integrations' });
|
||||
if (!(await heading.isVisible().catch(() => false))) {
|
||||
const connectionsButton = page.getByRole('button', { name: 'Connections' });
|
||||
if (await connectionsButton.isVisible().catch(() => false)) {
|
||||
await connectionsButton.click({ force: true });
|
||||
await waitForAppReady(page);
|
||||
await dismissWalkthroughIfPresent(page);
|
||||
}
|
||||
}
|
||||
await expect(heading).toBeVisible({ timeout: 20_000 });
|
||||
// Phase 2: "Composio" tab renamed to "Apps"
|
||||
await page.getByRole('tab', { name: 'Apps' }).click();
|
||||
// Phase 2: heading is now "Apps" (skills.integrations), "Composio Integrations" removed
|
||||
await expect(page.getByRole('heading', { name: 'Apps', exact: true })).toBeVisible({
|
||||
timeout: 20_000,
|
||||
});
|
||||
}
|
||||
|
||||
async function openGmailManageModal(page: Page) {
|
||||
@@ -190,8 +186,9 @@ test.describe('Composio triggers flow', () => {
|
||||
await page.reload();
|
||||
await waitForAppReady(page);
|
||||
await dismissWalkthroughIfPresent(page);
|
||||
await page.getByRole('tab', { name: 'Composio' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Composio Integrations' })).toBeVisible({
|
||||
// Phase 2: "Composio" tab renamed to "Apps"; heading is now "Apps"
|
||||
await page.getByRole('tab', { name: 'Apps' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Apps', exact: true })).toBeVisible({
|
||||
timeout: 20_000,
|
||||
});
|
||||
|
||||
|
||||
@@ -66,28 +66,30 @@ async function bootSkillsPage(page: Page, userId: string) {
|
||||
localStorage.removeItem('openhuman:walkthrough_pending');
|
||||
} catch {}
|
||||
});
|
||||
// Phase 2: /skills → /connections
|
||||
await page.evaluate(() => {
|
||||
window.location.hash = '/skills';
|
||||
window.location.hash = '/connections';
|
||||
});
|
||||
await expect
|
||||
.poll(async () => page.evaluate(() => window.location.hash), { timeout: 10_000 })
|
||||
.toContain('/skills');
|
||||
.toContain('/connections');
|
||||
await waitForAppReady(page);
|
||||
await dismissWalkthroughIfPresent(page);
|
||||
await page.getByRole('tab', { name: 'Composio' }).click();
|
||||
const heading = page.getByRole('heading', { name: 'Composio Integrations' });
|
||||
// Phase 2: "Composio" tab renamed to "Apps"
|
||||
await page.getByRole('tab', { name: 'Apps' }).click();
|
||||
const heading = page.getByRole('heading', { name: 'Apps', exact: true });
|
||||
if (!(await heading.isVisible().catch(() => false))) {
|
||||
const connectionsButton = page.getByRole('button', { name: 'Connections' });
|
||||
if (await connectionsButton.isVisible().catch(() => false)) {
|
||||
await connectionsButton.click({ force: true });
|
||||
await expect
|
||||
.poll(async () => page.evaluate(() => window.location.hash), { timeout: 10_000 })
|
||||
.toContain('/skills');
|
||||
.toContain('/connections');
|
||||
await waitForAppReady(page);
|
||||
await dismissWalkthroughIfPresent(page);
|
||||
}
|
||||
}
|
||||
await expect(page.getByRole('heading', { name: 'Composio Integrations' })).toBeVisible({
|
||||
await expect(page.getByRole('heading', { name: 'Apps', exact: true })).toBeVisible({
|
||||
timeout: 20_000,
|
||||
});
|
||||
}
|
||||
@@ -97,17 +99,18 @@ async function reloadSkills(page: Page) {
|
||||
}
|
||||
|
||||
async function ensureComposioSurface(page: Page) {
|
||||
const heading = page.getByRole('heading', { name: 'Composio Integrations' });
|
||||
// Phase 2: /skills → /connections, "Composio" tab renamed to "Apps"
|
||||
const heading = page.getByRole('heading', { name: 'Apps', exact: true });
|
||||
for (let attempt = 0; attempt < 3; attempt++) {
|
||||
await page.evaluate(() => {
|
||||
window.location.hash = '/skills';
|
||||
window.location.hash = '/connections';
|
||||
});
|
||||
await expect
|
||||
.poll(async () => page.evaluate(() => window.location.hash), { timeout: 10_000 })
|
||||
.toContain('/skills');
|
||||
.toContain('/connections');
|
||||
await waitForAppReady(page);
|
||||
await dismissWalkthroughIfPresent(page);
|
||||
await page.getByRole('tab', { name: 'Composio' }).click();
|
||||
await page.getByRole('tab', { name: 'Apps' }).click();
|
||||
if (await heading.isVisible().catch(() => false)) {
|
||||
return;
|
||||
}
|
||||
@@ -116,7 +119,7 @@ async function ensureComposioSurface(page: Page) {
|
||||
await connectionsButton.click({ force: true });
|
||||
await waitForAppReady(page);
|
||||
await dismissWalkthroughIfPresent(page);
|
||||
await page.getByRole('tab', { name: 'Composio' }).click();
|
||||
await page.getByRole('tab', { name: 'Apps' }).click();
|
||||
if (await heading.isVisible().catch(() => false)) {
|
||||
return;
|
||||
}
|
||||
@@ -126,6 +129,7 @@ async function ensureComposioSurface(page: Page) {
|
||||
}
|
||||
|
||||
async function assertSessionNotNuked(page: Page) {
|
||||
// Phase 2: /skills → /connections
|
||||
await expect
|
||||
.poll(async () =>
|
||||
page.evaluate(() => {
|
||||
@@ -145,7 +149,7 @@ async function assertSessionNotNuked(page: Page) {
|
||||
};
|
||||
})
|
||||
)
|
||||
.toEqual({ hash: '#/skills', hasToken: true, hasUser: true });
|
||||
.toEqual({ hash: '#/connections', hasToken: true, hasUser: true });
|
||||
}
|
||||
|
||||
async function openModal(page: Page) {
|
||||
|
||||
@@ -69,16 +69,18 @@ async function bootSkills(page: Page, userId: string): Promise<void> {
|
||||
await seedToolkits('ACTIVE');
|
||||
await bootRuntimeReadyGuestPage(page);
|
||||
await signInViaCallbackToken(page, userId);
|
||||
await page.goto('/#/skills');
|
||||
// Phase 2: /skills → /connections, "Composio" tab renamed to "Apps"
|
||||
await page.goto('/#/connections');
|
||||
await waitForAppReady(page);
|
||||
await dismissWalkthroughIfPresent(page);
|
||||
await page.getByRole('tab', { name: 'Composio' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Composio Integrations' })).toBeVisible({
|
||||
await page.getByRole('tab', { name: 'Apps' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Apps', exact: true })).toBeVisible({
|
||||
timeout: 20_000,
|
||||
});
|
||||
}
|
||||
|
||||
async function assertSessionAlive(page: Page): Promise<void> {
|
||||
// Phase 2: /skills → /connections
|
||||
await expect
|
||||
.poll(async () =>
|
||||
page.evaluate(() => {
|
||||
@@ -99,7 +101,7 @@ async function assertSessionAlive(page: Page): Promise<void> {
|
||||
};
|
||||
})
|
||||
)
|
||||
.toEqual({ hash: '#/skills', hasUser: true, hasToken: true });
|
||||
.toEqual({ hash: '#/connections', hasUser: true, hasToken: true });
|
||||
}
|
||||
|
||||
test.describe('Connector session guard matrix', () => {
|
||||
@@ -143,7 +145,8 @@ test.describe('Connector session guard matrix', () => {
|
||||
});
|
||||
await page.reload();
|
||||
await waitForAppReady(page);
|
||||
await page.getByRole('tab', { name: 'Composio' }).click();
|
||||
// Phase 2: "Composio" tab renamed to "Apps"
|
||||
await page.getByRole('tab', { name: 'Apps' }).click();
|
||||
await page.getByTestId('skill-install-composio-jira').click();
|
||||
const dialog = page.getByRole('dialog', { name: /Jira/i });
|
||||
await expect(dialog).toBeVisible();
|
||||
@@ -156,14 +159,16 @@ test.describe('Connector session guard matrix', () => {
|
||||
await seedToolkits('FAILED');
|
||||
await page.reload();
|
||||
await waitForAppReady(page);
|
||||
await page.getByRole('tab', { name: 'Composio' }).click();
|
||||
// Phase 2: "Composio" tab renamed to "Apps"
|
||||
await page.getByRole('tab', { name: 'Apps' }).click();
|
||||
await expect(page.getByTestId('skill-install-composio-discord')).toContainText('Discord');
|
||||
await assertSessionAlive(page);
|
||||
|
||||
await seedToolkits('EXPIRED');
|
||||
await page.reload();
|
||||
await waitForAppReady(page);
|
||||
await page.getByRole('tab', { name: 'Composio' }).click();
|
||||
// Phase 2: "Composio" tab renamed to "Apps"
|
||||
await page.getByRole('tab', { name: 'Apps' }).click();
|
||||
await expect(page.getByTestId('skill-install-composio-github')).toContainText(
|
||||
/Reconnect|GitHub/
|
||||
);
|
||||
|
||||
@@ -65,20 +65,15 @@ async function bootSkillsPage(page: Page, userId: string) {
|
||||
localStorage.setItem('openhuman:walkthrough_completed', 'true');
|
||||
localStorage.removeItem('openhuman:walkthrough_pending');
|
||||
} catch {}
|
||||
window.location.hash = '/skills';
|
||||
// Phase 2: /skills → /connections
|
||||
window.location.hash = '/connections';
|
||||
});
|
||||
await waitForAppReady(page);
|
||||
await dismissWalkthroughIfPresent(page);
|
||||
await page.getByRole('tab', { name: 'Composio' }).click();
|
||||
const heading = page.getByRole('heading', { name: 'Composio Integrations' });
|
||||
if (!(await heading.isVisible().catch(() => false))) {
|
||||
const connectionsButton = page.getByRole('button', { name: 'Connections' });
|
||||
if (await connectionsButton.isVisible().catch(() => false)) {
|
||||
await connectionsButton.click({ force: true });
|
||||
await waitForAppReady(page);
|
||||
await dismissWalkthroughIfPresent(page);
|
||||
}
|
||||
}
|
||||
// Phase 2: "Composio" tab renamed to "Apps"; heading is now "Apps" (skills.integrations)
|
||||
await page.getByRole('tab', { name: 'Apps' }).click();
|
||||
// Wait for the Apps tab grid to be visible — the h2 heading text is now "Apps"
|
||||
const heading = page.getByRole('heading', { name: 'Apps', exact: true });
|
||||
await expect(heading).toBeVisible({ timeout: 20_000 });
|
||||
}
|
||||
|
||||
@@ -103,7 +98,8 @@ test.describe('Gmail Integration Flows', () => {
|
||||
await page.reload();
|
||||
await waitForAppReady(page);
|
||||
await dismissWalkthroughIfPresent(page);
|
||||
await page.getByRole('tab', { name: 'Composio' }).click();
|
||||
// Phase 2: "Composio" tab renamed to "Apps"
|
||||
await page.getByRole('tab', { name: 'Apps' }).click();
|
||||
|
||||
await page.getByTestId('skill-install-composio-gmail').click();
|
||||
await expect(page.getByRole('dialog', { name: /Connect Gmail/i })).toBeVisible();
|
||||
@@ -129,13 +125,15 @@ test.describe('Gmail Integration Flows', () => {
|
||||
await seedConnector('FAILED');
|
||||
await page.reload();
|
||||
await waitForAppReady(page);
|
||||
await page.getByRole('tab', { name: 'Composio' }).click();
|
||||
// Phase 2: "Composio" tab renamed to "Apps"
|
||||
await page.getByRole('tab', { name: 'Apps' }).click();
|
||||
await expect(page.getByTestId('skill-install-composio-gmail')).toContainText(CONNECTOR_NAME);
|
||||
|
||||
await seedConnector('EXPIRED');
|
||||
await page.reload();
|
||||
await waitForAppReady(page);
|
||||
await page.getByRole('tab', { name: 'Composio' }).click();
|
||||
// Phase 2: "Composio" tab renamed to "Apps"
|
||||
await page.getByRole('tab', { name: 'Apps' }).click();
|
||||
await expect(page.getByTestId(`skill-install-composio-${TOOLKIT_SLUG}`)).toContainText(
|
||||
/Auth expired|Reconnect/i
|
||||
);
|
||||
@@ -143,8 +141,9 @@ test.describe('Gmail Integration Flows', () => {
|
||||
|
||||
test('execute and disconnect routes do not blank the skills page', async ({ page }) => {
|
||||
await callCoreRpc('openhuman.composio_execute', { tool: ACTION, arguments: {} });
|
||||
await page.getByRole('tab', { name: 'Composio' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Composio Integrations' })).toBeVisible();
|
||||
// Phase 2: "Composio" tab renamed to "Apps"; heading is now "Apps" (skills.integrations)
|
||||
await page.getByRole('tab', { name: 'Apps' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Apps', exact: true })).toBeVisible();
|
||||
|
||||
await callCoreRpc('openhuman.composio_delete_connection', { connection_id: CONNECTION_ID });
|
||||
const requests = await getRequestLog();
|
||||
|
||||
@@ -8,30 +8,37 @@ import {
|
||||
|
||||
test.describe('Google Meet Connections tab', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await bootAuthenticatedPage(page, 'pw-gmeet-connections-tab-user', '/skills?tab=meetings');
|
||||
// Phase 2: /skills → /connections; Phase 2b: MeetingBotsCard moved from
|
||||
// Tools tab to the new Talents tab (?tab=talents). Back-compat: ?tab=meetings
|
||||
// alias still works and resolves to talents.
|
||||
await bootAuthenticatedPage(page, 'pw-gmeet-connections-tab-user', '/connections?tab=talents');
|
||||
await waitForAppReady(page);
|
||||
await dismissWalkthroughIfPresent(page);
|
||||
});
|
||||
|
||||
test('opens the dedicated tab and shows a one-field meeting link modal', async ({ page }) => {
|
||||
test('opens the Talents tab and shows the meeting link modal', async ({ page }) => {
|
||||
await expect
|
||||
.poll(async () => page.evaluate(() => window.location.hash), { timeout: 10_000 })
|
||||
.toContain('/skills?tab=meetings');
|
||||
.toContain('/connections');
|
||||
|
||||
await expect(page.getByRole('tab', { name: 'Google Meet', exact: true })).toHaveAttribute(
|
||||
// Phase 2b: MeetingBotsCard moved to the Talents tab; there is no longer a
|
||||
// "Google Meet" sub-tab. The PillTabBar "Talents" pill carries the
|
||||
// aria-selected=true attribute when the Talents surface is active.
|
||||
await expect(page.getByRole('tab', { name: 'Talents', exact: true })).toHaveAttribute(
|
||||
'aria-selected',
|
||||
'true'
|
||||
);
|
||||
|
||||
await page.getByTestId('meeting-bots-banner').click();
|
||||
|
||||
// The modal has a Meeting link (url) field plus the "Your Name in This
|
||||
// Meeting" (respondTo) text field added in #3555. It stays Google-Meet only
|
||||
// — no other platforms.
|
||||
const dialog = page.getByRole('dialog', { name: 'Send OpenHuman to a meeting' });
|
||||
await expect(dialog).toBeVisible();
|
||||
await expect(dialog.getByLabel('Meeting link')).toBeVisible();
|
||||
await expect(dialog.locator('input[type="url"]')).toHaveCount(1);
|
||||
await expect(dialog.locator('input[type="text"]')).toHaveCount(1);
|
||||
await expect(dialog.getByText('Wake Phrase')).toHaveCount(0);
|
||||
await expect(dialog.getByText('Display name')).toHaveCount(0);
|
||||
await expect(dialog.getByText('Zoom')).toHaveCount(0);
|
||||
await expect(dialog.getByText('Microsoft Teams')).toHaveCount(0);
|
||||
});
|
||||
|
||||
@@ -34,7 +34,9 @@ test.describe('Guided tour gates', () => {
|
||||
await waitForAppReady(page);
|
||||
});
|
||||
|
||||
test('tour starts from home and can navigate forward to the skills step', async ({ page }) => {
|
||||
test('tour starts from home and can navigate forward to the connections step', async ({
|
||||
page,
|
||||
}) => {
|
||||
await armWalkthrough(page);
|
||||
|
||||
const panel = await tooltip(page);
|
||||
@@ -48,8 +50,11 @@ test.describe('Guided tour gates', () => {
|
||||
await expect.poll(async () => page.evaluate(() => window.location.hash)).toContain('/chat');
|
||||
await expect(page.locator('[data-walkthrough="chat-agent-panel"]')).toBeVisible();
|
||||
|
||||
// Phase 2: step 4 navigates to /connections (was /skills)
|
||||
await clickTourNext(page);
|
||||
await expect.poll(async () => page.evaluate(() => window.location.hash)).toContain('/skills');
|
||||
await expect
|
||||
.poll(async () => page.evaluate(() => window.location.hash))
|
||||
.toContain('/connections');
|
||||
await expect(page.locator('[data-walkthrough="skills-grid"]')).toBeVisible();
|
||||
});
|
||||
|
||||
|
||||
@@ -8,13 +8,14 @@ import {
|
||||
|
||||
test.describe('Insights Dashboard', () => {
|
||||
test('renders the memory workspace and actions toolbar', async ({ page }) => {
|
||||
await bootAuthenticatedPage(page, 'pw-insights-user', '/intelligence');
|
||||
// Phase 3: Memory moved from /activity (no Memory tab there anymore) to
|
||||
// /settings/intelligence which renders the full Intelligence page including
|
||||
// the Memory tab. The Memory tab is NOT dev-only in Intelligence.tsx (only
|
||||
// "council" is gated), so no developer mode seeding is needed.
|
||||
await bootAuthenticatedPage(page, 'pw-insights-user', '/settings/intelligence');
|
||||
await waitForAppReady(page);
|
||||
// /intelligence boots on the Tasks tab (default in Intelligence.tsx since
|
||||
// #2998). The walkthrough portal can sit on top of the pill bar and the
|
||||
// memory-workspace testid only mounts when tab=memory, so we dismiss the
|
||||
// overlay and click the Memory pill before asserting the panel chrome.
|
||||
await dismissWalkthroughIfPresent(page);
|
||||
// /settings/intelligence defaults to the Tasks tab — click Memory pill.
|
||||
await page.getByRole('tab', { name: 'Memory', exact: true }).click();
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Memory', exact: true })).toBeVisible({
|
||||
|
||||
@@ -12,8 +12,30 @@ import {
|
||||
|
||||
type MemorySource = { id: string; kind: string; label: string; enabled: boolean };
|
||||
|
||||
/**
|
||||
* Seeds developer mode via `persist:theme` localStorage so the Memory tab
|
||||
* (dev-gated in the Activity page since Phase 3) is visible after boot.
|
||||
* Must be called via `page.addInitScript` before navigation.
|
||||
*/
|
||||
async function seedDeveloperMode(page: Page): Promise<void> {
|
||||
await page.addInitScript(() => {
|
||||
try {
|
||||
const raw = localStorage.getItem('persist:theme');
|
||||
const parsed: Record<string, string> = raw ? (JSON.parse(raw) as Record<string, string>) : {};
|
||||
parsed.developerMode = JSON.stringify(true);
|
||||
localStorage.setItem('persist:theme', JSON.stringify(parsed));
|
||||
} catch {}
|
||||
});
|
||||
}
|
||||
|
||||
async function openMemory(page: Page): Promise<void> {
|
||||
await bootAuthenticatedPage(page, 'pw-intelligence-memory-ui', '/intelligence');
|
||||
// Phase 3: Memory moved out of /activity entirely — it now lives at
|
||||
// /settings/intelligence (the full Intelligence dev surface). The Memory tab
|
||||
// there is not dev-gated (only "council" is), so no developer mode seeding
|
||||
// is required for the tab itself; seedDeveloperMode is still called so any
|
||||
// code that checks developerMode elsewhere behaves consistently.
|
||||
await seedDeveloperMode(page);
|
||||
await bootAuthenticatedPage(page, 'pw-intelligence-memory-ui', '/settings/intelligence');
|
||||
await waitForAppReady(page);
|
||||
await dismissWalkthroughIfPresent(page);
|
||||
const memoryTab = page.getByRole('tab', { name: /^Memory$/ });
|
||||
|
||||
@@ -243,7 +243,8 @@ async function seedLocalStorage(page: Page) {
|
||||
}
|
||||
|
||||
async function navigateToMcpTab(page: Page) {
|
||||
await page.goto('/#/skills?tab=mcp');
|
||||
// Phase 2: /skills → /connections, ?tab=mcp → ?tab=tools (back-compat alias also works)
|
||||
await page.goto('/#/connections?tab=tools');
|
||||
await page.waitForSelector('#root', { state: 'visible', timeout: 20_000 });
|
||||
await page.locator('input[type="search"]').waitFor({ state: 'visible', timeout: 10_000 });
|
||||
await page.locator('table').waitFor({ state: 'visible', timeout: 10_000 });
|
||||
@@ -317,17 +318,21 @@ test.describe('MCP Tab — Table View & Filtering', () => {
|
||||
test('search filters both installed and registry servers', async ({ page }) => {
|
||||
const search = page.locator('input[type="search"]');
|
||||
await search.fill('notion');
|
||||
// Wait for the table to reflect the filtered results rather than using a fixed delay
|
||||
// Wait for a Notion row to appear AND for non-matching rows (e.g. "Memory
|
||||
// Server") to disappear — the table re-renders asynchronously and a naive
|
||||
// count() immediately after the first visible check can race against the
|
||||
// previous state still being in the DOM.
|
||||
await expect(
|
||||
page.locator('table tbody tr', { has: page.locator('td:has-text("Notion")') })
|
||||
).toBeVisible({ timeout: 5_000 });
|
||||
const rows = page.locator('table tbody tr');
|
||||
const count = await rows.count();
|
||||
expect(count).toBeGreaterThanOrEqual(1);
|
||||
for (let i = 0; i < count; i++) {
|
||||
const name = await rows.nth(i).locator('td:first-child').innerText();
|
||||
expect(name.toLowerCase()).toContain('notion');
|
||||
}
|
||||
await expect(
|
||||
page.locator('table tbody tr', { has: page.locator('td:has-text("Memory Server")') })
|
||||
).toHaveCount(0, { timeout: 5_000 });
|
||||
// The positive (Notion row present) + negative (Memory Server gone) checks
|
||||
// above already prove the filter works. Avoid iterating `td:first-child`
|
||||
// per row — the #3480 registry redesign changed the column layout, and the
|
||||
// table re-renders async (the old per-row loop raced + assumed name-first).
|
||||
await expect(page.locator('table tbody tr')).not.toHaveCount(0);
|
||||
});
|
||||
|
||||
test('no Smithery branding visible anywhere', async ({ page }) => {
|
||||
|
||||
@@ -10,7 +10,8 @@ interface PanelCheck {
|
||||
const panels: PanelCheck[] = [
|
||||
{ hash: '/settings', markers: ['Settings', 'Appearance', 'Notifications'] },
|
||||
{ hash: '/settings/memory-data', markers: ['Memory', 'Data', 'Storage'] },
|
||||
{ hash: '/intelligence', markers: ['Memory', 'Intelligence'] },
|
||||
// Phase 3: /intelligence → /activity; Memory tab is dev-gated, test only always-visible content
|
||||
{ hash: '/activity', markers: ['Tasks', 'Automations', 'Activity'] },
|
||||
{ hash: '/settings/developer-options', markers: ['Developer', 'Debug', 'Advanced'] },
|
||||
{
|
||||
hash: '/settings/billing',
|
||||
|
||||
@@ -7,14 +7,22 @@ interface RouteCheck {
|
||||
markers: string[];
|
||||
}
|
||||
|
||||
// Phase 2/3/6 IA revamp:
|
||||
// /skills → /connections (redirects; test new canonical route)
|
||||
// /intelligence → /activity (redirects; test new canonical route)
|
||||
// /human → /chat (redirects; already covered by /chat entry)
|
||||
const routes: RouteCheck[] = [
|
||||
{ hash: '/chat', markers: ['Threads', 'Chat', 'Message', 'New'] },
|
||||
{ hash: '/skills', markers: ['Skills', 'Skill', 'Install', 'Browse'] },
|
||||
// Connections page (was /skills) — tabs: Apps, Messaging, Tools, Explorer
|
||||
{ hash: '/connections', markers: ['Apps', 'Messaging', 'Tools', 'Explorer'] },
|
||||
{ hash: '/home', markers: ['Ask your assistant anything', 'Your device is connected'] },
|
||||
{ hash: '/channels', markers: ['Channels', 'Connect', 'Telegram', 'Discord'] },
|
||||
// /channels now redirects to /connections?tab=messaging
|
||||
{ hash: '/channels', markers: ['Messaging', 'Connections', 'Telegram', 'Discord'] },
|
||||
{ hash: '/notifications', markers: ['Notifications', 'Alerts', 'No alerts yet'] },
|
||||
{ hash: '/rewards', markers: ['Rewards', 'Referral', 'Credits', 'Invite'] },
|
||||
{ hash: '/settings', markers: ['Settings', 'Account', 'Billing', 'Advanced'] },
|
||||
// Activity page (was /intelligence) — tabs: Tasks, Automations, Background activity
|
||||
{ hash: '/activity', markers: ['Tasks', 'Automations', 'Background'] },
|
||||
{ hash: '/home', markers: ['Ask your assistant anything', 'Your device is connected'] },
|
||||
];
|
||||
|
||||
|
||||
@@ -2,21 +2,44 @@ import { expect, test } from '@playwright/test';
|
||||
|
||||
import { bootAuthenticatedPage, waitForAppReady } from '../helpers/core-rpc';
|
||||
|
||||
const routes = ['/home', '/human', '/chat', '/skills', '/intelligence', '/rewards', '/settings'];
|
||||
interface RouteEntry {
|
||||
route: string;
|
||||
/** Expected hash after any redirect. Defaults to the route itself. */
|
||||
expectedHash?: string;
|
||||
}
|
||||
|
||||
// Phase 2/3/6 IA revamp routes.
|
||||
// Back-compat redirects are included so the router redirect itself is tested.
|
||||
// /human → /chat (Phase 6)
|
||||
// /skills → /connections (Phase 2)
|
||||
// /intelligence → /activity (Phase 3)
|
||||
const ROUTES: RouteEntry[] = [
|
||||
{ route: '/home' },
|
||||
{ route: '/human', expectedHash: '/chat' }, // back-compat redirect
|
||||
{ route: '/chat' },
|
||||
{ route: '/connections' },
|
||||
{ route: '/skills', expectedHash: '/connections' }, // back-compat redirect
|
||||
{ route: '/activity' },
|
||||
{ route: '/intelligence', expectedHash: '/activity' }, // back-compat redirect
|
||||
{ route: '/rewards' },
|
||||
{ route: '/settings' },
|
||||
];
|
||||
|
||||
test.describe('Navigation', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await bootAuthenticatedPage(page, 'pw-navigation-user');
|
||||
});
|
||||
|
||||
for (const route of routes) {
|
||||
for (const { route, expectedHash } of ROUTES) {
|
||||
const landing = expectedHash ?? route;
|
||||
test(`renders ${route}`, async ({ page }) => {
|
||||
await page.goto(`/#${route}`);
|
||||
await waitForAppReady(page);
|
||||
|
||||
// After redirects the hash should begin with the final landing path.
|
||||
await expect
|
||||
.poll(async () => page.evaluate(() => window.location.hash))
|
||||
.toMatch(new RegExp(`^#${route.replace('/', '\\/')}`));
|
||||
.toMatch(new RegExp(`^#${landing.replace('/', '\\/')}`));
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const text = await page.locator('#root').innerText();
|
||||
|
||||
@@ -59,10 +59,10 @@ test.describe('Settings - Advanced Config', () => {
|
||||
test('renders the developer options route and its advanced entries', async ({ page }) => {
|
||||
await gotoSettingsRoute(page, '/settings/developer-options');
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Advanced' })).toBeVisible();
|
||||
await expect(page.getByRole('heading', { name: 'Developer & Diagnostics' })).toBeVisible();
|
||||
await expect(page.getByTestId('settings-nav-ai')).toBeVisible();
|
||||
await expect(page.getByTestId('settings-nav-composio')).toBeVisible();
|
||||
await expect(page.getByTestId('settings-nav-about')).toBeVisible();
|
||||
await expect(page.getByTestId('settings-nav-build-info')).toBeVisible();
|
||||
});
|
||||
|
||||
test('persists notification routing settings through core RPC', async ({ page }) => {
|
||||
|
||||
@@ -27,13 +27,14 @@ test.describe('Settings - Channels & Permissions', () => {
|
||||
});
|
||||
|
||||
test('allows switching default messaging channel', async ({ page }) => {
|
||||
await page.goto('/#/skills');
|
||||
// Phase 2: default messaging channel UI moved to /connections (Messaging tab)
|
||||
await page.goto('/#/connections?tab=messaging');
|
||||
await waitForAppReady(page);
|
||||
await dismissWalkthroughIfPresent(page);
|
||||
|
||||
const channelsTab = page.getByRole('tab', { name: 'Channels', exact: true });
|
||||
if (await channelsTab.isVisible().catch(() => false)) {
|
||||
await channelsTab.click();
|
||||
const messagingTab = page.getByRole('tab', { name: 'Messaging', exact: true });
|
||||
if (await messagingTab.isVisible().catch(() => false)) {
|
||||
await messagingTab.click();
|
||||
}
|
||||
|
||||
await expect(page.getByText('Default Messaging Channel').last()).toBeVisible();
|
||||
|
||||
@@ -92,17 +92,19 @@ test.describe('Settings - Feature Preferences', () => {
|
||||
|
||||
await expect(page.getByText('Features', { exact: true })).toBeVisible();
|
||||
await expect(page.getByTestId('settings-nav-screen-intelligence')).toBeVisible();
|
||||
await expect(page.getByTestId('settings-nav-messaging')).toBeVisible();
|
||||
// Phase 2: default messaging channel moved to /connections (Messaging tab);
|
||||
// the settings/features nav no longer has a dedicated "messaging" entry.
|
||||
await expect(page.getByTestId('settings-nav-notifications')).toBeVisible();
|
||||
await expect(page.getByTestId('settings-nav-tools')).toBeVisible();
|
||||
});
|
||||
|
||||
test('persists the default messaging channel through redux state', async ({ page }) => {
|
||||
await openAuthenticatedRoute(page, 'pw-settings-default-channel', '/skills');
|
||||
// Phase 2: default messaging channel moved to /connections (Messaging tab)
|
||||
await openAuthenticatedRoute(page, 'pw-settings-default-channel', '/connections?tab=messaging');
|
||||
|
||||
const channelsTab = page.getByRole('tab', { name: 'Channels', exact: true });
|
||||
if (await channelsTab.isVisible().catch(() => false)) {
|
||||
await channelsTab.click();
|
||||
const messagingTab = page.getByRole('tab', { name: 'Messaging', exact: true });
|
||||
if (await messagingTab.isVisible().catch(() => false)) {
|
||||
await messagingTab.click();
|
||||
}
|
||||
|
||||
await expect(page.getByText('Default Messaging Channel').last()).toBeVisible();
|
||||
|
||||
@@ -24,15 +24,16 @@ test.describe('Skill discovery (UI + core RPC)', () => {
|
||||
});
|
||||
|
||||
test('skills UI surface shows installed tools', async ({ page }) => {
|
||||
await page.goto('/#/skills');
|
||||
// /skills redirects to /connections (Phase 2 rename)
|
||||
await page.goto('/#/connections');
|
||||
await waitForAppReady(page);
|
||||
|
||||
const hash = await page.evaluate(() => window.location.hash);
|
||||
expect(String(hash)).toContain('/skills');
|
||||
expect(String(hash)).toContain('/connections');
|
||||
|
||||
const text = await page.locator('#root').innerText();
|
||||
expect(
|
||||
['Composio Integrations', 'Channels', 'Gmail', 'Notion', 'GitHub'].some(marker =>
|
||||
['Composio Integrations', 'Apps', 'Messaging', 'Gmail', 'Notion', 'GitHub'].some(marker =>
|
||||
text.includes(marker)
|
||||
)
|
||||
).toBe(true);
|
||||
|
||||
@@ -5,20 +5,20 @@ import { bootAuthenticatedPage, callCoreRpc, waitForAppReady } from '../helpers/
|
||||
test.describe('Skill lifecycle smoke', () => {
|
||||
test.beforeEach(async ({ page }, testInfo) => {
|
||||
const testSlug = testInfo.title.toLowerCase().replace(/[^a-z0-9]+/g, '-');
|
||||
await bootAuthenticatedPage(page, 'pw-skill-lifecycle-' + testSlug, '/skills');
|
||||
// Phase 2: /skills redirected to /connections
|
||||
await bootAuthenticatedPage(page, 'pw-skill-lifecycle-' + testSlug, '/connections');
|
||||
});
|
||||
|
||||
test('skills page mounts and the skills_list RPC is reachable', async ({ page }) => {
|
||||
test('connections page mounts and the workflows_list RPC is reachable', async ({ page }) => {
|
||||
await waitForAppReady(page);
|
||||
await expect
|
||||
.poll(async () => page.evaluate(() => window.location.hash), { timeout: 10_000 })
|
||||
.toContain('/skills');
|
||||
.toContain('/connections');
|
||||
|
||||
const text = await page.locator('#root').innerText();
|
||||
// Connections page tab labels (IA revamp): Apps/Messaging/Tools/Explorer/Talents.
|
||||
expect(
|
||||
['Composio Integrations', 'Install', 'Available', 'Channels'].some(marker =>
|
||||
text.includes(marker)
|
||||
)
|
||||
['Apps', 'Messaging', 'Tools', 'Explorer', 'Talents'].some(marker => text.includes(marker))
|
||||
).toBe(true);
|
||||
|
||||
const rpcResult = await callCoreRpc<unknown>('openhuman.workflows_list', {});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user