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/mobile/src/features/chat/components/AgentMessage.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { pickThinkingActivity } from "@posthog/core/sessions/thinkingActivities";
import { Brain } from "phosphor-react-native";
import { useState } from "react";
import { Pressable, Text, View } from "react-native";
import { formatRelativeTime } from "@/lib/format";
import { useThemeColors } from "@/lib/theme";
import { usePeriodicRerender } from "../hooks/usePeriodicRerender";
import { getRandomThinkingMessage } from "../utils/thinkingMessages";
import { CopyButton } from "./CopyButton";
import { MarkdownText } from "./MarkdownText";
import { ToolMessage } from "./ToolMessage";
Expand Down Expand Up @@ -110,7 +110,7 @@ export function AgentMessage({
{isLoading && !content && !thinkingText && (
<View className="max-w-[95%] px-4 py-1">
<Text className="font-mono text-[13px] text-gray-9 italic">
{getRandomThinkingMessage()}
{pickThinkingActivity(Math.random())}...
</Text>
</View>
)}
Expand Down
10 changes: 5 additions & 5 deletions apps/mobile/src/features/chat/components/ToolMessage.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
import {
formatPosthogExecBody,
getPostHogExecDisplay,
isPostHogExecTool,
} from "@posthog/core/sessions/posthogExecDisplay";
import { useRouter } from "expo-router";
import {
ArrowsClockwise,
Expand All @@ -22,11 +27,6 @@ import {
TouchableOpacity,
View,
} from "react-native";
import {
formatPosthogExecBody,
getPostHogExecDisplay,
isPostHogExecTool,
} from "@/features/chat/utils/posthogExecDisplay";
import { McpAppHost } from "@/features/mcp/components/McpAppHost";
import { isMcpToolName } from "@/features/mcp/utils/mcpToolName";
import {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { describe, expect, it } from "vitest";
import {
formatPosthogExecBody,
getPostHogExecDisplay,
isPostHogExecTool,
} from "./posthogExecDisplay";
} from "@posthog/core/sessions/posthogExecDisplay";
import { describe, expect, it } from "vitest";

describe("isPostHogExecTool", () => {
it("matches the bare posthog exec tool", () => {
Expand Down
109 changes: 0 additions & 109 deletions apps/mobile/src/features/chat/utils/posthogExecDisplay.ts

This file was deleted.

23 changes: 0 additions & 23 deletions apps/mobile/src/features/chat/utils/thinkingMessages.test.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ vi.mock("@/features/chat", () => ({
deriveToolKind: () => "other",
}));

vi.mock("@/features/chat/utils/thinkingMessages", () => ({
getRandomThinkingActivity: () => "Thinking",
vi.mock("@posthog/core/sessions/thinkingActivities", () => ({
pickThinkingActivity: () => "Thinking",
}));

vi.mock("@/lib/theme", () => ({
Expand Down
8 changes: 5 additions & 3 deletions apps/mobile/src/features/tasks/components/TaskSessionView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type {
PortableSessionEvent as SessionEvent,
PortableSessionNotification as SessionNotification,
} from "@posthog/core/sessions/portableSessionEvents";
import { pickThinkingActivity } from "@posthog/core/sessions/thinkingActivities";
import {
ArrowDown,
Brain,
Expand All @@ -24,7 +25,6 @@ import {
ToolMessage,
type ToolStatus,
} from "@/features/chat";
import { getRandomThinkingActivity } from "@/features/chat/utils/thinkingMessages";
import { useThemeColors } from "@/lib/theme";
import type {
CloudPendingPermissionRequest,
Expand Down Expand Up @@ -732,7 +732,9 @@ function useElapsedTimer() {

function ThinkingIndicator() {
const [dots, setDots] = useState(1);
const [activity, setActivity] = useState(getRandomThinkingActivity);
const [activity, setActivity] = useState(() =>
pickThinkingActivity(Math.random()),
);
const elapsed = useElapsedTimer();
const themeColors = useThemeColors();

Expand All @@ -745,7 +747,7 @@ function ThinkingIndicator() {

useEffect(() => {
const interval = setInterval(() => {
setActivity(getRandomThinkingActivity());
setActivity(pickThinkingActivity(Math.random()));
}, 2000);
return () => clearInterval(interval);
}, []);
Expand Down
85 changes: 85 additions & 0 deletions packages/core/src/sessions/posthogExecDisplay.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { parseMcpToolName } from "@posthog/shared";

const POSTHOG_SERVER_RE = /^(?:plugin_)?posthog(?:_[^_]+)*$/;
const POSTHOG_VERB_RE =
/^\s*(tools|search|info|schema|call)(?:\s+([\s\S]*))?\s*$/;
const POSTHOG_CALL_BODY_RE = /^(?:--json\s+)?([a-zA-Z0-9_-]+)\s*([\s\S]*)$/;
const POSTHOG_TOOL_NAME_RE = /^([a-zA-Z0-9_-]+)\s*([\s\S]*)$/;

export interface PostHogExecDisplay {
label: string;
input?: string;
}

export function isPostHogExecTool(toolName: string): boolean {
const mcp = parseMcpToolName(toolName);
return !!mcp && mcp.tool === "exec" && POSTHOG_SERVER_RE.test(mcp.server);
}

export function getPostHogExecDisplay(
toolInput: unknown,
): PostHogExecDisplay | null {
if (!toolInput || typeof toolInput !== "object") return null;
const input = toolInput as { command?: unknown; input?: unknown };
if (typeof input.command !== "string") return null;
const match = input.command.match(POSTHOG_VERB_RE);
if (!match) return null;
const verb = match[1] as "tools" | "search" | "info" | "schema" | "call";
const rest = (match[2] ?? "").trim();
const explicitInput = readExplicitInput(input.input);

switch (verb) {
case "tools":
return { label: "List tools", input: undefined };
case "search":
return {
label: "Search tools",
input: explicitInput ?? (rest || undefined),
};
case "info":
return { label: rest ? `Read ${rest}` : "Read tool", input: undefined };
case "schema": {
const schema = rest.match(POSTHOG_TOOL_NAME_RE);
if (!schema) return { label: "Inspect schema", input: undefined };
const path = explicitInput ?? ((schema[2] ?? "").trim() || undefined);
return {
label: path
? `Inspect ${schema[1]}.${path}`
: `Inspect ${schema[1]} fields`,
input: undefined,
};
}
case "call": {
const call = rest.match(POSTHOG_CALL_BODY_RE);
if (!call) return null;
return {
label: call[1],
input: explicitInput ?? ((call[2] ?? "").trim() || undefined),
};
}
}
}

function readExplicitInput(value: unknown): string | undefined {
if (value === undefined || value === null) return undefined;
if (typeof value === "string") return value.trim() || undefined;
try {
return JSON.stringify(value);
} catch {
return undefined;
}
}

export function formatPosthogExecBody(
input: string | undefined,
): string | undefined {
if (!input) return undefined;
try {
const parsed = JSON.parse(input);
if (parsed && typeof parsed === "object")
return JSON.stringify(parsed, null, 2);
} catch {
return input;
}
return input;
}
17 changes: 17 additions & 0 deletions packages/core/src/sessions/thinkingActivities.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { describe, expect, it } from "vitest";
import {
pickNextThinkingActivity,
pickThinkingActivity,
THINKING_ACTIVITIES,
} from "./thinkingActivities";

describe("thinking activities", () => {
it("selects from a bounded random value", () => {
expect(pickThinkingActivity(0)).toBe("Booping");
expect(pickThinkingActivity(1)).toBe(THINKING_ACTIVITIES.at(-1));
});

it("always advances when the random pick matches the current activity", () => {
expect(pickNextThinkingActivity("Booping", 0)).toBe("Crunching");
});
});
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
// Random thinking messages displayed while AI is generating
// Based on posthog/frontend/src/scenes/max/utils/thinkingMessages.ts

export const THINKING_MESSAGES = [
export const THINKING_ACTIVITIES = [
"Booping",
"Crunching",
"Digging",
Expand Down Expand Up @@ -86,13 +83,21 @@ export const THINKING_MESSAGES = [
"Puttering",
"Whiffling",
"Thinking",
];
] as const;

export function getRandomThinkingActivity(): string {
const randomIndex = Math.floor(Math.random() * THINKING_MESSAGES.length);
return THINKING_MESSAGES[randomIndex];
export function pickThinkingActivity(randomValue: number): string {
const bounded = Math.max(0, Math.min(randomValue, 0.999999999999));
return THINKING_ACTIVITIES[Math.floor(bounded * THINKING_ACTIVITIES.length)];
}

export function getRandomThinkingMessage(): string {
return `${getRandomThinkingActivity()}...`;
export function pickNextThinkingActivity(
current: string,
randomValue: number,
): string {
const picked = pickThinkingActivity(randomValue);
if (picked !== current || THINKING_ACTIVITIES.length <= 1) return picked;
const index = THINKING_ACTIVITIES.indexOf(
current as (typeof THINKING_ACTIVITIES)[number],
);
return THINKING_ACTIVITIES[(index + 1) % THINKING_ACTIVITIES.length];
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Plugs } from "@phosphor-icons/react";
import {
getPostHogExecDisplay,
isPostHogExecTool,
} from "../../posthog-mcp/utils/posthog-exec-display";
} from "@posthog/core/sessions/posthogExecDisplay";
import { useChatThreadChrome } from "../../sessions/components/chat-thread/chatThreadChrome";
import { ToolRow } from "../../sessions/components/session-update/ToolRow";
import {
Expand Down
4 changes: 2 additions & 2 deletions packages/ui/src/features/permissions/McpPermission.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { mcpToolKey, readMcpToolDescriptor } from "@posthog/shared";
import {
formatPosthogExecBody,
getPostHogExecDisplay,
isPostHogExecTool,
} from "@posthog/ui/features/posthog-mcp/utils/posthog-exec-display";
} from "@posthog/core/sessions/posthogExecDisplay";
import { mcpToolKey, readMcpToolDescriptor } from "@posthog/shared";
import { formatInput } from "@posthog/ui/features/sessions/components/session-update/toolCallUtils";
import { ActionSelector } from "@posthog/ui/primitives/ActionSelector";
import { Box, Code } from "@radix-ui/themes";
Expand Down
Loading
Loading