Skip to content

Commit ea3a8e6

Browse files
authored
Merge branch 'main' into 03-16-docs
2 parents d6deca7 + 4f5ee10 commit ea3a8e6

File tree

29 files changed

+346
-341
lines changed

29 files changed

+346
-341
lines changed

apps/code/ARCHITECTURE.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,26 @@ const useTaskStore = create<TaskState>((set) => ({
281281
}));
282282
```
283283

284+
### Learned Hints
285+
286+
The settings store (`src/renderer/features/settings/stores/settingsStore.ts`) provides a reusable "learned hints" system for progressive feature discovery. Hints are shown a limited number of times until the user demonstrates they've learned the behavior.
287+
288+
```typescript
289+
// In the store: hints is Record<string, { count: number; learned: boolean }>
290+
const store = useFeatureSettingsStore.getState();
291+
292+
// Check if a hint should still be shown (max N times, not yet learned)
293+
if (store.shouldShowHint("my-hint-key", 3)) {
294+
store.recordHintShown("my-hint-key");
295+
toast.info("Did you know?", "You can do X with Y.");
296+
}
297+
298+
// When the user demonstrates the behavior, mark it learned (stops showing)
299+
store.markHintLearned("my-hint-key");
300+
```
301+
302+
Hint state is persisted via `electronStorage`. Use this pattern instead of ad-hoc boolean flags when introducing new discoverable features.
303+
284304
## Services
285305

286306
Services encapsulate business logic and exist in both processes:
Lines changed: 8 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,8 @@
1-
import { z } from "zod";
2-
3-
export const cloudRegion = z.enum(["us", "eu", "dev"]);
4-
export type CloudRegion = z.infer<typeof cloudRegion>;
5-
6-
export const startGitHubFlowInput = z.object({
7-
region: cloudRegion,
8-
projectId: z.number(),
9-
});
10-
export type StartGitHubFlowInput = z.infer<typeof startGitHubFlowInput>;
11-
12-
export const startGitHubFlowOutput = z.object({
13-
success: z.boolean(),
14-
error: z.string().optional(),
15-
});
16-
export type StartGitHubFlowOutput = z.infer<typeof startGitHubFlowOutput>;
17-
18-
export const cancelGitHubFlowOutput = z.object({
19-
success: z.boolean(),
20-
error: z.string().optional(),
21-
});
22-
export type CancelGitHubFlowOutput = z.infer<typeof cancelGitHubFlowOutput>;
1+
export {
2+
type CloudRegion,
3+
cloudRegion,
4+
type StartIntegrationFlowInput as StartGitHubFlowInput,
5+
type StartIntegrationFlowOutput as StartGitHubFlowOutput,
6+
startIntegrationFlowInput as startGitHubFlowInput,
7+
startIntegrationFlowOutput as startGitHubFlowOutput,
8+
} from "../integration-flow-schemas";
Lines changed: 6 additions & 194 deletions
Original file line numberDiff line numberDiff line change
@@ -1,101 +1,25 @@
1-
import * as http from "node:http";
2-
import type { Socket } from "node:net";
31
import { getCloudUrlFromRegion } from "@shared/constants/oauth";
42
import { shell } from "electron";
5-
import { inject, injectable } from "inversify";
6-
import { MAIN_TOKENS } from "../../di/tokens";
3+
import { injectable } from "inversify";
74
import { logger } from "../../utils/logger";
8-
import type { DeepLinkService } from "../deep-link/service";
9-
import type {
10-
CancelGitHubFlowOutput,
11-
CloudRegion,
12-
StartGitHubFlowOutput,
13-
} from "./schemas";
5+
import type { CloudRegion, StartGitHubFlowOutput } from "./schemas";
146

157
const log = logger.scope("github-integration-service");
168

17-
const PROTOCOL = "posthog-code";
18-
const TIMEOUT_MS = 300_000; // 5 minutes
19-
const DEV_CALLBACK_PORT = 8239; // Different from OAuth's 8237 and MCP's 8238
20-
21-
// Use HTTP callback in development, deep link in production
22-
const IS_DEV = process.defaultApp || false;
23-
24-
interface PendingFlow {
25-
resolve: (success: boolean) => void;
26-
reject: (error: Error) => void;
27-
timeoutId: NodeJS.Timeout;
28-
server?: http.Server;
29-
connections?: Set<Socket>;
30-
}
31-
329
@injectable()
3310
export class GitHubIntegrationService {
34-
private pendingFlow: PendingFlow | null = null;
35-
36-
constructor(
37-
@inject(MAIN_TOKENS.DeepLinkService)
38-
private readonly deepLinkService: DeepLinkService,
39-
) {
40-
this.deepLinkService.registerHandler("github-connected", () =>
41-
this.handleCallback(),
42-
);
43-
log.info("Registered github-connected handler for deep links");
44-
}
45-
46-
private handleCallback(): boolean {
47-
if (!this.pendingFlow) {
48-
log.warn("Received GitHub callback but no pending flow");
49-
return false;
50-
}
51-
const { resolve, timeoutId } = this.pendingFlow;
52-
clearTimeout(timeoutId);
53-
this.pendingFlow = null;
54-
resolve(true);
55-
return true;
56-
}
57-
58-
private getCallbackUrl(): string {
59-
return IS_DEV
60-
? `http://localhost:${DEV_CALLBACK_PORT}/github-callback`
61-
: `${PROTOCOL}://github-connected`;
62-
}
63-
6411
public async startFlow(
6512
region: CloudRegion,
6613
projectId: number,
6714
): Promise<StartGitHubFlowOutput> {
6815
try {
69-
this.cancelFlow();
70-
7116
const cloudUrl = getCloudUrlFromRegion(region);
72-
const callbackUrl = this.getCallbackUrl();
73-
const authorizeUrl = `${cloudUrl}/api/environments/${projectId}/integrations/authorize/?kind=github&next=${encodeURIComponent(callbackUrl)}`;
74-
75-
const success = IS_DEV
76-
? await this.waitForHttpCallback(authorizeUrl)
77-
: await this.waitForDeepLinkCallback(authorizeUrl);
17+
const next = `${cloudUrl}/projects/${projectId}`;
18+
const authorizeUrl = `${cloudUrl}/api/environments/${projectId}/integrations/authorize/?kind=github&next=${encodeURIComponent(next)}`;
7819

79-
return { success };
80-
} catch (error) {
81-
return {
82-
success: false,
83-
error: error instanceof Error ? error.message : "Unknown error",
84-
};
85-
}
86-
}
20+
log.info("Opening GitHub authorization URL in browser");
21+
await shell.openExternal(authorizeUrl);
8722

