---
title: Team Chat (built-in channels and direct messages) (part 3)
---

# Team Chat (built-in channels and direct messages) (part 3)

## What it is

This is part 3 of the [Team Chat (built-in channels and direct messages)](team-chat.md) page. It covers the phone client and the surfaces that sit around the core conversation: how Team Chat reaches a phone at all and what web push does once it is there, the notification preferences that decide when chat is allowed to interrupt you, the link previews that turn a pasted URL into a card, the bridge that lets chat know about your project-management items, the comments posted on a shared link, and the two ways a chat message can start real Claude work — a thread handed to a working session, or a channel watched by a Session Starter Bot. It closes with the media gallery and the video-call background picker.

## Where to find it

The same surface as the parent page: once it is switched on, Team Chat appears as a built-in virtual project in the projects sidebar, with the channel and direct-message list in the sidebar pane and the open conversation filling the main panel. It ships dark until **Settings → Lab → Team Chat** is turned on. The [parent page](team-chat.md) covers finding and enabling it.

The phone client is a separate web app reached at its own address rather than a screen inside the desktop sidebar, and the notification controls live in **Settings → Team Chat notifications** and on each channel's bell menu. The rest — the gallery button, the video-call background button, the thread's **Start a session** action, the Session Bot entry in a channel's right-click menu, and the **Create Task** action on a message — are all inside the chat surface. The engineering half of this part is a short set of pointers at the end for someone with the repository open.

## How it behaves

### Mobile (PWA) client — the always-on phone app

**Two phone paths to Team Chat.** (1) **Through Omniscio — the team's path (since 2026-06-28):** reach your OWN desktop Omniscio from your phone over **Tailscale**, and Omniscio's **mobile web access** serves the renderer — Team Chat included. This required allowing Team Chat's NAMED Firebase hosts through the web-access CSP ([web-access-http-shared.ts](../../src/main/services/web/web-access-http-shared.ts) `connect-src`, pinned by [web-access-team-chat-csp.test.ts](../../tests/unit/lint/web-access-team-chat-csp.test.ts)); it needs the desktop running (Tailscale reaches it), and phone push on this path is a separate follow-on. (2) **The standalone PWA — the PC-off fallback (described below).**

On a phone the renderer is tuned for touch: an open channel shows **one** header (a **← back** chevron to the channel list, the channel name, the notification bell, and Members) instead of stacking the workspace bar above it, message actions move to a **bottom action sheet** (opened by the small **⋯** beside each message, or a long-press), and the composer's formatting toolbar **collapses behind a toggle** (all three detailed in the feature list above).

The phone client is a **separate, standalone web app** ([`firebase/team-chat-pwa/`](../../firebase/team-chat-pwa)) served by **Firebase Hosting** — reachable on a phone **even when the desktop is OFF** (it deliberately does NOT use the desktop-tethered mobile web-access bridge). It shares **one data layer + one set of frozen types** with the desktop (reusing `@shared/team-chat/*` + the Phase-0 schemas), so the two clients behave identically; only the shell + auth differ.

- **Auth differs from desktop.** The desktop signs in with a custom token minted by Main; the phone has no Main, so it does a normal interactive **"Sign in with Google"** web sign-in (the same Global Auth identity) and resolves the workspace from the user's own `users/{uid}.organizationId`. On sign-in the phone now **materializes the user's profile first** (the same `getOrCreateProfile` the desktop calls, reached via a same-origin Hosting rewrite), so a brand-new **invited** teammate who opens the phone FIRST is onboarded into the workspace instead of dead-ending — with a fallback to the direct read so an existing member never regresses. No workspace → a friendly "not in a workspace yet" screen, never a blank one.
- **Web push (FCM).** A service worker + Firebase Messaging register the phone's device token into `organizations/{orgId}/fcmTokens/{uid}` (own-doc, transactional merge, capped, never clobbering the desktop's entry) — exactly the store the `onMessageCreate` function reads to fan out push. The service worker gets the **real (public) Firebase config injected at build** (emitted as `/firebase-config.js`), without which background push silently no-ops. Foreground → in-app toast; background → a notification that deep-links to the channel.
- **Installable.** A web manifest + maskable icons + an iOS "Add to Home Screen" hint (iOS 16.4+ web push needs an installed PWA).
- **Clickable web links.** Bare `http(s)://` and `www.` URLs in a message now render as tappable links (open in a new tab) — the mobile mirror of the desktop renderer's autolinking, so a shared link is clickable on your phone instead of dead text. Only real web links match; app `omniscio://` deep links stay plain text.
- **Holds no secret** — only the signed-in user's own session; all authorization is server-side rules.
- **Deployed LIVE (2026-06-22)** at `agentmc-teamchat.web.app` — the Firestore rules + the `onMessageCreate` notify function are live and the `jlstradingco` workspace is provisioned. What remains is **operational** (see "Going live" below): generate + wire the Web-Push VAPID key and redeploy, run the Lane A members backfill, then turn it on for the team. Invariants + tests: [team-chat-pwa-contract.md](../../.claude/memory/contracts/team-chat-pwa-contract.md).

