Skip to content

Commit fa79ae2

Browse files
authored
feat(background-agents): base branch and cloud diffs (#1149)
1 parent f865468 commit fa79ae2

10 files changed

Lines changed: 130 additions & 12 deletions

File tree

apps/twig/src/renderer/features/message-editor/components/DiffStatsIndicator.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,31 @@ import { useQuery } from "@tanstack/react-query";
44

55
interface DiffStatsIndicatorProps {
66
repoPath: string | null | undefined;
7+
overrideStats?: {
8+
filesChanged: number;
9+
linesAdded: number;
10+
linesRemoved: number;
11+
} | null;
712
}
813

9-
export function DiffStatsIndicator({ repoPath }: DiffStatsIndicatorProps) {
10-
const { data: diffStats } = useQuery({
14+
export function DiffStatsIndicator({
15+
repoPath,
16+
overrideStats,
17+
}: DiffStatsIndicatorProps) {
18+
const { data: localStats } = useQuery({
1119
queryKey: ["diff-stats", repoPath],
1220
queryFn: () =>
1321
trpcVanilla.git.getDiffStats.query({
1422
directoryPath: repoPath as string,
1523
}),
16-
enabled: !!repoPath,
24+
enabled: !!repoPath && !overrideStats,
1725
staleTime: 5000,
1826
refetchInterval: 5000,
1927
placeholderData: (prev) => prev,
2028
});
2129

30+
const diffStats = overrideStats ?? localStats;
31+
2232
if (!diffStats || diffStats.filesChanged === 0) {
2333
return null;
2434
}

apps/twig/src/renderer/features/message-editor/components/MessageEditor.tsx

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,24 +22,36 @@ interface ModeAndBranchRowProps {
2222
modeOption?: SessionConfigOption;
2323
onModeChange?: () => void;
2424
repoPath?: string | null;
25+
cloudBranch?: string | null;
26+
cloudDiffStats?: {
27+
filesChanged: number;
28+
linesAdded: number;
29+
linesRemoved: number;
30+
} | null;
2531
disabled?: boolean;
2632
}
2733

2834
function ModeAndBranchRow({
2935
modeOption,
3036
onModeChange,
3137
repoPath,
38+
cloudBranch,
39+
cloudDiffStats,
3240
disabled,
3341
}: ModeAndBranchRowProps) {
34-
const { currentBranch, diffStats } = useGitQueries(repoPath ?? undefined);
42+
const { currentBranch: gitBranch, diffStats } = useGitQueries(
43+
repoPath ?? undefined,
44+
);
45+
const currentBranch = cloudBranch ?? gitBranch;
3546

3647
const showModeIndicator = !!onModeChange;
3748
const showBranchSelector = !!currentBranch;
49+
const effectiveDiffStats = cloudDiffStats ?? diffStats;
3850
const showDiffStats =
39-
diffStats &&
40-
(diffStats.filesChanged > 0 ||
41-
diffStats.linesAdded > 0 ||
42-
diffStats.linesRemoved > 0);
51+
effectiveDiffStats &&
52+
(effectiveDiffStats.filesChanged > 0 ||
53+
effectiveDiffStats.linesAdded > 0 ||
54+
effectiveDiffStats.linesRemoved > 0);
4355

4456
if (!showModeIndicator && !showBranchSelector) {
4557
return null;
@@ -61,7 +73,10 @@ function ModeAndBranchRow({
6173
)}
6274
</Flex>
6375
<Flex align="center" gap="2" style={{ minWidth: 0, overflow: "hidden" }}>
64-
<DiffStatsIndicator repoPath={repoPath} />
76+
<DiffStatsIndicator
77+
repoPath={repoPath}
78+
overrideStats={cloudDiffStats}
79+
/>
6580
{showBranchSelector && showDiffStats && (
6681
<Flex
6782
align="center"
@@ -127,6 +142,8 @@ export const MessageEditor = forwardRef<EditorHandle, MessageEditorProps>(
127142
const disabled = context?.disabled ?? false;
128143
const isLoading = context?.isLoading ?? false;
129144
const repoPath = context?.repoPath;
145+
const cloudBranch = context?.cloudBranch;
146+
const cloudDiffStats = context?.cloudDiffStats;
130147
const isSubmitDisabled = disabled || !isOnline;
131148

132149
const {
@@ -297,6 +314,8 @@ export const MessageEditor = forwardRef<EditorHandle, MessageEditorProps>(
297314
modeOption={modeOption}
298315
onModeChange={onModeChange}
299316
repoPath={repoPath}
317+
cloudBranch={cloudBranch}
318+
cloudDiffStats={cloudDiffStats}
300319
disabled={disabled}
301320
/>
302321
</Flex>

apps/twig/src/renderer/features/message-editor/stores/draftStore.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,18 @@ import type { EditorContent } from "../utils/content";
77

88
type SessionId = string;
99

10+
export interface CloudDiffStats {
11+
filesChanged: number;
12+
linesAdded: number;
13+
linesRemoved: number;
14+
}
15+
1016
export interface EditorContext {
1117
sessionId: string;
1218
taskId: string | undefined;
1319
repoPath: string | null | undefined;
20+
cloudBranch?: string | null;
21+
cloudDiffStats?: CloudDiffStats | null;
1422
disabled: boolean;
1523
isLoading: boolean;
1624
}
@@ -78,13 +86,22 @@ export const useDraftStore = create<DraftStore>()(
7886
sessionId,
7987
taskId: context.taskId ?? existing?.taskId,
8088
repoPath: context.repoPath ?? existing?.repoPath,
89+
cloudBranch: context.cloudBranch ?? existing?.cloudBranch,
90+
cloudDiffStats: context.cloudDiffStats ?? existing?.cloudDiffStats,
8191
disabled: context.disabled ?? existing?.disabled ?? false,
8292
isLoading: context.isLoading ?? existing?.isLoading ?? false,
8393
};
8494
if (
8595
existing?.sessionId === newContext.sessionId &&
8696
existing?.taskId === newContext.taskId &&
8797
existing?.repoPath === newContext.repoPath &&
98+
existing?.cloudBranch === newContext.cloudBranch &&
99+
existing?.cloudDiffStats?.filesChanged ===
100+
newContext.cloudDiffStats?.filesChanged &&
101+
existing?.cloudDiffStats?.linesAdded ===
102+
newContext.cloudDiffStats?.linesAdded &&
103+
existing?.cloudDiffStats?.linesRemoved ===
104+
newContext.cloudDiffStats?.linesRemoved &&
88105
existing?.disabled === newContext.disabled &&
89106
existing?.isLoading === newContext.isLoading
90107
) {

apps/twig/src/renderer/features/sessions/components/SessionView.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,12 @@ interface SessionViewProps {
4545
onBashCommand?: (command: string) => void;
4646
onCancelPrompt: () => void;
4747
repoPath?: string | null;
48+
cloudBranch?: string | null;
49+
cloudDiffStats?: {
50+
filesChanged: number;
51+
linesAdded: number;
52+
linesRemoved: number;
53+
} | null;
4854
hasError?: boolean;
4955
errorTitle?: string;
5056
errorMessage?: string;
@@ -68,6 +74,8 @@ export function SessionView({
6874
onBashCommand,
6975
onCancelPrompt,
7076
repoPath,
77+
cloudBranch,
78+
cloudDiffStats,
7179
hasError = false,
7280
errorTitle,
7381
errorMessage = DEFAULT_ERROR_MESSAGE,
@@ -126,6 +134,8 @@ export function SessionView({
126134
setContext(sessionId, {
127135
taskId,
128136
repoPath,
137+
cloudBranch,
138+
cloudDiffStats,
129139
disabled: !isRunning,
130140
isLoading: !!isPromptPending,
131141
});

apps/twig/src/renderer/features/task-detail/components/TaskDetail.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,11 @@ export function TaskDetail({ task: initialTask }: TaskDetailProps) {
3434
const workspace = useWorkspaceStore((state) => state.workspaces[taskId]);
3535
const effectiveRepoPath = useCwd(taskId);
3636
const { currentBranch } = useGitQueries(effectiveRepoPath ?? undefined);
37-
const branchName = currentBranch ?? workspace?.branchName;
37+
const isCloud =
38+
workspace?.mode === "cloud" || task.latest_run?.environment === "cloud";
39+
const branchName = isCloud
40+
? (workspace?.baseBranch ?? task.latest_run?.branch ?? currentBranch)
41+
: (currentBranch ?? workspace?.branchName);
3842

3943
const [filePickerOpen, setFilePickerOpen] = useState(false);
4044

apps/twig/src/renderer/features/task-detail/components/TaskLogsPanel.tsx

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { BackgroundWrapper } from "@components/BackgroundWrapper";
22
import { ErrorBoundary } from "@components/ErrorBoundary";
3+
import {
4+
useCloudBranchChangedFiles,
5+
useCloudPrChangedFiles,
6+
} from "@features/git-interaction/hooks/useGitQueries";
37
import { tryExecuteTwigCommand } from "@features/message-editor/commands";
48
import { useDraftStore } from "@features/message-editor/stores/draftStore";
59
import { SessionView } from "@features/sessions/components/SessionView";
@@ -23,7 +27,7 @@ import { useQueryClient } from "@tanstack/react-query";
2327
import { logger } from "@utils/logger";
2428
import { getTaskRepository } from "@utils/repository";
2529
import { toast } from "@utils/toast";
26-
import { useCallback, useEffect, useRef } from "react";
30+
import { useCallback, useEffect, useMemo, useRef } from "react";
2731

2832
const log = logger.scope("task-logs-panel");
2933

@@ -74,6 +78,27 @@ export function TaskLogsPanel({ taskId, task }: TaskLogsPanelProps) {
7478
? task.latest_run.state.slack_thread_url
7579
: undefined;
7680

81+
// Cloud diff stats — reuses React Query cache from ChangesPanel
82+
const cloudBranch = isCloud
83+
? (workspace?.baseBranch ?? task.latest_run?.branch ?? null)
84+
: null;
85+
const cloudRepo = isCloud ? (task.repository ?? null) : null;
86+
const { data: prFiles } = useCloudPrChangedFiles(prUrl);
87+
const { data: branchFiles } = useCloudBranchChangedFiles(
88+
!prUrl ? cloudRepo : null,
89+
!prUrl ? cloudBranch : null,
90+
);
91+
const cloudDiffStats = useMemo(() => {
92+
if (!isCloud) return null;
93+
const files = prUrl ? prFiles : branchFiles;
94+
if (!files || files.length === 0) return null;
95+
return {
96+
filesChanged: files.length,
97+
linesAdded: files.reduce((sum, f) => sum + (f.linesAdded ?? 0), 0),
98+
linesRemoved: files.reduce((sum, f) => sum + (f.linesRemoved ?? 0), 0),
99+
};
100+
}, [isCloud, prUrl, prFiles, branchFiles]);
101+
77102
const isRunning = isCloud
78103
? isCloudRunNotTerminal
79104
: session?.status === "connected";
@@ -337,6 +362,8 @@ export function TaskLogsPanel({ taskId, task }: TaskLogsPanelProps) {
337362
onBashCommand={isCloud ? undefined : handleBashCommand}
338363
onCancelPrompt={handleCancelPrompt}
339364
repoPath={repoPath}
365+
cloudBranch={cloudBranch}
366+
cloudDiffStats={cloudDiffStats}
340367
hasError={hasError}
341368
errorTitle={errorTitle}
342369
errorMessage={errorMessage}

packages/agent/src/server/agent-server.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,5 +384,33 @@ describe("AgentServer HTTP Mode", () => {
384384
expect(prompt).toContain("Create a draft pull request");
385385
expect(prompt).toContain("gh pr create --draft");
386386
});
387+
388+
it("includes --base flag when baseBranch is configured", () => {
389+
server = new AgentServer({
390+
port,
391+
jwtPublicKey: TEST_PUBLIC_KEY,
392+
repositoryPath: repo.path,
393+
apiUrl: "http://localhost:8000",
394+
apiKey: "test-api-key",
395+
projectId: 1,
396+
mode: "interactive",
397+
taskId: "test-task-id",
398+
runId: "test-run-id",
399+
baseBranch: "add-yolo-to-readme",
400+
});
401+
const prompt = (
402+
server as unknown as TestableServer
403+
).buildCloudSystemPrompt();
404+
expect(prompt).toContain(
405+
"gh pr create --draft --base add-yolo-to-readme",
406+
);
407+
});
408+
409+
it("omits --base flag when baseBranch is not configured", () => {
410+
const s = createServer();
411+
const prompt = (s as unknown as TestableServer).buildCloudSystemPrompt();
412+
expect(prompt).toContain("gh pr create --draft`");
413+
expect(prompt).not.toContain("--base");
414+
});
387415
});
388416
});

packages/agent/src/server/agent-server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -747,7 +747,7 @@ After completing the requested changes:
747747
1. Create a new branch with a descriptive name based on the work done
748748
2. Stage and commit all changes with a clear commit message
749749
3. Push the branch to origin
750-
4. Create a draft pull request using \`gh pr create --draft\` with a descriptive title and body
750+
4. Create a draft pull request using \`gh pr create --draft${this.config.baseBranch ? ` --base ${this.config.baseBranch}` : ""}\` with a descriptive title and body
751751
752752
Important:
753753
- Always create the PR as a draft. Do not ask for confirmation.

packages/agent/src/server/bin.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ program
5050
"--mcpServers <json>",
5151
"MCP servers config as JSON array (ACP McpServer[] format)",
5252
)
53+
.option("--baseBranch <branch>", "Base branch for PR creation")
5354
.action(async (options) => {
5455
const envResult = envSchema.safeParse(process.env);
5556

@@ -99,6 +100,7 @@ program
99100
taskId: options.taskId,
100101
runId: options.runId,
101102
mcpServers,
103+
baseBranch: options.baseBranch,
102104
});
103105

104106
process.on("SIGINT", async () => {

packages/agent/src/server/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,5 @@ export interface AgentServerConfig {
1313
runId: string;
1414
version?: string;
1515
mcpServers?: RemoteMcpServer[];
16+
baseBranch?: string;
1617
}

0 commit comments

Comments
 (0)