Add Incognito -- a conversation the platform does not save to chat
history or use for memory. It is one feature across four surfaces, and
this skill is the map plus the one piece no other skill mounts: the
chat-header Incognito toggle (ChatPrivacyToggle) and its
mid-conversation confirmation dialog.
| Tier | Who sets it | Where | Skill |
|---|---|---|---|
| Organization gate | Admin | Account → Advanced → Allow users to control chat privacy | /iblai-vibe-account |
| Agent policy | Agent admin | Agent → Privacy → Incognito: Users Decide / Always Incognito / Never Incognito | /iblai-vibe-agent-privacy |
| User default | Each user | Profile → Privacy: Normal / Anonymized / Incognito | /iblai-vibe-profile |
| This conversation | Each user | The header toggle (this skill) | — |
Incognito used to be called Private Mode; the names in the UI, the
toasts and the dialog all say Incognito now. The wire names did not
change (disable_chathistory, chat_privacy_mode).



Common setup (brand, conventions, env files, verification): see docs/skill-setup.md.
How the tiers resolve
The backend answers one question per (org, user, agent, session):
GET …/chat-privacy-effective/ → { mode, source, is_locked }.
modeisnormal,anonymizedordisabled(disabled = Incognito). Incognito withholds the message text: no chat-history row is written and nothing feeds memory. Each turn is still recorded against the user for usage, and the conversation's own turns are kept for 24 hours, refreshed on every turn, as the agent's context — so the agent follows the conversation. Anonymized strips the account identity from the history rows only; the per-turn usage record keeps the user and the text.sourcesays which tier won:mentor(the agent),tenant,user(profile default),session(this conversation), ordefault.is_lockedis true when the agent or the organization decided — the user cannot change it from the conversation.
Precedence, highest first:
- Agent — Always Incognito (
disable_chathistoryon the agent): every conversation is incognito; the pill is locked on. - Agent — Never Incognito (
disable_privacy_modeon the agent): the toggle disappears, and a user whose profile default is Incognito is saved anonymized instead. - Organization gate off: no user control anywhere — no toggle, no
Incognito sub-tab, no profile Privacy tab; every chat is saved
(
chat-privacy-effectiveanswersmode: normal,source: tenant,is_locked: true). - This session (
disable_chathistoryon the session): what the toggle writes. - User default (
chat_privacy_mode): Normal, Anonymized, or Incognito for every conversation unless a tier above overrides. - Default: normal.
Prerequisites
- Auth must be set up first (
/iblai-vibe-auth) - MCP server + skills configured (
@iblai/mcpin.mcp.json) - A chat surface to put the toggle on (
/iblai-vibe-agent-chat) — the toggle reads the active session id and message list from the shared chat slice, so it only makes sense next to a mounted chat. - The organization gate on, or nothing renders (by design — see the render gate below).
Step 1: Check Environment
Before proceeding, check for an iblai.env in the project root. Look for
PLATFORM, DOMAIN, and TOKEN variables. If the file does not exist or
is missing these variables, tell the user:
"You need an iblai.env with your platform configuration. Download the
template and fill in your values:
curl -o iblai.env https://raw.githubusercontent.com/iblai/vibe/refs/heads/main/iblai.env"
Step 2: Mount the toggle in the chat header
Place it in the header's right-hand cluster, next to the credit balance and the notification bell — all three are 40×40 icon buttons at rest; Incognito grows into a labelled pill when it is on.
// components/navbar/incognito-toggle.tsx
"use client";
import { ChatPrivacyToggle } from "@iblai/iblai-js/web-containers";
export function IncognitoToggle({
tenantKey,
username,
mentorId,
}: {
tenantKey: string;
username: string;
/** The agent of the open chat; omit off chat pages and the toggle hides. */
mentorId?: string;
}) {
return <ChatPrivacyToggle org={tenantKey} userId={username} mentor={mentorId} />;
}
Safe to render unconditionally: it returns null when identity is
missing, when the organization gate is off, or when the agent is
Never Incognito — except while the current conversation is already
incognito, when the pill stays visible so the state is not lost.
Hosts on the SDK's PlatformNavbar pass the same three values as its
privacyToggle config (PlatformNavbarPrivacyToggleConfig) and null
off chat pages.
Step 3: Use MCP Tools for Customization
get_component_info("ChatPrivacyToggle")
get_hook_info("useChatPrivacy")
get_component_info("AgentPrivacyTab")
<ChatPrivacyToggle> Props
Import from @iblai/iblai-js/web-containers.
| Prop | Type | Required | Description |
|---|---|---|---|
org |
string |
Yes | Organization key |
userId |
string |
Yes | Username of the signed-in user |
mentor |
string |
No | Agent UUID of the open chat. Without it the toggle renders nothing (it cannot start a session) |
className |
string |
No | Extra classes on the trigger button |
What the toggle does
A hat-and-glasses icon button; when Incognito is on it turns into a blue gradient pill labelled Incognito. What a click does depends on the state:
| State | Click |
|---|---|
| Normal, no user message yet | Starts a fresh session with disable_chathistory: true — no dialog. Once a message is sent, that conversation stays in Incognito |
| Incognito, no user message yet | Starts a fresh normal session — the way out of an empty incognito chat |
| Normal, messages sent | Opens "Enable Incognito for this chat?" (below); on confirm, flips this session's disable_chathistory to true |
| Incognito, messages sent | Flips disable_chathistory back to false on the same session — history resumes, no dialog |
| Locked on | No action; the tooltip says why |
| A reply is streaming | Waits ("Wait for the reply to finish before changing Incognito.") |
The mid-conversation dialog
Enable Incognito for this chat? From here on, new messages in this conversation won't be saved, and the assistant stops using your earlier messages. Your earlier messages stay saved in your history. You can turn Incognito off again anytime to resume saving.
Cancel · Enable Incognito
Only the mid-conversation enable asks. Turning it off again, and either direction on an empty chat, act immediately with a toast.
Locked states and their tooltips
| Why | Tooltip |
|---|---|
Agent is Always Incognito (source: mentor) |
"This agent always runs in Incognito — no chat history or memory is kept for conversations with it." |
Profile default is Incognito (source: user) |
"Incognito is on for all your chats from your profile's Privacy settings. To save history, change it there, then start a new chat." |
| Conversation has no owner (started in Incognito, or a resume was refused) | "Incognito stays on for this conversation, so nothing in it is saved. Start a new chat to save history." |
| Profile default is Anonymized and messages were sent | "Incognito can't be turned on for this conversation. Start a new chat and turn Incognito on before sending a message." |
A refused action toasts the backend's own reason when it sent one (the 403 from an agent locked while the chat was open), otherwise the generic message for that action.
Custom UI: useChatPrivacy
The toggle is a thin view over one hook, exported from
@iblai/iblai-js/web-containers, for hosts that want their own
control:
import { useChatPrivacy } from "@iblai/iblai-js/web-containers";
const {
featureEnabled, // organization gate
privacyModeDisabled, // agent is Never Incognito (or a tier above locked a non-incognito mode)
effective, // { mode, source, is_locked }
isEffectiveReady, // don't act before the first resolution lands
messageCount, // user-authored messages in the open chat
isTurnInFlight,
canResumeHistory, // false once this browser knows the session has no owner
startPrivateChat, // new session, disable_chathistory: true
startNormalChat, // new session, normal
disableChatHistory, // (true | false) on the current session
} = useChatPrivacy({ org, userId, mentor, onActionError });
onActionError receives { action, status?, message? } — action is
startPrivateChat, startNormalChat, disableChatHistory or
enableChatHistory; a refused resume arrives as enableChatHistory
with 400 (admin) or 404 (own user).
REST API
All under ${dmUrl} where dmUrl is NEXT_PUBLIC_API_BASE_URL. Auth:
Authorization: Token <token>.
| Tier | Method | Path | Body / notes |
|---|---|---|---|
| Organization | GET / PATCH | /api/ai-account/orgs/{org}/users/{username}/chat-privacy-config/ |
{ "allow_user_chat_privacy_control": boolean } — PATCH is admin-only (403 otherwise); the {username} segment is the caller, the value is organization-wide |
| Agent | PUT | /api/ai-mentor/orgs/{org}/users/{username}/mentors/{mentor_unique_id}/settings/ |
{ "disable_chathistory": boolean, "disable_privacy_mode": boolean } — see /iblai-vibe-agent-privacy |
| User | GET / POST | /api/ai-account/orgs/{org}/users/{username}/chat-privacy-settings/ |
{ "chat_privacy_mode": "normal" | "anonymized" | "disabled" } |
| Resolved | GET | /api/ai-account/orgs/{org}/users/{username}/chat-privacy-effective/?mentor={uuid}&session={id} |
{ mode, source, is_locked } |
| Session | POST | /api/ai-mentor/orgs/{org}/users/{username}/sessions/{session_id}/disable-chathistory/ |
{ "disable_chathistory": boolean }. false is refused for a session with no owner (400 admin / 404 own user) |
A new session starts incognito with POST …/sessions/ and
{ "mentor": uuid, "disable_chathistory": true }; the backend creates
that session without an owner, which is why it can never leave
Incognito once it has messages.
Related Exports
From @iblai/iblai-js/web-containers:
ChatPrivacyToggle,ChatPrivacyToggleProps— the header toggle.useChatPrivacy,ChatPrivacyAction,ChatPrivacyActionError,toChatPrivacyActionError— the hook behind it.PlatformNavbar,PlatformNavbarPrivacyToggleConfig— the SDK navbar and itsprivacyToggleslot.ProfilewithtargetTab="privacy"— the user tier (/iblai-vibe-profile).
From @iblai/iblai-js/web-containers/next:
AgentPrivacyTab— the agent tier (/iblai-vibe-agent-privacy).
From @iblai/iblai-js/data-layer (chatPrivacyApiSlice):
useGetTenantChatPrivacyConfigQuery,useUpdateTenantChatPrivacyConfigMutation— the organization gate.useGetUserChatPrivacySettingsQuery,useUpdateUserChatPrivacySettingsMutation— the user default.useGetChatPrivacyEffectiveQuery— the resolved mode.useUpdateSessionDisableChathistoryMutation— the session flip.ChatPrivacyModeEnum,CHAT_PRIVACY_MODES, and the typesChatPrivacyMode,ChatPrivacyEffective,TenantChatPrivacyConfig,UserChatPrivacySettings,MentorChatPrivacyPublicSettings.
From @iblai/iblai-js/playwright: getChatPrivacyToggle,
expectChatPrivacyState, expectChatPrivacySource,
expectChatPrivacyLocked, clickChatPrivacyToggle,
confirmEnableChatPrivacyMidSession, turnOffChatPrivacyMidSession,
cancelEnableChatPrivacyMidSession.
SDK source: packages/web-containers/src/components/chat-privacy/chat-privacy-toggle.tsx,
hooks/use-chat-privacy.ts, the organization gate row at
components/profile/advanced/chat-privacy.tsx, and the data layer under
packages/data-layer/src/features/chat-privacy/.
Step 4: Verify
Run /iblai-vibe-ops-test before telling the user the work is ready:
pnpm build-- must pass with zero errorspnpm test-- vitest must pass- Start dev server and touch test:
pnpm dev & npx playwright screenshot http://localhost:3000/ /tmp/incognito.png
Stable selectors: chat-privacy-toggle (data-state="on" | "off",
data-source, aria-pressed, aria-disabled),
chat-privacy-confirm-dialog, chat-privacy-confirm-cancel,
chat-privacy-confirm-action.
Important Notes
- Redux store: Must include
mentorReducerandmentorMiddleware(they registerchatPrivacyApiSliceand the shared chat slice the toggle reads) initializeDataLayer(): 5 args (v1.2+)@reduxjs/toolkit: Deduplicated via webpack aliases innext.config.ts- Peer deps:
sonnerand@iblai/iblai-web-mentormust be installed (pnpm add sonner @iblai/iblai-web-mentor) - Incognito is per conversation, not per message. Turning it on mid-conversation keeps the earlier messages in history and stops saving from that point; the assistant also stops using the earlier turns. Turning it off resumes saving on the same conversation — messages sent while it was on are gone. What Incognito withholds is the message text: no history row, nothing to memory. Usage is still metered per turn, and the incognito turns stay available to the agent as context for 24 hours.
- An empty chat started in Incognito has no owner. The backend
creates it that way, so once it has messages it cannot be turned
back; the toggle locks and points the user to a new chat. This
browser remembers such sessions in
localStorage(iblai_chat_privacy_stays_incognito_sessions). - Anonymized is not Incognito. A profile default of Anonymized keeps the chat-history rows but strips the account identity from them; the per-turn usage record still carries the user and the text. The pill stays off and the agent still uses prior turns. Those conversations have no owner either, so Incognito cannot be enabled on them mid-way.
- The user-facing name is Incognito everywhere — the toggle, the
agent's Privacy tab, the profile Privacy tab, the toasts and the
dialog. Wire fields keep their older names:
disable_chathistory,disable_privacy_mode,chat_privacy_mode: "disabled", and thementorquery parameter. - Brand guidelines: BRAND.md