88-
public cancelFlow(): CancelGitHubFlowOutput {
89-
try {
90-
if (this.pendingFlow) {
91-
if (this.pendingFlow.server) {
92-
this.cleanupHttpServer();
93-
} else {
94-
clearTimeout(this.pendingFlow.timeoutId);
95-
this.pendingFlow.reject(new Error("GitHub flow cancelled"));
96-
this.pendingFlow = null;
97-
}
98-
}
9923
return { success: true };
10024
} catch (error) {
10125
return {
@@ -104,116 +28,4 @@ export class GitHubIntegrationService {
10428
};
10529
}
10630
}
107-
108-
private async waitForDeepLinkCallback(
109-
authorizeUrl: string,
110-
): Promise<boolean> {
111-
return new Promise<boolean>((resolve, reject) => {
112-
const timeoutId = setTimeout(() => {
113-
this.pendingFlow = null;
114-
reject(new Error("Authorization timed out"));
115-
}, TIMEOUT_MS);
116-
117-
this.pendingFlow = { resolve, reject, timeoutId };
118-
119-
shell.openExternal(authorizeUrl).catch((error) => {
120-
clearTimeout(timeoutId);
121-
this.pendingFlow = null;
122-
reject(new Error(`Failed to open browser: ${error.message}`));
123-
});
124-
});
125-
}
126-
127-
private async waitForHttpCallback(authorizeUrl: string): Promise<boolean> {
128-
return new Promise<boolean>((resolve, reject) => {
129-
const connections = new Set<Socket>();
130-
131-
const server = http.createServer((req, res) => {
132-
if (!req.url) {
133-
res.writeHead(400);
134-
res.end();
135-
return;
136-
}
137-
138-
const url = new URL(req.url, `http://localhost:${DEV_CALLBACK_PORT}`);
139-
140-
if (url.pathname === "/github-callback") {
141-
res.writeHead(200, { "Content-Type": "text/html" });
142-
res.end(this.getCallbackHtml());
143-
this.cleanupHttpServer();
144-
resolve(true);
145-
} else {
146-
res.writeHead(404);
147-
res.end();
148-
}
149-
});
150-
151-
server.on("connection", (conn) => {
152-
connections.add(conn);
153-
conn.on("close", () => connections.delete(conn));
154-
});
155-
156-
const timeoutId = setTimeout(() => {
157-
this.cleanupHttpServer();
158-
reject(new Error("Authorization timed out"));
159-
}, TIMEOUT_MS);
160-
161-
this.pendingFlow = { resolve, reject, timeoutId, server, connections };
162-
163-
server.listen(DEV_CALLBACK_PORT, () => {
164-
log.info(
165-
`Dev GitHub callback server listening on port ${DEV_CALLBACK_PORT}`,
166-
);
167-
shell.openExternal(authorizeUrl).catch((error) => {
168-
this.cleanupHttpServer();
169-
reject(new Error(`Failed to open browser: ${error.message}`));
170-
});
171-
});
172-
173-
server.on("error", (error) => {
174-
this.cleanupHttpServer();
175-
reject(new Error(`Failed to start callback server: ${error.message}`));
176-
});
177-
});
178-
}
179-
180-
private getCallbackHtml(): string {
181-
return `<!DOCTYPE html>
182-
<html class="radix-themes" data-is-root-theme="true" data-accent-color="orange" data-gray-color="slate" data-has-background="true" data-panel-background="translucent" data-radius="none" data-scaling="100%">
183-
<head>
184-
<meta charset="utf-8">
185-
<title>GitHub connected</title>
186-
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@radix-ui/themes@3.1.6/styles.css">
187-
<script src="https://cdn.tailwindcss.com"></script>
188-
<style>
189-
@layer utilities {
190-
.text-gray-12 { color: var(--gray-12); }
191-
.text-gray-11 { color: var(--gray-11); }
192-
.bg-gray-1 { background-color: var(--gray-1); }
193-
}
194-
</style>
195-
</head>
196-
<body class="dark bg-gray-1 h-screen overflow-hidden flex flex-col items-center justify-center m-0 gap-2">
197-
<h1 class="text-gray-12 text-xl font-semibold">GitHub connected!</h1>
198-
<p class="text-gray-11 text-sm">You can close this window and return to PostHog Code.</p>
199-
<script>setTimeout(() => window.close(), 500);</script>
200-
</body>
201-
</html>`;
202-
}
203-
204-
private cleanupHttpServer(): void {
205-
if (this.pendingFlow?.server) {
206-
if (this.pendingFlow.connections) {
207-
for (const conn of this.pendingFlow.connections) {
208-
conn.destroy();
209-
}
210-
this.pendingFlow.connections.clear();
211-
}
212-
this.pendingFlow.server.close();
213-
}
214-
if (this.pendingFlow?.timeoutId) {
215-
clearTimeout(this.pendingFlow.timeoutId);
216-
}
217-
this.pendingFlow = null;
218-
}
21931
}

apps/code/src/main/services/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@
33
* This file is auto-generated by vite-plugin-auto-services.ts
44
*/
55

6+
import "./integration-flow-schemas.js";
67
import "./posthog-analytics.js";
78
import "./settingsStore.js";
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { z } from "zod";
2+
3+
export const cloudRegion = z.enum(["us", "eu", "dev"]);
4+
export type CloudRegion = z.infer<typeof cloudRegion>;
5+
6+
export const startIntegrationFlowInput = z.object({
7+
region: cloudRegion,
8+
projectId: z.number(),
9+
});
10+
export type StartIntegrationFlowInput = z.infer<
11+
typeof startIntegrationFlowInput
12+
>;
13+
14+
export const startIntegrationFlowOutput = z.object({
15+
success: z.boolean(),
16+
error: z.string().optional(),
17+
});
18+
export type StartIntegrationFlowOutput = z.infer<
19+
typeof startIntegrationFlowOutput
20+
>;

apps/code/src/main/services/updates/service.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -686,21 +686,21 @@ describe("UpdatesService", () => {
686686
expect(mockAutoUpdater.checkForUpdates).toHaveBeenCalled();
687687
});
688688

689-
it("performs check every hour", async () => {
689+
it("performs check every 24 hours", async () => {
690690
await initializeService(service);
691691

692692
const initialCallCount =
693693
mockAutoUpdater.checkForUpdates.mock.calls.length;
694694

695-
// Advance 1 hour
696-
await vi.advanceTimersByTimeAsync(60 * 60 * 1000);
695+
// Advance 24 hours
696+
await vi.advanceTimersByTimeAsync(24 * 60 * 60 * 1000);
697697

698698
expect(mockAutoUpdater.checkForUpdates.mock.calls.length).toBe(
699699
initialCallCount + 1,
700700
);
701701

702-
// Advance another hour
703-
await vi.advanceTimersByTimeAsync(60 * 60 * 1000);
702+
// Advance another 24 hours
703+
await vi.advanceTimersByTimeAsync(24 * 60 * 60 * 1000);
704704

705705
expect(mockAutoUpdater.checkForUpdates.mock.calls.length).toBe(
706706
initialCallCount + 2,

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export class UpdatesService extends TypedEventEmitter<UpdatesEvents> {
2222
private static readonly SERVER_HOST = "https://update.electronjs.org";
2323
private static readonly REPO_OWNER = "PostHog";
2424
private static readonly REPO_NAME = "code";
25-
private static readonly CHECK_INTERVAL_MS = 60 * 60 * 1000; // 1 hour
25+
private static readonly CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
2626
private static readonly CHECK_TIMEOUT_MS = 60 * 1000; // 1 minute timeout for checks
2727
private static readonly DISABLE_ENV_FLAG = "ELECTRON_DISABLE_AUTO_UPDATE";
2828
private static readonly SUPPORTED_PLATFORMS = ["darwin", "win32"];

apps/code/src/main/trpc/routers/github-integration.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { container } from "../../di/container";
22
import { MAIN_TOKENS } from "../../di/tokens";
33
import {
4-
cancelGitHubFlowOutput,
54
startGitHubFlowInput,
65
startGitHubFlowOutput,
76
} from "../../services/github-integration/schemas";
@@ -18,8 +17,4 @@ export const githubIntegrationRouter = router({
1817
.mutation(({ input }) =>
1918
getService().startFlow(input.region, input.projectId),
2019
),
21-
22-
cancelFlow: publicProcedure
23-
.output(cancelGitHubFlowOutput)
24-
.mutation(() => getService().cancelFlow()),
2520
});

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8556,7 +8556,7 @@ export namespace Schemas {
85568556
values?: null | undefined;
85578557
};
85588558
export type InstallCustomAuthTypeEnum = "api_key" | "oauth";
8559-
export type InstallSourceEnum = "posthog" | "twig";
8559+
export type InstallSourceEnum = "posthog" | "posthog-code";
85608560
export type InstallCustom = {
85618561
name: string;
85628562
url: string;
@@ -8565,7 +8565,7 @@ export namespace Schemas {
85658565
description?: string | undefined;
85668566
oauth_provider_kind?: string | undefined;
85678567
install_source?: (InstallSourceEnum & unknown) | undefined;
8568-
twig_callback_url?: string | undefined;
8568+
posthog_code_callback_url?: string | undefined;
85698569
};
85708570
export type InterestingNote = { text: string; line_refs: string };
85718571
export type JsonrpcEnum = "2.0";
@@ -14423,9 +14423,9 @@ export namespace Endpoints {
1442314423
requestFormat: "json";
1442414424
parameters: {
1442514425
query: {
14426-
install_source?: ("posthog" | "twig") | undefined;
14426+
install_source?: ("posthog" | "posthog-code") | undefined;
1442714427
server_id: string;
14428-
twig_callback_url?: string | undefined;
14428+
posthog_code_callback_url?: string | undefined;
1442914429
};
1443014430
path: { project_id: string };
1443114431
};

0 commit comments

Comments
 (0)