Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 9 additions & 8 deletions apps/api/src/handlers/mcp/slack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import {
getSlackThreadReplyFooterMessageTs,
removeSlackThreadReplyFooter,
resolveSlackThreadFooterContext,
resolveSlackThreadLinkedPr,
resolveSlackThreadLinkedPrs,
resolveSlackThreadLivePreviewUrl,
setLatestSlackBotReply,
setSlackThreadReplyFooterMessageTs,
Expand Down Expand Up @@ -138,8 +138,8 @@ async function buildLateBoundSlackRootFooterText(params: {
// The explicit-mention marker is per-thread, so a brand-new root message
// can never carry it; only the linked PR and live preview need resolving
// here. PR metadata lives in taskPullRequests and is resolved by task id.
const [linkedPr, livePreviewUrl] = await Promise.all([
resolveSlackThreadLinkedPr({
const [linkedPrs, livePreviewUrl] = await Promise.all([
resolveSlackThreadLinkedPrs({
taskId: params.taskId,
prRepo: null,
prNumber: null,
Expand All @@ -149,7 +149,8 @@ async function buildLateBoundSlackRootFooterText(params: {

return buildSlackThreadFooterText({
taskUrl: params.taskUrl,
linkedPr,
linkedPr: linkedPrs[0] ?? null,
linkedPrs,
livePreviewUrl,
explicitMentionRequired: false,
});
Expand Down Expand Up @@ -177,15 +178,15 @@ async function buildLateBoundAutomationRootFooterBlocks(params: {
const automationLabel =
getTriggerableBackgroundAutomationDescriptorByKey(workItem.automationKey)
?.label ?? workItem.automationKey.replaceAll('_', ' ');
const linkedPr = await resolveSlackThreadLinkedPr({
const linkedPrs = await resolveSlackThreadLinkedPrs({
taskId: params.taskId,
prRepo: null,
prNumber: null,
});
return buildAutomationRootFooterBlocks({
automationLabel,
taskUrl: params.taskUrl,
linkedPrUrl: linkedPr?.prUrl ?? null,
linkedPrUrls: linkedPrs.map((pr) => pr.prUrl),
});
}

Expand All @@ -200,15 +201,15 @@ async function buildLateBoundCustomAutomationRootFooterBlocks(params: {
return null;
}

const linkedPr = await resolveSlackThreadLinkedPr({
const linkedPrs = await resolveSlackThreadLinkedPrs({
taskId: params.taskId,
prRepo: null,
prNumber: null,
});
return buildAutomationRootFooterBlocks({
automationLabel: automation.name,
taskUrl: params.taskUrl,
linkedPrUrl: linkedPr?.prUrl ?? null,
linkedPrUrls: linkedPrs.map((pr) => pr.prUrl),
});
}

Expand Down
29 changes: 21 additions & 8 deletions apps/web/src/app/(sandbox)/task/[taskId]/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export const Header = ({ session: { taskRun, task, taskId } }: HeaderProps) => {
const repo = taskRun?.payload?.repo;
const prRepo = taskRun?.prRepo;
const prNumber = taskRun?.prNumber;
const pullRequests = taskRun?.pullRequests ?? [];

const badges = [
(environmentId || repo) && (
Expand All @@ -45,14 +46,26 @@ export const Header = ({ session: { taskRun, task, taskId } }: HeaderProps) => {
iconClassName="text-muted-foreground"
/>
),
prRepo && prNumber && (
<PullRequestBadge
key="pr"
repo={prRepo}
prNumber={prNumber}
iconClassName="text-muted-foreground"
/>
),
...(pullRequests.length > 0
? pullRequests.map((pullRequest) => (
<PullRequestBadge
key={`pr:${pullRequest.repository}:${pullRequest.prNumber}`}
repo={pullRequest.repository}
prNumber={pullRequest.prNumber}
url={pullRequest.prUrl}
iconClassName="text-muted-foreground"
/>
))
: prRepo && prNumber
? [
<PullRequestBadge
key="pr"
repo={prRepo}
prNumber={prNumber}
iconClassName="text-muted-foreground"
/>,
]
: []),
].filter(Boolean);

const updateTaskTitle = useMutation(trpc.tasks.updateTitle.mutationOptions());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ export interface SandboxConnectionTarget {
export type SessionTaskRun = TaskRunDetail & {
prRepo: string | null;
prNumber: number | null;
pullRequests?: Array<{
repository: string;
prNumber: number;
prUrl?: string;
}>;
previewProxyBaseUrl?: string;
/** Server-derived: whether a failed start may be relaunched. */
canRetryFailedStart?: boolean;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -421,7 +421,26 @@ export function TaskInfoPanel({
</tr>
)}

{taskRun.prRepo && taskRun.prNumber && (
{(taskRun.pullRequests?.length ?? 0) > 0 ? (
<tr>
<td className="pr-4 py-1 align-top whitespace-nowrap">
Pull Requests
</td>
<td className="py-1">
<div className="flex max-w-72 flex-col items-start gap-2">
{taskRun.pullRequests?.map((pullRequest) => (
<PullRequestBadge
key={`${pullRequest.repository}:${pullRequest.prNumber}`}
repo={pullRequest.repository}
prNumber={pullRequest.prNumber}
url={pullRequest.prUrl}
iconClassName="text-muted-foreground"
/>
))}
</div>
</td>
</tr>
) : taskRun.prRepo && taskRun.prNumber ? (
<tr>
<td className="pr-4 py-1 align-top whitespace-nowrap">
Pull Request
Expand All @@ -434,7 +453,7 @@ export function TaskInfoPanel({
/>
</td>
</tr>
)}
) : null}

{linkedWorkItems.length > 0 ? (
<tr>
Expand Down
6 changes: 4 additions & 2 deletions apps/web/src/components/sandbox/PullRequestBadge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,24 @@ import { cn } from '@/lib/utils';
interface PullRequestBadgeProps {
repo: string;
prNumber: number;
url?: string;
className?: string;
iconClassName?: string;
}

export function PullRequestBadge({
repo,
prNumber,
url,
className,
iconClassName,
}: PullRequestBadgeProps) {
const url = `https://github.com/${repo}/pull/${prNumber}`;
const pullRequestUrl = url ?? `https://github.com/${repo}/pull/${prNumber}`;
const repoName = repo.split('/')[1] ?? repo;

return (
<a
href={url}
href={pullRequestUrl}
target="_blank"
rel="noopener noreferrer"
className={cn(
Expand Down
54 changes: 54 additions & 0 deletions apps/web/src/lib/server/task-runs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,63 @@ type TaskPullRequestLink = {
taskId: string;
repository: string;
prNumber: number;
prUrl?: string;
sourceControlProvider: SourceControlProvider;
};

export const getTaskPullRequestsByTaskId = async (
taskIds: string[],
): Promise<
Record<
string,
Array<Pick<TaskPullRequestLink, 'repository' | 'prNumber' | 'prUrl'>>
>
> => {
if (taskIds.length === 0) {
return {};
}

const results = await db
.select({
taskId: taskPullRequests.taskId,
repository: taskPullRequests.repository,
prNumber: taskPullRequests.prNumber,
prUrl: taskPullRequests.prUrl,
})
.from(taskPullRequests)
.where(
and(
inArray(taskPullRequests.taskId, taskIds),
isNotNull(taskPullRequests.repository),
isNotNull(taskPullRequests.prNumber),
),
)
.orderBy(taskPullRequests.taskId, desc(taskPullRequests.detectedAt));

const pullRequestsByTask = new Map<
string,
Array<Pick<TaskPullRequestLink, 'repository' | 'prNumber' | 'prUrl'>>
>();

for (const row of results) {
if (!row.repository || row.prNumber === null) {
continue;
}

const pullRequests = pullRequestsByTask.get(row.taskId) ?? [];
if (!pullRequests.some((pr) => pr.prUrl === row.prUrl)) {
pullRequests.push({
repository: row.repository,
prNumber: row.prNumber,
prUrl: row.prUrl ?? undefined,
});
pullRequestsByTask.set(row.taskId, pullRequests);
}
}

return Object.fromEntries(pullRequestsByTask);
};

export const getLatestTaskPullRequestsByTaskId = async (
taskIds: string[],
): Promise<Record<string, TaskPullRequestLink>> => {
Expand Down
14 changes: 13 additions & 1 deletion apps/web/src/trpc/commands/sandbox-session/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -504,16 +504,28 @@ type ResolvedSandboxTaskAccess = Extract<
type SandboxTaskRunDetail = TaskRunDetail & {
prRepo: string | null;
prNumber: number | null;
pullRequests?: Array<{
repository: string;
prNumber: number;
prUrl?: string;
}>;
};

function applyResolvedTaskPullRequestFallback<T extends TaskRunDetail>(
taskRun: T,
taskTaskRun: ResolvedSandboxTaskAccess['task']['taskRun'],
): T & { prRepo: string | null; prNumber: number | null } {
): T & {
prRepo: string | null;
prNumber: number | null;
pullRequests: NonNullable<
ResolvedSandboxTaskAccess['task']['taskRun']
>['pullRequests'];
} {
return {
...taskRun,
prRepo: taskTaskRun?.prRepo ?? null,
prNumber: taskTaskRun?.prNumber ?? null,
pullRequests: taskTaskRun?.pullRequests ?? [],
};
}

Expand Down
34 changes: 21 additions & 13 deletions apps/web/src/trpc/commands/tasks/by-id.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import type {
import {
getArtifactsForTask,
getLatestTaskPullRequestsByTaskId,
getTaskPullRequestsByTaskId,
} from '@/lib/server';
import { resolveTaskCreatorDisplay } from '@/lib/server/tasks';

Expand Down Expand Up @@ -66,19 +67,24 @@ async function getTaskByIdForCurrentOrg(
includeArtifacts = false,
}: { taskId: string; includeArtifacts?: boolean },
): Promise<TaskWithAssociations | null> {
const [[result], taskPullRequestsByTaskId, inferenceUsage] =
await Promise.all([
db
.select({ task: tasks, user: users, taskRun: taskRuns })
.from(tasks)
.leftJoin(users, eq(tasks.initiatorUserId, users.id))
.leftJoin(taskRuns, eq(taskRuns.taskId, tasks.id))
.where(and(eq(tasks.id, taskId), isNull(tasks.deletedAt)))
.orderBy(desc(taskRuns.id))
.limit(1),
getLatestTaskPullRequestsByTaskId([taskId]),
getTaskInferenceUsageByTaskId(taskId),
]);
const [
[result],
taskPullRequestsByTaskId,
allTaskPullRequestsByTaskId,
inferenceUsage,
] = await Promise.all([
db
.select({ task: tasks, user: users, taskRun: taskRuns })
.from(tasks)
.leftJoin(users, eq(tasks.initiatorUserId, users.id))
.leftJoin(taskRuns, eq(taskRuns.taskId, tasks.id))
.where(and(eq(tasks.id, taskId), isNull(tasks.deletedAt)))
.orderBy(desc(taskRuns.id))
.limit(1),
getLatestTaskPullRequestsByTaskId([taskId]),
getTaskPullRequestsByTaskId([taskId]),
getTaskInferenceUsageByTaskId(taskId),
]);

if (!result) {
return null;
Expand All @@ -87,6 +93,7 @@ async function getTaskByIdForCurrentOrg(
const { task, user, taskRun } = result;
const creator = resolveTaskCreatorDisplay(task, user);
const latestPullRequest = taskPullRequestsByTaskId[taskId];
const pullRequests = allTaskPullRequestsByTaskId[taskId] ?? [];

const taskData: TaskWithAssociations = {
...task,
Expand All @@ -98,6 +105,7 @@ async function getTaskByIdForCurrentOrg(
...taskRun,
prRepo: latestPullRequest?.repository ?? null,
prNumber: latestPullRequest?.prNumber ?? null,
pullRequests,
}
: null,
inferenceUsage,
Expand Down
5 changes: 5 additions & 0 deletions apps/web/src/types/task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ export type ArtifactWithContent = {
export type TaskRunWithPullRequest = TaskRun & {
prRepo: string | null;
prNumber: number | null;
pullRequests?: Array<{
repository: string;
prNumber: number;
prUrl?: string;
}>;
};

export type TaskWithAssociations = Task & {
Expand Down
18 changes: 18 additions & 0 deletions packages/communication/src/__tests__/chat-messages.test.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading