Skip to content

Commit 283e948

Browse files
authored
Merge branch 'main' into feat/diff-context-expand
2 parents 4ae9a13 + fba8bef commit 283e948

71 files changed

Lines changed: 2017 additions & 549 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/code-release.yml

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -124,12 +124,6 @@ jobs:
124124
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
125125
run: pnpm --filter code run publish
126126

127-
- name: Publish GitHub release
128-
env:
129-
GH_TOKEN: ${{ steps.app-token.outputs.token }}
130-
APP_VERSION: ${{ steps.version.outputs.version }}
131-
run: gh release edit "v$APP_VERSION" --repo PostHog/code --draft=false
132-
133127
publish-windows:
134128
runs-on: windows-latest
135129
permissions:
@@ -202,3 +196,28 @@ jobs:
202196
APP_VERSION: ${{ steps.version.outputs.version }}
203197
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
204198
run: pnpm --filter code run publish
199+
200+
finalize-release:
201+
needs: [publish-macos, publish-windows]
202+
runs-on: ubuntu-latest
203+
permissions:
204+
contents: write
205+
steps:
206+
- name: Get app token
207+
id: app-token
208+
uses: getsentry/action-github-app-token@d4b5da6c5e37703f8c3b3e43abb5705b46e159cc # v3
209+
with:
210+
app_id: ${{ secrets.GH_APP_ARRAY_RELEASER_APP_ID }}
211+
private_key: ${{ secrets.GH_APP_ARRAY_RELEASER_PRIVATE_KEY }}
212+
213+
- name: Extract version from tag
214+
id: version
215+
run: |
216+
TAG_VERSION="${GITHUB_REF#refs/tags/v}"
217+
echo "version=$TAG_VERSION" >> "$GITHUB_OUTPUT"
218+
219+
- name: Publish GitHub release
220+
env:
221+
GH_TOKEN: ${{ steps.app-token.outputs.token }}
222+
APP_VERSION: ${{ steps.version.outputs.version }}
223+
run: gh release edit "v$APP_VERSION" --repo PostHog/code --draft=false

apps/code/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@
123123
"@opentelemetry/resources": "^2.5.0",
124124
"@opentelemetry/sdk-logs": "^0.208.0",
125125
"@opentelemetry/semantic-conventions": "^1.39.0",
126-
"@parcel/watcher": "^2.5.1",
126+
"@parcel/watcher": "^2.5.6",
127127
"@phosphor-icons/react": "^2.1.10",
128128
"@posthog/agent": "workspace:*",
129129
"@posthog/electron-trpc": "workspace:*",

apps/code/src/main/services/file-watcher/service.ts

