)}
- {activeTab === 'meetings' && }
-
- {activeTab === 'composio' && (
+ {activeTab === 'apps' && (
@@ -941,15 +964,23 @@ export default function Skills() {
)}
- {activeTab === 'composio' && otherGroups.map(group => renderGroup(group))}
+ {activeTab === 'apps' && otherGroups.map(group => renderGroup(group))}
- {activeTab === 'skills' &&
}
+ {activeTab === 'explorer' &&
}
- {activeTab === 'mcp' && (
-
+ {activeTab === 'tools' && (
+
+ {/* MCP Servers */}
)}
+
+ {activeTab === 'talents' && (
+
+ {/* Meeting bots live under Talents (moved out of Tools) */}
+
+
+ )}
>
}
diff --git a/app/src/pages/WorkflowNew.test.tsx b/app/src/pages/WorkflowNew.test.tsx
index 96f4de9b0..23c39d81a 100644
--- a/app/src/pages/WorkflowNew.test.tsx
+++ b/app/src/pages/WorkflowNew.test.tsx
@@ -30,7 +30,7 @@ const renderPage = () =>
} />
- dashboard } />
+
dashboard } />
);
@@ -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',
diff --git a/app/src/pages/WorkflowNew.tsx b/app/src/pages/WorkflowNew.tsx
index 2d2f924a2..cf4851e45 100644
--- a/app/src/pages/WorkflowNew.tsx
+++ b/app/src/pages/WorkflowNew.tsx
@@ -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=,
- * 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=, 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() {
} />
+
} />
+
} />
+
} />
+
+ );
+}
+
+const renderAt = (path: string) =>
+ render(
+
+
+
+ );
+
+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();
+ });
+});
diff --git a/app/src/pages/__tests__/Activity.test.tsx b/app/src/pages/__tests__/Activity.test.tsx
new file mode 100644
index 000000000..b797ed9ee
--- /dev/null
+++ b/app/src/pages/__tests__/Activity.test.tsx
@@ -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: () =>
,
+}));
+vi.mock('../../components/intelligence/IntelligenceTasksTab', () => ({
+ default: () =>
,
+}));
+vi.mock('../../components/intelligence/WorkflowsTab', () => ({
+ default: () =>
,
+}));
+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: () =>
}));
+
+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(
+
+
+
+ );
+}
+
+// ---------------------------------------------------------------------------
+// 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();
+ });
+});
diff --git a/app/src/pages/__tests__/AppRoutes.phase6.test.tsx b/app/src/pages/__tests__/AppRoutes.phase6.test.tsx
new file mode 100644
index 000000000..fc2abe377
--- /dev/null
+++ b/app/src/pages/__tests__/AppRoutes.phase6.test.tsx
@@ -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 (
+
+ {/* The real /chat route (Assistant surface). */}
+ chat } />
+ {/* Phase 6 back-compat redirect. */}
+