Technical docs
Internal Chat
Overview
Support reference for the current internal chat UI.
Current chat modes
- The current frontend chat peer modes are:
- `aiTeammate`
- `ally`
- Sidebar `Chats` opens the shared chat surface. It is an action item, not navigation to a dedicated `/ai` page.
- `Alt+C` toggles `Chats`.
- Internal chat uses one shared session model for panel, floating, and minimized views.
- On short desktop viewports, docked and floating chats stay open instead of auto-minimizing. Floating chat position and size are normalized to fit the available viewport.
What users can do
- Start a new conversation, open history, rename a conversation, or delete it.
- In the shared `Chats` surface, users first see a history screen across Ally and AI teammates, then open a conversation or start a fresh chat from a target chip.
- On a shared chat screen, the conversation selector is scoped to the current Ally or AI teammate target in both panel and floating layouts, even when the shared module is hosted by Ally.
- A new-chat welcome screen shows up to three recent conversations for that target. If more exist, `View All` opens the same per-target conversation selector.
- The cross-teammate shared history screen is separate and does not show the per-target selector.
- The shared panel, floater, and Docs chat host use the same organization-level internal-chat history source.
- History loads `20` conversations per page and automatically loads another page when the user reaches the list sentinel.
- Entering at least `3` search characters automatically searches conversation titles and message content across the current user's Ally and AI teammate chats.
- The target strip uses active teammates and includes the current user's personal Ally.
- Conversation history rows can show a generated conversation icon when the backend provides a valid `conversation_icon_id`; otherwise they render text-only identity without that icon tile.
- Send text, reply to a message, stop generation, and send attachments.
- Internal-chat sends include the current page path as optional message context for the AI workflow. While Docs is open, they also include the open document's storage path. Selecting non-blank document text adds that text to the next send; Markdown source and raw code/JSON selections include 1-based line and column positions, while visual Markdown selections include text without source coordinates.
- The composer shows selected text in a chip labeled `Selected text will be sent as context`. Its `Remove selected text from context` button drops the selection from the next send without closing the document. Hovering or focusing the chip shows a preview of up to 300 characters. The document selection remains visibly highlighted when focus moves to chat, and selection context clears on a different document, when switching between visual and source editors, or when leaving Docs. The document path is still sent without selected text.
- The internal-chat send endpoint accepts an optional `context` object with `current_page` required when the object is supplied, and optional `document_path`, `selected_text`, `selection_start_line`, `selection_start_column`, `selection_end_line`, and `selection_end_column`. The backend places this context in the workflow runtime and AI prompt for the message; it is not a separate attachment or a change to the user message text.
- Use `Enter` to send from the current internal-chat composer.
- Use `Shift+Enter` or `Alt+Enter` to insert a newline without sending.
- Use `@` teammate mentions and `#` storage mentions when mention sources are available.
- `@` mentions resolve from the teammate catalog for the current org. In AI-teammate chat, that list can also include Ally; the currently open teammate is excluded from the mention list. Personal Ally employees are excluded from the mention catalog.
- `#` opens the `Files and folders` source and searches both entry types on demand. Results show the entry name and full storage path.
- In AI teammate chat, `#` results are limited to that teammate's readable shared folders and, when Access Group rules are enforced, the initiating user's readable folders. In personal Ally chat, search follows the owner's readable Storage scope when enforcement is on; otherwise it can span the organization's Storage tree.
- Selected storage mentions preserve whether the reference is a file or folder and are displayed as `#name` in message text.
- Recall previously sent user messages with `ArrowUp` / `ArrowDown` when the composer is otherwise idle.
- Leave feedback on assistant replies:
- `Like`
- `Dislike`
- `Feedback` comment editor
- Failed system-error assistant messages support comment-only feedback.
- Use voice mode for Ally and AI teammate chat when that chat has realtime voice enabled.
Reasoning and display behavior
- The composer shows a `Reasoning` control only when the resolved model supports reasoning.
- User-facing reasoning selections are:
- `Off`
- `Low`
- `Medium`
- `High`
- Reasoning preference is persisted in local storage and scoped by user, org, peer, entity, and conversation. A draft selection can carry forward into the first persisted conversation.
- The reasoning selector is hidden while voice mode is active.
- Assistant messages can persist a preface with reasoning, tool calls, and source cards, so those details survive reloads.
- Message and reasoning text rendered through the shared AI Markdown component supports bare HTTP(S) URLs, inline-code URLs, explicit Markdown links, and angle-bracket autolinks.
- When a bare HTTP(S) URL contains a malformed trailing backtick at a punctuation, symbol, whitespace, or end-of-text boundary, the renderer removes the backtick from the URL and keeps any following punctuation or prose outside the link. Explicit Markdown links, angle-bracket autolinks, and encoded backticks are not rewritten by this repair.
- Same-origin links use in-app navigation. External links open in a new tab.
- Chats that are already at the bottom stay pinned during streaming, thinking updates, and panel/viewport resize. If the user scrolls away, expanding a reasoning/tool section does not force them back to the bottom.
- Message context menus show the exact `Sent` time to all users.
- Organization admins and owners additionally see `Started`, `Ended`, and exact processing `Duration` for completed automated replies when the stored timing is trustworthy.
- Developers additionally see author, role/type, shortened conversation and message identifiers, status/streaming state, and available feedback metadata.
- Processing timing is omitted for streaming, failed, in-progress, placeholder, and system-error messages. For eligible completed replies, the UI uses persisted completion time or derives it from the persisted generation duration.
Mobile and onboarding behavior
- On mobile, the internal-chat panel renders full width above the shared bottom nav instead of as a desktop-sized floating panel.
- Tapping outside the mobile panel overlay closes it.
- The onboarding URL flag `?onboarding=true` auto-opens Ally once the scoped chat state has restored.
- Onboarding Ally bootstrap prefers the latest server conversation once per user/org scope, so users land in the current onboarding thread instead of a blank draft.
- The onboarding floater is positioned bottom-right on desktop when the saved chat mode restores as a floating session.
REST endpoints the current UI uses
- Cross-teammate history for the current user:
- `GET /api/organizations/{orgId}/internal-chat/conversations?page=...&limit=...`
- `GET /api/organizations/{orgId}/internal-chat/conversations/search?q=...&limit=...`
- The list response includes `page`, `limit`, `total`, and `hasMore`. Rows include conversation title/icon/preview fields plus the associated AI employee identity.
- List rows have a last message, belong to the current user through an active internal-chat participant, and exclude deleted conversations and deleted AI employees.
- List and search results are additionally authorization-scoped. Owners and admins can access conversations for every AI employee in the organization, including personal Allies. Members are limited to regular AI teammates granted through Access Groups plus their own personal Ally. Removing a member's access to a teammate removes that teammate's conversations from history even when the member was a participant.
- Ally employee resolution:
- `GET /api/organizations/{orgId}/employees/ally`
- The current frontend resolves the signed-in user's personal Ally employee first, then uses that employee ID with the shared employee chat routes below.
- AI teammate and Ally chat:
- `GET /api/organizations/{orgId}/employees/{employee_id}/messages`
- `POST /api/organizations/{orgId}/employees/{employee_id}/messages`
- `POST /api/organizations/{orgId}/employees/{employee_id}/conversations/new`
- `GET /api/organizations/{orgId}/employees/{employee_id}/conversations`
- `GET /api/organizations/{orgId}/employees/{employee_id}/conversations/search`
- `PATCH /api/organizations/{orgId}/employees/{employee_id}/conversations/{conversation_id}/title`
- `DELETE /api/organizations/{orgId}/employees/{employee_id}/conversations/{conversation_id}`
- `POST /api/organizations/{orgId}/employees/{employee_id}/conversations/{conversation_id}/stop-generation` with `{ "run_id": "<uuid>" }`
- Stop-generation behavior:
- The request targets one exact conversation and generation run.
- A run mismatch returns `404 Generation not found`.
- A run that is already terminal returns `200` with `Generation is not in progress`.
- After the server accepts an abort, the frontend disables repeated stop requests until the tracked run reaches its terminal state.
- Capability bootstrap used by the current frontend:
- `GET /api/init/internal-chat-capabilities`
- Current response only exposes Ally model availability.
Feedback and realtime
- Canonical internal-chat feedback endpoint:
- `PUT /api/organizations/{orgId}/conversations/{conversation_id}/messages/{message_id}/feedback`
- Feedback comments are capped at `500` characters.
- Only the conversation owner can update feedback.
- `429` feedback responses are surfaced as rate-limit failures, and `409` is used for messages that do not accept feedback.
- Internal chat displays persisted system notifications as system messages in the transcript, including after reload. The backend publishes new system logs only after their database transaction commits, using `internal.message` for internal conversations. System notices are not ordinary assistant replies.
- Internal chat subscribes to:
- `internal.message`
- `internal.message_updated`
- `voice_thinking_started`
- `voice_thinking_ended`
- `internal.message_updated` is the channel used to sync feedback edits and other message updates into the transcript.
- Streaming tool activity uses `tool_call_started`, `tool_call_done`, and `tool_call_failed` events. Pending text and reasoning buffers are flushed before a tool event so transcript ordering stays stable.
- Oversized tool payloads can be omitted from websocket events and loaded from the persisted message `content_payload`. The final `message_done` event is authoritative for completed message content.
- An unfinished assistant message remains the durable signal that generation is active after a reload.
- Messages with status `in_progress` are not rendered in the transcript or conversation previews.
Attachments and voice
- Shared chat input enforces attachment limits from `NEXT_PUBLIC_CHAT_MAX_ATTACHMENT_SIZE_MB` and `NEXT_PUBLIC_CHAT_MAX_TOTAL_ATTACHMENT_SIZE_MB` (defaults `10 MB` per file and `30 MB` total), with at most `10` attachments per message. The backend applies `CHAT_MAX_ATTACHMENTS`, `CHAT_MAX_ATTACHMENT_SIZE_MB`, and `CHAT_MAX_TOTAL_ATTACHMENT_SIZE_MB` (defaults `10`, `10 MB`, and `30 MB`); keep frontend and backend settings aligned.
- `Too many attachments` toast when the file-count cap is exceeded
- `File too large` toast when a file exceeds the per-file or total-size cap
- drag/drop overlay text `Drop files here`, or `Attachment limit reached` when the cap is already hit
- The shared chat composer now uses the same supported extension list as Storage uploads (see `storage.md`), including additional images, office and Apple documents, fonts, source files, SQLite databases and WAL/SHM companions. A selectable file must still pass backend extension/content validation; file type support does not imply inline rendering or AI parsing. The server uses the validated extension's canonical MIME instead of trusting the browser-supplied MIME.
- ZIP files can be selected or dropped in the shared composer, but the archive itself is not sent as an attachment. The browser extracts accepted entries in a worker and attaches the individual files subject to the same file-count, per-file, and total-size limits. Nested ZIPs and unsupported entry types are skipped, with an `Unsupported file type` notice for skipped files. While processing, the composer shows `Extracting ZIP…`; invalid, password-protected, or oversized archives show `Could not extract ZIP`. An XML attachment declared as `text/xml` or `application/xml` is accepted when detection finds the other equivalent XML MIME type.
- Message attachment URLs returned to authenticated internal chat clients use `/shared/attachments/{organizationId}/attachment/{attachmentId}` instead of direct object-storage URLs.
- Spreadsheet chat attachments are prepared as workflow attachment context for CSV and XLSX when the backend can parse them.
- File attachments included in AI prompt history use run-scoped shared URLs shaped `/shared/attachments/{organizationId}/run/{runId}/attachment/{attachmentId}`; the route returns `File not found` after the workflow run is finished.
- `.txt` and `.log` attachments use `text/plain`; `.md` and `.markdown` use `text/markdown`. The backend assigns canonical MIME metadata for accepted uploads based on file extension.
- Clipboard-pasted generic image names are rewritten to `screenshot-...`.
- During an active internal voice call, the composer can send files with optional text over the voice connection instead of the regular message POST. The connection must advertise attachment-message support; unsupported or disconnected sessions reject the send. A send waits for server acceptance after the user message is saved. If sending fails, the text and attachments remain in the composer for retry; successful sends clear them. Only one attachment send is pending at a time. If an already saved message cannot be delivered to the AI, the UI shows `Message saved, but attachments could not be delivered to AI`.
- Realtime voice session creation for AI teammates and Ally uses:
- `POST /chat/realtime/employee/create-session/{organization_id}/{employee_id}`
- Ally uses the personal employee ID returned by `GET /api/organizations/{orgId}/employees/ally`.
- Voice session creation returns a `conversation_id` so the chat UI can bind the realtime session to the correct conversation.
- Employee voice session creation requires exactly one of:
- `conversation_id`
- `create_new_conversation`