Skip to content
Merged
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
2 changes: 2 additions & 0 deletions apps/code/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"@types/better-sqlite3": "^7.6.13",
"@types/canvas-confetti": "^1.9.0",
"@types/node": "^24.0.0",
"@types/react": "^19.1.0",
"@types/react-dom": "^19.1.0",
Expand Down Expand Up @@ -155,6 +156,7 @@
"@xterm/addon-web-links": "^0.11.0",
"@xterm/xterm": "^5.5.0",
"better-sqlite3": "^12.8.0",
"canvas-confetti": "^1.9.4",
"chokidar": "^5.0.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
Expand Down
2 changes: 1 addition & 1 deletion apps/code/src/renderer/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ function App() {
key="main"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5, delay: showTransition ? 1.5 : 0 }}
transition={{ duration: 0.5, delay: showTransition ? 0.5 : 0 }}
>
<MainLayout />
</motion.div>
Expand Down
37 changes: 36 additions & 1 deletion apps/code/src/renderer/components/GlobalEventHandlers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ import { useCommandMenuStore } from "@stores/commandMenuStore";
import { useNavigationStore } from "@stores/navigationStore";
import { useSubscription } from "@trpc/tanstack-react-query";
import { clearApplicationStorage } from "@utils/clearStorage";
import { shipIt } from "@utils/confetti";
import { logger } from "@utils/logger";
import { useCallback, useEffect, useMemo } from "react";
import { useCallback, useEffect, useMemo, useRef } from "react";
import { useHotkeys } from "react-hotkeys-hook";

interface GlobalEventHandlersProps {
Expand Down Expand Up @@ -207,6 +208,40 @@ export function GlobalEventHandlers({
[handleSwitchTask],
);

// Konami code confetti
const konamiProgressRef = useRef(0);
useEffect(() => {
const sequence = [
"ArrowUp",
"ArrowUp",
"ArrowDown",
"ArrowDown",
"ArrowLeft",
"ArrowRight",
"ArrowLeft",
"ArrowRight",
"b",
"a",
];
const handleKey = (event: KeyboardEvent) => {
const key = event.key.length === 1 ? event.key.toLowerCase() : event.key;
const expected = sequence[konamiProgressRef.current];
if (key === expected) {
konamiProgressRef.current += 1;
if (konamiProgressRef.current === sequence.length) {
konamiProgressRef.current = 0;
shipIt();
}
} else {
konamiProgressRef.current = key === sequence[0] ? 1 : 0;
}
};
window.addEventListener("keydown", handleKey);
return () => {
window.removeEventListener("keydown", handleKey);
};
}, []);

// Mouse back/forward buttons
useEffect(() => {
const handleMouseButton = (event: MouseEvent) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@ import { getSuggestedBranchName } from "@features/git-interaction/utils/getSugge
import { invalidateGitBranchQueries } from "@features/git-interaction/utils/gitCacheKeys";
import { partitionByStaged } from "@features/git-interaction/utils/partitionByStaged";
import { updateGitCacheFromSnapshot } from "@features/git-interaction/utils/updateGitCache";
import { useOnboardingStore } from "@features/onboarding/stores/onboardingStore";
import { useSessionStore } from "@features/sessions/stores/sessionStore";
import { trpc, trpcClient } from "@renderer/trpc";
import type { ChangedFile } from "@shared/types";
import { ANALYTICS_EVENTS } from "@shared/types/analytics";
import { useQueryClient } from "@tanstack/react-query";
import { track } from "@utils/analytics";
import { celebrate } from "@utils/confetti";
import { logger } from "@utils/logger";
import { useMemo, useRef } from "react";

Expand Down Expand Up @@ -277,6 +279,12 @@ export function useGitInteraction(
trackGitAction(taskId, "create-pr", true, prStagingContext);
track(ANALYTICS_EVENTS.PR_CREATED, { task_id: taskId, success: true });

const onboarding = useOnboardingStore.getState();
if (!onboarding.hasShippedFirstPr) {
onboarding.markFirstPrShipped();
celebrate();
}

if (result.state) {
updateGitCacheFromSnapshot(queryClient, repoPath, result.state);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
} from "@shared/types/analytics";
import { useNavigationStore } from "@stores/navigationStore";
import { track } from "@utils/analytics";
import { shipIt } from "@utils/confetti";
import { AnimatePresence, LayoutGroup, motion } from "framer-motion";
import { useEffect, useRef } from "react";
import { useHotkeys } from "react-hotkeys-hook";
Expand Down Expand Up @@ -144,6 +145,7 @@ export function OnboardingFlow() {
github_connected: githubUserIntegrations.length > 0,
repo_skipped: repoSkipped,
});
shipIt();
completeOnboarding();
navigateToTaskInput();
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ const log = logger.scope("onboarding-store");
interface OnboardingStoreState {
currentStep: OnboardingStep;
hasCompletedOnboarding: boolean;
hasShippedFirstPr: boolean;
selectedProjectId: number | null;
}

interface OnboardingStoreActions {
setCurrentStep: (step: OnboardingStep) => void;
completeOnboarding: () => void;
markFirstPrShipped: () => void;
resetOnboarding: () => void;
resetSelections: () => void;
selectProjectId: (projectId: number | null) => void;
Expand All @@ -24,6 +26,7 @@ type OnboardingStore = OnboardingStoreState & OnboardingStoreActions;
const initialState: OnboardingStoreState = {
currentStep: "welcome",
hasCompletedOnboarding: false,
hasShippedFirstPr: false,
selectedProjectId: null,
};

Expand All @@ -37,6 +40,7 @@ export const useOnboardingStore = create<OnboardingStore>()(
log.info("completeOnboarding");
set({ hasCompletedOnboarding: true });
},
markFirstPrShipped: () => set({ hasShippedFirstPr: true }),
resetOnboarding: () => set({ ...initialState }),
resetSelections: () =>
set({
Expand All @@ -50,6 +54,7 @@ export const useOnboardingStore = create<OnboardingStore>()(
partialize: (state) => ({
currentStep: state.currentStep,
hasCompletedOnboarding: state.hasCompletedOnboarding,
hasShippedFirstPr: state.hasShippedFirstPr,
selectedProjectId: state.selectedProjectId,
}),
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ export function AdvancedSettings() {
return (
<Flex direction="column">
<SettingRow
label="Reset onboarding"
description="Re-run the onboarding tutorial on next app restart"
label="Reset onboarding and tours"
description="Re-run the onboarding tutorial and product tours on next app restart"
>
<Button
variant="soft"
Expand All @@ -29,23 +29,12 @@ export function AdvancedSettings() {
useSettingsDialogStore.getState().close();
useOnboardingStore.getState().resetOnboarding();
useSetupStore.getState().resetSetup();
useTourStore.getState().resetTours();
Comment thread
charlesvien marked this conversation as resolved.
}}
>
Reset
</Button>
</SettingRow>
<SettingRow
label="Reset product tours"
description="Re-run product tours on next app restart"
>
<Button
variant="soft"
size="1"
onClick={() => useTourStore.getState().resetTours()}
>
Reset
</Button>
</SettingRow>
<SettingRow
label="Clear application storage"
description="This will remove all locally stored application data"
Expand Down
61 changes: 61 additions & 0 deletions apps/code/src/renderer/utils/confetti.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import confetti from "canvas-confetti";

const POSTHOG_COLORS = ["#f54d00", "#f8be2a", "#1d4aff", "#000000", "#ffffff"];

function reducedMotion(): boolean {
return (
window.matchMedia?.("(prefers-reduced-motion: reduce)").matches ?? false
);
}

export function celebrate(options?: confetti.Options): void {
if (reducedMotion()) return;
confetti({
particleCount: 80,
spread: 70,
origin: { y: 0.7 },
colors: POSTHOG_COLORS,
...options,
});
}

export function shipIt(): void {
if (reducedMotion()) return;
const end = Date.now() + 1200;
const fire = () => {
confetti({
particleCount: 6,
angle: 60,
spread: 55,
origin: { x: 0, y: 0.8 },
colors: POSTHOG_COLORS,
});
confetti({
particleCount: 6,
angle: 120,
spread: 55,
origin: { x: 1, y: 0.8 },
colors: POSTHOG_COLORS,
});
if (Date.now() < end) requestAnimationFrame(fire);
};
fire();
}

export function fireFrom(
element: HTMLElement,
options?: confetti.Options,
): void {
if (reducedMotion()) return;
const rect = element.getBoundingClientRect();
const x = (rect.left + rect.width / 2) / window.innerWidth;
const y = (rect.top + rect.height / 2) / window.innerHeight;
confetti({
particleCount: 40,
spread: 60,
startVelocity: 35,
origin: { x, y },
colors: POSTHOG_COLORS,
...options,
});
}
23 changes: 20 additions & 3 deletions docs/DEEP-LINKS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ PostHog Code registers custom URL schemes so the desktop app can be opened with
| Development | `posthog-code-dev://` |
| Legacy (production only) | `twig://`, `array://` |

All schemes route through the same dispatcher. The host portion of the URL selects the handler (`task`, `inbox`, `new`, `plan`, `issue`, etc.).
All schemes route through the same dispatcher. The host portion of the URL selects the handler (`task`, `inbox`, `new`, `plan`, `issue`, `callback`, `integration`, `slack-integration`, `mcp-oauth-complete`).

If the app is not running, the OS launches it and the link is queued until the renderer is ready. If the app is minimised, it is restored and focused before the link is handled.

Expand Down Expand Up @@ -117,17 +117,33 @@ In development the same payload is delivered to `http://localhost:8237/callback`

### `posthog-code://integration`

OAuth callback for the GitHub App installation flow.
OAuth callback for the GitHub App installation flow. PostHog Cloud redirects to this URL after the user finishes the GitHub App install in their browser.

| Parameter | Description |
|---|---|
| `provider` | Integration provider (e.g. `github`) |
| `provider` | Integration provider, always `github` for this handler |
| `project_id` | PostHog project ID |
| `installation_id` | GitHub App installation ID |
| `status` | `success` or `error` |
| `error_code` | Error code on failure |
| `error_message` | Human-readable error message on failure |

The Slack integration uses its own [`slack-integration`](#posthog-codeslack-integration) handler; do not reuse this one for non-GitHub providers.

### `posthog-code://slack-integration`

OAuth callback for the Slack workspace install flow. PostHog Cloud redirects to this URL after the user authorises the PostHog Slack app and finishes the flow on the AccountConnected page.

| Parameter | Description |
|---|---|
| `project_id` | PostHog project ID (numeric) |
| `integration_id` | PostHog Slack integration row ID (numeric, set on success) |
| `status` | `success` or `error` (defaults to `success` if absent) |
| `error_code` | Error code on failure |
| `error_message` | Human-readable error message on failure |

The flow is started from the renderer by calling the `slackIntegration.startFlow` tRPC mutation, which opens the browser to PostHog Cloud's authorize endpoint. If the deep link is not received within five minutes, a `FlowTimedOut` event is emitted so the UI can surface a timeout state.

### `posthog-code://mcp-oauth-complete`

OAuth completion callback for MCP server integrations.
Expand All @@ -150,6 +166,7 @@ In development the same payload is delivered to `http://localhost:8238/mcp-oauth
| `new`, `plan`, `issue` | [apps/code/src/main/services/new-task-link/service.ts](../apps/code/src/main/services/new-task-link/service.ts) |
| `callback` | [apps/code/src/main/services/oauth/service.ts](../apps/code/src/main/services/oauth/service.ts) |
| `integration` | [apps/code/src/main/services/github-integration/service.ts](../apps/code/src/main/services/github-integration/service.ts) |
| `slack-integration` | [apps/code/src/main/services/slack-integration/service.ts](../apps/code/src/main/services/slack-integration/service.ts) |
| `mcp-oauth-complete` | [apps/code/src/main/services/mcp-callback/service.ts](../apps/code/src/main/services/mcp-callback/service.ts) |
| Scheme constants | [apps/code/src/shared/deeplink.ts](../apps/code/src/shared/deeplink.ts) |

Expand Down
16 changes: 16 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading