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
20 changes: 18 additions & 2 deletions products/desktop/.claude/skills/merging-prs/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ To merge, you enqueue the PR with a comment, then watch it until Trunk lands it.

When a developer says "merge this PR", "merge it when it's ready", "land it",
"ship it", or "babysit this PR", do the full loop below — enqueue **and** watch
to completion, reporting the outcome. See also [docs/merge-queue.md](../../../docs/merge-queue.md).
to completion, reporting the outcome.

`<n>` below is the PR number. Resolve the repo slug once if you need it:
`REPO=$(gh repo view --json nameWithOwner -q .nameWithOwner)`.
Expand All @@ -29,8 +29,21 @@ gh pr view <n> --json state,isDraft,mergeable,reviewDecision,statusCheckRollup
- **Failing required checks** (`statusCheckRollup`) → the queue will just reject
it. Report which checks are red and stop; fix them first. **Pending** checks
are fine — the queue waits for them.
- **Not yet approved** (`reviewDecision` empty or `REVIEW_REQUIRED`) → fine,
enqueue anyway. See below.
- **Merge conflicts** (`mergeable == "CONFLICTING"`) → report and stop; rebase first.

Enqueueing before approval is safe, and is the closest thing this repo has to
auto-merge. A submitted PR sits in Trunk's `Queued` state until GitHub's branch
protection on `main` is satisfied — one approving review, code-owner review, and
the required checks (`build`, `quality`, `unit-test`, `integration-test`,
`typecheck`) — and Trunk merges it once they land. Trunk is not a bypass actor on
those rules, so it cannot merge an unapproved or red PR.

The catch: **pushing new commits drops the PR from the queue.** If review
feedback is likely, either wait for approval before enqueueing, or re-enqueue
with `/trunk merge` after each push.

## 2. Enqueue

```bash
Expand Down Expand Up @@ -68,7 +81,10 @@ sleep 60
- Watch the **check run + PR state**, not `gh pr checks --watch`: the queue runs
CI on Trunk's own draft/`trunk-merge/**` branch, so this PR's own checks don't
reflect the queue's testing.
- Stop at the timeout with a status summary rather than looping forever.
- If it's parked in `Queued` waiting on a human review, say so once and slow the
cadence to ~5 minutes. Keep watching — the merge still has to be reported.
- Stop at the timeout with a status summary rather than looping forever. If it
was still waiting on review, say that's why and offer to keep watching.

## 4. Handle failure

Expand Down
20 changes: 13 additions & 7 deletions products/desktop/MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ monorepo; anything else that differs from the source at the pinned SHA is drift
be treated as a bug in the sync.

- Source: https://github.com/PostHog/code
- Pinned SHA: `5ac5892a2f566b18125b2be89e8e11f17a7218e8` (main)
- Imported: 2026-07-20; resynced: 2026-07-30
- Pinned SHA: `fc991d3eea2d1ac649e2502c0197b0fda9b19f58` (main)
- Imported: 2026-07-20; resynced: 2026-08-01

## Resync protocol (for a human or an agent)

Expand Down Expand Up @@ -45,14 +45,20 @@ The tree is a verbatim copy of the source at the pinned SHA except:
`packages/ui/src/features/inbox/CLAUDE.md` renamed to `AGENTS.md` plus a symlink, and
symlinks added in `packages/ui/src/features/{browser-tabs,canvas}/`. Upstream these to
PostHog/code so resyncs do not reintroduce the violations.
- `docs/testing.md`: the "Storybook Visual Regression" CI paragraphs are replaced with a
note that the storybook CI was removed post-merge (see `POST-MIGRATION.md` step 6). The
source still documents its own storybook workflow; reapply on resync.
- Local security patches (reapply on resync until the pin includes the upstream fix):
`apps/code/src/main/utils/encryption.ts` passes `{ authTagLength: 16 }` to
`createDecipheriv` (semgrep `gcm-no-tag-length`, ERROR). For the simple-git 3.36 RCE fix,
`packages/git/src/client.ts` opts into `unsafe.{allowUnsafeFsMonitor,allowUnsafeEditor,
allowUnsafePager}` (3.36's block-unsafe plugin otherwise rejects the hardcoded
core.fsmonitor perf flag and inherited GIT_EDITOR/PAGER), and `packages/git/src/queries.ts`
runs `git worktree list` through raw `execFile` instead of simple-git. All upstreamed to
PostHog/code (#4030).
`packages/git/package.json` bumps `simple-git` to `^3.36.0` (the source is on `^3.30.0`,
so the nested lockfile diverges there too), `packages/git/src/client.ts` opts into
`unsafe.{allowUnsafeFsMonitor,allowUnsafeEditor,allowUnsafePager}` (3.36's block-unsafe
plugin otherwise rejects the hardcoded core.fsmonitor perf flag and inherited
GIT_EDITOR/PAGER), and `packages/git/src/queries.ts` runs `git worktree list` through raw
`execFile` instead of simple-git. The upstream attempt (PostHog/code#4030) was closed
unmerged, so these stay local patches; restoring the four files from the previous
monorepo commit and refreshing the nested lockfile is the reapply.

The nested workspace is intentional: `products/desktop/` keeps its own `pnpm-workspace.yaml`,
lockfile, Biome config and Node 22, and is NOT in the root `pnpm-workspace.yaml` globs.
Expand Down
7 changes: 5 additions & 2 deletions products/desktop/apps/code/src/main/window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,11 +208,14 @@ export function createWindow(): void {
const platformWindowConfig =
process.platform === "darwin"
? {
titleBarStyle: "hiddenInset" as const,
// "hidden", not "hiddenInset": hiddenInset keeps macOS's own inset and
// ignores trafficLightPosition's y, which parked the dots near the
// bottom of the bar. "hidden" honours the position we ask for.
titleBarStyle: "hidden" as const,
// Centre the traffic lights vertically with the title bar's back/forward
// buttons (40px bar, 24px buttons → centre at y=20; 12px dots → top at 14).
// x mirrors y so the inset from the top and the left match.
trafficLightPosition: { x: 14, y: 14 },
trafficLightPosition: { x: 14, y: 12 },
// Exposes the titlebar-area-* CSS env vars so the renderer can
// clear the traffic lights exactly; their size varies by macOS
// version (bigger on Tahoe), so it must not hardcode a width.
Expand Down
4 changes: 2 additions & 2 deletions products/desktop/apps/mobile/src/app/inbox/[...id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -464,7 +464,7 @@ export default function ReportDetailScreen() {

{/* Title */}
<Text className="mb-2 font-semibold text-[18px] text-gray-12">
{report.title ?? "Untitled signal"}
{report.title ?? "Untitled report"}
</Text>