Lines changed: 0 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { existsSync } from "node:fs";
22
import fs from "node:fs/promises";
33
import path from "node:path";
44
import * as watcher from "@parcel/watcher";
5-
import { app } from "electron";
65
import { inject, injectable } from "inversify";
76
import { MAIN_TOKENS } from "../../di/tokens";
87
import { logger } from "../../utils/logger";
@@ -70,9 +69,6 @@ export class FileWatcherService extends TypedEventEmitter<FileWatcherEvents> {
7069
async startWatching(repoPath: string): Promise<void> {
7170
if (this.watchers.has(repoPath)) return;
7271

73-
await fs.mkdir(this.snapshotsDir, { recursive: true });
74-
await this.emitChangesSinceSnapshot(repoPath);
75-
7672
const pending: PendingChanges = {
7773
dirs: new Set(),
7874
files: new Set(),
@@ -107,53 +103,13 @@ export class FileWatcherService extends TypedEventEmitter<FileWatcherEvents> {
107103
if (!w) return;
108104

109105
if (w.pending.timer) clearTimeout(w.pending.timer);
110-
await this.saveSnapshot(repoPath);
111106
await this.watcherRegistry.unregister(w.filesId);
112107
for (const gitId of w.gitIds) {
113108
await this.watcherRegistry.unregister(gitId);
114109
}
115110
this.watchers.delete(repoPath);
116111
}
117112

118-
private get snapshotsDir(): string {
119-
return path.join(app.getPath("userData"), "snapshots");
120-
}
121-
122-
private snapshotPath(repoPath: string): string {
123-
return path.join(
124-
this.snapshotsDir,
125-
`${Buffer.from(repoPath).toString("base64url")}.snapshot`,
126-
);
127-
}
128-
129-
private async saveSnapshot(repoPath: string): Promise<void> {
130-
try {
131-
await watcher.writeSnapshot(repoPath, this.snapshotPath(repoPath), {
132-
ignore: IGNORE_PATTERNS,
133-
});
134-
} catch (error) {
135-
log.error("Failed to write snapshot:", error);
136-
}
137-
}
138-
139-
private async emitChangesSinceSnapshot(repoPath: string): Promise<void> {
140-
const snapshotPath = this.snapshotPath(repoPath);
141-
try {
142-
await fs.access(snapshotPath);
143-
} catch {
144-
return;
145-
}
146-
147-
const events = await watcher.getEventsSince(repoPath, snapshotPath, {
148-
ignore: IGNORE_PATTERNS,
149-
});
150-
151-
const affectedDirs = new Set(events.map((e) => path.dirname(e.path)));
152-
for (const dirPath of affectedDirs) {
153-
this.emit(FileWatcherEvent.DirectoryChanged, { repoPath, dirPath });
154-
}
155-
}
156-
157113
private async watchFiles(
158114
repoPath: string,
159115
pending: PendingChanges,

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -766,10 +766,10 @@ export class GitService extends TypedEventEmitter<GitServiceEvents> {
766766
".default_branch",
767767
]);
768768

769-
const defaultBranch =
770-
repoResult.exitCode === 0 && repoResult.stdout.trim()
771-
? repoResult.stdout.trim()
772-
: "main";
769+
if (repoResult.exitCode !== 0 || !repoResult.stdout.trim()) {
770+
return [];
771+
}
772+
const defaultBranch = repoResult.stdout.trim();
773773

774774
const result = await execGh([
775775
"api",

apps/code/src/main/services/workspace/service.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -446,8 +446,8 @@ export class WorkspaceService extends TypedEventEmitter<WorkspaceServiceEvents>
446446
let worktree: WorktreeInfo;
447447

448448
try {
449-
const defaultBranch = await getDefaultBranch(mainRepoPath).catch(
450-
() => "main",
449+
const defaultBranch = await getDefaultBranch(mainRepoPath).catch(() =>
450+
getCurrentBranch(mainRepoPath).then((b) => b ?? "main"),
451451
);
452452
const selectedBranch = branch ?? defaultBranch;
453453
const isTrunkSelected = selectedBranch === defaultBranch;

apps/code/src/renderer/api/posthogClient.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import type {
2+
SandboxEnvironment,
3+
SandboxEnvironmentInput,
24
SignalReportArtefact,
35
SignalReportArtefactsResponse,
46
SignalReportSignalsResponse,
@@ -504,6 +506,7 @@ export class PostHogAPIClient {
504506
taskId: string,
505507
branch?: string | null,
506508
resumeOptions?: { resumeFromRunId: string; pendingUserMessage: string },
509+
sandboxEnvironmentId?: string,
507510
): Promise<Task> {
508511
const teamId = await this.getTeamId();
509512
const body: Record<string, unknown> = { mode: "interactive" };
@@ -514,6 +517,9 @@ export class PostHogAPIClient {
514517
body.resume_from_run_id = resumeOptions.resumeFromRunId;
515518
body.pending_user_message = resumeOptions.pendingUserMessage;
516519
}
520+
if (sandboxEnvironmentId) {
521+
body.sandbox_environment_id = sandboxEnvironmentId;
522+
}
517523

518524
const data = await this.api.post(
519525
`/api/projects/{project_id}/tasks/{id}/run/`,
@@ -1164,4 +1170,89 @@ export class PostHogAPIClient {
11641170
return false;
11651171
}
11661172
}
1173+
1174+
// Sandbox Environments
1175+
1176+
async listSandboxEnvironments(): Promise<SandboxEnvironment[]> {
1177+
const teamId = await this.getTeamId();
1178+
const url = new URL(
1179+
`${this.api.baseUrl}/api/projects/${teamId}/sandbox_environments/`,
1180+
);
1181+
const response = await this.api.fetcher.fetch({
1182+
method: "get",
1183+
url,
1184+
path: `/api/projects/${teamId}/sandbox_environments/`,
1185+
});
1186+
if (!response.ok) {
1187+
throw new Error(
1188+
`Failed to fetch sandbox environments: ${response.statusText}`,
1189+
);
1190+
}
1191+
const data = await response.json();
1192+
return (data.results ?? data) as SandboxEnvironment[];
1193+
}
1194+
1195+
async createSandboxEnvironment(
1196+
input: SandboxEnvironmentInput,
1197+
): Promise<SandboxEnvironment> {
1198+
const teamId = await this.getTeamId();
1199+
const url = new URL(
1200+
`${this.api.baseUrl}/api/projects/${teamId}/sandbox_environments/`,
1201+
);
1202+
const response = await this.api.fetcher.fetch({
1203+
method: "post",
1204+
url,
1205+
path: `/api/projects/${teamId}/sandbox_environments/`,
1206+
overrides: {
1207+
body: JSON.stringify(input),
1208+
},
1209+
});
1210+
if (!response.ok) {
1211+
throw new Error(
1212+
`Failed to create sandbox environment: ${response.statusText}`,
1213+
);
1214+
}
1215+
return (await response.json()) as SandboxEnvironment;
1216+
}
1217+
1218+
async updateSandboxEnvironment(
1219+
id: string,
1220+
input: Partial<SandboxEnvironmentInput>,
1221+
): Promise<SandboxEnvironment> {
1222+
const teamId = await this.getTeamId();
1223+
const url = new URL(
1224+
`${this.api.baseUrl}/api/projects/${teamId}/sandbox_environments/${id}/`,
1225+
);
1226+
const response = await this.api.fetcher.fetch({
1227+
method: "patch",
1228+
url,
1229+
path: `/api/projects/${teamId}/sandbox_environments/${id}/`,
1230+
overrides: {
1231+
body: JSON.stringify(input),
1232+
},
1233+
});
1234+
if (!response.ok) {
1235+
throw new Error(
1236+
`Failed to update sandbox environment: ${response.statusText}`,
1237+
);
1238+
}
1239+
return (await response.json()) as SandboxEnvironment;
1240+
}
1241+
1242+
async deleteSandboxEnvironment(id: string): Promise<void> {
1243+
const teamId = await this.getTeamId();
1244+
const url = new URL(
1245+
`${this.api.baseUrl}/api/projects/${teamId}/sandbox_environments/${id}/`,
1246+
);
1247+
const response = await this.api.fetcher.fetch({
1248+
method: "delete",
1249+
url,
1250+
path: `/api/projects/${teamId}/sandbox_environments/${id}/`,
1251+
});
1252+
if (!response.ok) {
1253+
throw new Error(
1254+
`Failed to delete sandbox environment: ${response.statusText}`,
1255+
);
1256+
}
1257+
}
11671258
}

apps/code/src/renderer/components/permissions/PlanContent.tsx

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,43 @@
11
import { Box } from "@radix-ui/themes";
2+
import { useEffect, useRef } from "react";
23
import ReactMarkdown from "react-markdown";
34
import remarkGfm from "remark-gfm";
45

6+
const planScrollPosition = new Map<string, number>();
7+
58
interface PlanContentProps {
9+
id: string;
610
plan: string;
711
}
812

9-
export function PlanContent({ plan }: PlanContentProps) {
13+
export function PlanContent({ id, plan }: PlanContentProps) {
14+
const scrollRef = useRef<HTMLDivElement>(null);
15+
16+
useEffect(() => {
17+
const el = scrollRef.current;
18+
if (!el) return;
19+
20+
const position = planScrollPosition.get(id);
21+
if (position !== undefined) {
22+
el.scrollTop = position;
23+
}
24+
25+
const handleScroll = () => {
26+
planScrollPosition.set(id, el.scrollTop);
27+
};
28+
29+
el.addEventListener("scroll", handleScroll, { passive: true });
30+
31+
return () => {
32+
el.removeEventListener("scroll", handleScroll);
33+
};
34+
}, [id]);
35+
1036
return (
11-
<Box className="max-h-[50vh] max-w-[750px] overflow-y-auto rounded-lg border-2 border-blue-6 bg-blue-2 p-4">
37+
<Box
38+
ref={scrollRef}
39+
className="max-h-[50vh] max-w-[750px] overflow-y-auto rounded-lg border-2 border-blue-6 bg-blue-2 p-4"
40+
>
1241
<Box className="plan-markdown text-blue-12">
1342
<ReactMarkdown remarkPlugins={[remarkGfm]}>{plan}</ReactMarkdown>
1443
</Box>

apps/code/src/renderer/features/command-center/components/CommandCenterGrid.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ function GridCell({
6060

6161
const handleCellClick = useCallback(() => {
6262
setActiveTask(cell.taskId);
63+
const selection = window.getSelection();
64+
if (selection && !selection.isCollapsed) return;
6365
const actionSelector =
6466
cellRef.current?.querySelector<HTMLElement>("[tabindex='0']");
6567
actionSelector?.focus();
@@ -101,9 +103,7 @@ function GridCell({
101103
// biome-ignore lint/a11y/useKeyWithClickEvents lint/a11y/noStaticElementInteractions: click delegates focus to ActionSelector within
102104
<div
103105
ref={cellRef}
104-
className={`relative overflow-hidden bg-gray-1 focus-within:ring-2 focus-within:ring-accent-9 focus-within:ring-inset ${
105-
isActive ? "ring-2 ring-accent-9 ring-inset" : ""
106-
}`}
106+
className="relative overflow-hidden bg-gray-1"
107107
onClick={handleCellClick}
108108
onPointerDownCapture={handleCellPointerDownCapture}
109109
onFocusCapture={handleCellFocusCapture}
@@ -116,6 +116,9 @@ function GridCell({
116116
>
117117
<CommandCenterPanel cell={cell} isActiveSession={isActive} />
118118
</div>
119+
{isActive && (
120+
<div className="pointer-events-none absolute inset-0 border-2 border-accent-9" />
121+
)}
119122
{isDragActive && (
120123
// biome-ignore lint/a11y/noStaticElementInteractions: transparent overlay to capture drag events over session content
121124
<div

0 commit comments

Comments
 (0)