Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -128,4 +128,37 @@ describe("ActivityTimeline", () => {
expect(screen.getByText("Saved workspace context")).toBeVisible();
expect(useThreadNavigationStore.getState().scrollRequests).toEqual({});
});

it("hides injected custom instructions from conversation previews", () => {
renderTimeline(true, [
{
type: "user_message",
id: "custom-instructions-message",
content:
"Review this PR\n\n<user_custom_instructions>\nThe user has saved custom instructions that apply to all of their tasks. Follow them.\n\nNever update an existing PR description.\n</user_custom_instructions>",
timestamp: Date.parse("2026-07-17T09:05:00Z"),
},
]);

expect(screen.getByText("Review this PR")).toBeInTheDocument();
expect(screen.queryByText(/user_custom_instructions/)).toBeNull();
expect(
screen.queryByText("Never update an existing PR description."),
).toBeNull();
});

it("shows user-authored custom-instruction tag examples", () => {
renderTimeline(true, [
{
type: "user_message",
id: "literal-custom-instructions-message",
content:
"Render this example: <user_custom_instructions>be terse</user_custom_instructions>",
timestamp: Date.parse("2026-07-17T09:05:00Z"),
},
]);

expect(screen.getByText(/user_custom_instructions/)).toBeInTheDocument();
expect(screen.getByText(/be terse/)).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { ThreadTimestamp } from "@posthog/ui/features/canvas/components/ThreadTi
import { userDisplayName } from "@posthog/ui/features/canvas/utils/userDisplay";
import type { buildConversationItems } from "@posthog/ui/features/sessions/components/buildConversationItems";
import { extractChannelContext } from "@posthog/ui/features/sessions/components/session-update/channelContext";
import { extractCustomInstructions } from "@posthog/ui/features/sessions/components/session-update/customInstructions";
import { useThreadNavigationStore } from "@posthog/ui/features/sessions/threadNavigationStore";
import { Fragment, type KeyboardEvent, type ReactNode, useMemo } from "react";

Expand Down Expand Up @@ -83,7 +84,12 @@ function UserMessageRow({
() => extractChannelContext(content),
[content],
);
const displayContent = channelContext?.stripped ?? content;
const afterChannelContext = channelContext?.stripped ?? content;
const customInstructions = useMemo(
() => extractCustomInstructions(afterChannelContext),
[afterChannelContext],
Comment thread
tatoalo marked this conversation as resolved.
);
const displayContent = customInstructions?.stripped ?? afterChannelContext;
// The row itself is the hit target. `ThreadItem` renders an <article>, which a
// <button> may not wrap and which can't become one (quill's primitive takes no
// `render`), so it carries the button role and its own key handling.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ describe("resolvePromptRecall", () => {
{
name: "a custom instructions block",
content:
"fix the bug\n\n<user_custom_instructions>be terse</user_custom_instructions>",
"fix the bug\n\n<user_custom_instructions>\nThe user has saved custom instructions that apply to all of their tasks. Follow them.\n\nbe terse\n</user_custom_instructions>",
},
{
name: "a trailing attachment summary",
Expand All @@ -195,7 +195,7 @@ describe("resolvePromptRecall", () => {
{
name: "several injected blocks at once",
content:
'<channel_context channel="growth">CONTEXT.md body</channel_context>\n\nfix the bug\n\n<user_custom_instructions>be terse</user_custom_instructions>',
'<channel_context channel="growth">CONTEXT.md body</channel_context>\n\nfix the bug\n\n<user_custom_instructions>\nThe user has saved custom instructions that apply to all of their tasks. Follow them.\n\nbe terse\n</user_custom_instructions>',
},
])("strips $name from the recalled text", ({ content }) => {
expect(resolvePromptRecall([{ id: "m1", content }], null, -1)).toEqual({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,17 @@ describe("extractCustomInstructions", () => {

it("strips the element even when it is the only content", () => {
const result = extractCustomInstructions(
"<user_custom_instructions>\nbody\n</user_custom_instructions>",
"<user_custom_instructions>\nThe user has saved custom instructions that apply to all of their tasks. Follow them.\n\nbody\n</user_custom_instructions>",
);
expect(result?.body).toBe("body");
expect(result?.body).toContain("body");
expect(result?.stripped).toBe("");
});

it("preserves user-authored custom-instruction tags", () => {
const content =
"Render this example: <user_custom_instructions>be terse</user_custom_instructions>";

expect(extractCustomInstructions(content)).toBeNull();
expect(hasCustomInstructions(content)).toBe(false);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@
//
// The body shown is exactly what was sent in the prompt — parsed from the stored
// event, never re-read from the (possibly newer) live setting.
// The tag alone is not enough to identify injected metadata: users may include
// the same XML in examples they want displayed verbatim. Match the fixed
// preamble emitted by buildCustomInstructionsText so only system-generated
// blocks are hidden.
const CUSTOM_INSTRUCTIONS_REGEX =
/<user_custom_instructions\b[^>]*>([\s\S]*?)<\/user_custom_instructions>/;
/<user_custom_instructions\b[^>]*>(\r?\nThe user has saved custom instructions that apply to all of their tasks\. Follow them\.\r?\n\r?\n[\s\S]*?)<\/user_custom_instructions>/;

export function hasCustomInstructions(content: string): boolean {
return CUSTOM_INSTRUCTIONS_REGEX.test(content);
Expand Down
Loading