From f8b35b331835e781f66d817dbc7c6a672f390f82 Mon Sep 17 00:00:00 2001 From: Dotta <34892728+cryppadotta@users.noreply.github.com> Date: Mon, 2 Nov 2026 21:33:59 -0600 Subject: [PATCH] fix(ui): retain Slack conversation handoff after setup (#18722) ## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - Slack setup connects a person with an agent in Slack. > - Welcome delivery can complete setup before the person chooses Open Slack. > - Success and saved settings then offer no link to the conversation. > - This pull request retains Open Slack in both views. > - The benefit is that people can use the connected bot without finding its URL elsewhere. ## Linked Issues or Issue Description Refs: #18707. This is the next problem found during its deployed user walkthrough. **What happened?** Managed setup completed after welcome delivery. The Open Slack link disappeared. Success offered Done, and saved settings offered only Slack app configuration. The bot could answer, but the handoff to its conversation was lost. **Expected behavior** A person can open the connected bot from Success and from the saved connection. **Steps to reproduce** 1. Complete managed Slack setup with welcome delivery. 2. Wait for automatic Success before choosing Open Slack. 3. Choose Done and open the saved connection. No conversation link is available. ## What Changed - Retain Open Slack on Success while keeping the centered Done action. - Add Open Slack to completed Slack connection pages. Keep app configuration separate. - Use a saved DM only when its linked owner matches the current signed-in account. Other managers and unknown identities use the provider-link or Slack fallback. - Update the existing component regressions, three Storybook cases, setup guidance and feature recipe. The Slack UI owns the shared URL helper. This change adds no API or permission contract and leaves unfinished setup on its existing path. ## Verification - Focused Slack component and prompt suites: 40 passed. All related chat UI suites: 269 passed across 19 files. The settings regression switches from the installer to a second user and then to a signed-out state. - Repository typecheck and production build: passed. - Token gates, map structure and Storybook catalog checks: passed. The map/catalog Node suite passed all 30 tests; these are structural checks. - Storybook managed and own-app Success play assertions passed. The managed mobile view retained readable Open Slack and Done controls after scrolling. Saved settings play assertions passed after a fresh page load. Fixtures simulate Slack; no live provider actions were sent from Storybook. - The first local typecheck rejected an unsupported Storybook assertion option. It passed after using an exact-name regular expression. The settings story first hit a stale Vite dynamic-import cache; a normal reload fetched the current module and all settings play assertions passed. Restarting the owned review server later emitted Storybook manager state timeouts; all three production-page play cases passed again on the corrected head. - The preceding deployed fix produced real Slack DM and thread replies. This follow-up still needs deployed acceptance through its visible Open Slack link. - Greptile first found that a second manager could receive the installer's DM link. The correction checks the linked owner against the resolved account and falls back while identity is unknown. The two threads have replies and are resolved. Greptile reviewed corrected head 94a6946719e33391395d3c1dd482081e9118fad6 at 5/5 with no new findings. The optional Paperclip agent check ended neutral without a complete assessment; one fresh review was requested. No fresh secondary assessment was available at merge. - [All required CI suites passed](https://github.com/paperclipai/paperclip/actions/runs/41571276944) on corrected head 94a6946719e33391395d3c1dd482081e9118fad6. Full local Vitest was not repeated for this UI-only follow-up. ## Risks - Low risk. This adds navigation to existing Slack destinations. - Old connections without a known DM use their provider link or Slack. - No schema, credential, scope, identity-link or transport changes. The UI consumes the existing shared account identity query. ## Model Used OpenAI Codex, GPT-6 family, with reasoning, repository editing, browser use and command execution. The runtime does not expose a more precise model ID or context-window size. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --- DESIGN.md | 6 ++++-- doc/connections/slack-setup.md | 4 ++++ maps/feature-map/chat-channels.md | 4 ++++ .../apps/chat/ChatEndpoint.clipboard.test.tsx | 20 ++++++++++++++++++- ui/src/pages/apps/chat/ChatEndpointDetail.tsx | 4 ++++ ui/src/pages/apps/chat/ChatEndpointSetup.tsx | 7 ++++++- ui/src/pages/apps/chat/SlackSetupPrompt.tsx | 2 +- .../pages/apps/chat/slack/conversation-url.ts | 8 ++++++++ .../chat-management.stories.tsx | 1 + .../slack-automatic-setup.stories.tsx | 1 + .../slack-managed-setup.stories.tsx | 5 ++++- 11 files changed, 56 insertions(+), 6 deletions(-) create mode 100644 ui/src/pages/apps/chat/slack/conversation-url.ts 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 ?