{/* Meta row */}
Expand Down Expand Up @@ -642,7 +642,7 @@ export default function ReportDetailScreen() {
<DismissReportSheet
visible={dismissOpen}
reportId={report.id}
reportTitle={report.title?.trim() ? report.title : "Untitled signal"}
reportTitle={report.title?.trim() ? report.title : "Untitled report"}
onClose={() => setDismissOpen(false)}
onDismissed={handleDismissed}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ const ArchivedRow = memo(function ArchivedRow({
numberOfLines={2}
ellipsizeMode="tail"
>
{report.title ?? "Untitled signal"}
{report.title ?? "Untitled report"}
</Text>

<View className="mt-1 flex-row flex-wrap items-center gap-2">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ function ReportListRowComponent({ report, onPress }: ReportListRowProps) {
numberOfLines={2}
ellipsizeMode="tail"
>
{report.title ?? "Untitled signal"}
{report.title ?? "Untitled report"}
</Text>

<View className="mt-1 flex-row items-center gap-2">
Expand Down
1 change: 1 addition & 0 deletions products/desktop/apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"@posthog/core": "workspace:*",
"@posthog/di": "workspace:*",
"@posthog/host-router": "workspace:*",
"@posthog/harness": "workspace:*",
"@posthog/host-trpc": "workspace:*",
"@posthog/platform": "workspace:*",
"@posthog/shared": "workspace:*",
Expand Down
2 changes: 1 addition & 1 deletion products/desktop/apps/web/src/web-host-router.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { fetchPosthogPiModelCatalog } from "@posthog/agent/pi/model-catalog";
import { getLlmGatewayUrl } from "@posthog/agent/posthog-api";
import type { AuthService } from "@posthog/core/auth/auth";
import { AUTH_SERVICE } from "@posthog/core/auth/auth.module";
import { TEAM_SKILLS_SERVICE } from "@posthog/core/skills/identifiers";
import type { TeamSkillsService } from "@posthog/core/skills/teamSkillsService";
import { resolveService } from "@posthog/di/container";
import { fetchPosthogPiModelCatalog } from "@posthog/harness/extensions/posthog-provider/model-catalog";
import { analyticsRouter } from "@posthog/host-router/routers/analytics.router";
import { authRouter } from "@posthog/host-router/routers/auth.router";
import { canvasDataRouter } from "@posthog/host-router/routers/canvas-data.router";
Expand Down
49 changes: 49 additions & 0 deletions products/desktop/docs/LOCAL-DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,10 +127,59 @@ region you pick at login.

## Troubleshooting

### Feature flags never enabled (flag-gated UI missing)

If flag-gated surfaces (e.g. the MCP gateway behind `mcp-gateway`) never show up
even though the flag is enabled in your PostHog project, check
`VITE_POSTHOG_API_HOST` in `.env`: it must include the scheme
(`http://localhost:8010`, not `localhost:8010`). posthog-js concatenates the
host into request URLs verbatim, so a scheme-less value produces URLs like
`localhost:8010/flags/…` that the browser rejects as an invalid protocol —
every flag fetch fails silently and `isFeatureEnabled` returns `undefined` for
everything (flags never loaded). Prefer `node scripts/use-local-posthog.mjs`
over hand-editing; it writes the correct form.

To confirm what the running app sees, run in the renderer console (or via CDP):

```js
posthog.config.api_host; // must start with http:// or https://
posthog.isFeatureEnabled("mcp-gateway"); // undefined ⇒ flags never loaded
```

`.env` changes need a dev-server restart (`pnpm dev`) to take effect.

### "Invalid client_id" error during OAuth

The OAuth application in your local PostHog must have the client ID `DC5uRLVbGI02YQ82grxgnK6Qn12SXWpCqdPb60oZ`. Verify at http://localhost:8010/admin/posthog/oauthapplication/.

### "OAuth error: invalid_scope"

PostHog Code requests the wildcard scope `*` (see `OAUTH_SCOPES` in
`packages/shared/src/oauth.ts`). PostHog's OAuth server only grants `*` at
`/authorize` when the OAuth application's **scope ceiling is empty** — this is
the grandfathering path for the PostHog Code client. If the application has any
explicit `scopes` or `optional_scopes` configured, the wildcard is rejected with
`invalid_scope`.

Fix: clear the scope ceiling on your local OAuth application so it matches the
production app. Either edit it at
http://localhost:8010/admin/posthog/oauthapplication/ (empty the **Scopes** and
**Optional scopes** fields), or run in your PostHog repo:

```bash
python manage.py shell -c "
from posthog.models.oauth import OAuthApplication
app = OAuthApplication.objects.get(client_id='DC5uRLVbGI02YQ82grxgnK6Qn12SXWpCqdPb60oZ')
app.scopes = []
app.optional_scopes = []
app.save()
print('cleared scope ceiling for', app.client_id)
"
```

Then retry login. (Do not add `*` to the ceiling — an explicit ceiling never
grants the wildcard, even if `*` is listed.)

### "Redirect URI mismatch"

Make sure the OAuth application's redirect URIs include `http://localhost:8237/callback` and `http://localhost:8239/callback`. Check for trailing slashes.
Expand Down
4 changes: 0 additions & 4 deletions products/desktop/packages/agent/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,6 @@
"types": "./dist/pi/types.d.ts",
"import": "./dist/pi/types.js"
},
"./pi/model-catalog": {
"types": "./dist/pi/model-catalog.d.ts",
"import": "./dist/pi/model-catalog.js"
},
"./pr-url-detector": {
"types": "./dist/pr-url-detector.d.ts",
"import": "./dist/pr-url-detector.js"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -376,14 +376,18 @@ describe("buildSessionOptions", () => {
name: "omits the team_id header when POSTHOG_PROJECT_ID is unset",
projectId: undefined,
existingHeaders: undefined,
expected: "x-posthog-use-bedrock-fallback: true",
expected: [
"x-posthog-property-$ai_session_id: test-session",
"x-posthog-use-bedrock-fallback: true",
].join("\n"),
},
{
name: "forwards POSTHOG_PROJECT_ID as the team_id attribution header",
projectId: "42",
existingHeaders: undefined,
expected: [
"x-posthog-property-team_id: 42",
"x-posthog-property-$ai_session_id: test-session",
"x-posthog-use-bedrock-fallback: true",
].join("\n"),
},
Expand All @@ -394,6 +398,7 @@ describe("buildSessionOptions", () => {
expected: [
"x-posthog-property-task_id: task-abc",
"x-posthog-property-team_id: 42",
"x-posthog-property-$ai_session_id: test-session",
"x-posthog-use-bedrock-fallback: true",
].join("\n"),
},
Expand All @@ -411,6 +416,101 @@ describe("buildSessionOptions", () => {
expect(headers).toBe(expected);
});
});

describe("gateway turn tracing env", () => {
const KEYS = [
"CLAUDE_CODE_ENABLE_TELEMETRY",
"CLAUDE_CODE_ENHANCED_TELEMETRY_BETA",
"CLAUDE_CODE_PROPAGATE_TRACEPARENT",
"OTEL_TRACES_EXPORTER",
"OTEL_EXPORTER_OTLP_PROTOCOL",
"OTEL_EXPORTER_OTLP_ENDPOINT",
"TRACEPARENT",
"TRACESTATE",
] as const;
const original: Partial<Record<string, string | undefined>> = {};

beforeEach(() => {
for (const key of KEYS) {
original[key] = process.env[key];
delete process.env[key];
}
});

afterEach(() => {
for (const key of KEYS) {
const value = original[key];
if (value === undefined) {
delete process.env[key];
} else {
process.env[key] = value;
}
}
});

const gatewayEnv = {
anthropicBaseUrl: "https://gateway.example",
anthropicAuthToken: "tok",
openaiBaseUrl: "https://gateway.example/v1",
openaiApiKey: "tok",
};

it("enables per-turn traceparent when routed through the gateway", () => {
const env = buildSessionOptions({ ...makeParams(), gatewayEnv }).env;

expect(env?.CLAUDE_CODE_ENABLE_TELEMETRY).toBe("1");
expect(env?.CLAUDE_CODE_ENHANCED_TELEMETRY_BETA).toBe("1");
expect(env?.CLAUDE_CODE_PROPAGATE_TRACEPARENT).toBe("1");
expect(env?.OTEL_TRACES_EXPORTER).toBe("otlp");
expect(env?.OTEL_EXPORTER_OTLP_PROTOCOL).toBe("http/json");
expect(env?.OTEL_EXPORTER_OTLP_ENDPOINT).toBe("http://127.0.0.1:9");
});

it("honors a caller-supplied OTLP endpoint", () => {
process.env.OTEL_EXPORTER_OTLP_ENDPOINT =
"http://collector.internal:4318";

const env = buildSessionOptions({ ...makeParams(), gatewayEnv }).env;

expect(env?.OTEL_EXPORTER_OTLP_ENDPOINT).toBe(
"http://collector.internal:4318",
);
});

it("pins exporter and protocol so an inherited none can't disable tracing", () => {
process.env.OTEL_TRACES_EXPORTER = "none";
process.env.OTEL_EXPORTER_OTLP_PROTOCOL = "grpc";

const env = buildSessionOptions({ ...makeParams(), gatewayEnv }).env;

expect(env?.OTEL_TRACES_EXPORTER).toBe("otlp");
expect(env?.OTEL_EXPORTER_OTLP_PROTOCOL).toBe("http/json");
});

it("strips inherited TRACEPARENT so turns keep distinct trace ids", () => {
process.env.TRACEPARENT =
"00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01";
process.env.TRACESTATE = "vendor=x";

const env = buildSessionOptions({ ...makeParams(), gatewayEnv }).env;

expect(env?.TRACEPARENT).toBeUndefined();
expect(env?.TRACESTATE).toBeUndefined();
});

it("leaves BYOK sessions untouched", () => {
process.env.TRACEPARENT =
"00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01";

const env = buildSessionOptions(makeParams()).env;

expect(env?.CLAUDE_CODE_ENABLE_TELEMETRY).toBeUndefined();
expect(env?.CLAUDE_CODE_PROPAGATE_TRACEPARENT).toBeUndefined();
expect(env?.TRACEPARENT).toBe(
"00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01",
);
});
});
});

describe("buildSystemPrompt", () => {
Expand Down
Loading
Loading