Skip to content

Commit 147a2af

Browse files
committed
feat(cloud): cloud task rendering
chore: better pagination slurp chore: log writer flush chore: cleaner chore: cleaner
1 parent e97cb30 commit 147a2af

File tree

22 files changed

+888
-55
lines changed

22 files changed

+888
-55
lines changed

apps/twig/src/api/posthogClient.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,7 @@ export class PostHogAPIClient {
354354
task_id: taskId,
355355
id: runId,
356356
},
357-
body: updates,
357+
body: updates as Record<string, unknown>,
358358
},
359359
);
360360
return data as unknown as TaskRun;
@@ -383,6 +383,40 @@ export class PostHogAPIClient {
383383
}
384384
}
385385

386+
async getTaskRunSessionLogs(
387+
taskId: string,
388+
runId: string,
389+
options?: { limit?: number; after?: string },
390+
): Promise<StoredLogEntry[]> {
391+
try {
392+
const teamId = await this.getTeamId();
393+
const url = new URL(
394+
`${this.api.baseUrl}/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/session_logs/`,
395+
);
396+
url.searchParams.set("limit", String(options?.limit ?? 5000));
397+
if (options?.after) {
398+
url.searchParams.set("after", options.after);
399+
}
400+
const response = await this.api.fetcher.fetch({
401+
method: "get",
402+
url,
403+
path: `/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/session_logs/`,
404+
});
405+
406+
if (!response.ok) {
407+
log.warn(
408+
`Failed to fetch session logs: ${response.status} ${response.statusText}`,
409+
);
410+
return [];
411+
}
412+
413+
return (await response.json()) as StoredLogEntry[];
414+
} catch (err) {
415+
log.warn("Failed to fetch task run session logs", err);
416+
return [];
417+
}
418+
}
419+
386420
async getTaskLogs(taskId: string): Promise<StoredLogEntry[]> {
387421
try {
388422
const task = (await this.getTask(taskId)) as unknown as Task;

apps/twig/src/main/services/git/schemas.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,12 @@ export type GetCommitConventionsOutput = z.infer<
282282
typeof getCommitConventionsOutput
283283
>;
284284

285+
// getPrChangedFiles schemas
286+
export const getPrChangedFilesInput = z.object({
287+
prUrl: z.string(),
288+
});
289+
export const getPrChangedFilesOutput = z.array(changedFileSchema);
290+
285291
export const generateCommitMessageInput = z.object({
286292
directoryPath: z.string(),
287293
credentials: z.object({
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import { beforeEach, describe, expect, it, vi } from "vitest";
2+
3+
const mockExecGh = vi.hoisted(() => vi.fn());
4+
5+
vi.mock("@twig/git/gh", () => ({
6+
execGh: mockExecGh,
7+
}));
8+
9+
vi.mock("../../lib/logger.js", () => ({
10+
logger: {
11+
scope: () => ({
12+
info: vi.fn(),
13+
warn: vi.fn(),
14+
error: vi.fn(),
15+
debug: vi.fn(),
16+
}),
17+
},
18+
}));
19+
20+
import type { LlmGatewayService } from "../llm-gateway/service.js";
21+
import { GitService } from "./service.js";
22+
23+
describe("GitService.getPrChangedFiles", () => {
24+
let service: GitService;
25+
26+
beforeEach(() => {
27+
vi.clearAllMocks();
28+
service = new GitService({} as LlmGatewayService);
29+
});
30+
31+
it("flattens paginated GH API results and maps file statuses", async () => {
32+
mockExecGh.mockResolvedValue({
33+
exitCode: 0,
34+
stdout: JSON.stringify([
35+
[
36+
{
37+
filename: "src/new.ts",
38+
status: "added",
39+
additions: 10,
40+
deletions: 0,
41+
},
42+
{
43+
filename: "src/old.ts",
44+
status: "removed",
45+
additions: 0,
46+
deletions: 3,
47+
},
48+
],
49+
[
50+
{
51+
filename: "src/renamed-new.ts",
52+
status: "renamed",
53+
previous_filename: "src/renamed-old.ts",
54+
additions: 1,
55+
deletions: 1,
56+
},
57+
{
58+
filename: "src/changed.ts",
59+
status: "changed",
60+
additions: 4,
61+
deletions: 2,
62+
},
63+
],
64+
]),
65+
});
66+
67+
const result = await service.getPrChangedFiles(
68+
"https://github.com/posthog/twig/pull/123",
69+
);
70+
71+
expect(mockExecGh).toHaveBeenCalledWith([
72+
"api",
73+
"repos/posthog/twig/pulls/123/files",
74+
"--paginate",
75+
"--slurp",
76+
]);
77+
78+
expect(result).toEqual([
79+
{
80+
path: "src/new.ts",
81+
status: "added",
82+
originalPath: undefined,
83+
linesAdded: 10,
84+
linesRemoved: 0,
85+
},
86+
{
87+
path: "src/old.ts",
88+
status: "deleted",
89+
originalPath: undefined,
90+
linesAdded: 0,
91+
linesRemoved: 3,
92+
},
93+
{
94+
path: "src/renamed-new.ts",
95+
status: "renamed",
96+
originalPath: "src/renamed-old.ts",
97+
linesAdded: 1,
98+
linesRemoved: 1,
99+
},
100+
{
101+
path: "src/changed.ts",
102+
status: "modified",
103+
originalPath: undefined,
104+
linesAdded: 4,
105+
linesRemoved: 2,
106+
},
107+
]);
108+
});
109+
110+
it("returns empty array for non-GitHub PR URL", async () => {
111+
const result = await service.getPrChangedFiles(
112+
"https://example.com/pull/1",
113+
);
114+
expect(result).toEqual([]);
115+
expect(mockExecGh).not.toHaveBeenCalled();
116+
});
117+
118+
it("returns empty array when gh command fails", async () => {
119+
mockExecGh.mockResolvedValue({ exitCode: 1, stdout: "" });
120+
121+
const result = await service.getPrChangedFiles(
122+
"https://github.com/posthog/twig/pull/123",
123+
);
124+
125+
expect(result).toEqual([]);
126+
});
127+
});

apps/twig/src/main/services/git/service.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -667,6 +667,64 @@ export class GitService extends TypedEventEmitter<GitServiceEvents> {
667667
return { success: !!prUrl, message: prUrl ? "OK" : "No PR found", prUrl };
668668
}
669669

670+
public async getPrChangedFiles(prUrl: string): Promise<ChangedFile[]> {
671+
const match = prUrl.match(/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)/);
672+
if (!match) return [];
673+
674+
const [, owner, repo, number] = match;
675+
676+
try {
677+
const result = await execGh([
678+
"api",
679+
`repos/${owner}/${repo}/pulls/${number}/files`,
680+
"--paginate",
681+
"--slurp",
682+
]);
683+
684+
if (result.exitCode !== 0) return [];
685+
686+
const pages = JSON.parse(result.stdout) as Array<
687+
Array<{
688+
filename: string;
689+
status: string;
690+
previous_filename?: string;
691+
additions: number;
692+
deletions: number;
693+
}>
694+
>;
695+
const files = pages.flat();
696+
697+
return files.map((f) => {
698+
let status: ChangedFile["status"];
699+
switch (f.status) {
700+
case "added":
701+
status = "added";
702+
break;
703+
case "removed":
704+
status = "deleted";
705+
break;
706+
case "renamed":
707+
status = "renamed";
708+
break;
709+
default:
710+
status = "modified";
711+
break;
712+
}
713+
714+
return {
715+
path: f.filename,
716+
status,
717+
originalPath: f.previous_filename,
718+
linesAdded: f.additions,
719+
linesRemoved: f.deletions,
720+
};
721+
});
722+
} catch (error) {
723+
log.warn("Failed to fetch PR changed files", { prUrl, error });
724+
return [];
725+
}
726+
}
727+
670728
public async generateCommitMessage(
671729
directoryPath: string,
672730
credentials: LlmCredentials,

apps/twig/src/main/trpc/routers/git.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ import {
3636
getGitSyncStatusOutput,
3737
getLatestCommitInput,
3838
getLatestCommitOutput,
39+
getPrChangedFilesInput,
40+
getPrChangedFilesOutput,
3941
getPrTemplateInput,
4042
getPrTemplateOutput,
4143
ghStatusOutput,
@@ -254,6 +256,11 @@ export const gitRouter = router({
254256
getService().getCommitConventions(input.directoryPath, input.sampleSize),
255257
),
256258

259+
getPrChangedFiles: publicProcedure
260+
.input(getPrChangedFilesInput)
261+
.output(getPrChangedFilesOutput)
262+
.query(({ input }) => getService().getPrChangedFiles(input.prUrl)),
263+
257264
generateCommitMessage: publicProcedure
258265
.input(generateCommitMessageInput)
259266
.output(generateCommitMessageOutput)

apps/twig/src/renderer/components/HeaderRow.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { RightSidebarTrigger } from "@features/right-sidebar/components/RightSid
33
import { useRightSidebarStore } from "@features/right-sidebar/stores/rightSidebarStore";
44
import { SidebarTrigger } from "@features/sidebar/components/SidebarTrigger";
55
import { useSidebarStore } from "@features/sidebar/stores/sidebarStore";
6+
import { useWorkspaceStore } from "@features/workspace/stores/workspaceStore";
67
import { Box, Flex } from "@radix-ui/themes";
78
import { useHeaderStore } from "@stores/headerStore";
89
import { useNavigationStore } from "@stores/navigationStore";
@@ -28,6 +29,11 @@ export function HeaderRow() {
2829
(state) => state.setIsResizing,
2930
);
3031

32+
const activeTaskId = view.type === "task-detail" ? view.data?.id : undefined;
33+
const activeWorkspace = useWorkspaceStore((s) =>
34+
activeTaskId ? s.workspaces[activeTaskId] : undefined,
35+
);
36+
const isCloudTask = activeWorkspace?.mode === "cloud";
3137
const showRightSidebarSection = view.type === "task-detail";
3238

3339
const handleLeftSidebarMouseDown = (e: React.MouseEvent) => {
@@ -119,7 +125,7 @@ export function HeaderRow() {
119125
}}
120126
>
121127
<RightSidebarTrigger />
122-
<GitInteractionHeader taskId={view.data.id} />
128+
{!isCloudTask && <GitInteractionHeader taskId={view.data.id} />}
123129
{rightSidebarOpen && (
124130
<Box
125131
onMouseDown={handleRightSidebarMouseDown}

apps/twig/src/renderer/features/git-interaction/hooks/useGitQueries.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { trpcVanilla } from "@renderer/trpc";
2+
import type { ChangedFile } from "@shared/types";
23
import { useQuery } from "@tanstack/react-query";
34

45
const EMPTY_DIFF_STATS = { filesChanged: 0, linesAdded: 0, linesRemoved: 0 };
@@ -121,3 +122,16 @@ export function useGitQueries(repoPath?: string) {
121122
isLoading: isRepoLoading || changesLoading || syncLoading,
122123
};
123124
}
125+
126+
const EMPTY_FILES: ChangedFile[] = [];
127+
128+
export function useCloudPrChangedFiles(prUrl: string | null) {
129+
return useQuery({
130+
queryKey: ["pr-changed-files", prUrl],
131+
queryFn: () =>
132+
trpcVanilla.git.getPrChangedFiles.query({ prUrl: prUrl as string }),
133+
enabled: !!prUrl,
134+
staleTime: 5 * 60_000,
135+
placeholderData: EMPTY_FILES,
136+
});
137+
}

apps/twig/src/renderer/features/panels/components/LeafNodeRenderer.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1+
import { Cloud as CloudIcon } from "@phosphor-icons/react";
2+
import { Flex, Text } from "@radix-ui/themes";
13
import type { Task } from "@shared/types";
24
import type React from "react";
5+
import { useMemo } from "react";
6+
import { useWorkspaceStore } from "@/renderer/features/workspace/stores/workspaceStore";
37
import { useTabInjection } from "../hooks/usePanelLayoutHooks";
48
import type { SplitDirection } from "../store/panelLayoutStore";
59
import type { LeafPanel } from "../store/panelTypes";
@@ -44,6 +48,29 @@ export const LeafNodeRenderer: React.FC<LeafNodeRendererProps> = ({
4448
closeTab,
4549
);
4650

51+
const workspace = useWorkspaceStore((s) => s.workspaces[taskId]);
52+
const isCloud = workspace?.mode === "cloud";
53+
54+
const cloudEmptyState = useMemo(
55+
() =>
56+
isCloud ? (
57+
<Flex
58+
align="center"
59+
justify="center"
60+
height="100%"
61+
style={{ backgroundColor: "var(--gray-2)" }}
62+
>
63+
<Flex direction="column" align="center" gap="2">
64+
<CloudIcon size={24} className="text-gray-10" />
65+
<Text size="2" color="gray">
66+
Cloud runs are read-only
67+
</Text>
68+
</Flex>
69+
</Flex>
70+
) : undefined,
71+
[isCloud],
72+
);
73+
4774
const contentWithComponents = {
4875
...node.content,
4976
tabs,
@@ -62,6 +89,7 @@ export const LeafNodeRenderer: React.FC<LeafNodeRendererProps> = ({
6289
draggingTabPanelId={draggingTabPanelId}
6390
onAddTerminal={() => onAddTerminal(node.id)}
6491
onSplitPanel={(direction) => onSplitPanel(node.id, direction)}
92+
emptyState={cloudEmptyState}
6593
/>
6694
);
6795
};

0 commit comments

Comments
 (0)