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
4 changes: 2 additions & 2 deletions apps/code/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,8 @@
"node-pty": "1.1.0",
"posthog-node": "^5.35.6",
"radix-themes-tw": "0.2.3",
"react": "19.2.6",
"react-dom": "19.2.6",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-hotkeys-hook": "^4.4.4",
"react-scan": "^0.5.6",
"reflect-metadata": "^0.2.2",
Expand Down
25 changes: 19 additions & 6 deletions apps/mobile/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ pnpm --filter @posthog/mobile start

### Feature Folders

Code is organized by feature in `src/features/`. Each feature is self-contained with its own components, hooks, stores, and API logic.
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.

```
src/features/
Expand All @@ -46,18 +46,31 @@ src/features/
│ ├── hooks/
│ ├── stores/
│ └── types.ts
├── conversations/ # PostHog AI conversation list & management
│ ├── api.ts
├── inbox/ # Native inbox rendering and query hooks
│ ├── components/
│ ├── hooks/
│ └── stores/
└── tasks/ # Task management
├── api.ts
└── tasks/ # Native cloud-task rendering and host adapters
├── components/
├── hooks/
├── services/
└── stores/
```

### Portability boundary

Mobile and desktop use the same cloud-task architecture. New work must preserve these ownership rules:

- `@posthog/shared` owns runtime contracts and Zod schemas.
- `@posthog/api-client` owns authenticated PostHog HTTPS transport and its request/response types.
- `@posthog/core` owns cloud-task orchestration and headless presentation decisions, including sessions, queues, permissions, models, repositories, inbox rules, and automation semantics.
- `apps/mobile` owns Expo lifecycle, React Native rendering, gestures, sheets, notifications, audio, secure storage, and small persisted view-state stores.
- `@posthog/ui` owns the DOM/Quill renderer and web view state.

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.

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.

### File-Based Routing

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

