Skip to content
Draft
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
6 changes: 5 additions & 1 deletion apps/code/src/main/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,11 @@ if (shouldRefuseInternalChildBoot(app.isPackaged, process.env)) {
const isDev = !app.isPackaged;

// Set app name for single-instance lock, crashReporter, etc
const appName = isDev ? "posthog-code-dev" : "posthog-code";
// POSTHOG_CODE_APP_NAME (dev only) isolates userData + the single-instance
// lock so a second dev instance can run beside another checkout's app.
const appName = isDev
? (process.env.POSTHOG_CODE_APP_NAME ?? "posthog-code-dev")
: "posthog-code";
app.setName(isDev ? "PostHog Code (Development)" : "PostHog Code");

// Set userData path for @posthog/code
Expand Down
6 changes: 6 additions & 0 deletions apps/code/src/main/di/bindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,10 @@ import type {
} from "@posthog/workspace-server/services/archive/ports";
import type { AUTH_PROXY_AUTH } from "@posthog/workspace-server/services/auth-proxy/identifiers";
import type { AuthProxyAuth } from "@posthog/workspace-server/services/auth-proxy/ports";
import type {
EMBEDDED_APP_PROXY_AUTH,
EmbeddedAppProxyAuth,
} from "@posthog/workspace-server/services/embedded-app-proxy/identifiers";
import type {
ENRICHMENT_AUTH,
ENRICHMENT_FILE_READER,
Expand Down Expand Up @@ -361,6 +365,8 @@ export interface MainBindings {
// Auth proxy / mcp proxy
[AUTH_PROXY_AUTH]: AuthProxyAuth;
[MCP_PROXY_AUTH]: McpProxyAuth;
// EXPERIMENT (embedded webapp)
[EMBEDDED_APP_PROXY_AUTH]: EmbeddedAppProxyAuth;

// Archive / suspension host ports
[ARCHIVE_SESSION_CANCELLER]: SessionCanceller;
Expand Down
15 changes: 15 additions & 0 deletions apps/code/src/main/di/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ import { STORAGE_PATHS_SERVICE } from "@posthog/platform/storage-paths";
import { UPDATER_SERVICE } from "@posthog/platform/updater";
import { URL_LAUNCHER_SERVICE } from "@posthog/platform/url-launcher";
import { WORKSPACE_SETTINGS_SERVICE } from "@posthog/platform/workspace-settings";
import { getCloudUrlFromRegion } from "@posthog/shared";
import type { WorkspaceClient } from "@posthog/workspace-client/client";
import { databaseModule } from "@posthog/workspace-server/db/db.module";
import {
Expand Down Expand Up @@ -155,6 +156,8 @@ import { authProxyModule } from "@posthog/workspace-server/services/auth-proxy/a
import { AUTH_PROXY_AUTH } from "@posthog/workspace-server/services/auth-proxy/identifiers";
import { browserTabsModule } from "@posthog/workspace-server/services/browser-tabs/browser-tabs.module";
import { claudeCliSessionsModule } from "@posthog/workspace-server/services/claude-cli-sessions/claude-cli-sessions.module";
import { embeddedAppProxyModule } from "@posthog/workspace-server/services/embedded-app-proxy/embedded-app-proxy.module";
import { EMBEDDED_APP_PROXY_AUTH } from "@posthog/workspace-server/services/embedded-app-proxy/identifiers";
import { enrichmentModule } from "@posthog/workspace-server/services/enrichment/enrichment.module";
import {
ENRICHMENT_AUTH,
Expand Down Expand Up @@ -380,6 +383,18 @@ container.bind(AUTH_PROXY_AUTH).toDynamicValue((ctx) => ({
.get<AuthService>(MAIN_AUTH_SERVICE)
.authenticatedFetch(fetch, url, init),
}));
// EXPERIMENT (embedded webapp): local same-origin proxy for the iframe'd app
container.load(embeddedAppProxyModule);
container.bind(EMBEDDED_APP_PROXY_AUTH).toDynamicValue((ctx) => ({
authenticatedFetch: (url: string, init?: RequestInit) =>
ctx
.get<AuthService>(MAIN_AUTH_SERVICE)
.authenticatedFetch(fetch, url, init),
getUpstreamUrl: async () => {
const state = ctx.get<AuthService>(MAIN_AUTH_SERVICE).getState();
return getCloudUrlFromRegion(state.cloudRegion ?? "us");
},
}));
container.load(mcpProxyModule);
container.bind(MCP_PROXY_AUTH).toDynamicValue((ctx) => {
const auth = () => ctx.get<AuthService>(MAIN_AUTH_SERVICE);
Expand Down
2 changes: 2 additions & 0 deletions apps/code/src/main/trpc/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { connectivityRouter } from "@posthog/host-router/routers/connectivity.ro
import { contextMenuRouter } from "@posthog/host-router/routers/context-menu.router";
import { dashboardsRouter } from "@posthog/host-router/routers/dashboards.router";
import { deepLinkRouter } from "@posthog/host-router/routers/deep-link.router";
import { embeddedAppRouter } from "@posthog/host-router/routers/embedded-app.router";
import { enrichmentRouter } from "@posthog/host-router/routers/enrichment.router";
import { environmentRouter } from "@posthog/host-router/routers/environment.router";
import { externalAppsRouter } from "@posthog/host-router/routers/external-apps.router";
Expand Down Expand Up @@ -72,6 +73,7 @@ export const trpcRouter = router({
contextMenu: contextMenuRouter,
dev: devRouter,
discordPresence: discordPresenceRouter,
embeddedApp: embeddedAppRouter,
enrichment: enrichmentRouter,
environment: environmentRouter,
encryption: encryptionRouter,
Expand Down
2 changes: 2 additions & 0 deletions packages/host-router/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { connectivityRouter } from "./routers/connectivity.router";
import { contextMenuRouter } from "./routers/context-menu.router";
import { dashboardsRouter } from "./routers/dashboards.router";
import { deepLinkRouter } from "./routers/deep-link.router";
import { embeddedAppRouter } from "./routers/embedded-app.router";
import { enrichmentRouter } from "./routers/enrichment.router";
import { environmentRouter } from "./routers/environment.router";
import { externalAppsRouter } from "./routers/external-apps.router";
Expand Down Expand Up @@ -64,6 +65,7 @@ export const hostRouter = router({
contextMenu: contextMenuRouter,
dashboards: dashboardsRouter,
deepLink: deepLinkRouter,
embeddedApp: embeddedAppRouter,
enrichment: enrichmentRouter,
environment: environmentRouter,
externalApps: externalAppsRouter,
Expand Down
15 changes: 15 additions & 0 deletions packages/host-router/src/routers/embedded-app.router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { publicProcedure, router } from "@posthog/host-trpc/trpc";
import type { EmbeddedAppProxyService } from "@posthog/workspace-server/services/embedded-app-proxy/embedded-app-proxy";
import { EMBEDDED_APP_PROXY_SERVICE } from "@posthog/workspace-server/services/embedded-app-proxy/identifiers";
import { z } from "zod";

/** EXPERIMENT (embedded webapp): expose the local proxy URL to the renderer. */
export const embeddedAppRouter = router({
getUrl: publicProcedure
.output(z.object({ url: z.string() }))
.query(async ({ ctx }) => ({
url: await ctx.container
.get<EmbeddedAppProxyService>(EMBEDDED_APP_PROXY_SERVICE)
.ensureStarted(),
})),
});
30 changes: 29 additions & 1 deletion packages/ui/src/features/browser-tabs/BrowserTabStrip.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
BrainIcon,
GlobeIcon,
HouseIcon,
PlugsConnectedIcon,
RobotIcon,
Expand Down Expand Up @@ -35,6 +36,10 @@ import {
} from "@posthog/ui/features/canvas/hooks/useDashboards";
import { PERSONAL_CHANNEL_NAME } from "@posthog/ui/features/canvas/hooks/useTaskChannels";
import { SHORTCUTS } from "@posthog/ui/features/command/keyboard-shortcuts";
import {
embeddedAppTabPath,
embeddedAppTabView,
} from "@posthog/ui/features/embedded-app/embeddedAppTab";
import { useFeatureFlag } from "@posthog/ui/features/feature-flags/useFeatureFlag";
import { usePanelLayoutStore } from "@posthog/ui/features/panels/panelLayoutStore";
import { getLeafPanel } from "@posthog/ui/features/panels/panelStoreHelpers";
Expand Down Expand Up @@ -183,7 +188,12 @@ export function BrowserTabStrip() {
// Home) are tab targets too. useAppView normalizes both the /code routes and
// their /website mirrors to the same view.type, so a tab survives either space.
const view = useAppView();
const routeAppView: AppView | null = isAppView(view.type) ? view.type : null;
const routeAppView: string | null =
view.type === "embedded-app"
? embeddedAppTabView(view.embedPath ?? "/notebooks")
: isAppView(view.type)
? view.type
: null;

const { channels } = useChannels();
const { createChannel } = useChannelMutations();
Expand Down Expand Up @@ -533,6 +543,17 @@ export function BrowserTabStrip() {
pinned,
};
}
// An embedded PostHog webapp tab — label by its webapp path.
const embedPath = embeddedAppTabPath(appView);
if (embedPath !== null) {
return {
id: t.id,
label: `PostHog ${embedPath.split("?")[0] || "/"}`,
icon: <GlobeIcon size={14} />,
channelName: null,
pinned,
};
}
// A top-level app page (Inbox, Agents, Skills, …).
if (appView && isAppView(appView)) {
return {
Expand Down Expand Up @@ -601,6 +622,13 @@ export function BrowserTabStrip() {
} else {
navigate({ to: "/website/$channelId", params, state });
}
} else if (tab.appView && embeddedAppTabPath(tab.appView) !== null) {
// An embedded PostHog webapp tab — its route carries the path in search.
navigate({
to: "/embedded-app",
search: { path: embeddedAppTabPath(tab.appView) ?? undefined },
state,
});
} else if (tab.appView && isAppView(tab.appView)) {
// A top-level app page — back to its canonical route (literal `to` per
// case so the router types stay checked).
Expand Down
62 changes: 62 additions & 0 deletions packages/ui/src/features/browser-tabs/useOpenEmbeddedAppTab.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { useHostTRPCClient } from "@posthog/host-router/react";
import { openOrFocusTab, primaryWindow } from "@posthog/shared";
import { embeddedAppTabView } from "@posthog/ui/features/embedded-app/embeddedAppTab";
import { useNavigate } from "@tanstack/react-router";
import { useCallback } from "react";
import { applyLocalTransform, persistWrite, readMirror } from "./tabsSync";

/**
* EXPERIMENT (embedded webapp): open (or focus) a browser tab hosting the
* embedded PostHog webapp at `path`.
*
* A plain router navigation is in-tab by design (the strip's navigation
* effect retargets the active tab), so opening a NEW tab mirrors the strip's
* own flow: apply the openOrFocus transform to the mirror local-first,
* persist in the background, then navigate with the history entry stamped
* with the tab id so the effect activates that tab instead of retargeting.
*/
export function useOpenEmbeddedAppTab(): (path: string) => void {
const hostClient = useHostTRPCClient();
const navigate = useNavigate();

return useCallback(
(path: string) => {
const appView = embeddedAppTabView(path);
const windowId = primaryWindow(readMirror())?.id;
let tabId: string | undefined;
if (windowId) {
const input = {
windowId,
dashboardId: null,
taskId: null,
channelId: null,
channelSection: null,
appView,
};
const mintedId = crypto.randomUUID();
tabId = mintedId;
applyLocalTransform((s) => {
const result = openOrFocusTab(s, {
...input,
makeId: () => mintedId,
now: Date.now,
});
tabId = result.tabId;
return result.snapshot;
});
void persistWrite(() =>
hostClient.browserTabs.openOrFocus.mutate({
...input,
tabId: mintedId,
}),
);
}
void navigate({
to: "/embedded-app",
search: { path },
state: (prev) => ({ ...prev, tabId }),
});
},
[hostClient, navigate],
);
}
Loading
Loading