diff --git a/DESIGN.md b/DESIGN.md index 9d53a3c844b..8eada0c3c8d 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -44,8 +44,10 @@ footer: do not render Save & exit in a separate parent block below it. Check thi alignment in every step and conditional state, not just the first screen. Completed Slack setup is a success view: show the full agent avatar, success message, -and one centered Done action. Omit Save & exit after the connection has been -confirmed. The shared footer rule applies to its unfinished setup steps. +and one centered Done action. Retain an Open Slack link in the success message +and on the completed connection page so automatic completion does not remove +the way to use the bot. Omit Save & exit after the connection has been confirmed. +The shared footer rule applies to its unfinished setup steps. The New task form opens immediately. Load its code with the board and fill agent and project choices in place as their requests finish. Keep text entered diff --git a/doc/connections/slack-setup.md b/doc/connections/slack-setup.md index 51be34335b7..cdb39ccbd7c 100644 --- a/doc/connections/slack-setup.md +++ b/doc/connections/slack-setup.md @@ -163,6 +163,10 @@ The action records a durable request before sending. Repeating the same request returns its saved result; an uncertain delivery is never automatically resent. **Open Slack configuration** opens the saved app's OAuth & Permissions page. Legacy connections without an app ID open Slack's app list instead. +**Open Slack** remains available after automatic setup completion and on the +saved connection page. It opens the current user's bot DM when its workspace +and channel are known and its linked owner matches the signed-in user. Another +manager or an unknown identity uses the saved provider link or Slack instead. An explicit managed reconnect reinstalls the saved app and verifies the replacement bot credentials. Its request ID is saved before dispatch, so a duplicate completed diff --git a/maps/feature-map/chat-channels.md b/maps/feature-map/chat-channels.md index 7db0748a607..c6d2e34dfed 100644 --- a/maps/feature-map/chat-channels.md +++ b/maps/feature-map/chat-channels.md @@ -55,6 +55,10 @@ must resolve the selected Slack manager grant, reject malformed or missing IDs, and deny another company's or person's grant before provider calls. The channel integration suite exercises this route with a board fixture and provider fixtures; live Slack setup remains a separate acceptance check. +After welcome delivery completes setup automatically, verify **Open Slack** +remains on Success and on the saved connection page. Follow that visible link, +send a real DM and thread follow-up, then reload the catalog to confirm setup +stays complete. A welcome message alone is not an agent response. ### `external-thread` diff --git a/ui/src/pages/apps/chat/ChatEndpoint.clipboard.test.tsx b/ui/src/pages/apps/chat/ChatEndpoint.clipboard.test.tsx index 8b30ba2da62..dc6c52282e3 100644 --- a/ui/src/pages/apps/chat/ChatEndpoint.clipboard.test.tsx +++ b/ui/src/pages/apps/chat/ChatEndpoint.clipboard.test.tsx @@ -488,6 +488,8 @@ describe("chat setup and identity-link clipboard actions", () => { expect(container.querySelectorAll('aside nav button')).toHaveLength(4); expect(container.querySelector('h1')?.textContent).toBe("Success"); expect(container.textContent).toContain("You can now talk to Maya in Slack"); + expect([...container.querySelectorAll("a")].find(link => link.textContent?.trim() === "Open Slack")?.getAttribute("href")) + .toBe("https://app.slack.com/client/workspace-a/DTEST"); expect(container.querySelector('[aria-label="Maya avatar"]')).not.toBeNull(); expect(container.querySelector('main')?.textContent ?? container.textContent).not.toContain("Save & exit"); expect(container.textContent).not.toContain("Copy message"); @@ -505,9 +507,25 @@ describe("chat setup and identity-link clipboard actions", () => { it("keeps settings focused on channel access and the test-message action", async () => { mocks.tab = "settings"; - await render("slack", true); + const endpoint = await render("slack", true); expect(container.textContent).toContain("Send me a test message"); expect(container.textContent).toContain("Open Slack configuration"); + expect([...container.querySelectorAll("a")].some(link => link.textContent?.trim() === "Open Slack")).toBe(false); + flushSync(() => client.setQueryData(queryKeys.chatEndpoints.detail(endpoint.id), { + ...endpoint, status: "active", providerAccountId: "workspace-a", + setup: { ...endpoint.setup, step: "complete", slackAccount: { externalUserId: "UPERSON", paperclipUserId: "owner-user", status: "linked", dmChannelId: "DTEST" } }, + })); + await settle(); + expect([...container.querySelectorAll("a")].find(link => link.textContent?.trim() === "Open Slack")?.getAttribute("href")) + .toBe("https://app.slack.com/client/workspace-a/DTEST"); + flushSync(() => client.setQueryData(queryKeys.auth.session, { user: { id: "other-user" } })); + await settle(); + expect([...container.querySelectorAll("a")].find(link => link.textContent?.trim() === "Open Slack")?.getAttribute("href")) + .toBe("https://app.slack.com"); + flushSync(() => client.setQueryData(queryKeys.auth.session, null)); + await settle(); + expect([...container.querySelectorAll("a")].find(link => link.textContent?.trim() === "Open Slack")?.getAttribute("href")) + .toBe("https://app.slack.com"); expect(container.textContent).not.toContain("Message in a channel"); expect(container.textContent).not.toContain("Agent avatar"); expect(container.textContent).not.toContain("Manage action permissions"); diff --git a/ui/src/pages/apps/chat/ChatEndpointDetail.tsx b/ui/src/pages/apps/chat/ChatEndpointDetail.tsx index cd58f45fa9e..59db46978b5 100644 --- a/ui/src/pages/apps/chat/ChatEndpointDetail.tsx +++ b/ui/src/pages/apps/chat/ChatEndpointDetail.tsx @@ -7,6 +7,8 @@ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { agentAvatarUrl } from "@/lib/agent-avatar-url"; import { resolveAgentAppearance } from "@paperclipai/shared"; import { SlackTestMessageButton } from "./SlackTestMessageButton"; +import { slackConversationUrl } from "./slack/conversation-url"; +import { useAccountIdentity } from "@/api/companies-query"; import { AgentAvatar } from "@/components/AgentAvatar"; import { SlackSetupAdvanced } from "./SlackAppDetails"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; @@ -243,6 +245,7 @@ export function connectionHealthPresentation( } export function ChatEndpointDetail() { + const { userId, settled: identitySettled } = useAccountIdentity(); const { endpointId = "", tab: routeTab, reviewId } = useParams<{ endpointId: string; tab?: string; @@ -357,6 +360,7 @@ export function ChatEndpointDetail() {
Slack needs to confirm that it can reach your Paperclip instance.
} - {automaticSlack ?