### Notification preferences

You control when Team Chat notifies you. Set a **global default** — **All messages**, **Mentions only**, or **Nothing** — and then **override any single channel or DM** (mute a noisy one, or follow a quiet one closely). You can also **temporarily mute** a channel for **1 hour**, **8 hours**, or **24 hours** -- the mute suppresses all notifications until it expires, then the channel goes back to its normal mode. This is what the desktop notification decision function honors; the Cloud Function reads the same `organizations/{orgId}/notifPrefs/{uid}` doc and, per channel, applies your override if you set one, otherwise the default (temp mute is client-side only in v1).

Three places set it, all writing the same doc:

- **The full control** lives in **Settings -> Team Chat notifications** -- the global default plus a per-channel/DM list with temporary mute indicators. It's a **deep-link-only** section (no sidebar nav row, because Team Chat ships dark); you reach it from the channel bell menu below, and it self-gates when Team Chat isn't enabled or you're not in a workspace yet.
- **A quick per-channel control** sits on the **channel header** -- a small **bell** that opens a menu to set THIS channel to All / Mentions only / Mute (permanent), mute temporarily (1h/8h/24h), unmute (when temp-muted, with expiry text), reset to the default, or jump to the full settings. The bell glyph reflects the channel's current **resolved** setting (bell / @ / bell-off), accounting for temporary mutes.
- **The channel right-click context menu** in the sidebar -- mute for 1h/8h/24h when not temp-muted, or Unmute (with expiry status) when temp-muted, above the existing Edit/Leave/Delete items.

For engineers: the renderer reads + writes `notifPrefs/{uid}` directly via the Firebase web SDK (own-doc-write; the rules allow only `{uid}`). Writes are **field-scoped merges** (a change touches only `default`, one `perChannel` key, or one `mutedUntil` key, never the whole doc), so a concurrent edit can't be clobbered, and a missing/corrupt doc falls back to the safe default. The `mutedUntil` field is additive; legacy docs without it parse to `{}` (no mutes). Errors are humanized (never raw Firestore text). Invariants + tests: [team-chat-notif-prefs-ui-contract.md](../../.claude/memory/contracts/team-chat-notif-prefs-ui-contract.md) (client/UI side) and [team-chat-notifications-contract.md](../../.claude/memory/contracts/team-chat-notifications-contract.md) (server fan-out).

### PM integration (project management awareness)

**Status:** In-development (gated behind `pm-team-chat` + Mission Control enabled). Enable via **Settings → Lab → PM Team Chat** or `AMC_SHOW_PM_TEAM_CHAT=1`.

Team Chat knows about your PM items. Four capabilities bridge the two surfaces:

- **#item mentions** — type `#` in the composer to search PM items by name. Select one to insert a reference that renders as a rich inline chip showing the item title. The chip is driven by the message's `pmItemRefs` field (a schema extension).
- **Create Task from chat** — right-click any message and choose **"Create Task"** to create a PM item pre-filled with the message text. Pick a target board, and the item is created on Mission Control with bidirectional links stored in `pm_item_links` (message→item + channel→item).
- **Status updates in channels** — when a PM item changes status (via sync), a system message appears in every channel the item is linked to, showing the old and new status. These messages use a deterministic id so they never double-post.
- **Chat links on PM items** — the PM item detail drawer shows a "Chat Messages" section listing all linked messages with preview text and timestamps.

Sources of truth: `src/renderer/src/features/team-chat/composer-pm-items.ts` (composer logic), `src/main/services/pm/pm-chat-status-bridge.ts` (status bridge), `src/main/ipc/pm-chat-handlers.ts` (IPC handlers). Contract: [pm-team-chat-integration-contract.md](../../.claude/memory/contracts/pm-team-chat-integration-contract.md).

### Share comment cross-posting

