mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-04 11:01:16 +01:00
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 head94a6946719at 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 head94a6946719. 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
This commit is contained in:
1 parent
09162fe893
commit
f8b35b3318
11 files changed
+56
-6
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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() {
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{endpoint.provider === "speko" && <a href="https://platform.speko.ai/agents" target="_blank" rel="noreferrer" className="inline-flex items-center gap-1 text-sm text-primary underline">Open Speko<ExternalLink className="size-3.5" aria-hidden="true" /></a>}
|
||||
{endpoint.provider === "slack" && !setupIncomplete && <Button asChild variant="outline"><a href={slackConversationUrl(endpoint, identitySettled ? userId : null)} target="_blank" rel="noopener noreferrer">Open Slack <ExternalLink className="size-4" /></a></Button>}
|
||||
{slackUrl && <Button asChild variant="outline"><a href={slackUrl} target="_blank" rel="noopener noreferrer">Open Slack configuration <ExternalLink className="size-4" /></a></Button>}
|
||||
{setupIncomplete ? (
|
||||
<Button
|
||||
|
||||
@@ -4,6 +4,8 @@ import { buildSlackAppManifest, defaultSlackAppConfiguration } from "@paperclipa
|
||||
import { SlackAppDetails, SlackSetupAdvanced } from "./SlackAppDetails";
|
||||
import { SlackSetup } from "./slack/SlackSetup";
|
||||
import { isProvisionedSlackSetup, slackSetupState } from "./slack/setup-state";
|
||||
import { slackConversationUrl } from "./slack/conversation-url";
|
||||
import { useAccountIdentity } from "@/api/companies-query";
|
||||
import { defaultSlackAppName } from "./slack-app-name";
|
||||
import { GitHubChatSetup } from "./GitHubChatSetup";
|
||||
import { GitHubAgentTrustWarning } from "@/components/GitHubAgentTrustWarning";
|
||||
@@ -175,6 +177,7 @@ export function ChatEndpointSetup() {
|
||||
}
|
||||
|
||||
function ChatSdkEndpointSetup() {
|
||||
const { userId, settled: identitySettled } = useAccountIdentity();
|
||||
const [params, setParams] = useSearchParams();
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
@@ -666,6 +669,7 @@ function ChatSdkEndpointSetup() {
|
||||
<span>You can now talk to {selectedAgent?.name ?? endpoint.assignedAgentName} in Slack</span>
|
||||
<img src="/brands/apps/slack.svg" alt="" aria-hidden="true" className="size-6" />
|
||||
</p>
|
||||
<Button asChild variant="link"><a href={slackConversationUrl(endpoint, identitySettled ? userId : null)} target="_blank" rel="noopener noreferrer">Open Slack <ExternalLink className="size-4" /></a></Button>
|
||||
</div>
|
||||
<Button disabled={testConnection.isPending} onClick={() => testConnection.mutate()}>
|
||||
{testConnection.isPending && <Loader2 className="size-4 animate-spin" />}Done
|
||||
@@ -749,6 +753,7 @@ function ProviderConnectStep({
|
||||
values?: Record<string, string>,
|
||||
) => void;
|
||||
}) {
|
||||
const { userId, settled: identitySettled } = useAccountIdentity();
|
||||
const { pushToast } = useToast();
|
||||
const navigate = useNavigate();
|
||||
const slackBotToken = (credentials.botToken ?? "").trim();
|
||||
@@ -1538,7 +1543,7 @@ function ProviderConnectStep({
|
||||
<h1 className="text-xl font-bold">{automaticSlack ? "Send a message to your agent" : "Verify Slack connection"}</h1>
|
||||
{!automaticSlack && <p className="mt-1 text-sm text-muted-foreground">Slack needs to confirm that it can reach your Paperclip instance.</p>}
|
||||
</div>
|
||||
{automaticSlack ? <Button asChild variant="outline"><a href={endpoint.setup?.slackAccount?.dmChannelId && endpoint.providerAccountId ? `https://app.slack.com/client/${encodeURIComponent(endpoint.providerAccountId)}/${encodeURIComponent(endpoint.setup.slackAccount.dmChannelId)}` : endpoint.setup?.providerUrl ?? "https://app.slack.com"} target="_blank" rel="noopener noreferrer">Open Slack <ExternalLink className="size-4" /></a></Button> : slackRequestUrlInstructions}
|
||||
{automaticSlack ? <Button asChild variant="outline"><a href={slackConversationUrl(endpoint, identitySettled ? userId : null)} target="_blank" rel="noopener noreferrer">Open Slack <ExternalLink className="size-4" /></a></Button> : slackRequestUrlInstructions}
|
||||
{endpoint.setup?.webhookVerifiedAt ? (
|
||||
<p role="status" className="flex items-center gap-2 text-sm">
|
||||
<CheckCircle2 className="size-4 text-(--status-task-done)" />
|
||||
|
||||
@@ -26,7 +26,7 @@ For the own-app path, create Slack app automatically. Review Slack app name, Bot
|
||||
|
||||
7. Prepare the agreed destination. If using a channel, invite/add this bot to that channel through Slack's UI if authorized. In the connection's Settings, inspect Where this agent can work. Use its Refresh action if available, or reload the page after the bot invitation if the channel is missing. Inspect Allowed Channels and make sure the intended test channel is enabled; newly invited channels start enabled, while an explicitly disabled channel stays off until a person re-enables it. Do not invite the bot to unrelated channels, enable every channel, or broaden anyone's access just to get a test to pass. For a DM, check the connection permits direct messages and use my conversation with this bot. Return to the saved wizard if you visited Settings. Reading access and allowed response destinations are distinct; seeing a channel in Slack does not prove the bot is allowed to reply there.
|
||||
|
||||
8. Optional live qualification when explicitly requested: on either provisioned path, the wizard ends at Success and Done, with no additional test screen. Open the bot's DM directly if a conversation test was requested. The welcome DM and connection check alone do not prove an agent response. For a channel test or manual setup, send the wizard's suggested short message, for example @<actual bot display name> you there? Find the bot belonging to this Slack app in Slack's mention suggestions and select it. Slack's actual display name can differ from Paperclip's suggested username: for example, Paperclip may suggest @cedarqa while Slack shows cedar-qa. Preserve Slack's displayed punctuation and select the real bot; pasting plain text that resembles a mention may not notify it. Use the configured bot, not a literal example. For a channel test, use a mention rather than the slash connect command. Wait for a real response in the originating Slack thread. Open the connection's Conversations page, follow the matching Open task link, and verify that the assigned agent's run completed successfully; the agent's Runs page provides the execution result if the Slack reply has no task link. Send one short follow-up in the same thread without another mention and confirm continuity and a single delivered answer. Do not claim success from a typing indicator, a verified webhook, the wizard's checkmark, or a manually posted substitute reply. If a run stops or no answer arrives, inspect the task/run and connection Activity, distinguish credential/callback/destination/identity failures from an agent-runtime failure, and resolve the specific issue or report the remaining blocker without exposing secrets.
|
||||
8. Optional live qualification when explicitly requested: on either provisioned path, the wizard ends at Success and Done, with no additional test screen. Use Open Slack on Success or on the saved connection page if a conversation test was requested. The welcome DM and connection check alone do not prove an agent response. For a channel test or manual setup, send the wizard's suggested short message, for example @<actual bot display name> you there? Find the bot belonging to this Slack app in Slack's mention suggestions and select it. Slack's actual display name can differ from Paperclip's suggested username: for example, Paperclip may suggest @cedarqa while Slack shows cedar-qa. Preserve Slack's displayed punctuation and select the real bot; pasting plain text that resembles a mention may not notify it. Use the configured bot, not a literal example. For a channel test, use a mention rather than the slash connect command. Wait for a real response in the originating Slack thread. Open the connection's Conversations page, follow the matching Open task link, and verify that the assigned agent's run completed successfully; the agent's Runs page provides the execution result if the Slack reply has no task link. Send one short follow-up in the same thread without another mention and confirm continuity and a single delivered answer. Do not claim success from a typing indicator, a verified webhook, the wizard's checkmark, or a manually posted substitute reply. If a run stops or no answer arrives, inspect the task/run and connection Activity, distinguish credential/callback/destination/identity failures from an agent-runtime failure, and resolve the specific issue or report the remaining blocker without exposing secrets.
|
||||
|
||||
9. Finish and review. Use Done to finish setup. The message test is optional and does not waive required credentials, callback verification, or identity linking. If I declined testing, finish without claiming it passed. Confirm the connection is saved for the correct agent/workspace, inspect its permitted destinations and linked identity, and leave existing access restrictions and per-action approvals intact. Slack tools for this agent's eligible tasks and routines use this bot connection and the responsible user's linked Slack account; do not create a separate personal Slack MCP connection or promise all methods work regardless of scopes or plan. Additional communication instructions are optional and apply to new tasks; add them only if I supplied preferences. Other people link their own accounts using the Access page's connect instructions; they do not share the bot secrets or my identity.
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { ChatEndpoint } from "@/api/chatEndpoints";
|
||||
|
||||
export function slackConversationUrl(endpoint: Pick<ChatEndpoint, "providerAccountId" | "setup">, userId: string | null): string {
|
||||
const account = endpoint.setup?.slackAccount;
|
||||
return userId && account?.status === "linked" && account.paperclipUserId === userId && account.dmChannelId && endpoint.providerAccountId
|
||||
? `https://app.slack.com/client/${encodeURIComponent(endpoint.providerAccountId)}/${encodeURIComponent(account.dmChannelId)}`
|
||||
: endpoint.setup?.providerUrl ?? "https://app.slack.com";
|
||||
}
|
||||
@@ -166,6 +166,7 @@ export const TestMessage: Story = {
|
||||
await expect(await canvas.findByRole("button", { name: "Test message sent" })).toBeVisible();
|
||||
await expect(canvas.queryByRole("link", { name: "View message" })).not.toBeInTheDocument();
|
||||
await expect(canvas.getByRole("link", { name: "Open Slack configuration" })).toHaveAttribute("href", "https://api.slack.com/apps/ASTORY/oauth");
|
||||
await expect(canvas.getByRole("link", { name: /^Open Slack$/ })).toHaveAttribute("href", "https://app.slack.com");
|
||||
await expect(canvas.queryByText("Agent avatar")).not.toBeInTheDocument();
|
||||
await expect(canvas.queryByText("Message in a channel")).not.toBeInTheDocument();
|
||||
},
|
||||
|
||||
@@ -35,6 +35,7 @@ export const SendAMessage: Story = { name: "04 · Send a message", args: { scena
|
||||
export const Success: Story = { name: "04 · Success", args: { scenario: "success" }, play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
await expect(await canvas.findByRole("heading", { name: "Success" })).toBeVisible();
|
||||
await expect(canvas.getByRole("link", { name: /^Open Slack$/ })).toHaveAttribute("href", "https://app.slack.com/client/TSTORY/DSTORY");
|
||||
} };
|
||||
export const ChooseAgentSelected: Story = {
|
||||
name: "Choose agent · Advanced defaults", args: { scenario: "choose" }, parameters: { initialEntries: [`${start}&agentId=agent-codex`] },
|
||||
|
||||
@@ -57,7 +57,10 @@ export const AddToSlack: Story = { name: "02 · Connect · Returning workspace",
|
||||
} };
|
||||
export const MultipleWorkspaces: Story = { name: "02 · Connect · Choose workspace", args: { scenario: "create", workspaceCount: 2 } };
|
||||
export const Connect: Story = { name: "02 · Connect · Waiting for a message", args: { scenario: "verify" } };
|
||||
export const Success: Story = { name: "02 · Connect · Success", args: { scenario: "success" } };
|
||||
export const Success: Story = { name: "02 · Connect · Success", args: { scenario: "success" }, play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
await expect(await canvas.findByRole("link", { name: /^Open Slack$/ })).toHaveAttribute("href", "https://app.slack.com/client/TSTORY/DSTORY");
|
||||
} };
|
||||
export const ApprovalPending: Story = { name: "Recovery · Slack approval pending", args: { scenario: "declined" } };
|
||||
export const ApprovalDeclined: Story = { name: "Recovery · Slack approval declined", args: { scenario: "approval_denied" } };
|
||||
export const EventConfigurationPending: Story = { name: "Recovery · App configuration pending", args: { scenario: "manifest_pending" } };
|
||||
|
||||
Reference in new issue
Block a user