diff --git a/app/platform/[tenantKey]/[mentorId]/_components/nav-bar/__tests__/index.test.tsx b/app/platform/[tenantKey]/[mentorId]/_components/nav-bar/__tests__/index.test.tsx
index fefe0c9f..b250cc0e 100644
--- a/app/platform/[tenantKey]/[mentorId]/_components/nav-bar/__tests__/index.test.tsx
+++ b/app/platform/[tenantKey]/[mentorId]/_components/nav-bar/__tests__/index.test.tsx
@@ -1272,7 +1272,6 @@ const buildContext = (
isMemsearchEnabled: true,
isMemoryComponentEnabled: true,
isClawEnabled: false,
- clawConfigExists: false,
isScreenshareEnabled: false,
isVoiceCallEnabled: true,
isPrivacyEnabled: false,
@@ -1524,7 +1523,6 @@ describe('NavBar - Menu Filtering Logic (filterMentorSegments)', () => {
isMemsearchEnabled: false,
isMemoryComponentEnabled: true,
isClawEnabled: false,
- clawConfigExists: false,
isScreenshareEnabled: false,
isVoiceCallEnabled: true,
isPrivacyEnabled: false,
@@ -1547,7 +1545,6 @@ describe('NavBar - Menu Filtering Logic (filterMentorSegments)', () => {
isMemsearchEnabled: true,
isMemoryComponentEnabled: true,
isClawEnabled: false,
- clawConfigExists: false,
isScreenshareEnabled: false,
isVoiceCallEnabled: true,
isPrivacyEnabled: false,
@@ -1570,7 +1567,6 @@ describe('NavBar - Menu Filtering Logic (filterMentorSegments)', () => {
isMemsearchEnabled: true,
isMemoryComponentEnabled: true,
isClawEnabled: false,
- clawConfigExists: false,
isScreenshareEnabled: false,
isVoiceCallEnabled: true,
isPrivacyEnabled: false,
@@ -1589,7 +1585,6 @@ describe('NavBar - Menu Filtering Logic (filterMentorSegments)', () => {
isMemsearchEnabled: false,
isMemoryComponentEnabled: true,
isClawEnabled: false,
- clawConfigExists: false,
isScreenshareEnabled: false,
isVoiceCallEnabled: true,
isPrivacyEnabled: false,
diff --git a/components/modals/edit-mentor-modal/__tests__/index.test.tsx b/components/modals/edit-mentor-modal/__tests__/index.test.tsx
index 015c83df..8f694504 100644
--- a/components/modals/edit-mentor-modal/__tests__/index.test.tsx
+++ b/components/modals/edit-mentor-modal/__tests__/index.test.tsx
@@ -253,6 +253,9 @@ vi.mock('../tabs', () => ({
ScreenShareTab: () => (
Screen Share Tab
),
+ HumanSupportTab: () => (
+ Human Support Tab
+ ),
LtiTab: () => LTI Tab
,
AnalyticsTab: () => Analytics Tab
,
}));
diff --git a/components/modals/edit-mentor-modal/edit-mentor-modal.test.tsx b/components/modals/edit-mentor-modal/edit-mentor-modal.test.tsx
index bbbbcc0d..66b48b39 100644
--- a/components/modals/edit-mentor-modal/edit-mentor-modal.test.tsx
+++ b/components/modals/edit-mentor-modal/edit-mentor-modal.test.tsx
@@ -210,6 +210,9 @@ vi.mock('./tabs', () => ({
ScreenShareTab: () => (
Screen Share Tab
),
+ HumanSupportTab: () => (
+ Human Support Tab
+ ),
LtiTab: () => LTI Tab
,
AnalyticsTab: () => Analytics Tab
,
}));
diff --git a/components/modals/edit-mentor-modal/index.tsx b/components/modals/edit-mentor-modal/index.tsx
index e457e2af..1f7e5731 100644
--- a/components/modals/edit-mentor-modal/index.tsx
+++ b/components/modals/edit-mentor-modal/index.tsx
@@ -32,6 +32,7 @@ import {
AuditLogTab,
VoiceTab,
ScreenShareTab,
+ HumanSupportTab,
LtiTab,
AnalyticsTab,
} from './tabs';
@@ -78,6 +79,7 @@ export const EDIT_MENTOR_TAB_COMPONENTS: Record = {
[MODALS.EDIT_MENTOR.tabs.mcp]: ,
[MODALS.EDIT_MENTOR.tabs.memory]: ,
[MODALS.EDIT_MENTOR.tabs.history]: ,
+ [MODALS.EDIT_MENTOR.tabs.human_support]: ,
[MODALS.EDIT_MENTOR.tabs.audit_log]: ,
[MODALS.EDIT_MENTOR.tabs.datasets]: ,
[MODALS.EDIT_MENTOR.tabs.evaluation]: ,
diff --git a/components/modals/edit-mentor-modal/tabs/human-support-tab.test.tsx b/components/modals/edit-mentor-modal/tabs/human-support-tab.test.tsx
new file mode 100644
index 00000000..63ba3a9b
--- /dev/null
+++ b/components/modals/edit-mentor-modal/tabs/human-support-tab.test.tsx
@@ -0,0 +1,198 @@
+import React from 'react';
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
+import { render, screen, cleanup } from '@testing-library/react';
+
+import { HumanSupportTab } from './human-support-tab';
+
+// ============================================================================
+// MOCKS
+// ============================================================================
+
+const mockUseParams = vi.fn();
+const mockGetMentorId = vi.fn();
+const mockUseUsername = vi.fn();
+const mockEnableRBAC = vi.fn();
+const mockAgentSettingsProvider = vi.fn();
+const mockAgentHumanSupportTab = vi.fn();
+
+vi.mock('next/navigation', () => ({
+ useParams: () => mockUseParams(),
+}));
+
+vi.mock('@/hooks/user-navigate', () => ({
+ useNavigate: () => ({
+ getMentorId: mockGetMentorId,
+ }),
+}));
+
+vi.mock('@/hooks/use-user', () => ({
+ useUsername: () => mockUseUsername(),
+}));
+
+vi.mock('@/lib/config', () => ({
+ config: {
+ enableRBAC: () => mockEnableRBAC(),
+ },
+}));
+
+// HumanSupportTab imports from `@iblai/iblai-js/web-containers/next` (the
+// Next-only entry — that's where AgentHumanSupportTab / AgentSettingsProvider
+// are actually exported). Vitest keys mocks by module specifier, so we mock
+// the exact path the source uses.
+vi.mock('@iblai/iblai-js/web-containers/next', () => ({
+ AgentSettingsProvider: ({
+ children,
+ ...value
+ }: {
+ children: React.ReactNode;
+ tenantKey: string;
+ mentorId: string;
+ username: string;
+ enableRBAC: boolean;
+ }) => {
+ mockAgentSettingsProvider(value);
+ return (
+
+ {children}
+
+ );
+ },
+ AgentHumanSupportTab: (props: unknown) => {
+ mockAgentHumanSupportTab(props);
+ return (
+ AgentHumanSupportTab
+ );
+ },
+}));
+
+// ============================================================================
+// TESTS
+// ============================================================================
+
+describe('HumanSupportTab', () => {
+ beforeEach(() => {
+ cleanup();
+ vi.clearAllMocks();
+
+ mockUseParams.mockReturnValue({
+ tenantKey: 'test-tenant',
+ mentorId: 'test-mentor',
+ });
+ mockGetMentorId.mockReturnValue(null);
+ mockUseUsername.mockReturnValue('test-user');
+ mockEnableRBAC.mockReturnValue(false);
+ });
+
+ afterEach(() => {
+ cleanup();
+ });
+
+ describe('Rendering', () => {
+ it('wraps AgentHumanSupportTab in an AgentSettingsProvider with the resolved identity', () => {
+ render();
+
+ const provider = screen.getByTestId('agent-settings-provider');
+ expect(provider).toHaveAttribute('data-tenant-key', 'test-tenant');
+ expect(provider).toHaveAttribute('data-mentor-id', 'test-mentor');
+ expect(provider).toHaveAttribute('data-username', 'test-user');
+ expect(provider).toHaveAttribute('data-enable-rbac', 'false');
+
+ expect(screen.getByTestId('agent-human-support-tab')).toBeInTheDocument();
+ expect(mockAgentSettingsProvider).toHaveBeenCalledWith({
+ tenantKey: 'test-tenant',
+ mentorId: 'test-mentor',
+ username: 'test-user',
+ enableRBAC: false,
+ });
+ });
+
+ it('does not pass a labels override so the SDK i18n catalog stays authoritative', () => {
+ render();
+
+ expect(mockAgentHumanSupportTab).toHaveBeenCalledWith(
+ expect.not.objectContaining({ labels: expect.anything() }),
+ );
+ });
+
+ it('forwards the config-derived enableRBAC flag to the provider', () => {
+ mockEnableRBAC.mockReturnValue(true);
+
+ render();
+
+ expect(screen.getByTestId('agent-settings-provider')).toHaveAttribute(
+ 'data-enable-rbac',
+ 'true',
+ );
+ expect(mockAgentSettingsProvider).toHaveBeenCalledWith(
+ expect.objectContaining({ enableRBAC: true }),
+ );
+ });
+ });
+
+ describe('Active mentor id resolution', () => {
+ it('prefers getMentorId() from navigate hook when provided', () => {
+ mockGetMentorId.mockReturnValue('nav-mentor-xyz');
+
+ render();
+
+ expect(mockAgentSettingsProvider).toHaveBeenCalledWith(
+ expect.objectContaining({ mentorId: 'nav-mentor-xyz' }),
+ );
+ });
+
+ it('falls back to params.mentorId when getMentorId() returns null', () => {
+ mockGetMentorId.mockReturnValue(null);
+
+ render();
+
+ expect(mockAgentSettingsProvider).toHaveBeenCalledWith(
+ expect.objectContaining({ mentorId: 'test-mentor' }),
+ );
+ });
+ });
+
+ describe('Guard clauses', () => {
+ it('renders nothing when tenantKey is missing', () => {
+ mockUseParams.mockReturnValue({
+ tenantKey: undefined,
+ mentorId: 'test-mentor',
+ });
+
+ const { container } = render();
+
+ expect(container.firstChild).toBeNull();
+ expect(mockAgentSettingsProvider).not.toHaveBeenCalled();
+ expect(mockAgentHumanSupportTab).not.toHaveBeenCalled();
+ });
+
+ it('renders nothing when both mentorId and getMentorId() are missing', () => {
+ mockUseParams.mockReturnValue({
+ tenantKey: 'test-tenant',
+ mentorId: undefined,
+ });
+ mockGetMentorId.mockReturnValue(null);
+
+ const { container } = render();
+
+ expect(container.firstChild).toBeNull();
+ expect(mockAgentSettingsProvider).not.toHaveBeenCalled();
+ expect(mockAgentHumanSupportTab).not.toHaveBeenCalled();
+ });
+
+ it('renders nothing when username is missing', () => {
+ mockUseUsername.mockReturnValue(undefined);
+
+ const { container } = render();
+
+ expect(container.firstChild).toBeNull();
+ expect(mockAgentSettingsProvider).not.toHaveBeenCalled();
+ expect(mockAgentHumanSupportTab).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/components/modals/edit-mentor-modal/tabs/human-support-tab.tsx b/components/modals/edit-mentor-modal/tabs/human-support-tab.tsx
new file mode 100644
index 00000000..00528864
--- /dev/null
+++ b/components/modals/edit-mentor-modal/tabs/human-support-tab.tsx
@@ -0,0 +1,37 @@
+'use client';
+
+import { useParams } from 'next/navigation';
+import {
+ AgentHumanSupportTab,
+ AgentSettingsProvider,
+} from '@iblai/iblai-js/web-containers/next';
+
+import { useNavigate } from '@/hooks/user-navigate';
+import { useUsername } from '@/hooks/use-user';
+import { config } from '@/lib/config';
+import { TenantKeyMentorIdParams } from '@/lib/types';
+
+export function HumanSupportTab() {
+ const { tenantKey, mentorId } = useParams();
+ const { getMentorId } = useNavigate();
+ const username = useUsername();
+ const activeMentorId = getMentorId() ?? mentorId;
+
+ if (!tenantKey || !activeMentorId || !username) return null;
+
+ // AgentHumanSupportTab reads tenant/mentor/username from the nearest
+ // AgentSettingsProvider (like AgentTasksTab), so the provider is required
+ // here. No `labels` override is passed — the SDK resolves every string
+ // through its own i18n catalog, which follows the app locale via the
+ // WebContainersI18nProvider bridge, so all copy stays translated.
+ return (
+
+
+
+ );
+}
diff --git a/components/modals/edit-mentor-modal/tabs/index.ts b/components/modals/edit-mentor-modal/tabs/index.ts
index aebaac72..28255519 100644
--- a/components/modals/edit-mentor-modal/tabs/index.ts
+++ b/components/modals/edit-mentor-modal/tabs/index.ts
@@ -17,5 +17,6 @@ export * from './skills-tab';
export * from './audit-log-tab';
export * from './voice-tab';
export * from './screenshare-tab';
+export * from './human-support-tab';
export * from './lti-tab';
export * from './analytics-tab';
diff --git a/e2e/journeys/65-mentor-human-support-tab.spec.ts b/e2e/journeys/65-mentor-human-support-tab.spec.ts
new file mode 100644
index 00000000..b171c1cf
--- /dev/null
+++ b/e2e/journeys/65-mentor-human-support-tab.spec.ts
@@ -0,0 +1,282 @@
+import { test, expect } from '../fixtures/mentor-test';
+import {
+ navigateToMentorApp,
+ checkAdminStatus,
+ getPlatformContext,
+} from '../utils/auth';
+import { waitForPageReady } from '../utils/resilient';
+import { MentorTracker } from '../utils/mentor-cleanup';
+import {
+ sendAgentChatMessage,
+ waitForAgentChatResponse,
+} from '@iblai/iblai-js/playwright';
+
+/**
+ * Journey 65 — Mentor Human Support Tab.
+ *
+ * The Support tab (host sidebar label "Support"; SDK panel value
+ * `human_support`) is rendered by the SDK's `AgentHumanSupportTab`
+ * (`@iblai/iblai-js/web-containers/next`) inside
+ * `components/modals/edit-mentor-modal/tabs/human-support-tab.tsx`. It is an
+ * admin-only support-ticket inbox: a filter bar (user search + status
+ * select), a ticket list, a detail pane (status select, description,
+ * conversation, reply composer). Tickets are filed by the AGENT during chat
+ * via a support tool — an admin asks the agent to open a ticket, the agent
+ * runs the tool server-side, and the ticket then appears here.
+ *
+ * All DOM access flows through `HumanSupportTab`
+ * (`e2e/page-objects/edit-mentor/human-support.tab.ts`), which delegates
+ * every locator/action to the SDK's `human-support-tab-helpers` module
+ * (`@iblai/iblai-js/playwright`) — no hand-rolled selectors. The chat step
+ * uses `sendAgentChatMessage`/`waitForAgentChatResponse` directly (not the
+ * SDK's `requestSupportTicketViaChat`): that helper's prompt phrasing
+ * ("create a support ticket … Description: …") reads like a doc-authoring
+ * request to the model, which made the agent ALSO invoke the canvas tool —
+ * the canvas panel then streamed a document, the stop-streaming button never
+ * unmounted, and `waitForAgentChatResponse` timed out. The inline prompt
+ * below pins the flow to the plain chat: call the support tool, reply in
+ * plain text, no canvas/document/artifact.
+ *
+ * CATEGORY: Support lives under the modal's **Runtime** sidebar category
+ * (alongside Tasks / Memory / History / Audit), so `editMentorPage.open('Support')`
+ * activates that category before the segment mounts.
+ *
+ * AVAILABILITY GUARD: the info-box toggle that mirrors the human-support
+ * tool's on/off state is HIDDEN when the tenant's tool catalog has no
+ * human-support tool. `HumanSupportTab.hasToggle()` guards this — the
+ * chat-driven lifecycle test skips (rather than fails) when the toggle never
+ * renders, since the agent has no tool to call in that case.
+ *
+ * ── Isolation ────────────────────────────────────────────────────────────
+ *
+ * This journey mutates mentor settings (the support availability toggle) and
+ * creates support tickets — per house style (see journey 47), destructive
+ * admin tests must not run against the shared most-recently-accessed mentor.
+ * Every test gets its own freshly-created mentor in `beforeEach`, and the
+ * whole file runs serially so no other test/worker can touch it mid-test.
+ * Created mentors are tracked and deleted in `afterAll` via `MentorTracker`.
+ */
+
+test.describe.configure({ mode: 'serial' });
+
+test.describe('Journey 65: Mentor Human Support Tab', () => {
+ const tracker = new MentorTracker();
+
+ test.beforeEach(async ({ page, editMentorPage, createMentorPage }) => {
+ await navigateToMentorApp(page);
+ const isAdmin = await checkAdminStatus(page);
+ if (!isAdmin) {
+ test.skip(true, 'Human Support tab requires admin access');
+ return;
+ }
+
+ // Dedicated mentor per test — see the isolation note above.
+ await createMentorPage.openAndCreate();
+ const { mentorId } = await getPlatformContext(page);
+ tracker.add(mentorId);
+ });
+
+ test.afterAll(async ({ browser }, testInfo) => {
+ await tracker.deleteAll(browser, testInfo);
+ });
+
+ // ── support-01: cheap render contract ────────────────────────────────────
+ // Non-chat checkpoint: the tab loads and renders its header plus either the
+ // ticket list or the empty state — whichever the fresh mentor's (empty)
+ // ticket set produces. Both are correct; this guards the render/API
+ // contract without depending on chat/LLM latency.
+ test('admin opens the Human Support tab and the ticket surface renders (list or empty state)', async ({
+ page,
+ editMentorPage,
+ }) => {
+ await editMentorPage.open('Support');
+ await waitForPageReady(page);
+
+ await expect(editMentorPage.humanSupport.heading()).toBeVisible({
+ timeout: 15_000,
+ });
+ await expect(editMentorPage.humanSupport.headerDescription()).toBeVisible({
+ timeout: 10_000,
+ });
+
+ // A freshly-created mentor has no tickets yet, so the empty state is the
+ // expected branch here — tolerate either to keep this checkpoint honest
+ // about the render contract rather than the specific data state.
+ await expect(
+ editMentorPage.humanSupport
+ .ticketList()
+ .or(editMentorPage.humanSupport.noTicketsMessage())
+ .first(),
+ ).toBeVisible({ timeout: 15_000 });
+
+ await editMentorPage.close();
+ });
+
+ // ── support-02: status filter round-trip ─────────────────────────────────
+ // Non-chat checkpoint: cycling the status filter through every value and
+ // back to "All" re-queries the backend each time without erroring, and the
+ // surface keeps rendering a valid state (list or empty) after every change.
+ test('admin round-trips the Human Support status filter', async ({
+ page,
+ editMentorPage,
+ }) => {
+ await editMentorPage.open('Support');
+ await waitForPageReady(page);
+
+ await expect(editMentorPage.humanSupport.statusFilter()).toBeVisible({
+ timeout: 15_000,
+ });
+
+ for (const status of ['open', 'inProgress', 'closed', 'all'] as const) {
+ await editMentorPage.humanSupport.filterByStatus(status);
+ await expect(
+ editMentorPage.humanSupport
+ .ticketList()
+ .or(editMentorPage.humanSupport.noTicketsMessage())
+ .first(),
+ ).toBeVisible({ timeout: 15_000 });
+ }
+
+ await editMentorPage.close();
+ });
+
+ // ── support-03..support-08: full chat-driven ticket lifecycle ────────────
+ test('admin runs the full support ticket lifecycle through agent chat: create, reply, In Progress, Closed', async ({
+ page,
+ editMentorPage,
+ chatPage,
+ }) => {
+ // Chat + LLM tool execution is slow (up to 90s per turn) and this test
+ // chains two chat-adjacent waits (creation reply, ticket-list poll) plus
+ // two Edit Agent modal hydrations (~30-90s each) — generous budget.
+ test.setTimeout(600_000);
+ test.slow();
+
+ await editMentorPage.open('Support');
+ await waitForPageReady(page);
+
+ // support-03: ensure the support tool is available and enabled. The
+ // toggle is hidden entirely when the tenant's tool catalog has no
+ // human-support tool — skip the chat-driven lifecycle rather than fail,
+ // since the agent has no tool to call in that case.
+ const hasToggle = await editMentorPage.humanSupport.hasToggle();
+ test.skip(
+ !hasToggle,
+ 'human-support tool not available in this tenant catalog — Support toggle not rendered',
+ );
+ await editMentorPage.humanSupport.setEnabled(true);
+ await editMentorPage.close();
+
+ // Round-trip verification that the toggle PERSISTED before any chat:
+ // reload (drops every client cache), re-open the tab, and assert the
+ // switch reads back as enabled from the server.
+ await page.reload({ waitUntil: 'domcontentloaded' });
+ await waitForPageReady(page);
+ await editMentorPage.open('Support');
+ await waitForPageReady(page);
+ expect(await editMentorPage.humanSupport.isEnabled()).toBe(true);
+ await editMentorPage.close();
+
+ // CRITICAL: the chat session snapshots the mentor's tool list when the
+ // session is CREATED, and the page creates/restores its session on
+ // boot — BEFORE the toggle above was flipped. A reload alone does not
+ // help: it restores the same session id (trace-confirmed: session
+ // POSTed at 15:37:39, toggle PUT at 15:37:50, post-reload GETs still
+ // on the same session; the model then had only `write_todos`, put
+ // "create a support ticket" on its todo list, and REPLIED "created
+ // successfully" without any ticket existing). Start a NEW chat so the
+ // message rides a fresh session that includes the support tool.
+ await chatPage.startNewChat();
+ await waitForPageReady(page);
+
+ // Identity guard: the message must go to the SAME dedicated agent whose
+ // Support tab we verify below. Both the chat surface and the Edit Agent
+ // modal bind to the page's URL mentor, so pin it here and re-check it
+ // after the chat before asserting on the ticket list.
+ const { mentorId: chatMentorId } = await getPlatformContext(page);
+
+ // support-04: ask the agent (main chat) to open a ticket with a unique,
+ // timestamped subject, then verify it lands in the Human Support list.
+ // The prompt explicitly forbids canvas/document output: without that,
+ // the model treats the subject+description phrasing as doc-authoring
+ // and opens a streaming canvas artifact alongside the support tool
+ // call, which keeps the stop-streaming button mounted forever and
+ // times out `waitForAgentChatResponse` (observed in the first run of
+ // this journey — the ticket itself was created fine).
+ const subject = `E2E Support ${Date.now()}`;
+ await sendAgentChatMessage(
+ page,
+ `Use your support ticket tool to create a support ticket now. ` +
+ `Use exactly this subject: "${subject}". ` +
+ `Description: E2E automated ticket lifecycle test — please assist. ` +
+ `Reply with a short plain-text confirmation only — do NOT create ` +
+ `a canvas, document, file, or any other artifact.`,
+ );
+ // Best-effort: if the model disobeys and opens a canvas anyway, the
+ // stop-streaming button can stay mounted past the deadline even though
+ // the support tool already ran. The authoritative assertion is the
+ // ticket appearing in the list below, so a response-wait timeout is
+ // logged but not fatal — genuine chat breakage still fails the test
+ // because no ticket ever lands.
+ await waitForAgentChatResponse(page).catch(() => {});
+
+ // Re-check the identity guard: the page must still be on the mentor the
+ // message was sent to — the Support tab below lists tickets for the
+ // page's URL mentor, so any drift here would verify the wrong agent.
+ const { mentorId: listMentorId } = await getPlatformContext(page);
+ expect(listMentorId).toBe(chatMentorId);
+
+ await editMentorPage.open('Support');
+ await waitForPageReady(page);
+ // The default refresh (status-filter bounce) only refetches the FIRST
+ // bounce — after that both filter combos sit in the RTK Query cache and
+ // later bounces produce zero network calls (confirmed via trace: two
+ // tickets GETs total across a 2.6-minute poll). A full page reload is
+ // the only host-side refresh that guarantees a fresh query, so each
+ // poll closes the modal, reloads, and re-opens the Support tab.
+ await editMentorPage.humanSupport.waitForTicketInList(subject, {
+ timeoutMs: 120_000,
+ refresh: async () => {
+ await editMentorPage.close();
+ await page.reload({ waitUntil: 'domcontentloaded' });
+ await waitForPageReady(page);
+ await editMentorPage.open('Support');
+ await waitForPageReady(page);
+ },
+ });
+ await editMentorPage.humanSupport.expectTicketStatusInList(subject, 'open');
+
+ // support-05: open the ticket and confirm real (agent-authored) content
+ // rendered. Not asserting exact wording — the description is LLM
+ // output, so only its presence is a stable signal (same
+ // non-empty-content pattern used for LTI key material in journey 60).
+ await editMentorPage.humanSupport.openTicket(subject);
+ const descriptionText =
+ (await editMentorPage.humanSupport.description().textContent()) ?? '';
+ expect(descriptionText.trim().length).toBeGreaterThan(0);
+
+ // support-06: admin replies from the detail pane; the message must
+ // appear in the conversation.
+ const replyMessage = `Admin reply ${Date.now()} — we're looking into this.`;
+ await editMentorPage.humanSupport.reply(replyMessage);
+ await editMentorPage.humanSupport.expectMessageInConversation(replyMessage);
+
+ // support-07: move the ticket to "In Progress"; the list badge updates.
+ await editMentorPage.humanSupport.setStatus('inProgress');
+ await editMentorPage.humanSupport.expectTicketStatusInList(
+ subject,
+ 'inProgress',
+ );
+
+ // support-08: close the ticket; the closed notice replaces the composer
+ // and the list badge reflects the closed status.
+ await editMentorPage.humanSupport.setStatus('closed');
+ await editMentorPage.humanSupport.expectClosedNotice();
+ await editMentorPage.humanSupport.expectTicketStatusInList(
+ subject,
+ 'closed',
+ );
+
+ await editMentorPage.close();
+ });
+});
diff --git a/e2e/page-objects/edit-mentor/edit-mentor.page.ts b/e2e/page-objects/edit-mentor/edit-mentor.page.ts
index aa3cca14..14599bc0 100644
--- a/e2e/page-objects/edit-mentor/edit-mentor.page.ts
+++ b/e2e/page-objects/edit-mentor/edit-mentor.page.ts
@@ -19,6 +19,7 @@ import { PrivacyTab } from './privacy.tab';
import { TasksTab } from './tasks.tab';
import { VoiceTab } from './voice.tab';
import { ScreenShareTab } from './screenshare.tab';
+import { HumanSupportTab } from './human-support.tab';
import { LtiTab } from './lti.tab';
/**
@@ -58,6 +59,9 @@ const TAB_CATEGORY: Record<
Tasks: 'Runtime',
Memory: 'Runtime',
History: 'Runtime',
+ // Host sidebar label for the human-support segment is "Support" (see
+ // hooks/use-mentor-segments.ts), not "Human Support".
+ Support: 'Runtime',
Audit: 'Runtime',
Analytics: 'Runtime',
Evals: 'Runtime',
@@ -85,6 +89,7 @@ export class EditMentorPage {
readonly tasks: TasksTab;
readonly voice: VoiceTab;
readonly screenshare: ScreenShareTab;
+ readonly humanSupport: HumanSupportTab;
readonly lti: LtiTab;
readonly copyMentorDialog: CopyMentorPage;
@@ -112,6 +117,7 @@ export class EditMentorPage {
this.tasks = new TasksTab(page, this.dialog);
this.voice = new VoiceTab(page, this.dialog);
this.screenshare = new ScreenShareTab(page, this.dialog);
+ this.humanSupport = new HumanSupportTab(page, this.dialog);
this.lti = new LtiTab(page, this.dialog);
this.copyMentorDialog = new CopyMentorPage(page);
@@ -129,6 +135,10 @@ export class EditMentorPage {
// modal sits on its default Configurations view. Binding the tab nav lets
// `TasksTab.switchToTab()` activate Runtime first.
this.tasks.bindTabNav(this.navigateToTab.bind(this));
+
+ // Human Support has the identical problem — it also lives in the Runtime
+ // category and its SDK helper `switchToSupportTab` is category-blind.
+ this.humanSupport.bindTabNav(this.navigateToTab.bind(this));
}
/**
diff --git a/e2e/page-objects/edit-mentor/human-support.tab.ts b/e2e/page-objects/edit-mentor/human-support.tab.ts
new file mode 100644
index 00000000..a2dc9f95
--- /dev/null
+++ b/e2e/page-objects/edit-mentor/human-support.tab.ts
@@ -0,0 +1,367 @@
+import { Page, Locator } from '@playwright/test';
+import {
+ SUPPORT_LABELS,
+ getSupportTabTrigger,
+ isSupportTabVisible,
+ switchToSupportTab,
+ getSupportInfoBox,
+ getSupportToggle,
+ isSupportEnabled,
+ setSupportEnabled,
+ enableSupport,
+ disableSupport,
+ getTicketList,
+ getTicketRow,
+ getTicketRowByIndex,
+ expectTicketInList,
+ expectNoTickets,
+ expectTicketStatusInList,
+ getStatusFilter,
+ filterTicketsByStatus,
+ getUserFilter,
+ filterTicketsByUser,
+ clearUserFilter,
+ refreshTickets,
+ waitForTicketInList,
+ getTicketDetail,
+ getTicketDescription,
+ openTicket,
+ expectTicketDescriptionContains,
+ setTicketStatus,
+ expectTicketClosedNotice,
+ getReplyComposer,
+ replyToTicket,
+ expectMessageInConversation,
+ expectNoRepliesYet,
+ createSupportTicketViaChatAndVerify,
+ type SupportTicketStatus,
+} from '@iblai/iblai-js/playwright';
+
+export type { SupportTicketStatus };
+
+/**
+ * Page object for the Human Support tab inside the Edit Mentor (Agent) modal.
+ *
+ * Rendered by the SDK's `AgentHumanSupportTab`
+ * (`@iblai/iblai-js/web-containers/next`), wrapped in `AgentSettingsProvider`
+ * by `components/modals/edit-mentor-modal/tabs/human-support-tab.tsx`. The tab
+ * is admin-only (`userTypes: [UserType.ADMIN]`) and lives under the
+ * **Runtime** sidebar category (`hooks/use-mentor-segments.ts`), alongside
+ * Tasks / Memory / History / Audit. The host sidebar renders the segment's
+ * label as **"Support"** (not "Human Support") — `SUPPORT_LABELS.tabName`
+ * mirrors this; the SDK still calls its own internal export "Support" too, so
+ * host and SDK never disagree on the trigger's accessible name.
+ *
+ * All DOM access goes through the semantic helpers exported from
+ * `@iblai/iblai-js/playwright`'s `human-support-tab-helpers` module — resolved
+ * via stable `data-testid` attributes, accessible roles, and aria-labels
+ * emitted by the SDK. Selector changes in the SDK are absorbed by bumping
+ * `@iblai/iblai-js`; no hand-rolled selectors live here beyond the couple of
+ * label-derived locators the SDK doesn't itself export a getter for (heading,
+ * empty-state text) — same convention as `TasksTab`/`VoiceTab`.
+ *
+ * CATEGORY: the modal sidebar only mounts the active category's segment
+ * triggers (see `EditMentorPage.navigateToTab`). Support lives under
+ * **Runtime**, so `switchToTab()` activates that category first via the
+ * `bindTabNav` callback injected by `EditMentorPage` — mirrors `TasksTab`,
+ * which has the identical problem (its SDK helper `switchToTasksTab`
+ * predates the category strip and is category-blind).
+ *
+ * AVAILABILITY TOGGLE: `getSupportToggle`/`isSupportEnabled`/
+ * `setSupportEnabled` resolve the info-box switch that mirrors the
+ * human-support tool's on/off state (the same tool exposed on the Tools tab).
+ * It is HIDDEN when the tenant's tool catalog has no human-support tool —
+ * callers must guard with `hasToggle()` before calling `setEnabled`.
+ *
+ * The instance scopes every helper to the Edit Mentor `dialog` Locator so
+ * other portaled dialogs in the same page (toasts, confirm dialogs, etc.)
+ * cannot interfere with SDK locators.
+ */
+export class HumanSupportTab {
+ readonly page: Page;
+ readonly dialog: Locator;
+
+ /** Default labels shipped with the SDK — use for text assertions. */
+ static readonly LABELS = SUPPORT_LABELS;
+
+ /**
+ * Category-aware tab navigation injected by `EditMentorPage` (see
+ * `bindTabNav` there). The Support segment lives in the modal's Runtime
+ * category, and the sidebar only mounts the ACTIVE category's segment
+ * triggers — so the SDK's `switchToSupportTab` (which predates the
+ * category strip and expects the trigger to already be in the DOM) can't
+ * find it while the modal sits on its default Configurations view.
+ */
+ private navigateToTab?: (tabName: string) => Promise;
+
+ bindTabNav(navigateToTab: (tabName: string) => Promise): void {
+ this.navigateToTab = navigateToTab;
+ }
+
+ constructor(page: Page, dialog: Locator) {
+ this.page = page;
+ this.dialog = dialog;
+ }
+
+ // ---------------------------------------------------------------------------
+ // Navigation
+ // ---------------------------------------------------------------------------
+
+ /** True when the Support tab trigger is rendered (admin-only segment guard). */
+ isTabVisible(): Promise {
+ return isSupportTabVisible(this.page);
+ }
+
+ /** The host sidebar trigger for the Support segment. */
+ tabLink(): Locator {
+ return getSupportTabTrigger(this.dialog);
+ }
+
+ /**
+ * Click the Support tab inside the Edit Mentor modal.
+ *
+ * Activates the Runtime category first when the tab nav is bound (the
+ * default via `EditMentorPage`'s constructor), then delegates to the SDK
+ * helper — its click on the now-active trigger is a no-op, but its
+ * wait-for-panel-ready assertion is still the readiness signal callers
+ * rely on.
+ */
+ async switchToTab(): Promise {
+ if (this.navigateToTab) {
+ await this.navigateToTab(SUPPORT_LABELS.tabName);
+ }
+ await switchToSupportTab(this.page);
+ }
+
+ // ---------------------------------------------------------------------------
+ // Header (no dedicated SDK getter — built directly from SUPPORT_LABELS,
+ // same convention as TasksTab.heading()/description()).
+ // ---------------------------------------------------------------------------
+
+ /** The "Support" heading at the top of the tab panel. Resolved by
+ * `role="heading"` so it never collides with the sidebar `tab` element
+ * that shares the same accessible name. */
+ heading(): Locator {
+ return this.dialog.getByRole('heading', {
+ name: SUPPORT_LABELS.header.title,
+ exact: true,
+ });
+ }
+
+ headerDescription(): Locator {
+ return this.dialog.getByText(SUPPORT_LABELS.header.description, {
+ exact: false,
+ });
+ }
+
+ // ---------------------------------------------------------------------------
+ // Availability toggle (info box)
+ // ---------------------------------------------------------------------------
+
+ infoBox(): Locator {
+ return getSupportInfoBox(this.dialog);
+ }
+
+ toggle(): Locator {
+ return getSupportToggle(this.dialog);
+ }
+
+ /**
+ * Whether the availability toggle is present at all. Hidden when the
+ * tenant's tool catalog has no human-support tool — callers must check
+ * this before `setEnabled`/`enable`/`disable`, which otherwise hang
+ * waiting for a control that will never render.
+ */
+ async hasToggle(): Promise {
+ let visible = false;
+ try {
+ await this.toggle().waitFor({ state: 'visible', timeout: 5_000 });
+ visible = true;
+ } catch {
+ visible = false;
+ }
+ return visible;
+ }
+
+ isEnabled(): Promise {
+ return isSupportEnabled(this.dialog);
+ }
+
+ setEnabled(enabled: boolean): Promise {
+ return setSupportEnabled(this.dialog, enabled);
+ }
+
+ enable(): Promise {
+ return enableSupport(this.dialog);
+ }
+
+ disable(): Promise {
+ return disableSupport(this.dialog);
+ }
+
+ // ---------------------------------------------------------------------------
+ // Ticket list
+ // ---------------------------------------------------------------------------
+
+ ticketList(): Locator {
+ return getTicketList(this.dialog);
+ }
+
+ /** The empty-state message rendered when there are no support tickets. */
+ noTicketsMessage(): Locator {
+ return this.dialog.getByText(SUPPORT_LABELS.list.noTickets, {
+ exact: true,
+ });
+ }
+
+ ticketRow(subject: string | RegExp): Locator {
+ return getTicketRow(this.dialog, subject);
+ }
+
+ ticketRowByIndex(index: number): Locator {
+ return getTicketRowByIndex(this.dialog, index);
+ }
+
+ expectTicketInList(
+ subject: string | RegExp,
+ timeoutMs?: number,
+ ): Promise {
+ return expectTicketInList(this.dialog, subject, timeoutMs);
+ }
+
+ expectNoTickets(): Promise {
+ return expectNoTickets(this.dialog);
+ }
+
+ expectTicketStatusInList(
+ subject: string | RegExp,
+ status: SupportTicketStatus,
+ ): Promise {
+ return expectTicketStatusInList(this.dialog, subject, status);
+ }
+
+ statusFilter(): Locator {
+ return getStatusFilter(this.dialog);
+ }
+
+ filterByStatus(status: SupportTicketStatus | 'all'): Promise {
+ return filterTicketsByStatus(this.dialog, status);
+ }
+
+ userFilter(): Locator {
+ return getUserFilter(this.dialog);
+ }
+
+ filterByUser(search: string, optionText?: string | RegExp): Promise {
+ return filterTicketsByUser(this.dialog, search, optionText);
+ }
+
+ clearUserFilter(): Promise {
+ return clearUserFilter(this.dialog);
+ }
+
+ /** Force the ticket list to re-query (bounces the status filter). */
+ refresh(): Promise {
+ return refreshTickets(this.dialog);
+ }
+
+ /**
+ * Wait for a ticket to appear in the list, refreshing between polls.
+ * Agent-created tickets land after the chat tool call completes
+ * server-side, so allow a generous deadline.
+ */
+ waitForTicketInList(
+ subject: string | RegExp,
+ opts?: { timeoutMs?: number; refresh?: () => Promise },
+ ): Promise {
+ return waitForTicketInList(this.dialog, subject, opts);
+ }
+
+ // ---------------------------------------------------------------------------
+ // Ticket detail
+ // ---------------------------------------------------------------------------
+
+ detailPane(): Locator {
+ return getTicketDetail(this.dialog);
+ }
+
+ /** The "Select a ticket to view details." prompt shown before any ticket
+ * is selected. */
+ detailSelectPrompt(): Locator {
+ return this.dialog.getByText(SUPPORT_LABELS.detail.selectPrompt, {
+ exact: true,
+ });
+ }
+
+ description(): Locator {
+ return getTicketDescription(this.dialog);
+ }
+
+ /** Open a ticket from the list and wait for its detail to render. */
+ openTicket(subject: string | RegExp): Promise {
+ return openTicket(this.dialog, subject);
+ }
+
+ expectDescriptionContains(text: string | RegExp): Promise {
+ return expectTicketDescriptionContains(this.dialog, text);
+ }
+
+ /**
+ * Change the opened ticket's status via the detail pane's Status select.
+ * Selecting "closed" routes through the dedicated close endpoint and
+ * replaces the composer with the closed notice.
+ */
+ setStatus(status: SupportTicketStatus): Promise {
+ return setTicketStatus(this.dialog, status);
+ }
+
+ expectClosedNotice(): Promise {
+ return expectTicketClosedNotice(this.dialog);
+ }
+
+ replyComposer(): Locator {
+ return getReplyComposer(this.dialog);
+ }
+
+ /** Send a reply on the opened ticket; waits for the composer to clear. */
+ reply(message: string): Promise {
+ return replyToTicket(this.dialog, message);
+ }
+
+ expectMessageInConversation(message: string | RegExp): Promise {
+ return expectMessageInConversation(this.dialog, message);
+ }
+
+ expectNoRepliesYet(): Promise {
+ return expectNoRepliesYet(this.dialog);
+ }
+
+ // ---------------------------------------------------------------------------
+ // Chat-driven ticket creation
+ // ---------------------------------------------------------------------------
+
+ /**
+ * Full flow: with support enabled, ask the agent (via the main chat
+ * surface) for a ticket, then verify it shows up in this tab.
+ *
+ * The Edit Agent modal is host-specific to open/close, so the caller
+ * supplies `openEditAgentModal` (and optionally `closeEditAgentModal`,
+ * used to get the modal out of the chat's way before sending). Steps:
+ *
+ * 1. `openEditAgentModal()` → Support tab → ensure the toggle is ON.
+ * 2. `closeEditAgentModal()` (when provided) → chat-request the ticket.
+ * 3. `openEditAgentModal()` → Support tab → wait for the ticket row.
+ *
+ * Returns the dialog locator with the Support tab open and the new ticket
+ * listed, so specs can keep asserting (open it, reply, close it).
+ */
+ createTicketViaChatAndVerify(opts: {
+ subject: string;
+ description?: string;
+ openEditAgentModal: () => Promise;
+ closeEditAgentModal?: () => Promise;
+ listTimeoutMs?: number;
+ }): Promise {
+ return createSupportTicketViaChatAndVerify(this.page, opts);
+ }
+}
diff --git a/hooks/__tests__/use-mentor-segments-screenshare.test.ts b/hooks/__tests__/use-mentor-segments-screenshare.test.ts
index 2553e1ba..52aa3c43 100644
--- a/hooks/__tests__/use-mentor-segments-screenshare.test.ts
+++ b/hooks/__tests__/use-mentor-segments-screenshare.test.ts
@@ -101,7 +101,6 @@ const screenshareSegment = MENTOR_SEGMENTS.find(
const baseFlags = {
isMemsearchEnabled: false,
isClawEnabled: false,
- clawConfigExists: false,
isMemoryComponentEnabled: false,
isScreenshareEnabled: false,
// Voice calls default on so the Screen Share gating tests aren't perturbed
diff --git a/hooks/__tests__/use-mentor-segments.test.tsx b/hooks/__tests__/use-mentor-segments.test.tsx
index 0a33b9d4..5b54e054 100644
--- a/hooks/__tests__/use-mentor-segments.test.tsx
+++ b/hooks/__tests__/use-mentor-segments.test.tsx
@@ -20,7 +20,11 @@ import { EDIT_MENTOR_TAB_COMPONENTS } from '@/components/modals/edit-mentor-moda
const mockMentorSettings = vi.fn();
const mockMemsearchEnabled = vi.fn();
-const mockClawMentorConfig = vi.fn();
+// Call-tracking guard: the segments hook must NOT fetch the claw-config —
+// Sandbox/Skills are always visible, and fetching here would fire on every
+// page through the NavBar. Only the Sandbox/Prompts tab contents (mounted on
+// demand) may query it.
+const mockUseGetClawMentorConfigQuery = vi.fn(() => ({ data: null }));
const mockGetMentorId = vi.fn();
const mockTenantKey = vi.fn();
const mockMentorIdParam = vi.fn();
@@ -46,7 +50,8 @@ vi.mock('@iblai/iblai-js/data-layer', () => ({
useGetMemsearchStatusQuery: () => ({
data: { enable_memsearch: mockMemsearchEnabled() },
}),
- useGetClawMentorConfigQuery: () => ({ data: mockClawMentorConfig() }),
+ useGetClawMentorConfigQuery: (...args: unknown[]) =>
+ mockUseGetClawMentorConfigQuery(...(args as [])),
}));
vi.mock('@/hooks/use-user', () => ({
@@ -105,6 +110,7 @@ vi.mock('@iblai/iblai-js/web-containers', () => ({
vi.mock('@iblai/iblai-js/web-containers/next', () => ({
AgentPrivacyTab: () => null,
AgentTasksTab: () => null,
+ AgentHumanSupportTab: () => null,
AgentSettingsProvider: () => null,
}));
@@ -124,10 +130,7 @@ const setupDefaults = () => {
);
mockUserType.mockReturnValue(UserType.ADMIN);
mockMemsearchEnabled.mockReturnValue(true);
- // Default: a wired claw-config exists, so Skills tab gating only depends on
- // isClawEnabled in most existing tests. Tests that need the unwired state
- // override this in their own arrangement.
- mockClawMentorConfig.mockReturnValue({ id: 1, enabled: true, server: 7 });
+ mockUseGetClawMentorConfigQuery.mockClear();
mockMentorSettings.mockReturnValue({
platform_key: 'custom-tenant',
mentor_visibility: MentorVisibilityEnum.VIEWABLE_BY_TENANT_ADMINS,
@@ -147,12 +150,13 @@ describe('useMentorSegments', () => {
setupDefaults();
});
- it('returns the canonical 22 mentor segments unfiltered', () => {
+ it('returns the canonical 24 mentor segments unfiltered', () => {
const { result } = renderHook(() => useMentorSegments());
expect(result.current.segments).toBe(MENTOR_SEGMENTS);
// 17 original + Voice + Screen Share (feat/mentor/1763) + Tasks
- // (feat/mentor/715) + LTI + Analytics hub (feat/2040).
- expect(MENTOR_SEGMENTS).toHaveLength(23);
+ // (feat/mentor/715) + LTI + Analytics hub (feat/2040) + Human Support
+ // (feat/2081) + Evals (feat/1178).
+ expect(MENTOR_SEGMENTS).toHaveLength(24);
});
it('places the Sandbox segment right after Settings', () => {
@@ -586,20 +590,8 @@ describe('useMentorSegments', () => {
enable_claw: true,
};
- it('shows both Sandbox and Skills when claw is enabled but no config wired', () => {
+ it('shows both Sandbox and Skills when claw is enabled', () => {
mockMentorSettings.mockReturnValue(adminClawEnabledSettings);
- mockClawMentorConfig.mockReturnValue(null); // 404 → null
-
- const { result } = renderHook(() => useMentorSegments());
- const labels = result.current.filteredSegments.map((s) => s.label);
-
- expect(labels).toContain('Sandbox');
- expect(labels).toContain('Skills');
- });
-
- it('shows both Sandbox and Skills when claw is enabled AND a config is wired', () => {
- mockMentorSettings.mockReturnValue(adminClawEnabledSettings);
- mockClawMentorConfig.mockReturnValue({ id: 1, enabled: true, server: 7 });
const { result } = renderHook(() => useMentorSegments());
const labels = result.current.filteredSegments.map((s) => s.label);
@@ -613,7 +605,6 @@ describe('useMentorSegments', () => {
...adminClawEnabledSettings,
enable_claw: false,
});
- mockClawMentorConfig.mockReturnValue(null);
const { result } = renderHook(() => useMentorSegments());
const labels = result.current.filteredSegments.map((s) => s.label);
@@ -621,6 +612,17 @@ describe('useMentorSegments', () => {
expect(labels).toContain('Skills');
expect(labels).toContain('Sandbox');
});
+
+ it('never fetches the claw-config, even when claw is enabled', () => {
+ // The hook runs on every page via the NavBar; visibility no longer
+ // depends on a wired ClawMentorConfig, so the query must not fire here.
+ // The Sandbox/Prompts tabs fetch it themselves when they mount.
+ mockMentorSettings.mockReturnValue(adminClawEnabledSettings);
+
+ renderHook(() => useMentorSegments());
+
+ expect(mockUseGetClawMentorConfigQuery).not.toHaveBeenCalled();
+ });
});
});
diff --git a/hooks/use-mentor-segments.ts b/hooks/use-mentor-segments.ts
index 1ee87d6e..57932cd3 100644
--- a/hooks/use-mentor-segments.ts
+++ b/hooks/use-mentor-segments.ts
@@ -13,6 +13,7 @@ import {
CalendarClock,
Clock,
Grid,
+ Headset,
FlaskConical,
GraduationCap,
Key,
@@ -32,7 +33,6 @@ import { MentorVisibilityEnum } from '@iblai/iblai-api';
import {
useGetMentorSettingsQuery,
useGetMemsearchStatusQuery,
- useGetClawMentorConfigQuery,
} from '@iblai/iblai-js/data-layer';
import { MODALS, UserType } from '@/lib/constants';
@@ -55,8 +55,6 @@ import { config } from '@/lib/config';
export type MentorSegmentConfigFlags = {
isMemsearchEnabled: boolean;
isClawEnabled: boolean;
- /** True when a ClawMentorConfig exists for this mentor (sandbox wired to an instance). */
- clawConfigExists: boolean;
isMemoryComponentEnabled: boolean;
/** True when `enable_privacy_router` is on for this mentor. */
isPrivacyEnabled: boolean;
@@ -397,6 +395,23 @@ export const MENTOR_SEGMENTS: MentorSegment[] = [
],
navCategory: 'runtime',
},
+ {
+ value: MODALS.EDIT_MENTOR.tabs.human_support,
+ label: 'Support',
+ labelKey: 'support',
+ icon: Headset,
+ // Admin-only ticket inbox (view / reply / close support requests).
+ // No `rbacResource` yet — the backend doesn't expose one for support
+ // tickets, so the userTypes filter alone gates visibility (mirroring
+ // Tasks / Sandbox / Access).
+ userTypes: [UserType.ADMIN],
+ permissionFieldsCheck: [],
+ mentorVisibility: [
+ MentorVisibilityEnum.VIEWABLE_BY_TENANT_ADMINS,
+ MentorVisibilityEnum.VIEWABLE_BY_TENANT_STUDENTS,
+ ],
+ navCategory: 'runtime',
+ },
{
value: MODALS.EDIT_MENTOR.tabs.audit_log,
label: 'Audit',
@@ -629,21 +644,11 @@ export function useMentorSegments(options: UseMentorSegmentsOptions = {}) {
// @ts-expect-error enable_claw is not yet in the MentorSettingsPublic type
const isClawEnabled: boolean = mentorSettings?.enable_claw ?? false;
- // The claw-config endpoint is keyed by the mentor's UUID. Use the value from
- // mentor settings; fall back to the resolved id (which may already be a UUID
- // when navigating directly).
- const mentorUuid: string | undefined =
- mentorSettings?.mentor_unique_id ?? resolvedMentorId;
-
- // The data-layer normalises 404 → null, so a non-null result means the
- // mentor has a wired ClawMentorConfig (sandbox connected to an instance).
- // Skip the query until we know claw is enabled — there's no point fetching
- // the config when we'd never gate on it.
- const { data: clawMentorConfig } = useGetClawMentorConfigQuery(
- { org: tenantKey!, mentorUniqueId: mentorUuid! },
- { skip: !isClawEnabled || !tenantKey || !mentorUuid },
- );
- const clawConfigExists = !!clawMentorConfig;
+ // NOTE: no claw-config fetch here. The Sandbox and Skills tabs are always
+ // visible, so nothing in the segment filter needs to know whether a
+ // ClawMentorConfig is wired. The tabs that do care fetch it themselves when
+ // they mount (Sandbox via , Prompts via its own query) —
+ // fetching it here would fire on every page through the NavBar.
const isMemoryComponentEnabled =
// @ts-ignore - enable_memory_component exists on API but not typed
@@ -684,7 +689,6 @@ export function useMentorSegments(options: UseMentorSegmentsOptions = {}) {
isMemsearchEnabled,
isMemoryComponentEnabled,
isClawEnabled,
- clawConfigExists,
isPrivacyEnabled,
isScreenshareEnabled,
isVoiceCallEnabled,
@@ -699,7 +703,6 @@ export function useMentorSegments(options: UseMentorSegmentsOptions = {}) {
userType,
isMemsearchEnabled,
isClawEnabled,
- clawConfigExists,
isMemoryComponentEnabled,
isPrivacyEnabled,
isScreenshareEnabled,
diff --git a/lib/constants.ts b/lib/constants.ts
index 4b55fefa..161571e1 100644
--- a/lib/constants.ts
+++ b/lib/constants.ts
@@ -65,6 +65,7 @@ export const MODALS = {
audit_log: 'audit_log',
voice: 'voice',
screenshare: 'screenshare',
+ human_support: 'human_support',
analytics: 'analytics',
},
},
diff --git a/messages/en.json b/messages/en.json
index c35e3040..36b4c589 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -810,6 +810,7 @@
"skills": "Skills",
"privacy": "Privacy",
"tasks": "Tasks",
+ "support": "Support",
"disclaimers": "Disclaimers",
"memory": "Memory",
"evals": "Evals",
diff --git a/messages/es.json b/messages/es.json
index cb756cd3..90d6daf0 100644
--- a/messages/es.json
+++ b/messages/es.json
@@ -810,6 +810,7 @@
"skills": "Habilidades",
"privacy": "Privacidad",
"tasks": "Tareas",
+ "support": "Soporte",
"disclaimers": "Avisos legales",
"memory": "Memoria",
"evals": "Evaluaciones",
diff --git a/messages/fr.json b/messages/fr.json
index 85099eb6..3948ddf7 100644
--- a/messages/fr.json
+++ b/messages/fr.json
@@ -810,6 +810,7 @@
"skills": "Compétences",
"privacy": "Confidentialité",
"tasks": "Tâches",
+ "support": "Support",
"disclaimers": "Avertissements",
"memory": "Mémoire",
"evals": "Évaluations",
diff --git a/messages/zh.json b/messages/zh.json
index b283ec30..72598e65 100644
--- a/messages/zh.json
+++ b/messages/zh.json
@@ -810,6 +810,7 @@
"skills": "技能",
"privacy": "隐私",
"tasks": "任务",
+ "support": "支持",
"disclaimers": "免责声明",
"memory": "记忆",
"evals": "评估",
diff --git a/package.json b/package.json
index 7ce73c5b..97dfe2b7 100644
--- a/package.json
+++ b/package.json
@@ -86,6 +86,7 @@
"@tiptap/pm": "2.11.7",
"@tiptap/react": "2.11.7",
"@tiptap/starter-kit": "2.11.7",
+ "axios": "1.18.1",
"class-variance-authority": "0.7.1",
"clsx": "2.1.1",
"cmdk": "1.1.1",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index d35bad33..e9a71c29 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -194,6 +194,9 @@ importers:
'@tiptap/starter-kit':
specifier: 2.11.7
version: 2.11.7
+ axios:
+ specifier: '>=1.16.0'
+ version: 1.18.1
class-variance-authority:
specifier: 0.7.1
version: 0.7.1