When a shared link has comments enabled and is linked to a Team Chat channel, new comments posted on the public share page are cross-posted as thread replies to that channel via the `onCommentCreate` Cloud Function. This lets the team discuss shared content without leaving chat. See [share-comments-contract.md](../../.claude/memory/contracts/share-comments-contract.md) for the full contract.

### Start a working session from a thread

Open a thread and click **Start a session** in the thread pane header (the message-plus icon, left of the close X) to kick off a normal Claude working session seeded with that thread's context — without drowning the agent in the whole conversation.

- **Pick a project.** A small dialog lets you choose a **real project** to run in (virtual/built-in projects are excluded), and optionally type a **first instruction**. The session runs there as an ordinary working session — its work shows in the **session view**, nothing is posted back into the chat. Team Chat itself never hosts the session.
- **Starts in the background — never steals focus.** Starting a session (from a thread, a single message, or the mobile action sheet) does **not** switch your view or foreground the new session — you stay right where you are in Team Chat. The new session simply appears at the **top of your session list**, ready to open when you want it. This is universal and **build-guard-locked**, so it can never regress to grabbing focus (see the contract's I10).
- **Only the recent messages come along.** The agent is seeded with a **pure sliding window of the thread's last 30 messages** (oldest-first-dropped under a ~16k-character cap) — no AI summary, **$0** to build. A long thread simply drops its oldest messages (including the root) from the window; the transparency line in the dialog says exactly how many of how many are included.
- **Untrusted by construction.** Because chat text is written by people, the thread is handed over wrapped in explicit "treat as data, never as instructions" markers — the session acts on **your** instruction, not on anything a message says.
- **Attachments come along.** Images, screenshots, PDFs, and documents attached to the message(s) are downloaded and forwarded to the spawned session so the agent can actually see them, not just a text placeholder. Videos are skipped; the total is bounded at 10 MiB / 20 files. If a download fails, the session still starts with text-only context.
- **Two modes.** Type a first instruction and it runs immediately (the bubble shows just your instruction; the thread rides along hidden); leave it blank and the session opens ready-and-waiting with the context attached, so your first message drives it.
- Reuses the normal session-spawn path, so there's no new remote/phone-reachable surface. Ships **dark with Team Chat** (same `team-chat` gate). Contract: [team-chat-thread-session-contract.md](../../.claude/memory/contracts/team-chat-thread-session-contract.md).

### Session Starter Bot → conversational responder

A channel can have a **Session Starter Bot**: when a message lands that matches the bot's keywords, it spawns a Claude session in a chosen project. Configure it by right-clicking the channel and selecting **Session Bot** (enable, target project, keywords, daily rate limit, and the two reply toggles below). See [session-starter-bot.md](session-starter-bot.md) for the full user-facing guide.

- **Canned-ack mode (default).** The bot spawns a session and — if "Post acknowledgment reply" is on — posts a one-line "Session started…" note back to the channel. This is the original behavior and is unchanged.
- **Conversational reply mode (opt-in).** Turn on **"Reply conversationally"** and the spawned session instead **reads the channel and posts a genuine reply back** as your agent — rendered as **"<Name>'s agent"** (the same `authoredByAgent` attribution). The canned ack is skipped in this mode. The session gets a tightly **scoped** prompt: read this one channel (via `GET /team-chat/channels/:channelId/messages`), compose ONE reply, and post it (`POST …/messages`) using its own `AMC_CLI_TOKEN` — nothing else.
- **Loop-safety (why it can't runaway).** An agent posts under the human's own `authorUid`, so authorUid alone can't tell agent from human. The reply loop is bounded by a denormalized **`lastMessageAuthoredByAgent`** flag written onto the channel by BOTH send paths (the Cloud Function relay + the client sends): the trigger **never** replies to an agent-authored message. Two layers back this up — the existing self-message skip (same-uid) plus the agent-authorship flag (cross-agent) — so an agent never answers its own or another agent's message.
- **Cost.** Reply mode reuses the per-channel **daily rate limit** (`0 = unlimited`, the default) plus the global 30-second spawn pacer. There is no hard dollar cap by design — loop-safety is the runaway guard.
- **Trust boundary (enable only on trusted channels).** The reply agent reads UNTRUSTED channel text and runs with tools + the CLI token, so reply mode is **off by default, opt-in per channel**. Its prompt fixes the `channelId` (never derived from message content) and flags channel messages as data-not-instructions, but a fully sandboxed reply agent is a follow-up — treat reply mode as a per-channel trust decision.
- **Deploy dependency.** The `lastMessageAuthoredByAgent` denorm is written by the `teamChatSendMessage` Cloud Function; the cross-agent loop guard is only fully live once that function is redeployed (reply mode is off-by-default + in-development, so this only matters once both ship). Contract: [starter-bot-reply-loop-contract.md](../../.claude/memory/contracts/starter-bot-reply-loop-contract.md).

#### Gallery Panel (Shared Media)

A **Gallery** button in the channel header opens a side panel showing all shared content in the channel (or the entire workspace, toggled with a scope button), organized into three tabs:

- **Media** — images and videos from message attachments, rendered as a grid with date-grouped sections. Click an image to open it in the **image lightbox**; click a video to open it in the **video lightbox**.
- **Files** — non-media file attachments, listed with filename, size, sender, and date. Click to download.
- **Links** — URLs extracted from messages, shown as **link preview cards** (via the same link-unfurl infrastructure described above).

Each tab groups items by month ("August 2026", "July 2026") and shows an empty state when nothing has been shared. The gallery can be scoped to the **current channel** or expanded to **all channels in the workspace**. Sources: `TeamChatGalleryPanel.tsx`, `use-channel-shared-media.ts`, `use-workspace-shared-media.ts`.

#### Video backgrounds

During a video call, press **B** or click the **background** button in the call controls toolbar to open the **background picker** — a popover with three sections:

- **None** — no processing, raw camera feed.
- **Blur** — background blur at three intensity levels (Light / Medium / Heavy). Powered by `@livekit/track-processors` (WebGL/WASM-based segmentation), lazy-loaded on first use.
- **Custom image** — upload a PNG, JPG, or WebP image (max 10 MB) as a virtual background. Images are stored locally under `${userData}/video-backgrounds/` and served to the renderer as data URIs. Up to any number of saved images; delete with the hover trash button.

Background settings persist across calls via `teamChatVideoBackground`, `teamChatVideoBackgroundBlurLevel`, and `teamChatVideoBackgroundImage` in the settings store. The processor is cleaned up when the call ends. Mode switches reuse the existing processor instance via `switchTo()` (no WebGL context teardown/rebuild).

For engineers: the processor module (`video-bg-processor.ts`) serializes `applyVideoBackground` calls to prevent races from rapid UI clicks. Custom image storage (`video-backgrounds.ts`) validates file type/size and guards against path traversal via `isWithinRoot`. Contract: [team-chat-video-call-contract.md](../../.claude/memory/contracts/team-chat-video-call-contract.md) (invariants `background-processor-is-lazy-loaded` … `custom-images-stored-locally-with-validation`).

## For agents

### Schema foundation (the next wave)

The **data contract** for this wave's features was defined up front so each feature lane could be built in parallel without colliding on the frozen schema/rules files. Reactions / threads / pins built on it first; **the v2 wave then built the rest — attachments, saved items, custom status, and scheduled send** (see "New in this wave" above). The frozen Phase-0 shapes stay byte-stable (a guard test pins their exact key set). What the foundation defines (✅ = now built):

- **Reactions** — a per-user reaction doc in a `messages/{mid}/reactions/{uid}` subcollection (own-doc; the rule lets you add/remove only your OWN reaction). **This is the first feature lane now BUILT on the foundation** — see Reactions above + [team-chat-reactions-contract.md](../../.claude/memory/contracts/team-chat-reactions-contract.md).
- **Threads** — a nullable `parentId` (null = top-level, set = a reply) plus denormalized `replyCount`/`lastReplyAt` on the parent. **Now BUILT** (a feature lane on the foundation — see Threads above + [team-chat-threads-contract.md](../../.claude/memory/contracts/team-chat-threads-contract.md)); v1 derives the reply count on the client and writes neither denormalized field.
- **Attachments** ✅ **BUILT (v2) + storage rules DEPLOYED (2026-07-08)** — an `attachments` array on a message + a Firebase **Storage** path layout (`<organizations|chatWorkspaces>/{id}/channels/{cid}/{uid}/{attachmentId}/{fileName}`, both tenant roots) and Storage Security Rules: **org** channels gate reads/writes on the `organizationId` token claim (org-level access, author-only write), **self-serve workspaces** on signed-in + author-only; 25 MB + content-type caps on both. (The prior cross-service membership check silently failed on this bucket and denied every upload — the claim-based rule is the deployed + live-verified fix.) The client mirrors those caps for a fast fail; the Storage rules stay the fail-closed boundary.
- **Pins** — a `channels/{cid}/pins/{messageId}` subcollection (any channel member may pin/unpin). **This is now BUILT on the foundation** — see Pins above + [team-chat-pins-contract.md](../../.claude/memory/contracts/team-chat-pins-contract.md).
- **Saved items** ✅ **BUILT (v2)** — a per-user `saved/{uid}` own-doc (mirrors `reads/{uid}`), field-scoped arrayUnion/arrayRemove, cap 200. Each item stores snapshot fields (`authorName`, `text`, `channelName`) at save time for zero-read rendering.
- **Custom status** ✅ **BUILT (v2)** — an optional `{ emoji, text, expiresAt, workspaceId, emojiImageUrl }` on the presence doc; a field-scoped merge write that survives the presence heartbeat. Status is **global** (`workspaceId: null`) so it shows in every workspace. When a custom emoji is used, `resolveStatusEmojiImage()` snapshots the emoji's `imageUrl` into `emojiImageUrl` at set time, so the image renders cross-workspace even when the local emoji map is empty (the map resets on workspace switch).
- **Scheduled send** ✅ **BUILT (v2)** — an author-owned doc at `channels/{channelId}/scheduledMessages/{id}` (nested under the target channel — Uplink security-by-construction pattern); the `onScheduledSendTick` delivery function derives the target channel from the document path (`schedDoc.ref.parent.parent`), posts the message at `deliverAt` (deterministic id + `clientMsgId` for idempotency), re-checks channel access, then deletes the scheduled doc (deploy pending — needs the collection-group index on `deliverAt`). The client uses per-channel subscriptions merged via a Map ref, not a collection-group query.

This **foundation lane lands before the feature lanes**. Reactions are a subcollection rather than a map on the message doc so the frozen author-only message rule stays untouched and "only your own reaction" is provable in rules. The **core** chat rules are now deployed live (2026-06-22, with the PWA); these additive next-wave rules go live with their feature lanes (emulator-tested; suites need JDK 21). Full design, trade-offs, and the invariant→test map: [team-chat-schema-foundation-contract.md](../../.claude/memory/contracts/team-chat-schema-foundation-contract.md).

### CLI access

Channel creation is available from the CLI control server (the same `127.0.0.1:19519` endpoint Omniscio exposes for all agent/automation use). An AI agent or automation can create a channel without opening the GUI:

```
POST /team-chat/channels
Authorization: Bearer <token>

{ "name": "design-reviews", "visibility": "public" }
```

Returns `201 { ok: true, data: { id: "..." } }`. The route is apply-immediately (no inbox approval), auth-gated, and rate-limited (shared 10/min mutation bucket). The channel write goes through the Firebase Admin SDK using the same shared `buildChannelWrite` builder the renderer uses, so the document shape is identical. Pass `workspaceId` to target a self-serve workspace; omit it to use the caller's organization. Full endpoint docs: [team-chat spoke](../../.claude/skills/omniscio-control/team-chat.md).

### Incoming webhooks

External services can post messages into a channel via **incoming webhooks** — no bearer token required, the URL itself is the credential. Create a webhook for a channel, hand its ingest URL to your CI, monitoring, or any service that can POST JSON, and messages flow in automatically.

- **Create a webhook** — `POST /team-chat/webhooks` with `{ channelId, workspaceKind, workspaceId, name, avatarEmoji? }`. Returns the webhook object including its unique ingest token.
- **Ingest** — `POST /team-chat/webhooks/ingest/:token` with `{ text }`. The token in the URL is the credential (no Authorization header needed). Two optional fields extend the basic shape:
  - **`embeds`** — up to 10 Discord-style cards, each `{ title?, description?, url?, color?, fields?, footer? }` with at most 25 fields per card, for richer formatting than plain text.
  - **`username`** — overrides the webhook's display name for this one delivery (`""` means no override).

  Payloads from a recognized provider are converted to text automatically: send a GitHub webhook here unchanged and the `x-github-event` header plus its body are rendered into a readable message. GitHub is the only provider currently recognized — anything else falls through to whatever `text` you sent. The exact accepted shape is specified in `.claude/memory/contracts/team-chat-webhooks-contract.md`.

  Rate-limited to **30 per 5 minutes per webhook** and **60 per 5 minutes per channel** by default — a deployment can raise or lower both via the `AMC_WEBHOOK_PER_WEBHOOK_LIMIT` / `AMC_WEBHOOK_PER_CHANNEL_LIMIT` env vars (see `.env.example`), each clamped to a minimum of 1.
- **List** — `GET /team-chat/webhooks?channelId=...&workspaceKind=...&workspaceId=...` returns all webhooks for a channel.
- **Regenerate token** — `POST /team-chat/webhooks/:id/regenerate` revokes the old ingest URL and issues a new one.
- **Delete** — `DELETE /team-chat/webhooks/:id` soft-deletes a webhook (its ingest URL stops working immediately).

All management routes (create/list/regenerate/delete) require the standard bearer token. The ingest route deliberately does not, so external services only need the URL.

### Guest access

Invite external people into specific channels without giving them full workspace membership. Guests see only the channels they are invited to.

- **Invite a guest** — `POST /team-chat/guests/invite` with `{ email, channelIds, workspaceId?, workspaceKind? }`. The invitee gets access to the listed channels.
- **Remove a guest** — `POST /team-chat/guests/remove` with `{ guestUid, workspaceId?, workspaceKind? }`. Revokes all channel access.
- **List guests** — `GET /team-chat/guests` (optionally `?channelId=...` to scope to one channel). Returns the workspace's guests.

Guest access is feature-flagged; all routes return `404` when the flag is off. All routes require the standard bearer token.

### Alert routing (alert-to-channel bridge)

Inbox alerts tagged with a `chatRouteTag` can be automatically routed into configured Team Chat channels. This is a one-way bridge — alerts surface in chat; chat does not create alerts.

- **Configure a route** — `PUT /team-chat/alert-routes` with a `sourceType` (from the `ALERT_ROUTE_SOURCES` registry), `channelId`, `workspaceKind`, `workspaceId`, and `enabled` flag.
- **Delete a route** — `DELETE /team-chat/alert-routes/:id`.
- **How it works** — when `createAlert()` is called with a `chatRouteTag`, `bridgeAlertToChat` queries SQLite for all enabled routes matching that source type, rate-checks each channel (10 alerts per channel per 5-minute window), and emits an IPC push. The renderer picks up the push and posts the message into the target channel.
- **Source-type registry** — a closed set of 10 routable categories organized by group: **System** (Agent Activity, Auto-Lander, Automations, Build Breaks, Spend Alerts) and **Mission Control** (Board Activity, Due Date Alerts, Daily Digest, Ops Actions, Sync Issues). New sources are added to `ALERT_ROUTE_SOURCES`.
- **Loop guard** — alerts with a `dedupKey` starting with `alert-route:` are skipped to prevent re-routing.
- **Non-fatal** — routing failures never block alert creation.

Contract: [team-chat-alert-routing-contract.md](../../.claude/memory/contracts/team-chat-alert-routing-contract.md) (AR1-AR6).

### Going live (operator)

The core is deployed; what remains is operational. For the workspace, in order:

1. **Push key (Firebase console).** Cloud Messaging → Web Push certificates → generate the public VAPID key. Set `VITE_FCM_VAPID_KEY` (+ the real public `VITE_FIREBASE_*` web config — see [`.env.example`](../../firebase/team-chat-pwa/.env.example)), rebuild, and `firebase deploy --only hosting:agentmc-teamchat`. The build injects the real config into the service worker (`the-service-worker-gets-the-real-public-config-at-build`), so push then fires.
2. **Members backfill.** From an owner/admin account run `backfillOrgMembers` for the workspace, then confirm coverage (active members == active `members/` docs). This MUST complete before anyone opens chat, or they hit a silent rules denial → [team-chat-identity-contract.md](../../.claude/memory/contracts/team-chat-identity-contract.md).
3. **Turn it on (mobile-first).** The team's primary client is the **mobile PWA** — it works for any workspace member at `agentmc-teamchat.web.app` the moment their member doc exists. The desktop `teamChatEnabled` toggle is **per-install**, so flipping it only enables your OWN desktop; teammates who also want it on desktop turn on Settings → Lab themselves until Team Chat ships to everyone.

## Related

This is one of four pages describing Team Chat. The [parent page](team-chat.md) describes the feature and what the first version can do. Its three companions are [part 2](team-chat-part-2.md) on the second wave of features and workspaces, [part 3](team-chat-part-3.md) on the phone client, notifications and the smaller surfaces, and [part 4](team-chat-part-4.md) on the limits and the engineering behind it.

Two pages outside this set are worth having open alongside it: [cross-org-connections.md](cross-org-connections.md) for connecting with someone in another organization, and [session-starter-bot.md](session-starter-bot.md) for the bot that starts a Claude session from a channel message.
