mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-21 08:22:20 +02:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - Chat connectors let people use the same tasks and agent tools from external conversations. > - Agents need communication guidance that fits the conversation medium. > - That guidance belongs in the original task context, without repeated instructions on each turn. > - Connection owners also need clear settings and a consistent way to remove a connection. > - This pull request adds initial Slack guidance, optional connection instructions, and chat connection menus. > - The benefit is clearer Slack replies with the existing Paperclip workflow and permissions. ## Linked Issues or Issue Description **What existing behavior does this improve?** Agent replies in Slack and chat connection management in the Apps catalog. **Current behavior** Slack tasks do not carry a saved communication profile. The catalog shows a separate Manage button and does not offer removal on every chat connection row. **Proposed behavior** Save Slack guidance when a new conversation creates a task. Restore that original guidance when a model session is rebuilt. Do not append it to ordinary follow-ups. Expose optional additional instructions in Slack Settings. Put Manage and Remove connection in a three-dot menu for all chat providers. Keep Finish setup visible for drafts. **Reason and benefit** Small answers fit in Slack. Substantial deliverables use ordinary document or artifact tools with a useful Slack summary. Connection settings apply to new tasks and cannot change permissions. Users can remove both active and unfinished chat connections from the catalog. **Breaking changes** Two additive database columns store endpoint preferences and the initial conversation snapshot. Existing endpoints default to empty preferences. Existing conversations keep their original behavior. Non-Slack guidance is unchanged. Related public context: https://github.com/paperclipai/paperclip/pull/13741 improves native chat recovery. This change adds communication context to those existing execution paths. A search found no duplicate communication-guidance PR. ## What Changed - Add a provider-guidance registry, enabled for Slack first. - Persist optional endpoint communication instructions and capture an immutable snapshot when a conversation creates a task. - Resolve guidance from the verified company-scoped connection. Restore it for fresh native and legacy sessions without per-turn reminders, extra model calls, or extra context queries. - Add the Slack Settings field, validation, audit coverage, and Storybook save/error states. - Add Manage and Remove connection menus for all seven chat providers. Keep the draft setup button. Require removal confirmation and allow retry after failure. - Add regression coverage, an active/draft menu story, and connector documentation. ## Verification All CI checks are green for5f48df4e0. Greptile scored this head 5/5 with no actionable findings. No review threads remain unresolved. - Passed `pnpm -r typecheck` and `pnpm build` on PR head5f48df4e0. - Passed design-token checks, UI typecheck, and all 20 catalog tests after rebase. Tests cover all seven providers, active/draft removal, confirmation, cache refresh, errors, and cancellation. - Verified the active/draft menu in Storybook. The interaction test runs without browser console errors. - Passed focused guidance, endpoint persistence/isolation, heartbeat trust, native context, ACPX, adapter utility, and CLI recovery tests. Full UI and CLI groups passed (6,512 and 502 tests). - Tested real Slack conversations on staging: concise updates with public links, a planning question with buttons, a saved plan, a saved report, task creation and assignment, and explicit detailed output. Old tasks retained original preferences after an edit; a new task used the changed preferences. Restored the staging setting afterward. - Existing safe progress remained visible without duplicate final replies or private reasoning. - Broad local tests found resource/time-sensitive failures that passed targeted reruns. One Cursor archive-download fixture failed on both this branch and the unchanged main checkout. The full local suite is not claimed clean. All PR-head CI test shards passed, including general, serialized, Runner, and browser suites. The redundant local full-suite rerun was stopped after CI completed successfully. - Live delegation was not tested because the staging company has only one agent. Live testing also found separate latency and runner task-editing capability gaps; this PR does not add connector-specific workflow behavior to hide them. ## Risks - Prompt guidance changes the form of new Slack replies. Explicit requests for detail still take precedence. - The additive migration is idempotent. Conversation snapshots remain fixed when connection settings change. - Native and legacy recovery must preserve the initial context without duplicates; targeted tests cover these paths. - Removing a connection stops new work through the existing lifecycle action. It retains Paperclip task history and does not delete the external app or bot. ## Model Used OpenAI GPT-6 through Codex, with repository editing, shell tools, and browser testing. The host does not expose a more specific model ID or context-window size. No separate model calls were added to the product. ## 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 (targeted suites; broad local limitations are listed above) - [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 --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
343 lines
10 KiB
TypeScript
343 lines
10 KiB
TypeScript
import { api } from "./client";
|
|
import type {
|
|
SlackAppConfiguration,
|
|
UpdateChatEndpointInput,
|
|
PhotonProjectInspection,
|
|
PhotonChannelConfiguration,
|
|
ChatPublicationBatchStatus,
|
|
ChatPublicationState,
|
|
ChatPublicationSummary,
|
|
ChatActivityItem,
|
|
ChatFileTransferResolutionPrecondition,
|
|
} from "@paperclipai/shared";
|
|
export type {
|
|
ChatPublicationSummary,
|
|
ChatActivityItem,
|
|
} from "@paperclipai/shared";
|
|
|
|
export type ChatProvider =
|
|
"slack" | "github" | "discord" | "microsoft-teams" | "telegram" | "agentmail" | "imessage-photon";
|
|
export type ChatEndpointStatus =
|
|
| "draft"
|
|
| "verifying"
|
|
| "active"
|
|
| "paused"
|
|
| "attention"
|
|
| "revoked"
|
|
| "archived";
|
|
|
|
export type ChatEndpointSetupAction =
|
|
"configure" | "verify" | "pause" | "resume" | "reconnect" | "remove";
|
|
|
|
export interface ChatEndpointResource {
|
|
id: string;
|
|
type: string;
|
|
providerResourceId: string;
|
|
label: string;
|
|
availability: "available" | "unavailable" | "removed";
|
|
enabled: boolean;
|
|
detail?: string | null;
|
|
participants?: string[];
|
|
}
|
|
|
|
export interface ChatIdentityLink {
|
|
id: string;
|
|
principalId: string;
|
|
/** Latest discovery-only connect command received by this endpoint. */
|
|
lastConnectAt?: string | null;
|
|
externalLabel: string;
|
|
externalDetail?: string | null;
|
|
paperclipUserId?: string | null;
|
|
paperclipUserLabel?: string | null;
|
|
status: "linked" | "pending" | "revoked";
|
|
}
|
|
|
|
export interface ChatConversation {
|
|
id: string;
|
|
externalLabel: string;
|
|
externalUrl?: string | null;
|
|
issueId?: string | null;
|
|
issueIdentifier?: string | null;
|
|
issueTitle?: string | null;
|
|
state:
|
|
"active" | "waiting" | "completed" | "unavailable" | "endpoint_removed";
|
|
updatedAt: string;
|
|
lastPublicationStatus?: ChatPublicationState | null;
|
|
}
|
|
|
|
export interface ExternalChannelBindingSummary {
|
|
endpointId: string;
|
|
provider: ChatProvider;
|
|
botLabel?: string | null;
|
|
externalLabel: string;
|
|
externalUrl?: string | null;
|
|
conversationId: string;
|
|
publicationState?: string | null;
|
|
assignedAgentLocked: true;
|
|
}
|
|
|
|
export interface ChatIdentityLinkPreview {
|
|
selfService?: boolean;
|
|
canConfirm?: boolean;
|
|
endpointId: string;
|
|
companyId: string;
|
|
companyName: string;
|
|
companyPrefix: string;
|
|
provider: ChatProvider;
|
|
providerAccountLabel?: string | null;
|
|
botLabel?: string | null;
|
|
externalLabel: string;
|
|
externalDetail?: string | null;
|
|
expiresAt: string;
|
|
}
|
|
|
|
export interface ChatEndpoint {
|
|
communicationInstructions?: string;
|
|
publicationMode?: "automatic" | "explicit";
|
|
externalExecutionPolicy?: "restricted" | "agent";
|
|
id: string;
|
|
companyId: string;
|
|
provider: ChatProvider;
|
|
status: ChatEndpointStatus;
|
|
assignedAgentId: string;
|
|
assignedAgentName: string;
|
|
connectionId?: string | null;
|
|
providerAccountId?: string | null;
|
|
providerAccountLabel?: string | null;
|
|
botLabel?: string | null;
|
|
botUsername?: string | null;
|
|
botExternalId?: string | null;
|
|
photonAllocation?: "dedicated" | "shared";
|
|
allowDirectMessages?: boolean;
|
|
allowGroupChats?: boolean;
|
|
allowUnlinkedPeople: boolean;
|
|
replyMode?: "subscribed" | "mention_each_reply" | null;
|
|
healthMessage?: string | null;
|
|
lastError?: string | null;
|
|
lastActivityAt?: string | null;
|
|
resources?: ChatEndpointResource[];
|
|
identityLinks?: ChatIdentityLink[];
|
|
conversations?: ChatConversation[];
|
|
activity?: ChatActivityItem[];
|
|
setup?: {
|
|
step: string;
|
|
testStartedAt?: string | null;
|
|
testSkipped?: boolean;
|
|
authorizationUrl?: string | null;
|
|
providerUrl?: string | null;
|
|
webhookUrl?: string | null;
|
|
messagingEndpoint?: string | null;
|
|
command?: string | null;
|
|
slackApp?: SlackAppConfiguration;
|
|
webhookVerifiedAt?: string | null;
|
|
webhookSecretConfigured?: boolean;
|
|
callbackSurfaces?: {
|
|
events: ChatCallbackSurfaceState;
|
|
interactivity: ChatCallbackSurfaceState;
|
|
slashCommands: ChatCallbackSurfaceState;
|
|
};
|
|
callbacksNeedUpdate?: boolean;
|
|
};
|
|
}
|
|
|
|
export interface ChatCallbackSurfaceState {
|
|
status: "current" | "stale" | "unverified";
|
|
observedAt?: string | null;
|
|
}
|
|
|
|
export interface ChatEndpointSetupSecret {
|
|
webhookSecret: string;
|
|
}
|
|
|
|
type ListResponse<T> =
|
|
| T[]
|
|
| {
|
|
items?: T[];
|
|
endpoints?: T[];
|
|
resources?: T[];
|
|
principals?: T[];
|
|
conversations?: T[];
|
|
deliveries?: T[];
|
|
publications?: T[];
|
|
};
|
|
|
|
function rows<T>(response: ListResponse<T>): T[] {
|
|
if (Array.isArray(response)) return response;
|
|
return (
|
|
response.items ??
|
|
response.endpoints ??
|
|
response.resources ??
|
|
response.principals ??
|
|
response.conversations ??
|
|
response.deliveries ??
|
|
response.publications ??
|
|
[]
|
|
);
|
|
}
|
|
|
|
export const chatEndpointsApi = {
|
|
list: async (companyId: string) =>
|
|
rows(
|
|
await api.get<ListResponse<ChatEndpoint>>(
|
|
`/companies/${companyId}/chat-endpoints`,
|
|
),
|
|
),
|
|
get: (endpointId: string) =>
|
|
api.get<ChatEndpoint>(`/chat-endpoints/${endpointId}`),
|
|
create: (
|
|
companyId: string,
|
|
input: { provider: ChatProvider; assignedAgentId: string },
|
|
) => api.post<ChatEndpoint>(`/companies/${companyId}/chat-endpoints`, input),
|
|
update: (
|
|
endpointId: string,
|
|
input: UpdateChatEndpointInput,
|
|
) => api.patch<ChatEndpoint>(`/chat-endpoints/${endpointId}`, input),
|
|
setup: (
|
|
endpointId: string,
|
|
input: {
|
|
action: ChatEndpointSetupAction;
|
|
credentials?: Record<string, string>;
|
|
photon?: PhotonChannelConfiguration;
|
|
},
|
|
) => api.post<ChatEndpoint>(`/chat-endpoints/${endpointId}/setup`, input),
|
|
inspectPhoton: (endpointId: string, input: { projectId: string; projectSecret: string }) =>
|
|
api.post<PhotonProjectInspection>(`/chat-endpoints/${endpointId}/photon/inspect`, input),
|
|
generateSetupSecret: (endpointId: string) =>
|
|
api.post<ChatEndpointSetupSecret>(
|
|
`/chat-endpoints/${endpointId}/setup-secret`,
|
|
{},
|
|
),
|
|
test: (endpointId: string) =>
|
|
api.post<ChatEndpoint>(`/chat-endpoints/${endpointId}/test`, {}),
|
|
finishSlackSetup: (endpointId: string) => api.post<ChatEndpoint>(`/chat-endpoints/${endpointId}/finish`, {}),
|
|
setupTestStatus: (endpointId: string) => api.get<{ messageReceivedAt: string | null }>(`/chat-endpoints/${endpointId}/test-status`),
|
|
requestIdentityAccess: (token: string) => api.post<{ status: "member" | "pending_approval" }>("/chat-identity-links/request-access", { token }),
|
|
listResources: async (endpointId: string) =>
|
|
rows(
|
|
await api.get<ListResponse<ChatEndpointResource>>(
|
|
`/chat-endpoints/${endpointId}/resources`,
|
|
),
|
|
),
|
|
updateResources: (
|
|
endpointId: string,
|
|
resources: Array<{ id: string; enabled: boolean }>,
|
|
) =>
|
|
api.put<ChatEndpointResource[]>(`/chat-endpoints/${endpointId}/resources`, {
|
|
resources,
|
|
}),
|
|
listPrincipals: async (endpointId: string) =>
|
|
rows(
|
|
await api.get<ListResponse<ChatIdentityLink>>(
|
|
`/chat-endpoints/${endpointId}/principals`,
|
|
),
|
|
),
|
|
createLinkIntent: (endpointId: string, principalId: string) =>
|
|
api.post<{ confirmationUrl: string }>(
|
|
`/chat-endpoints/${endpointId}/principals/${principalId}/link-intent`,
|
|
{},
|
|
),
|
|
revokeLink: (endpointId: string, principalId: string) =>
|
|
api.delete<void>(
|
|
`/chat-endpoints/${endpointId}/principals/${principalId}/link`,
|
|
),
|
|
previewIdentityLink: (token: string) =>
|
|
api.get<ChatIdentityLinkPreview>(
|
|
`/chat-identity-links/preview?token=${encodeURIComponent(token)}`,
|
|
),
|
|
confirmIdentityLink: (token: string) =>
|
|
api.post<{ ok: true; endpointId: string }>("/chat-identity-links/confirm", {
|
|
token,
|
|
}),
|
|
listConversations: async (endpointId: string) =>
|
|
rows(
|
|
await api.get<ListResponse<ChatConversation>>(
|
|
`/chat-endpoints/${endpointId}/conversations`,
|
|
),
|
|
),
|
|
listActivity: async (endpointId: string) =>
|
|
rows(
|
|
await api.get<ListResponse<ChatActivityItem>>(
|
|
`/chat-endpoints/${endpointId}/activity`,
|
|
),
|
|
),
|
|
listActivityPage: (endpointId: string, cursor?: string) =>
|
|
api.get<{ items: ChatActivityItem[]; nextCursor: string | null }>(
|
|
`/chat-endpoints/${endpointId}/activity?limit=25${cursor ? `&cursor=${encodeURIComponent(cursor)}` : ""}`,
|
|
),
|
|
getIssueBinding: (issueId: string) =>
|
|
api.get<ExternalChannelBindingSummary | null>(
|
|
`/issues/${issueId}/chat-binding`,
|
|
),
|
|
replayDelivery: (endpointId: string, deliveryId: string) =>
|
|
api.post<void>(
|
|
`/chat-endpoints/${endpointId}/deliveries/${deliveryId}/replay`,
|
|
{},
|
|
),
|
|
replayPublication: (endpointId: string, publicationId: string) =>
|
|
api.post<void>(
|
|
`/chat-endpoints/${endpointId}/publications/${publicationId}/replay`,
|
|
{},
|
|
),
|
|
resolvePublication: (
|
|
endpointId: string,
|
|
publicationId: string,
|
|
action: "mark_delivered" | "retry_anyway" | "cancel",
|
|
fileTransfer?: ChatFileTransferResolutionPrecondition,
|
|
) =>
|
|
api.post<void>(
|
|
`/chat-endpoints/${endpointId}/publications/${publicationId}/resolve`,
|
|
{
|
|
action,
|
|
...(fileTransfer
|
|
? {
|
|
fileTransfer: {
|
|
phase: fileTransfer.phase,
|
|
version: fileTransfer.version,
|
|
},
|
|
}
|
|
: {}),
|
|
},
|
|
),
|
|
resolveAction: (
|
|
endpointId: string,
|
|
actionId: string,
|
|
action: "mark_delivered" | "retry_anyway" | "cancel",
|
|
) =>
|
|
api.post<void>(
|
|
`/chat-endpoints/${endpointId}/actions/${actionId}/resolve`,
|
|
{ action },
|
|
),
|
|
getPublicationBatchStatus: (
|
|
endpointId: string,
|
|
conversationId: string,
|
|
publicationId: string,
|
|
) =>
|
|
api.get<ChatPublicationBatchStatus>(
|
|
`/chat-endpoints/${endpointId}/conversations/${conversationId}/publications/${publicationId}/status`,
|
|
),
|
|
publishComment: (
|
|
endpointId: string,
|
|
conversationId: string,
|
|
commentId: string,
|
|
) =>
|
|
api.post<ChatPublicationSummary>(
|
|
`/chat-endpoints/${endpointId}/conversations/${conversationId}/publications`,
|
|
{ commentId },
|
|
),
|
|
publishBoardMessage: (
|
|
endpointId: string,
|
|
conversationId: string,
|
|
body: string,
|
|
idempotencyKey: string,
|
|
attachmentIds: string[] = [],
|
|
) =>
|
|
api.post<ChatPublicationSummary>(
|
|
`/chat-endpoints/${endpointId}/conversations/${conversationId}/publications`,
|
|
{
|
|
body,
|
|
idempotencyKey,
|
|
...(attachmentIds.length ? { attachmentIds } : {}),
|
|
},
|
|
),
|
|
};
|