Skip to content

Commit 006da57

Browse files
Finalize mobile portability boundary
Generated-By: PostHog Code Task-Id: 40c57a59-b4e1-4760-8e56-ecd03e9c2f0f
1 parent 60ed26d commit 006da57

19 files changed

Lines changed: 106 additions & 248 deletions

File tree

apps/mobile/README.md

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ pnpm --filter @posthog/mobile start
3232

3333
### Feature Folders
3434

35-
Code is organized by feature in `src/features/`. Each feature is self-contained with its own components, hooks, stores, and API logic.
35+
Code is organized by feature in `src/features/`. Features own native components, one-source hooks, and view state. They do not own copies of cloud contracts, transport, orchestration, or presentation rules.
3636

3737
```
3838
src/features/
@@ -46,18 +46,31 @@ src/features/
4646
│ ├── hooks/
4747
│ ├── stores/
4848
│ └── types.ts
49-
├── conversations/ # PostHog AI conversation list & management
50-
│ ├── api.ts
49+
├── inbox/ # Native inbox rendering and query hooks
5150
│ ├── components/
5251
│ ├── hooks/
5352
│ └── stores/
54-
└── tasks/ # Task management
55-
├── api.ts
53+
└── tasks/ # Native cloud-task rendering and host adapters
5654
├── components/
5755
├── hooks/
56+
├── services/
5857
└── stores/
5958
```
6059

60+
### Portability boundary
61+
62+
Mobile and desktop use the same cloud-task architecture. New work must preserve these ownership rules:
63+
64+
- `@posthog/shared` owns runtime contracts and Zod schemas.
65+
- `@posthog/api-client` owns authenticated PostHog HTTPS transport and its request/response types.
66+
- `@posthog/core` owns cloud-task orchestration and headless presentation decisions, including sessions, queues, permissions, models, repositories, inbox rules, and automation semantics.
67+
- `apps/mobile` owns Expo lifecycle, React Native rendering, gestures, sheets, notifications, audio, secure storage, and small persisted view-state stores.
68+
- `@posthog/ui` owns the DOM/Quill renderer and web view state.
69+
70+
Do not add a mobile API facade, duplicate a shared type, or re-export a core helper through a mobile file. Import the owning package directly. If desktop and mobile need different visuals, add a headless descriptor or decision function to core and keep two thin renderers.
71+
72+
Intentional host differences are limited to platform capabilities and view state. Mobile may persist native navigation state, cached picker snapshots, optimistic attachment echoes, and notification preferences; it must not implement retries, reconnection, transport parsing, task lifecycle, or cross-store decisions in those stores.
73+
6174
### File-Based Routing
6275

6376
Routes for the screens are defined by the file structure in `src/app/` using expo-router.
@@ -270,4 +283,4 @@ Defined in `eas.json`:
270283

271284
**Local vs Cloud builds:**
272285
- Cloud (default): Runs on Expo's servers, no local Xcode needed
273-
- Local (`--local`): Runs on your machine, faster iteration, requires Xcode/Android SDK
286+
- Local (`--local`): Runs on your machine, faster iteration, requires Xcode/Android SDK

