[feat] Connect Slack and Telegram from the agent page - #6737
Conversation
…anch channels/telegram-ui)
…ngs-ui (F1) Scaffold only: ChannelsPage/ChannelConnectFlow/ChannelManagePanel + helpers/icons/ types under agenta-settings-ui/src/channels/. Imports resolve (@agenta/ui/ui + phosphor are package deps). NOT exported from the package index and imported nowhere, so the app build is untouched. Wiring to the real backend follows (F2/F3).
…ls data layer (F2) web/oss/src/state/channels/api.ts: createTelegramHostedBindLink (direct authed call via fetchJson, pending codegen) and archive/unarchive connection wrappers over the generated client. Used by the agent-page connect flow wiring (F3).
… rail slot) in STATUS.md
…al connections (F3/F4 first pass) - export ChannelsPage from @agenta/settings-ui. - AgentChannelsCard (web/oss): loads real connections (queryChannelConnections), maps them to the design's per-platform shape, renders ChannelsPage in an antd Drawer. Read-only first pass: shows real connected state for Slack + Telegram on the agent page; connect/disconnect actions wired next. - AgentOverviewBody: a channels rail slot; AgentOverview passes AgentChannelsCard. Does not touch the old Settings channels screen.
… page (F3)
Opening the Telegram connect panel now mints the real one-time bind link via
createTelegramHostedBindLink({application:{id:appId}}) (which also ensures the
project's hosted connection), and the 'Continue in Telegram' button opens that
real deep link; a preparing state shows until it returns. Threaded an optional
onConnectHostedTelegram action through ChannelsPage -> ChannelConnectFlow, default
falls back to the first-pass placeholder so the component still compiles standalone.
Compiles cleanly on the stack. Browser QA + QR image + disconnect/Slack wiring next.
…s clean + pushed)
ChannelManagePanel's Disconnect now calls the real archiveChannelConnection via an onDisconnect action threaded through ChannelsPage; the design connection carries the backend connectionId (added to the type), supplied by AgentChannelsCard from the mapped rows. Keeps the panel open on failure. Compiles on the stack.
…not channels code)
…s (Codex P2 #5,#6) - schemas.ts: the nested flags object dropped is_hosted (strict nested object); added it + passthrough, and typed deleted_at. Hosted now maps to hosted, not custom. - AgentChannelsCard.mapConnections skips rows with deleted_at, so an archived connection no longer shows as connected or blocks reconnect.
…tion 1, Codex P1 #4 backend) ensure_hosted_telegram_connection now points the project's one hosted Telegram connection at the CALLING agent: if the answering (default) agent already matches, it is a no-op; if a different agent answers, its references are retargeted to this app. This is the backend for 'disconnect from agent X and connect here'. Added _reference_id (str-normalized id compare) and tests for retarget + no-op.
… and history docs
…onnect (Codex P1 #3) The Telegram link-click no longer manufactures a fake connection with no id. Instead AgentChannelsCard refetches connections after minting the link (the mint already ensured the real connection and retargeted it to this agent) and after disconnect, so the card reflects the real backend state with the real connection id -> Disconnect now targets a real connection.
…t (Codex P1 #7) A failed bind-link mint (e.g. 404 when the hosted bot is not configured) now shows a clear error and a Try again button instead of leaving the panel on 'Preparing your link…' forever.
…chive - GET /catalog/channels/telegram_hosted/bindings/?connection_id= returns the chats a /start bound to the hosted connection (project-scoped, 404 without a hosted bot). The connect UI polls it to tell a minted link apart from a confirmed bind. - The bind-link response carries connection_id. - unarchive_connection restores the agents archived with the connection (same instant). A reconnect after a disconnect recreated the default agent and hit the (connection, slug) and one-default unique keys with a 500 on the mint. Claude-Session: https://claude.ai/code/session_011TDtGKUC7vQxxVjusKHMaH
…#4) AgentChannelsCard resolves the connection's answering agent (its default channel-agent's referenced app id) and its name from the apps list, and sets answeredHere + answeringAgentName on the design connection. The card now shows: not connected -> Connect; connected as this agent -> manage; connected as another agent -> 'Answers as <name> · connect here', and opening it shows the connect flow with a note that connecting switches Telegram to this agent (the backend retarget makes it real). Compiles on the stack.
… Codex round-two fixes - Drop the duplicate answeredHere / answeringAgentName fields that a parallel commit added on ChannelConnection; connection.agent + connectionScope() already carry the three states. This restores the package type check. - A connection with no answering agent reads as "unassigned": the row and the manage panel offer "Connect here" instead of hiding a failed assignment. - The hosted Telegram poll runs while the QR is on screen too, so a phone scan is detected without the button click; the link expiry is fixed once at mint. - The custom setup guard resets on cancel, so leaving and returning loads again. - The QR surface is dark-on-light in both themes. - When the backend holds more than one connection per platform, a live one wins over a pending one, and a pending one over a revoked one. - Both hosts ignore a reload that finishes after a newer one. - API: archiving an already archived connection keeps its original timestamp, so the agents archived with it still come back on unarchive. Claude-Session: https://claude.ai/code/session_011TDtGKUC7vQxxVjusKHMaH
|
Codex (gpt-astra, xhigh) reviewed the full diff. Verdict: the shared desktop and mobile presentation is sound; the connection lifecycle is not yet reliable. Fixed on the branch in 62ddd63: one agent-scope model (a parallel commit had added a second one and broke the type check), an "unassigned" state so a failed agent assignment is visible and repairable, the bindings poll on the QR step with one fixed expiry, the custom-setup guard reset, a live row preferred over a pending one, stale reloads ignored in both hosts, idempotent archive, and a fixed dark-on-light QR surface. Deferred, because they need backend work or a wider refactor:
The full list with file references is in |
Railway Preview Environment
|
…nders Markdown - The turn-start indicator on Telegram is now a real "Thinking…" message plus the typing action. While the turn runs, the outbox keeps the chat alive: it re-sends the typing action every four seconds (a new adapter port, signal_activity, a no-op on platforms without one), moves the indicator's dots, and edits the answer so far into the message as records land. The loop stops on turn_ended, on a done record, or after twenty minutes. - A turn that ends with no answer edits the indicator into a plain "the run failed" line instead of leaving "Thinking…" forever. - The render layer converts the agent's Markdown into Telegram's HTML tag set (headings, lists, code, bold, italic, links, quotes, tables), dependency-free, and chunks between blocks so no message holds a half tag. The adapter escapes only plain parts; html parts pass through. Claude-Session: https://claude.ai/code/session_011TDtGKUC7vQxxVjusKHMaH
… as another agent Claude-Session: https://claude.ai/code/session_011TDtGKUC7vQxxVjusKHMaH
…nd a Channel debug switch - Removed the Slack hosted/own-app sections and the agent roster from Settings (and the drawers, forms and tests only they used): connecting and choosing the answering agent happen on the agent page now. buildSlackInstallUrl moved to state/channels/api. - Spaces (not functional yet), threads, inbox events and outbox events render only when the per-user "Channel debug" preference is on, on desktop and /m, the same pattern as the playground inspector switch. - Import-order lint fixes in the foundation files that kept the stack's lint job red. Claude-Session: https://claude.ai/code/session_011TDtGKUC7vQxxVjusKHMaH
…al switches it off
- connection.data.allowed_senders ("Allowed users" on the agent page): when the
list names anyone, resolve() drops a sender outside it before any space is
provisioned. Empty means everyone.
- A Telegram 401 on a bot-API call raises ChannelCredentialRevoked; the outbox
records the row as credential_revoked and deactivates the connection, so the
card reads "token revoked" instead of retrying forever.
Claude-Session: https://claude.ai/code/session_011TDtGKUC7vQxxVjusKHMaH
…rants, and the allow-list - Summary rows (bot, workspace or account, token with Update for a custom bot or app, status, connected date), "Answers in" from the connection's spaces with a Slack channel picker over discovery and a Telegram hint, the two Behavior switches written as one grant per chat kind (both on removes them), Allowed users on Telegram saved to the connection's allow-list, a collapsed Advanced section with the shipped defaults, and Disconnect behind a confirm. - Connected to another agent: the retarget offer plus "Use your own bot for this agent", which opens the connect flow in custom mode. - A platform may hold a hosted and a custom connection; the card shows the one that answers as this agent first. Revoked credentials read as such on the row and open the token update or a reconnect. - The Telegram unavailable state links to the self-hosting docs page. - Stories for every new state. Claude-Session: https://claude.ai/code/session_011TDtGKUC7vQxxVjusKHMaH
|
Update after the design review on the canvas. The connected view is now the agreed design and wired for real: summary rows with status, "Answers in" from the connection's spaces (Slack channel picker over discovery, Telegram hint), the two Behavior switches written as one grant per chat kind, Allowed users on Telegram enforced by a new allow-list on the connection, Advanced collapsed, Disconnect. "Connected to another agent" adds "Use your own bot for this agent". A Telegram 401 now deactivates the connection and the card says so. Telegram shows progress while the agent works and renders Markdown. Settings > Channels shows connections only, with the logs behind a "Channel debug" preference. A self-hosting docs page for the hosted bot is included. Live-checked on the dev stack: switches write and remove the grant rows, the allow-list drops an outside sender and lets a listed one through (answer received), the own-bot path opens the custom flow. Not verified live: the Slack channel picker and a real token revocation (both unit-tested and in stories). |
compose_input puts a "From <name> (@username, <platform> id <id>):" text part before each message's own words, so an agent that serves several people can address them and keep them apart. The Telegram adapter now keeps the sender's display name and username next to the id; Slack sends only the id, which is what the agent gets there. Claude-Session: https://claude.ai/code/session_011TDtGKUC7vQxxVjusKHMaH
Stacked on #6724 (hosted Telegram bot), which sits on #6679 (custom Telegram bot). Review the diff against
channels/telegram-hosted. Do not merge yet: the whole channels stack merges bottom-up into the release branch.Context
Connecting an agent to Slack or Telegram lived only in Settings > Channels, a technical screen built around connections, agents, spaces and grants. The designed connect screen belongs on the agent page: a user opens an agent, taps Connect, and the agent answers in the chat. A channel connection is one per project and answers as one agent, so the card has to say which agent a connection points at, and let this agent take it over.
Changes
The agent overview gets a Channels card in its rail (desktop and /m). Each platform row is in one of three states:
A hosted Telegram connection is created the moment a link is minted, before anyone taps Start in Telegram. So a fourth row state, "Not linked yet", covers a connection with no bound chat and always opens the connect flow instead of the manage view. Two small backend additions make that possible: the bind-link response returns the connection id, and a new read endpoint lists the chats bound to a hosted connection.
Before:
After:
The flows:
One builder in
@agenta/settings-uiholds every backend call and the row mapping; the desktop host wraps it in an antd Drawer and the /m host in a responsive Sheet. Per the frontend conventions the calls go through the generated client, which is regenerated for the channels resource only.A bug found during the live run is fixed on the way: after Disconnect, Connect again returned a 500. Archiving a connection archives its channel agents, but unarchiving did not restore them, so the reconnect tried to create a second "default" agent and hit the unique keys. Unarchive now restores the agents that were archived with the connection.
Left out on purpose, so nothing shows as a placeholder: the chat list, the direct-message and group switches, the advanced policy rows and the allowed-user-id list. The old Settings screen is untouched.
Tests
tscclean for@agenta/settings-ui,web/ossandweb/mobile; eslint clean; the QR encoder has 7 unit tests; Storybook stories cover every card and flow state (co-located in the package)./startfrom a Telegram account, the hosted Slack redirect reaching Slack's authorize page, the custom Slack manifest and fields, /m, dark mode. Captures: https://claude.ai/code/artifact/05425944-2133-40f2-9c51-35fa726fa245What to QA
https://claude.ai/code/session_011TDtGKUC7vQxxVjusKHMaH