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 @@ -4,6 +4,7 @@ import {
type EditorContent,
extractFilePaths,
xmlToContent,
xmlToPlainText,
} from "./content";

describe("xmlToContent", () => {
Expand Down Expand Up @@ -196,6 +197,49 @@ describe("xmlToContent", () => {
expect(extractFilePaths(content)).toEqual(["src/sub", "src/a.ts"]);
});

it("xmlToPlainText renders folder mentions as @mentions", () => {
expect(
xmlToPlainText('look at <folder path="products/agentic_tests" /> please'),
).toBe("look at @products/agentic_tests please");
});

it("xmlToPlainText renders file mentions as @mentions", () => {
expect(
xmlToPlainText('see <file path="src/foo/bar.ts" /> for details'),
).toBe("see @foo/bar.ts for details");
});

it("xmlToPlainText renders structured chip types as @label", () => {
expect(
xmlToPlainText(
'investigate <error id="err-1" /> and <feature_flag id="flag-2" />',
),
).toBe("investigate @err-1 and @flag-2");
});

it("xmlToPlainText leaves plain text untouched", () => {
expect(xmlToPlainText("ship the fix")).toBe("ship the fix");
});

it("xmlToPlainText renders github_pr and github_issue mentions", () => {
expect(
xmlToPlainText(
'<github_pr number="42" title="Add login" url="https://github.com/x/y/pull/42" />',
),
).toBe("@#42 - Add login");
expect(
xmlToPlainText(
'<github_issue number="7" title="" url="https://github.com/x/y/issues/7" />',
),
).toBe("@#7");
});

it("xmlToPlainText passes through non-chip XML-like text", () => {
expect(xmlToPlainText("use Array<string> and <div> tags")).toBe(
"use Array<string> and <div> tags",
);
});
Comment thread
charlesvien marked this conversation as resolved.

it("round-trips contentToXml for a mix of text and chips", () => {
const content: EditorContent = {
segments: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,10 @@ export function xmlToContent(xml: string): EditorContent {
return { segments };
}

export function xmlToPlainText(xml: string): string {
return contentToPlainText(xmlToContent(xml));
}

export function isContentEmpty(
content: EditorContent | null | string,
): boolean {
Expand Down
74 changes: 67 additions & 7 deletions apps/code/src/renderer/sagas/task/task-creation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,6 @@ vi.mock("@features/sessions/service/service", () => ({
}),
}));

vi.mock("@renderer/utils/generateTitle", () => ({
createFileTagRegex: () => /<file\s+path="([^"]+)"\s*\/>/g,
}));

vi.mock("@utils/logger", () => ({
logger: {
scope: () => ({
Expand Down Expand Up @@ -409,7 +405,7 @@ describe("TaskCreationSaga", () => {
);
});

it("sets fallback title when description is attachment-only", async () => {
it("renders attachment-only description as @mention title", async () => {
const createdTask = createTask();
const startedTask = createTask({ latest_run: createRun() });
const createTaskMock = vi.fn().mockResolvedValue(createdTask);
Expand All @@ -430,20 +426,84 @@ describe("TaskCreationSaga", () => {

await saga.run({
taskDescription: '<file path="/tmp/code.ts" />',
content: '<file path="/tmp/code.ts" />',
repository: "posthog/posthog",
workspaceMode: "cloud",
branch: "main",
});

expect(createTaskMock).toHaveBeenCalledWith(
expect.objectContaining({
title: "Reading attachment\u2026",
title: "@tmp/code.ts",
description: '<file path="/tmp/code.ts" />',
}),
);
});

it("falls back to Untitled when description is empty", async () => {
const createdTask = createTask();
const startedTask = createTask({ latest_run: createRun() });
const createTaskMock = vi.fn().mockResolvedValue(createdTask);
const createTaskRunMock = vi.fn().mockResolvedValue(createRun());
const startTaskRunMock = vi.fn().mockResolvedValue(startedTask);

const saga = new TaskCreationSaga({
posthogClient: {
createTask: createTaskMock,
deleteTask: vi.fn(),
getTask: vi.fn(),
createTaskRun: createTaskRunMock,
startTaskRun: startTaskRunMock,
sendRunCommand: vi.fn(),
updateTask: vi.fn(),
} as never,
});

await saga.run({
content: " ",
repository: "posthog/posthog",
workspaceMode: "cloud",
branch: "main",
});

expect(createTaskMock).toHaveBeenCalledWith(
expect.objectContaining({ title: "Untitled" }),
);
});

it("renders folder mentions as readable @mention in title", async () => {
const createdTask = createTask();
const startedTask = createTask({ latest_run: createRun() });
const createTaskMock = vi.fn().mockResolvedValue(createdTask);
const createTaskRunMock = vi.fn().mockResolvedValue(createRun());
const startTaskRunMock = vi.fn().mockResolvedValue(startedTask);

const saga = new TaskCreationSaga({
posthogClient: {
createTask: createTaskMock,
deleteTask: vi.fn(),
getTask: vi.fn(),
createTaskRun: createTaskRunMock,
startTaskRun: startTaskRunMock,
sendRunCommand: vi.fn(),
updateTask: vi.fn(),
} as never,
});

await saga.run({
content:
'look at <folder path="products/agentic_tests" /> and tell me what you see',
repository: "posthog/posthog",
workspaceMode: "cloud",
branch: "main",
});

expect(createTaskMock).toHaveBeenCalledWith(
expect.objectContaining({
title: "look at @products/agentic_tests and tell me what you see",
}),
);
});

it("truncates title to 255 chars", async () => {
const longText = "x".repeat(300);
const createdTask = createTask();
Expand Down
6 changes: 3 additions & 3 deletions apps/code/src/renderer/sagas/task/task-creation.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { buildPromptBlocks } from "@features/editor/utils/prompt-builder";
import { xmlToPlainText } from "@features/message-editor/utils/content";
import { DEFAULT_PANEL_IDS } from "@features/panels/constants/panelConstants";
import { usePanelLayoutStore } from "@features/panels/store/panelLayoutStore";
import { useProvisioningStore } from "@features/provisioning/stores/provisioningStore";
Expand All @@ -18,7 +19,6 @@ import type {
import { Saga, type SagaLogger } from "@posthog/shared";
import type { PostHogAPIClient } from "@renderer/api/posthogClient";
import { trpcClient } from "@renderer/trpc";
import { createFileTagRegex } from "@renderer/utils/generateTitle";
import { getTaskRepository } from "@renderer/utils/repository";
import {
type ExecutionMode,
Expand Down Expand Up @@ -401,9 +401,9 @@ export class TaskCreationSaga extends Saga<
name: "task_creation",
execute: async () => {
const description = input.taskDescription ?? input.content ?? "";
const plainText = description.replace(createFileTagRegex(), "").trim();
const plainText = xmlToPlainText(description).trim();
const result = await this.deps.posthogClient.createTask({
title: (plainText || "Reading attachment\u2026").slice(0, 255),
title: (plainText || "Untitled").slice(0, 255),
description,
repository: repository ?? undefined,
github_integration:
Expand Down
26 changes: 26 additions & 0 deletions apps/code/src/renderer/utils/generateTitle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,32 @@ describe("enrichDescriptionWithFileContent", () => {
]);
expect(result).toBe("[Attached: image.jpg]\n\ntext content");
});

it("returns description unchanged for folder-only input", async () => {
const description = '<folder path="src/components" />';
const result = await enrichDescriptionWithFileContent(description);
expect(result).toBe(description);
expect(mockReadAbsoluteFile).not.toHaveBeenCalled();
});

it("reads file and drops folder for mixed file+folder input", async () => {
mockReadAbsoluteFile.mockResolvedValue("file body");
const description =
'<file path="/tmp/a.ts" /><folder path="src/components" />';
const result = await enrichDescriptionWithFileContent(description);
expect(result).toBe("file body");
expect(mockReadAbsoluteFile).toHaveBeenCalledTimes(1);
expect(mockReadAbsoluteFile).toHaveBeenCalledWith({
filePath: "/tmp/a.ts",
});
});

it("treats non-chip XML-like text as real content", async () => {
const description = "<div>hello world</div>";
const result = await enrichDescriptionWithFileContent(description);
expect(result).toBe(description);
expect(mockReadAbsoluteFile).not.toHaveBeenCalled();
});
});

describe("generateTitleAndSummary", () => {
Expand Down
26 changes: 14 additions & 12 deletions apps/code/src/renderer/utils/generateTitle.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { fetchAuthState } from "@features/auth/hooks/authQueries";
import { xmlToContent } from "@features/message-editor/utils/content";
import { trpcClient } from "@renderer/trpc";
import { logger } from "@utils/logger";

const log = logger.scope("title-generator");

export const createFileTagRegex = () => /<file\s+path="([^"]+)"\s*\/>/g;
const ATTACHED_FILES_REGEX = /^\[?Attached files:.*]?$/gm;
const PASTED_TEXT_SNIPPET_LIMIT = 500;

Expand Down Expand Up @@ -55,18 +55,20 @@ export async function enrichDescriptionWithFileContent(
description: string,
filePaths: string[] = [],
): Promise<string> {
const stripped = description
.replace(createFileTagRegex(), "")
const parsed = xmlToContent(description);
const stripped = parsed.segments
.flatMap((seg) => (seg.type === "text" ? [seg.text] : []))
.join("")
.replace(ATTACHED_FILES_REGEX, "")
.replace(/^\d+\.\s*$/gm, "")
.trim();

if (stripped.length > 0) return description;

const paths =
filePaths.length > 0
? filePaths
: [...description.matchAll(createFileTagRegex())].map((m) => m[1]);
const chipFilePaths = parsed.segments.flatMap((seg) =>
seg.type === "chip" && seg.chip.type === "file" ? [seg.chip.id] : [],
);
const paths = filePaths.length > 0 ? filePaths : chipFilePaths;

if (paths.length === 0) return description;

Expand All @@ -76,13 +78,13 @@ export async function enrichDescriptionWithFileContent(
return `[Attached: ${getFileName(filePath)}]`;
}
try {
const content = await trpcClient.fs.readAbsoluteFile.query({
const fileContent = await trpcClient.fs.readAbsoluteFile.query({
filePath,
});
if (content) {
return content.length > PASTED_TEXT_SNIPPET_LIMIT
? content.slice(0, PASTED_TEXT_SNIPPET_LIMIT)
: content;
if (fileContent) {
return fileContent.length > PASTED_TEXT_SNIPPET_LIMIT
? fileContent.slice(0, PASTED_TEXT_SNIPPET_LIMIT)
: fileContent;
}
return `[Attached: ${getFileName(filePath)}]`;
} catch {
Expand Down
Loading