Skip to content
Closed
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
20 changes: 18 additions & 2 deletions apps/api/src/tools/task-board/enqueue-super-agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const task = { id: "board_1", title: "Fix the thing", description: null };
const pr = { number: 7, url: "https://github.com/x/y/pull/7" };

const CONFLICT_LEAD = "MERGE CONFLICT";
const FEEDBACK_LEAD = "A reviewer requested changes";
const FEEDBACK_LEAD = "Changes were requested";
const CONTINUE_LEAD = "already has an open pull request";
const OPEN_A_PR = "commit on a new branch, push, and open a pull request";

Expand Down Expand Up @@ -60,7 +60,7 @@ describe("buildSuperAgentTaskPrompt", () => {
expect(p).not.toContain(FEEDBACK_LEAD);
});

it("a reviewer change request leads with the feedback block", () => {
it("a change request leads with the feedback block", () => {
const p = buildSuperAgentTaskPrompt(task, {
pr,
feedback: "QA Agent: the button is broken",
Expand All @@ -70,6 +70,22 @@ describe("buildSuperAgentTaskPrompt", () => {
expect(p).not.toContain(CONFLICT_LEAD);
});

/**
* "Comment & re-run" from the board: a person's comment is the lead, with no
* PR and no reviewer involved. Without this the comment posted and the run
* started from the title, which reads to the user as being ignored.
*/
it("caller feedback leads even with no PR", () => {
const p = buildSuperAgentTaskPrompt(task, {
feedback: "Use the design system tokens, not raw hex",
});
expect(p).toContain(FEEDBACK_LEAD);
expect(p).toContain("Use the design system tokens, not raw hex");
expect(p).toContain("Address this feedback.");
expect(p).not.toContain(CONFLICT_LEAD);
expect(p).not.toContain(CONTINUE_LEAD);
});

it("conflict resolution wins over feedback when both are set", () => {
const p = buildSuperAgentTaskPrompt(task, {
pr,
Expand Down
7 changes: 4 additions & 3 deletions apps/api/src/tools/task-board/enqueue-super-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ export async function reactToSuperAgentDelegation(
*/
/** Options that steer the Super Agent prompt for a re-run on an existing PR. */
export type SuperAgentPromptOpts = {
/** A reviewer's change request — leads the re-run prompt. */
/** A change request — a reviewer's, or a person's comment on the card. Leads
* the re-run prompt. */
feedback?: string;
/** The PR already under review, so the re-run updates it in place instead
* of opening a second PR. */
Expand Down Expand Up @@ -119,8 +120,8 @@ export function buildSuperAgentTaskPrompt(
opts?.feedback
? [
opts.pr
? `A reviewer requested changes on the existing pull request #${opts.pr.number} (${opts.pr.url}):`
: "A reviewer requested changes on your previous work:",
? `Changes were requested on the existing pull request #${opts.pr.number} (${opts.pr.url}):`
: "Changes were requested on your previous work:",
opts.feedback,
opts.pr
? `Load the repo, then CHECK OUT that PR's branch (e.g. \`gh pr checkout ${opts.pr.number}\`) before editing, address the feedback, commit, and push to update the SAME pull request — do NOT open a new one or start a new branch.`
Expand Down
21 changes: 13 additions & 8 deletions apps/api/src/tools/task-board/rerun.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,22 +290,24 @@ export const TASK_BOARD_ITEM_RERUN = defineTool({
idempotentHint: false,
openWorldHint: true,
},
// ponytail: no `feedback` / "what to do differently" input from the CALLER.
// A re-run on an existing PR is not blind, though: the dispatch funnel picks
// up the reviewer's outstanding change request by itself
// (`outstandingReviewFeedback`), so the run continues from there instead of
// restarting. Add a caller-supplied lead when someone needs to say something
// the reviewers did not.
inputSchema: z.object({
id: z.string().describe("The task board item to re-run."),
/** Takes precedence over carried reviewer feedback (see `wantsCarry`). */
feedback: z
.string()
.optional()
.describe(
"What the run should do differently. Leads the prompt, taking " +
"precedence over carried-over reviewer feedback.",
),
}),
outputSchema: z.object({
status: z.string().describe("The task's lane after the re-run was queued."),
supersededThreadIds: z
.array(z.string())
.describe("Runs that were failed to make room for this one."),
}),
handler: async ({ id }, ctx) => {
handler: async ({ id, feedback }, ctx) => {
requireAuth(ctx);
await ctx.access.check();

Expand Down Expand Up @@ -379,7 +381,10 @@ export const TASK_BOARD_ITEM_RERUN = defineTool({
});
emitTaskBoardUpdated(organizationId, updated);

await enqueueSuperAgentForTask(ctx, updated, { userInitiated: true });
await enqueueSuperAgentForTask(ctx, updated, {
userInitiated: true,
...(feedback?.trim() ? { feedback: feedback.trim() } : {}),
});

return { status: updated.status, supersededThreadIds };
},
Expand Down
84 changes: 8 additions & 76 deletions apps/web/src/components/chat/pills/chat-mode-row.tsx
Original file line number Diff line number Diff line change
@@ -1,95 +1,27 @@
import type { ReactNode } from "react";
import type { VirtualMCPEntity } from "@decocms/shared/sdk/types";
import { useOptionalChatStream, useOptionalChatTask } from "../context";
import { BranchPill } from "./branch-pill";
import { TaskPill } from "./task-pill";
import { getActiveGithubRepo } from "@/lib/github-repo";
import { shouldStartBranchAsCms } from "@/sdk/fast-preview";
import { useProjectContext } from "@/sdk";
import { authClient } from "@/lib/auth-client";
import { branchUserLabel } from "@decocms/shared/branch-name";

interface PureProps {
branchPill: ReactNode;
}

/**
* Pure layout — used by tests. Renders the branch pill (when present) in the
* parent flex flow. Returns null when there is nothing to show.
*
* The runtime choice (Cloud sandbox vs This device) is NOT surfaced here — it
* lives in the "Smart" model selector's Cloud ⟷ This device toggle, which
* writes through the same `pendingAgentOption`. A standalone pill here was
* redundant, so this row only carries the branch pill.
*/
/** Pure layout, used by tests. */
export function ChatModeRowPure({ branchPill }: PureProps) {
if (!branchPill) return null;
return <>{branchPill}</>;
}

interface SmartProps {
virtualMcp: VirtualMCPEntity | null | undefined;
currentBranch: string | null;
}

/**
* Smart wrapper. Renders the BranchPill for agents imported from GitHub —
* `metadata.githubRepo` exists AND has an attached `connectionId` (an
* authenticated user repo, not a public-template clone). Start Website agents
* populate `metadata.githubRepo.url` for the template but leave `connectionId`
* unset; branches aren't meaningful there.
*
* Locked flag is derived from `useOptionalChatStream().messages.length > 0`.
*/
export function ChatModeRow({ virtualMcp, currentBranch }: SmartProps) {
const stream = useOptionalChatStream();
const taskCtx = useOptionalChatTask();
const locked =
(stream?.messages ?? []).length > 0 || (taskCtx?.isThreadLocked ?? false);
const setCurrentTaskBranch = taskCtx?.setCurrentTaskBranch;
const createTask = taskCtx?.createTask;
const createBranchAsCms = shouldStartBranchAsCms(
virtualMcp?.metadata,
taskCtx?.activeTask?.metadata,
);

/** The header context control is the task, not the branch, so `TaskPill` renders here. */
export function ChatModeRow({ virtualMcp }: SmartProps) {
const githubRepo = getActiveGithubRepo(virtualMcp);
const connectionId = githubRepo?.connectionId;

const { data: session } = authClient.useSession();
const userId = session?.user?.id ?? "";
const userLabel = branchUserLabel(session?.user);
const { org } = useProjectContext();

const branchPill =
githubRepo && connectionId ? (
<BranchPill
// Remount on repo/connection change so search/tab/highlight state
// from the previous repo doesn't leak into the new one's picker.
key={`${connectionId}:${githubRepo.owner}/${githubRepo.name}`}
orgId={org.id}
orgSlug={org.slug}
userId={userId}
userLabel={userLabel}
virtualMcpId={virtualMcp?.id ?? ""}
connectionId={connectionId}
owner={githubRepo.owner}
repo={githubRepo.name}
sandboxMap={virtualMcp?.metadata?.sandboxMap}
value={currentBranch}
onChange={(next) => {
if (setCurrentTaskBranch) void setCurrentTaskBranch(next);
}}
onCreateBranch={(next) => {
if (createBranchAsCms && createTask) {
createTask({ branch: next });
} else if (setCurrentTaskBranch) {
void setCurrentTaskBranch(next);
}
}}
locked={locked}
placement="chat"
/>
) : null;

return <ChatModeRowPure branchPill={branchPill} />;
const taskPill = githubRepo?.connectionId ? (
<TaskPill placement="header" />
) : null;
return <ChatModeRowPure branchPill={taskPill} />;
}
Loading
Loading