Separate GitHub bot opt-in and add review detail pages

Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-07 16:17:14 -05:00
1 parent 7c1d2503db
commit 7dcdbe7eb6
36 files changed
+647 -204

No files matched your search

+1 -1
View File
@@ -39,7 +39,7 @@ export function AppsSidebar() {
);
if (pathname.endsWith("/apps/chat/connect")) return <ChatSetupSidebar />;
const chatDetail = pathname.match(/\/apps\/chat\/([^/]+)(?:\/(?:settings|access|reviews|conversations|activity))?\/?$/);
const chatDetail = pathname.match(/\/apps\/chat\/([^/]+)(?:\/(?:settings|access|conversations|activity)|\/reviews(?:\/[^/]+)?)?\/?$/);
if (chatDetail) return <ChatDetailSidebar endpointId={chatDetail[1]} NavItem={SidebarNavItem} />;
return (
+1 -1
View File
@@ -27,7 +27,7 @@ export function AppsSidebar() {
});
if (pathname.endsWith("/apps/chat/connect")) return <ChatSetupSidebar />;
const chatDetail = pathname.match(/\/apps\/chat\/([^/]+)(?:\/(?:settings|access|reviews|conversations|activity))?\/?$/);
const chatDetail = pathname.match(/\/apps\/chat\/([^/]+)(?:\/(?:settings|access|conversations|activity)|\/reviews(?:\/[^/]+)?)?\/?$/);
if (chatDetail) return <ChatDetailSidebar endpointId={chatDetail[1]} />;
return (
@@ -104,6 +104,31 @@ describe("Chat connectors visibility gate", () => {
await vi.waitFor(() => expect(Boolean(container.querySelector("[data-chat-setup]"))).toBe(provider === "agentmail"));
if (provider !== "agentmail") expect(container.querySelector("[data-redirect]")).not.toBeNull();
});
it.each([true, false])("gates GitHub setup by its own flag, not chat (%s)", async (githubEnabled) => {
api.provider = "github";
api.settings.mockResolvedValue({ enableChatConnectors: !githubEnabled, enableGitHubReviewBots: githubEnabled });
await render();
expect(Boolean(container.querySelector("[data-chat-setup]"))).toBe(githubEnabled);
});
it.each(["github", "slack"])("gates each existing endpoint by provider with only GitHub enabled (%s)", async (provider) => {
api.endpointId = "endpoint-1";
api.get.mockResolvedValue({ provider });
api.settings.mockResolvedValue({ enableChatConnectors: false, enableGitHubReviewBots: true });
await render();
await vi.waitFor(() => expect(api.get).toHaveBeenCalledWith("endpoint-1"));
await vi.waitFor(() => expect(Boolean(container.querySelector("[data-chat-setup]"))).toBe(provider === "github"));
});
it("keeps GitHub agent channels visible while hiding Slack under the independent flag", async () => {
api.settings.mockResolvedValue({ enableChatConnectors: false, enableGitHubReviewBots: true });
api.list.mockResolvedValue([
{ provider: "github", id: "github", assignedAgentId: "agent-1", status: "active" },
{ provider: "slack", id: "slack", assignedAgentId: "agent-1", status: "active" },
]);
flushSync(() => root.render(<QueryClientProvider client={client}><AgentChannelsPanel companyId="company-1" agentId="agent-1" /></QueryClientProvider>));
await flushReact();
expect(container.querySelector('a[href="/apps/chat/github/settings"]')).not.toBeNull();
expect(container.querySelector('a[href="/apps/chat/slack/settings"]')).toBeNull();
});
it("waits without exposing setup, then enables the route after explicit opt-in", async () => {
let resolve!: (value: unknown) => void;
api.settings.mockReturnValue(
@@ -3,22 +3,41 @@ import { useQuery } from "@tanstack/react-query";
import { chatEndpointsApi } from "@/api/chatEndpoints";
import { queryKeys } from "@/lib/queryKeys";
import { Button } from "@/components/ui/button";
import { useChatConnectorsEnabled } from "@/hooks/useChatConnectorsEnabled";
import {
useChatConnectorsEnabled,
chatProviderVisible,
} from "@/hooks/useChatConnectorsEnabled";
import { Navigate, useParams, useSearchParams } from "@/lib/router";
function DefaultEmailEndpointGate({ endpointId, children }: { endpointId: string; children: ReactNode }) {
function ProviderEndpointGate({
endpointId,
children,
}: {
endpointId: string;
children: ReactNode;
}) {
const endpoint = useQuery({
queryKey: queryKeys.chatEndpoints.detail(endpointId),
queryFn: () => chatEndpointsApi.get(endpointId),
});
if (endpoint.isError) return (
<div className="space-y-3 p-6">
<p role="alert" className="text-sm text-destructive">{endpoint.error.message}</p>
<Button variant="outline" onClick={() => void endpoint.refetch()}>Retry</Button>
</div>
);
const { enabled, githubEnabled } = useChatConnectorsEnabled();
if (endpoint.isError)
return (
<div className="space-y-3 p-6">
<p role="alert" className="text-sm text-destructive">
{endpoint.error.message}
</p>
<Button variant="outline" onClick={() => void endpoint.refetch()}>
Retry
</Button>
</div>
);
if (!endpoint.data) return null;
return endpoint.data.provider === "agentmail" ? <>{children}</> : <Navigate to="/apps" replace />;
return chatProviderVisible(endpoint.data.provider, enabled, githubEnabled) ? (
<>{children}</>
) : (
<Navigate to="/apps" replace />
);
}
export function ChatConnectorsExperimentalGate({
@@ -26,13 +45,21 @@ export function ChatConnectorsExperimentalGate({
}: {
children: ReactNode;
}) {
const { enabled, loaded } = useChatConnectorsEnabled();
const { enabled, githubEnabled, loaded } = useChatConnectorsEnabled();
const [params] = useSearchParams();
const { endpointId } = useParams<{ endpointId: string }>();
if (!endpointId && params.get("provider") === "agentmail") return <>{children}</>;
if (!endpointId && params.get("provider") === "agentmail")
return <>{children}</>;
if (!loaded) return null;
if (!enabled) return endpointId
? <DefaultEmailEndpointGate endpointId={endpointId}>{children}</DefaultEmailEndpointGate>
: <Navigate to="/apps" replace />;
return <>{children}</>;
if (endpointId)
return (
<ProviderEndpointGate endpointId={endpointId}>
{children}
</ProviderEndpointGate>
);
return chatProviderVisible(params.get("provider"), enabled, githubEnabled) ? (
<>{children}</>
) : (
<Navigate to="/apps" replace />
);
}
@@ -5,7 +5,7 @@ import { Button } from "@/components/ui/button";
import { StatusBadge } from "@/components/StatusBadge";
import { queryKeys } from "@/lib/queryKeys";
import { Link } from "@/lib/router";
import { useChatConnectorsEnabled } from "@/hooks/useChatConnectorsEnabled";
import { useChatConnectorsEnabled, chatProviderVisible } from "@/hooks/useChatConnectorsEnabled";
const providerNames: Record<ChatProvider, string> = {
slack: "Slack",
@@ -24,7 +24,7 @@ export function AgentChannelsPanel({
companyId: string;
agentId: string;
}) {
const { enabled } = useChatConnectorsEnabled();
const { enabled, githubEnabled } = useChatConnectorsEnabled();
const query = useQuery({
queryKey: queryKeys.chatEndpoints.list(companyId),
queryFn: () => chatEndpointsApi.list(companyId),
@@ -32,7 +32,7 @@ export function AgentChannelsPanel({
});
const endpoints = (query.data ?? []).filter(
(endpoint) =>
endpoint.assignedAgentId === agentId && endpoint.status !== "archived" && (endpoint.provider === "agentmail" || enabled),
endpoint.assignedAgentId === agentId && endpoint.status !== "archived" && chatProviderVisible(endpoint.provider, enabled, githubEnabled),
);
return (
<section className="max-w-3xl space-y-5">
@@ -56,7 +56,7 @@ export function AgentChannelsPanel({
<div className="rounded-lg border border-dashed border-border p-5">
<p className="text-sm font-medium">No channels connected</p>
<p className="mt-1 text-sm text-muted-foreground">
{enabled ? "Connect AgentMail, Slack, GitHub Code Review Bot, Discord, Microsoft Teams, or Telegram from Connectors." : "Connect AgentMail from Connectors."}
{enabled || githubEnabled ? "Connect an agent identity from Connectors." : "Connect AgentMail from Connectors."}
</p>
<Button asChild className="mt-3" variant="outline" size="sm">
<Link to="/apps">Open Connectors</Link>
@@ -51,7 +51,6 @@ export function ChatDetailSidebar({
to={`/apps/chat/${endpointId}/reviews`}
label="Reviews"
icon={GitPullRequest}
end
/>
)}
<NavItem
@@ -18,7 +18,7 @@ import { Textarea } from "@/components/ui/textarea";
import { useToast } from "@/context/ToastContext";
import { Link } from "@/lib/router";
import { queryKeys } from "@/lib/queryKeys";
import { useChatConnectorsEnabled } from "@/hooks/useChatConnectorsEnabled";
import { useChatConnectorsEnabled, chatProviderVisible } from "@/hooks/useChatConnectorsEnabled";
import { issuesApi } from "@/api/issues";
import {
boardSendDraftKey,
@@ -109,15 +109,15 @@ const filePhaseLabels: Record<ChatFileTransferPhase, string> = {
};
export function useIssueChatBinding(companyId: string, issueId: string) {
const { enabled } = useChatConnectorsEnabled();
const queryEnabled = enabled && Boolean(companyId && issueId) && !issueId.startsWith("chat:");
const { enabled, githubEnabled } = useChatConnectorsEnabled();
const queryEnabled = (enabled || githubEnabled) && Boolean(companyId && issueId) && !issueId.startsWith("chat:");
const query = useQuery({
queryKey: ["issue-chat-binding", companyId, issueId],
queryFn: () => chatEndpointsApi.getIssueBinding(issueId),
enabled: queryEnabled,
});
return {
binding: queryEnabled ? (query.data ?? null) : null,
binding: queryEnabled && chatProviderVisible(query.data?.provider, enabled, githubEnabled) ? (query.data ?? null) : null,
isLoading: queryEnabled && query.isLoading,
};
}