**Local vs Cloud builds:**
- Cloud (default): Runs on Expo's servers, no local Xcode needed
- Local (`--local`): Runs on your machine, faster iteration, requires Xcode/Android SDK
- Local (`--local`): Runs on your machine, faster iteration, requires Xcode/Android SDK
5 changes: 3 additions & 2 deletions apps/mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@
"phosphor-react-native": "^3.0.2",
"posthog-react-native": "^4.18.0",
"posthog-react-native-session-replay": "^1.6.0",
"react": "19.2.6",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-native": "0.81.5",
"react-native-keyboard-controller": "1.18.5",
"react-native-reanimated": "~4.1.1",
Expand All @@ -83,7 +84,7 @@
"@types/react-test-renderer": "^19.1.0",
"@vitejs/plugin-react": "^4.7.0",
"react-native-svg-transformer": "^1.5.3",
"react-test-renderer": "^19.2.6",
"react-test-renderer": "19.1.0",
"tailwindcss": "^3.4.18",
"typescript": "~5.9.2",
"vite": "^6.4.1",
Expand Down
2 changes: 1 addition & 1 deletion apps/mobile/src/app/mcp-servers/add-custom.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Text } from "@components/text";
import type { McpAuthType } from "@posthog/api-client/types";
import { router } from "expo-router";
import { Lock } from "phosphor-react-native";
import { useState } from "react";
Expand All @@ -14,7 +15,6 @@ import {
import { FloatingMcpHeader } from "@/features/mcp/components/FloatingMcpHeader";
import { useMcpInstallations } from "@/features/mcp/hooks";
import { installCustomWithOAuth } from "@/features/mcp/oauth";
import type { McpAuthType } from "@/features/mcp/types";
import { useScreenInsets } from "@/hooks/useScreenInsets";
import { logger } from "@/lib/logger";
import { useThemeColors } from "@/lib/theme";
Expand Down
8 changes: 4 additions & 4 deletions apps/mobile/src/app/mcp-servers/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { Text } from "@components/text";
import type {
McpRecommendedServer,
McpServerInstallation,
} from "@posthog/api-client/types";
import { useRouter } from "expo-router";
import { MagnifyingGlass, Plus, PuzzlePiece } from "phosphor-react-native";
import { useMemo, useState } from "react";
Expand All @@ -17,10 +21,6 @@ import {
recommendedToRowProps,
} from "@/features/mcp/components/McpServerRow";
import { useMcpInstallations, useMcpMarketplace } from "@/features/mcp/hooks";
import type {
McpRecommendedServer,
McpServerInstallation,
} from "@/features/mcp/types";
import { useScreenInsets } from "@/hooks/useScreenInsets";
import { useThemeColors } from "@/lib/theme";

Expand Down
6 changes: 3 additions & 3 deletions apps/mobile/src/app/mcp-servers/installation/[id].tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { Text } from "@components/text";
import type { McpApprovalState } from "@posthog/api-client/types";
import { isStdioMcpServer } from "@posthog/core/mcp-servers/presentation";
import { router, useLocalSearchParams } from "expo-router";
import {
ArrowsClockwise,
Expand Down Expand Up @@ -28,8 +30,6 @@ import {
} from "@/features/mcp/hooks";
import { reauthorizeInstallation } from "@/features/mcp/oauth";
import { getMcpConnectionManager } from "@/features/mcp/service";
import type { McpApprovalState } from "@/features/mcp/types";
import { isStdioServer } from "@/features/mcp/types";
import { useScreenInsets } from "@/hooks/useScreenInsets";
import { logger } from "@/lib/logger";
import { useThemeColors } from "@/lib/theme";
Expand Down Expand Up @@ -75,7 +75,7 @@ export default function McpInstallationDetailScreen() {
);
}

const stdio = isStdioServer(installation);
const stdio = isStdioMcpServer(installation);

const handleEnabledChange = (enabled: boolean) => {
updateMutation.mutate({
Expand Down
4 changes: 2 additions & 2 deletions apps/mobile/src/app/mcp-servers/template/[id].tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Text } from "@components/text";
import { isStdioMcpServer } from "@posthog/core/mcp-servers/presentation";
import { router, useLocalSearchParams } from "expo-router";
import { Lock, Warning } from "phosphor-react-native";
import { useMemo, useState } from "react";
Expand All @@ -17,7 +18,6 @@ import {
useMcpMarketplace,
} from "@/features/mcp/hooks";
import { installTemplateWithOAuth } from "@/features/mcp/oauth";
import { isStdioServer } from "@/features/mcp/types";
import { useScreenInsets } from "@/hooks/useScreenInsets";
import { logger } from "@/lib/logger";
import { openExternalUrl } from "@/lib/openExternalUrl";
Expand Down Expand Up @@ -71,7 +71,7 @@ export default function McpTemplateDetailScreen() {
);
}

const stdio = isStdioServer(template);
const stdio = isStdioMcpServer(template);

const handleInstall = async () => {
if (!template) return;
Expand Down
2 changes: 1 addition & 1 deletion apps/mobile/src/app/task/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Text } from "@components/text";
import type { CreateTaskOptions } from "@posthog/api-client/types";
import {
findRepositoryOption,
isRepositorySelectionComplete,
Expand Down Expand Up @@ -79,7 +80,6 @@ import {
pendingTaskPromptStoreApi,
} from "@/features/tasks/stores/pendingTaskPromptStore";
import { useTaskStore } from "@/features/tasks/stores/taskStore";
import type { CreateTaskOptions } from "@/features/tasks/types";
import { useScreenInsets } from "@/hooks/useScreenInsets";
import { logger } from "@/lib/logger";
import { getPostHogApiClient } from "@/lib/posthogApiClient";
Expand Down
5 changes: 3 additions & 2 deletions apps/mobile/src/features/chat/components/ToolMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
getPostHogExecDisplay,
isPostHogExecTool,
} from "@posthog/core/sessions/posthogExecDisplay";
import { parseMcpToolName } from "@posthog/shared";
import { useRouter } from "expo-router";
import {
ArrowsClockwise,
Expand All @@ -28,7 +29,6 @@ import {
View,
} from "react-native";
import { McpAppHost } from "@/features/mcp/components/McpAppHost";
import { isMcpToolName } from "@/features/mcp/utils/mcpToolName";
import {
getColorForClass,
highlightCode,
Expand Down Expand Up @@ -942,7 +942,8 @@ export function ToolMessage({
// MCP App tools render via the WebView host — skip PostHog exec (which has
// its own renderer above) and only kick in once the tool finished or while
// it's running so we don't show empty WebView shells for pending tools.
const isMcpAppTool = !isPostHogExec && isMcpToolName(effectiveToolName);
const isMcpAppTool =
!isPostHogExec && parseMcpToolName(effectiveToolName) !== undefined;

if (isMcpAppTool && !isPending) {
return (
Expand Down
6 changes: 3 additions & 3 deletions apps/mobile/src/features/inbox/activityLog.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import type { AnySignalReportArtefact } from "@posthog/shared/domain-types";
import { describe, expect, it } from "vitest";
import {
attributionLabel,
parseDiffLines,
selectActivityArtefacts,
shortSha,
taskRunLabel,
} from "./activityLog";
} from "@posthog/core/inbox/activityLog";
import type { AnySignalReportArtefact } from "@posthog/shared/domain-types";
import { describe, expect, it } from "vitest";

function commit(id: string, createdAt: string): AnySignalReportArtefact {
return {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Text } from "@components/text";
import { shortSha } from "@posthog/core/inbox/activityLog";
import type { CommitContent } from "@posthog/shared/domain-types";
import { CaretDown, CaretRight } from "phosphor-react-native";
import { useState } from "react";
import { ActivityIndicator, Pressable, View } from "react-native";
import { useThemeColors } from "@/lib/theme";
import { shortSha } from "../activityLog";
import { useCommitDiff } from "../hooks/useInboxReports";
import { DiffBlock } from "./DiffBlock";

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { Text } from "@components/text";
import { taskRunLabel } from "@posthog/core/inbox/activityLog";
import type { TaskRunArtefactContent } from "@posthog/shared/domain-types";
import { useRouter } from "expo-router";
import { CaretRight } from "phosphor-react-native";
import { Pressable, View } from "react-native";
import { useTask } from "@/features/tasks";
import { useThemeColors } from "@/lib/theme";
import { taskRunLabel } from "../activityLog";

export function ArtefactTaskRun({
content,
Expand Down
2 changes: 1 addition & 1 deletion apps/mobile/src/features/inbox/components/DiffBlock.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Text } from "@components/text";
import { parseDiffLines } from "@posthog/core/inbox/activityLog";
import { ScrollView, View } from "react-native";
import { parseDiffLines } from "../activityLog";

const LINE_CLASS: Record<string, string> = {
add: "bg-status-success/15 text-status-success",
Expand Down
10 changes: 5 additions & 5 deletions apps/mobile/src/features/inbox/components/ReportActivity.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { Text } from "@components/text";
import {
type ActivityArtefact,
attributionLabel,
selectActivityArtefacts,
} from "@posthog/core/inbox/activityLog";
import type { AnySignalReportArtefact } from "@posthog/shared/domain-types";
import { ClockCounterClockwise } from "phosphor-react-native";
import { useMemo } from "react";
import { View } from "react-native";
import { formatRelativeTime } from "@/lib/format";
import { useThemeColors } from "@/lib/theme";
import {
type ActivityArtefact,
attributionLabel,
selectActivityArtefacts,
} from "../activityLog";
import { ArtefactCommit } from "./ArtefactCommit";
import { ArtefactTaskRun } from "./ArtefactTaskRun";

Expand Down
2 changes: 1 addition & 1 deletion apps/mobile/src/features/inbox/components/TinderView.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Text } from "@components/text";
import type { CreateTaskOptions } from "@posthog/api-client/types";
import {
formatSignalReportSummaryMarkdown,
inboxStatusLabel,
Expand Down Expand Up @@ -29,7 +30,6 @@ import {
import { MarkdownText } from "@/features/chat/components/MarkdownText";
import { usePreferencesStore } from "@/features/preferences/stores/preferencesStore";
import { useCloudTaskConfigOptions } from "@/features/tasks/hooks/useCloudTaskConfigOptions";
import type { CreateTaskOptions } from "@/features/tasks/types";
import {
ANALYTICS_EVENTS,
computeReportAgeHours,
Expand Down
11 changes: 4 additions & 7 deletions apps/mobile/src/features/mcp/components/McpAppHost.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Text } from "@components/text";
import type { McpUiDisplayMode } from "@modelcontextprotocol/ext-apps/app-bridge";
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { isSafeExternalUrl } from "@posthog/shared";
import { isSafeExternalUrl, parseMcpToolName } from "@posthog/shared";
import * as WebBrowser from "expo-web-browser";
import { ArrowsIn, ArrowsOut, Warning } from "phosphor-react-native";
import { useCallback, useMemo, useRef, useState } from "react";
Expand All @@ -22,7 +22,6 @@ import { sandboxProxyHtml } from "../sandbox/sandboxProxyHtml";
import { useMcpUiResource } from "../sandbox/useMcpUiResource";
import { type Phase, useMobileAppBridge } from "../sandbox/useMobileAppBridge";
import { getMcpConnectionManager } from "../service";
import { parseMcpToolName } from "../utils/mcpToolName";

interface McpAppHostProps {
/** Raw tool name from the agent — `mcp__<server>__<tool>`. */
Expand Down Expand Up @@ -60,14 +59,12 @@ export function McpAppHost(props: McpAppHostProps) {
const installations = useMcpInstallations();
const installation = useMemo(() => {
if (!parsed) return null;
return (
installations.data?.find((i) => i.name === parsed.serverName) ?? null
);
return installations.data?.find((i) => i.name === parsed.server) ?? null;
}, [installations.data, parsed]);

const uiResource = useMcpUiResource({
installation,
toolName: parsed?.toolName ?? "",
toolName: parsed?.tool ?? "",
});

const webViewRef = useRef<WebView | null>(null);
Expand Down Expand Up @@ -122,7 +119,7 @@ export function McpAppHost(props: McpAppHostProps) {
const { handleWebViewMessage } = useMobileAppBridge({
webViewRef,
uiResource: uiResource.data?.resource ?? null,
serverName: parsed?.serverName ?? "",
serverName: parsed?.server ?? "",
toolDefinition: uiResource.data?.tool ?? null,
toolInput: props.toolArgs ?? null,
existingToolResult:
Expand Down
11 changes: 7 additions & 4 deletions apps/mobile/src/features/mcp/components/McpServerRow.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { Text } from "@components/text";
import type {
McpRecommendedServer,
McpServerInstallation,
} from "@posthog/api-client/types";
import { isStdioMcpServer } from "@posthog/core/mcp-servers/presentation";
import { CaretRight, Lock, Warning } from "phosphor-react-native";
import type { ReactNode } from "react";
import { Pressable, View } from "react-native";
import { useThemeColors } from "@/lib/theme";
import type { McpRecommendedServer, McpServerInstallation } from "../types";
import { isStdioServer } from "../types";
import { ServerIcon } from "./ServerIcon";

interface McpServerRowProps {
Expand Down Expand Up @@ -119,7 +122,7 @@ export function recommendedToRowProps(
title: template.name,
description: template.description,
authType: template.auth_type,
isStdio: isStdioServer(template),
isStdio: isStdioMcpServer(template),
installed: installedNames.has(template.name),
iconKey: template.icon_key,
onPress: () => onPress(template),
Expand All @@ -134,7 +137,7 @@ export function installationToRowProps(
title: installation.display_name || installation.name,
subtitle: installation.url,
authType: installation.auth_type,
isStdio: isStdioServer(installation),
isStdio: isStdioMcpServer(installation),
needsReauth: installation.needs_reauth,
installed: true,
iconKey: installation.icon_key,
Expand Down
6 changes: 3 additions & 3 deletions apps/mobile/src/features/mcp/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { getPostHogApiClient } from "@/lib/posthogApiClient";
import type {
InstallCustomMcpServerOptions,
InstallMcpTemplateOptions,
McpApprovalState,
UpdateMcpServerInstallationOptions,
} from "./types";
} from "@posthog/api-client/types";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { getPostHogApiClient } from "@/lib/posthogApiClient";

const mcpKeys = {
all: ["mcp"] as const,
Expand Down
6 changes: 6 additions & 0 deletions apps/mobile/src/features/mcp/mcpUiResource.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export interface McpUiResource {
uri: string;
html: string;
csp?: Record<string, unknown>;
permissions?: Record<string, Record<string, unknown>>;
}
Loading
Loading