apps/mobile/src/app/mcp-servers/add-custom.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Text } from "@components/text";
2+
import type { McpAuthType } from "@posthog/api-client/types";
23
import { router } from "expo-router";
34
import { Lock } from "phosphor-react-native";
45
import { useState } from "react";
@@ -14,7 +15,6 @@ import {
1415
import { FloatingMcpHeader } from "@/features/mcp/components/FloatingMcpHeader";
1516
import { useMcpInstallations } from "@/features/mcp/hooks";
1617
import { installCustomWithOAuth } from "@/features/mcp/oauth";
17-
import type { McpAuthType } from "@/features/mcp/types";
1818
import { useScreenInsets } from "@/hooks/useScreenInsets";
1919
import { logger } from "@/lib/logger";
2020
import { useThemeColors } from "@/lib/theme";

apps/mobile/src/app/mcp-servers/index.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
import { Text } from "@components/text";
2+
import type {
3+
McpRecommendedServer,
4+
McpServerInstallation,
5+
} from "@posthog/api-client/types";
26
import { useRouter } from "expo-router";
37
import { MagnifyingGlass, Plus, PuzzlePiece } from "phosphor-react-native";
48
import { useMemo, useState } from "react";
@@ -17,10 +21,6 @@ import {
1721
recommendedToRowProps,
1822
} from "@/features/mcp/components/McpServerRow";
1923
import { useMcpInstallations, useMcpMarketplace } from "@/features/mcp/hooks";
20-
import type {
21-
McpRecommendedServer,
22-
McpServerInstallation,
23-
} from "@/features/mcp/types";
2424
import { useScreenInsets } from "@/hooks/useScreenInsets";
2525
import { useThemeColors } from "@/lib/theme";
2626

apps/mobile/src/app/mcp-servers/installation/[id].tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { Text } from "@components/text";
2+
import type { McpApprovalState } from "@posthog/api-client/types";
3+
import { isStdioMcpServer } from "@posthog/core/mcp-servers/presentation";
24
import { router, useLocalSearchParams } from "expo-router";
35
import {
46
ArrowsClockwise,
@@ -28,8 +30,6 @@ import {
2830
} from "@/features/mcp/hooks";
2931
import { reauthorizeInstallation } from "@/features/mcp/oauth";
3032
import { getMcpConnectionManager } from "@/features/mcp/service";
31-
import type { McpApprovalState } from "@/features/mcp/types";
32-
import { isStdioServer } from "@/features/mcp/types";
3333
import { useScreenInsets } from "@/hooks/useScreenInsets";
3434
import { logger } from "@/lib/logger";
3535
import { useThemeColors } from "@/lib/theme";
@@ -75,7 +75,7 @@ export default function McpInstallationDetailScreen() {
7575
);
7676
}
7777

78-
const stdio = isStdioServer(installation);
78+
const stdio = isStdioMcpServer(installation);
7979

8080
const handleEnabledChange = (enabled: boolean) => {
8181
updateMutation.mutate({

apps/mobile/src/app/mcp-servers/template/[id].tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Text } from "@components/text";
2+
import { isStdioMcpServer } from "@posthog/core/mcp-servers/presentation";
23
import { router, useLocalSearchParams } from "expo-router";
34
import { Lock, Warning } from "phosphor-react-native";
45
import { useMemo, useState } from "react";
@@ -17,7 +18,6 @@ import {
1718
useMcpMarketplace,
1819
} from "@/features/mcp/hooks";
1920
import { installTemplateWithOAuth } from "@/features/mcp/oauth";
20-
import { isStdioServer } from "@/features/mcp/types";
2121
import { useScreenInsets } from "@/hooks/useScreenInsets";
2222
import { logger } from "@/lib/logger";
2323
import { openExternalUrl } from "@/lib/openExternalUrl";
@@ -71,7 +71,7 @@ export default function McpTemplateDetailScreen() {
7171
);
7272
}
7373

74-
const stdio = isStdioServer(template);
74+
const stdio = isStdioMcpServer(template);
7575

7676
const handleInstall = async () => {
7777
if (!template) return;

apps/mobile/src/features/chat/components/ToolMessage.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
getPostHogExecDisplay,
44
isPostHogExecTool,
55
} from "@posthog/core/sessions/posthogExecDisplay";
6+
import { parseMcpToolName } from "@posthog/shared";
67
import { useRouter } from "expo-router";
78
import {
89
ArrowsClockwise,
@@ -28,7 +29,6 @@ import {
2829
View,
2930
} from "react-native";
3031
import { McpAppHost } from "@/features/mcp/components/McpAppHost";
31-
import { isMcpToolName } from "@/features/mcp/utils/mcpToolName";
3232
import {
3333
getColorForClass,
3434
highlightCode,
@@ -942,7 +942,8 @@ export function ToolMessage({
942942
// MCP App tools render via the WebView host — skip PostHog exec (which has
943943
// its own renderer above) and only kick in once the tool finished or while
944944
// it's running so we don't show empty WebView shells for pending tools.
945-
const isMcpAppTool = !isPostHogExec && isMcpToolName(effectiveToolName);
945+
const isMcpAppTool =
946+
!isPostHogExec && parseMcpToolName(effectiveToolName) !== undefined;
946947

947948
if (isMcpAppTool && !isPending) {
948949
return (

apps/mobile/src/features/mcp/components/McpAppHost.tsx

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Text } from "@components/text";
22
import type { McpUiDisplayMode } from "@modelcontextprotocol/ext-apps/app-bridge";
33
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
4-
import { isSafeExternalUrl } from "@posthog/shared";
4+
import { isSafeExternalUrl, parseMcpToolName } from "@posthog/shared";
55
import * as WebBrowser from "expo-web-browser";
66
import { ArrowsIn, ArrowsOut, Warning } from "phosphor-react-native";
77
import { useCallback, useMemo, useRef, useState } from "react";
@@ -22,7 +22,6 @@ import { sandboxProxyHtml } from "../sandbox/sandboxProxyHtml";
2222
import { useMcpUiResource } from "../sandbox/useMcpUiResource";
2323
import { type Phase, useMobileAppBridge } from "../sandbox/useMobileAppBridge";
2424
import { getMcpConnectionManager } from "../service";
25-
import { parseMcpToolName } from "../utils/mcpToolName";
2625

2726
interface McpAppHostProps {
2827
/** Raw tool name from the agent — `mcp__<server>__<tool>`. */
@@ -60,14 +59,12 @@ export function McpAppHost(props: McpAppHostProps) {
6059
const installations = useMcpInstallations();
6160
const installation = useMemo(() => {
6261
if (!parsed) return null;
63-
return (
64-
installations.data?.find((i) => i.name === parsed.serverName) ?? null
65-
);
62+
return installations.data?.find((i) => i.name === parsed.server) ?? null;
6663
}, [installations.data, parsed]);
6764

6865
const uiResource = useMcpUiResource({
6966
installation,
70-
toolName: parsed?.toolName ?? "",
67+
toolName: parsed?.tool ?? "",
7168
});
7269

7370
const webViewRef = useRef<WebView | null>(null);
@@ -122,7 +119,7 @@ export function McpAppHost(props: McpAppHostProps) {
122119
const { handleWebViewMessage } = useMobileAppBridge({
123120
webViewRef,
124121
uiResource: uiResource.data?.resource ?? null,
125-
serverName: parsed?.serverName ?? "",
122+
serverName: parsed?.server ?? "",
126123
toolDefinition: uiResource.data?.tool ?? null,
127124
toolInput: props.toolArgs ?? null,
128125
existingToolResult:

apps/mobile/src/features/mcp/components/McpServerRow.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import { Text } from "@components/text";
2+
import type {
3+
McpRecommendedServer,
4+
McpServerInstallation,
5+
} from "@posthog/api-client/types";
6+
import { isStdioMcpServer } from "@posthog/core/mcp-servers/presentation";
27
import { CaretRight, Lock, Warning } from "phosphor-react-native";
38
import type { ReactNode } from "react";
49
import { Pressable, View } from "react-native";
510
import { useThemeColors } from "@/lib/theme";
6-
import type { McpRecommendedServer, McpServerInstallation } from "../types";
7-
import { isStdioServer } from "../types";
811
import { ServerIcon } from "./ServerIcon";
912

1013
interface McpServerRowProps {
@@ -119,7 +122,7 @@ export function recommendedToRowProps(
119122
title: template.name,
120123
description: template.description,
121124
authType: template.auth_type,
122-
isStdio: isStdioServer(template),
125+
isStdio: isStdioMcpServer(template),
123126
installed: installedNames.has(template.name),
124127
iconKey: template.icon_key,
125128
onPress: () => onPress(template),
@@ -134,7 +137,7 @@ export function installationToRowProps(
134137
title: installation.display_name || installation.name,
135138
subtitle: installation.url,
136139
authType: installation.auth_type,
137-
isStdio: isStdioServer(installation),
140+
isStdio: isStdioMcpServer(installation),
138141
needsReauth: installation.needs_reauth,
139142
installed: true,
140143
iconKey: installation.icon_key,

apps/mobile/src/features/mcp/hooks.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
2-
import { getPostHogApiClient } from "@/lib/posthogApiClient";
31
import type {
42
InstallCustomMcpServerOptions,
53
InstallMcpTemplateOptions,
64
McpApprovalState,
75
UpdateMcpServerInstallationOptions,
8-
} from "./types";
6+
} from "@posthog/api-client/types";
7+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
8+
import { getPostHogApiClient } from "@/lib/posthogApiClient";
99

1010
const mcpKeys = {
1111
all: ["mcp"] as const,
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export interface McpUiResource {
2+
uri: string;
3+
html: string;
4+
csp?: Record<string, unknown>;
5+
permissions?: Record<string, Record<string, unknown>>;
6+
}

0 commit comments

Comments
 (0)