Skip to content
Open
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
9 changes: 9 additions & 0 deletions apps/code/src/main/services/inbox-link/service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,15 @@ describe("InboxLinkService", () => {
expect(listener).toHaveBeenCalledWith({ reportId: "abc-123" });
});

it("ignores a trailing slug segment after the report id", () => {
const listener = vi.fn();
service.on(InboxLinkEvent.OpenReport, listener);

deepLinkService.trigger("inbox", "abc-123/fix-inbox--Add-foo");

expect(listener).toHaveBeenCalledWith({ reportId: "abc-123" });
});

it("returns false and does not emit when the path is empty", () => {
const listener = vi.fn();
service.on(InboxLinkEvent.OpenReport, listener);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import {
} from "@radix-ui/themes";
import { useTRPC } from "@renderer/trpc";
import { EXTERNAL_LINKS } from "@renderer/utils/links";
import { getDeeplinkProtocol } from "@shared/deeplink";
import { buildInboxDeeplink } from "@shared/deeplink";
import type {
ActionabilityJudgmentArtefact,
ActionabilityJudgmentContent,
Expand Down Expand Up @@ -480,7 +480,9 @@ export function ReportDetailPane({
onClick={async () => {
try {
await navigator.clipboard.writeText(
`${getDeeplinkProtocol(import.meta.env.DEV)}://inbox/${report.id}`,
buildInboxDeeplink(report.id, report.title, {
isDevBuild: import.meta.env.DEV,
}),
);
fireDetailAction("copy_link");
toast.success("Link copied");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ export function useDiscussReport({

const prompt = buildDiscussReportPrompt({
reportId,
reportTitle,
question,
isDevBuild: import.meta.env.DEV,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,22 @@ describe("buildDiscussReportPrompt", () => {
});
expect(prompt).toContain("brief readout");
});

it("appends a slugified title suffix to the deep link", () => {
const prompt = buildDiscussReportPrompt({
reportId: "abc123",
reportTitle: "fix(inbox): Add foo",
isDevBuild: false,
});
expect(prompt).toContain("posthog-code://inbox/abc123/fix-inbox--Add-foo");
});

it("omits the slug suffix when the title is blank", () => {
const prompt = buildDiscussReportPrompt({
reportId: "abc123",
reportTitle: " ",
isDevBuild: false,
});
expect(prompt).toContain("posthog-code://inbox/abc123)");
});
});
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import { getDeeplinkProtocol } from "@shared/deeplink";
import { buildInboxDeeplink } from "@shared/deeplink";

interface BuildDiscussReportPromptOptions {
reportId: string;
reportTitle?: string | null;
question?: string;
isDevBuild: boolean;
}

export function buildDiscussReportPrompt({
reportId,
reportTitle,
question,
isDevBuild,
}: BuildDiscussReportPromptOptions): string {
const trimmedQuestion = question?.trim();
const reportLink = `${getDeeplinkProtocol(isDevBuild)}://inbox/${reportId}`;
const reportLink = buildInboxDeeplink(reportId, reportTitle, { isDevBuild });
const intro = `Discuss PostHog inbox report ${reportId} ([inbox item](${reportLink})). Use the inbox MCP tools to fetch the report,`;
return trimmedQuestion
? `${intro} then answer this first: ${trimmedQuestion}`
Expand Down
71 changes: 71 additions & 0 deletions apps/code/src/shared/deeplink.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { describe, expect, it } from "vitest";
import { buildInboxDeeplink } from "./deeplink";

describe("buildInboxDeeplink", () => {
it("returns just the UUID when no title is given", () => {
expect(buildInboxDeeplink("abc-123", null, { isDevBuild: false })).toBe(
"posthog-code://inbox/abc-123",
);
expect(
buildInboxDeeplink("abc-123", undefined, { isDevBuild: false }),
).toBe("posthog-code://inbox/abc-123");
expect(buildInboxDeeplink("abc-123", "", { isDevBuild: false })).toBe(
"posthog-code://inbox/abc-123",
);
});

it("emits `--` for runs that mix a colon with other unsafe chars", () => {
expect(
buildInboxDeeplink("abc-123", "fix(inbox): Add foo", {
isDevBuild: false,
}),
).toBe("posthog-code://inbox/abc-123/fix-inbox--Add-foo");
});

it("emits a single `-` for a colon-only run", () => {
expect(
buildInboxDeeplink("abc-123", "feat:bar", { isDevBuild: false }),
).toBe("posthog-code://inbox/abc-123/feat-bar");
});

it("omits the slug when the title slugifies to empty", () => {
expect(buildInboxDeeplink("abc-123", ":::", { isDevBuild: false })).toBe(
"posthog-code://inbox/abc-123",
);
expect(buildInboxDeeplink("abc-123", " ", { isDevBuild: false })).toBe(
"posthog-code://inbox/abc-123",
);
});

it("uses the dev scheme when isDevBuild is true", () => {
expect(
buildInboxDeeplink("abc-123", "Hello World", { isDevBuild: true }),
).toBe("posthog-code-dev://inbox/abc-123/Hello-World");
});

it("preserves URL-unreserved punctuation (- _ . ~)", () => {
expect(
buildInboxDeeplink("abc-123", "v1.2.3_final~ish", { isDevBuild: false }),
).toBe("posthog-code://inbox/abc-123/v1.2.3_final~ish");
});

it("collapses runs of unsafe punctuation into a single hyphen", () => {
expect(
buildInboxDeeplink("abc-123", "Cost $5, 50% off!", { isDevBuild: false }),
).toBe("posthog-code://inbox/abc-123/Cost-5-50-off");
});

it("folds accented Latin letters to their ASCII base", () => {
expect(
buildInboxDeeplink("abc-123", "café résumé naïve", { isDevBuild: false }),
).toBe("posthog-code://inbox/abc-123/cafe-resume-naive");
});

it("hyphenizes non-Latin scripts that have no ASCII fold", () => {
expect(
buildInboxDeeplink("abc-123", "Hello Привет world", {
isDevBuild: false,
}),
).toBe("posthog-code://inbox/abc-123/Hello-world");
});
});
Comment thread
Twixes marked this conversation as resolved.
35 changes: 35 additions & 0 deletions apps/code/src/shared/deeplink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,38 @@ export function isPostHogCodeDeeplink(
return false;
}
}

/**
* Build the deep link URL for an inbox report. The optional title is slugified
* and appended as a trailing path segment for human-readable sharing; the
* receiver only reads the UUID, so the slug is purely cosmetic.
*
* Slug rules:
* - Accented Latin letters are folded to their ASCII base (`café` → `cafe`)
* via NFD decomposition + combining-mark stripping.
* - Letters, digits, and the URL-unreserved punctuation `_ . ~` are kept
* verbatim (case preserved).
* - Any run of other characters collapses to a single `-`, except runs that
* mix a colon with other unsafe chars collapse to `--`. This preserves the
* title-like break in `fix(inbox): Add foo` → `fix-inbox--Add-foo` while
* keeping standalone colons compact (`feat:bar` → `feat-bar`) and unrelated
* runs single (`Cost $5, 50% off` → `Cost-5-50-off`).
* - Leading and trailing hyphens are stripped.
*/
export function buildInboxDeeplink(
reportId: string,
title: string | null | undefined,
{ isDevBuild }: { isDevBuild: boolean },
): string {
const base = `${getDeeplinkProtocol(isDevBuild)}://inbox/${reportId}`;
const slug = title
? title
.normalize("NFD")
.replace(/\p{M}/gu, "")
.replace(/[^a-zA-Z0-9_.~]+/g, (run) =>
run.includes(":") && /[^:]/.test(run) ? "--" : "-",
)
.replace(/^-+|-+$/g, "")
: "";
return slug ? `${base}/${slug}` : base;
}
Loading