Skip to content

Commit 52fcd91

Browse files
Share session presentation semantics
Generated-By: PostHog Code Task-Id: 40c57a59-b4e1-4760-8e56-ecd03e9c2f0f
1 parent 431749d commit 52fcd91

15 files changed

Lines changed: 145 additions & 394 deletions

File tree

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1+
import { pickThinkingActivity } from "@posthog/core/sessions/thinkingActivities";
12
import { Brain } from "phosphor-react-native";
23
import { useState } from "react";
34
import { Pressable, Text, View } from "react-native";
45
import { formatRelativeTime } from "@/lib/format";
56
import { useThemeColors } from "@/lib/theme";
67
import { usePeriodicRerender } from "../hooks/usePeriodicRerender";
7-
import { getRandomThinkingMessage } from "../utils/thinkingMessages";
88
import { CopyButton } from "./CopyButton";
99
import { MarkdownText } from "./MarkdownText";
1010
import { ToolMessage } from "./ToolMessage";
@@ -110,7 +110,7 @@ export function AgentMessage({
110110
{isLoading && !content && !thinkingText && (
111111
<View className="max-w-[95%] px-4 py-1">
112112
<Text className="font-mono text-[13px] text-gray-9 italic">
113-
{getRandomThinkingMessage()}
113+
{pickThinkingActivity(Math.random())}...
114114
</Text>
115115
</View>
116116
)}

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
import {
2+
formatPosthogExecBody,
3+
getPostHogExecDisplay,
4+
isPostHogExecTool,
5+
} from "@posthog/core/sessions/posthogExecDisplay";
16
import { useRouter } from "expo-router";
27
import {
38
ArrowsClockwise,
@@ -22,11 +27,6 @@ import {
2227
TouchableOpacity,
2328
View,
2429
} from "react-native";
25-
import {
26-
formatPosthogExecBody,
27-
getPostHogExecDisplay,
28-
isPostHogExecTool,
29-
} from "@/features/chat/utils/posthogExecDisplay";
3030
import { McpAppHost } from "@/features/mcp/components/McpAppHost";
3131
import { isMcpToolName } from "@/features/mcp/utils/mcpToolName";
3232
import {

apps/mobile/src/features/chat/utils/posthogExecDisplay.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { describe, expect, it } from "vitest";
21
import {
32
formatPosthogExecBody,
43
getPostHogExecDisplay,
54
isPostHogExecTool,
6-
} from "./posthogExecDisplay";
5+
} from "@posthog/core/sessions/posthogExecDisplay";
6+
import { describe, expect, it } from "vitest";
77

88
describe("isPostHogExecTool", () => {
99
it("matches the bare posthog exec tool", () => {

apps/mobile/src/features/chat/utils/posthogExecDisplay.ts

Lines changed: 0 additions & 109 deletions
This file was deleted.

apps/mobile/src/features/chat/utils/thinkingMessages.test.ts

Lines changed: 0 additions & 23 deletions
This file was deleted.

apps/mobile/src/features/tasks/components/TaskSessionView.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ vi.mock("@/features/chat", () => ({
2424
deriveToolKind: () => "other",
2525
}));
2626

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

3131
vi.mock("@/lib/theme", () => ({

apps/mobile/src/features/tasks/components/TaskSessionView.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
2+
import { pickThinkingActivity } from "@posthog/core/sessions/thinkingActivities";
13
import {
24
ArrowDown,
35
Brain,
@@ -20,7 +22,6 @@ import {
2022
ToolMessage,
2123
type ToolStatus,
2224
} from "@/features/chat";
23-
import { getRandomThinkingActivity } from "@/features/chat/utils/thinkingMessages";
2425
import { useThemeColors } from "@/lib/theme";
2526
import type {
2627
CloudPendingPermissionRequest,
@@ -730,7 +731,9 @@ function useElapsedTimer() {
730731

731732
function ThinkingIndicator() {
732733
const [dots, setDots] = useState(1);
733-
const [activity, setActivity] = useState(getRandomThinkingActivity);
734+
const [activity, setActivity] = useState(() =>
735+
pickThinkingActivity(Math.random()),
736+
);
734737
const elapsed = useElapsedTimer();
735738
const themeColors = useThemeColors();
736739

@@ -743,7 +746,7 @@ function ThinkingIndicator() {
743746

744747
useEffect(() => {
745748
const interval = setInterval(() => {
746-
setActivity(getRandomThinkingActivity());
749+
setActivity(pickThinkingActivity(Math.random()));
747750
}, 2000);
748751
return () => clearInterval(interval);
749752
}, []);
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { parseMcpToolName } from "@posthog/shared";
2+
3+
const POSTHOG_SERVER_RE = /^(?:plugin_)?posthog(?:_[^_]+)*$/;
4+
const POSTHOG_VERB_RE =
5+
/^\s*(tools|search|info|schema|call)(?:\s+([\s\S]*))?\s*$/;
6+
const POSTHOG_CALL_BODY_RE = /^(?:--json\s+)?([a-zA-Z0-9_-]+)\s*([\s\S]*)$/;
7+
const POSTHOG_TOOL_NAME_RE = /^([a-zA-Z0-9_-]+)\s*([\s\S]*)$/;
8+
9+
export interface PostHogExecDisplay {
10+
label: string;
11+
input?: string;
12+
}
13+
14+
export function isPostHogExecTool(toolName: string): boolean {
15+
const mcp = parseMcpToolName(toolName);
16+
return !!mcp && mcp.tool === "exec" && POSTHOG_SERVER_RE.test(mcp.server);
17+
}
18+
19+
export function getPostHogExecDisplay(
20+
toolInput: unknown,
21+
): PostHogExecDisplay | null {
22+
if (!toolInput || typeof toolInput !== "object") return null;
23+
const input = toolInput as { command?: unknown; input?: unknown };
24+
if (typeof input.command !== "string") return null;
25+
const match = input.command.match(POSTHOG_VERB_RE);
26+
if (!match) return null;
27+
const verb = match[1] as "tools" | "search" | "info" | "schema" | "call";
28+
const rest = (match[2] ?? "").trim();
29+
const explicitInput = readExplicitInput(input.input);
30+
31+
switch (verb) {
32+
case "tools":
33+
return { label: "List tools", input: undefined };
34+
case "search":
35+
return {
36+
label: "Search tools",
37+
input: explicitInput ?? (rest || undefined),
38+
};
39+
case "info":
40+
return { label: rest ? `Read ${rest}` : "Read tool", input: undefined };
41+
case "schema": {
42+
const schema = rest.match(POSTHOG_TOOL_NAME_RE);
43+
if (!schema) return { label: "Inspect schema", input: undefined };
44+
const path = explicitInput ?? ((schema[2] ?? "").trim() || undefined);
45+
return {
46+
label: path
47+
? `Inspect ${schema[1]}.${path}`
48+
: `Inspect ${schema[1]} fields`,
49+
input: undefined,
50+
};
51+
}
52+
case "call": {
53+
const call = rest.match(POSTHOG_CALL_BODY_RE);
54+
if (!call) return null;
55+
return {
56+
label: call[1],
57+
input: explicitInput ?? ((call[2] ?? "").trim() || undefined),
58+
};
59+
}
60+
}
61+
}
62+
63+
function readExplicitInput(value: unknown): string | undefined {
64+
if (value === undefined || value === null) return undefined;
65+
if (typeof value === "string") return value.trim() || undefined;
66+
try {
67+
return JSON.stringify(value);
68+
} catch {
69+
return undefined;
70+
}
71+
}
72+
73+
export function formatPosthogExecBody(
74+
input: string | undefined,
75+
): string | undefined {
76+
if (!input) return undefined;
77+
try {
78+
const parsed = JSON.parse(input);
79+
if (parsed && typeof parsed === "object")
80+
return JSON.stringify(parsed, null, 2);
81+
} catch {
82+
return input;
83+
}
84+
return input;
85+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { describe, expect, it } from "vitest";
2+
import {
3+
pickNextThinkingActivity,
4+
pickThinkingActivity,
5+
THINKING_ACTIVITIES,
6+
} from "./thinkingActivities";
7+
8+
describe("thinking activities", () => {
9+
it("selects from a bounded random value", () => {
10+
expect(pickThinkingActivity(0)).toBe("Booping");
11+
expect(pickThinkingActivity(1)).toBe(THINKING_ACTIVITIES.at(-1));
12+
});
13+
14+
it("always advances when the random pick matches the current activity", () => {
15+
expect(pickNextThinkingActivity("Booping", 0)).toBe("Crunching");
16+
});
17+
});

apps/mobile/src/features/chat/utils/thinkingMessages.ts renamed to packages/core/src/sessions/thinkingActivities.ts

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
1-
// Random thinking messages displayed while AI is generating
2-
// Based on posthog/frontend/src/scenes/max/utils/thinkingMessages.ts
3-
4-
export const THINKING_MESSAGES = [
1+
export const THINKING_ACTIVITIES = [
52
"Booping",
63
"Crunching",
74
"Digging",
@@ -86,13 +83,21 @@ export const THINKING_MESSAGES = [
8683
"Puttering",
8784
"Whiffling",
8885
"Thinking",
89-
];
86+
] as const;
9087

91-
export function getRandomThinkingActivity(): string {
92-
const randomIndex = Math.floor(Math.random() * THINKING_MESSAGES.length);
93-
return THINKING_MESSAGES[randomIndex];
88+
export function pickThinkingActivity(randomValue: number): string {
89+
const bounded = Math.max(0, Math.min(randomValue, 0.999999999999));
90+
return THINKING_ACTIVITIES[Math.floor(bounded * THINKING_ACTIVITIES.length)];
9491
}
9592

96-
export function getRandomThinkingMessage(): string {
97-
return `${getRandomThinkingActivity()}...`;
93+
export function pickNextThinkingActivity(
94+
current: string,
95+
randomValue: number,
96+
): string {
97+
const picked = pickThinkingActivity(randomValue);
98+
if (picked !== current || THINKING_ACTIVITIES.length <= 1) return picked;
99+
const index = THINKING_ACTIVITIES.indexOf(
100+
current as (typeof THINKING_ACTIVITIES)[number],
101+
);
102+
return THINKING_ACTIVITIES[(index + 1) % THINKING_ACTIVITIES.length];
98103
}

0 commit comments

Comments
 (0)