tags",
+ );
+ });
+
it("round-trips contentToXml for a mix of text and chips", () => {
const content: EditorContent = {
segments: [
diff --git a/apps/code/src/renderer/features/message-editor/utils/content.ts b/apps/code/src/renderer/features/message-editor/utils/content.ts
index 5207e5547..1b65fc612 100644
--- a/apps/code/src/renderer/features/message-editor/utils/content.ts
+++ b/apps/code/src/renderer/features/message-editor/utils/content.ts
@@ -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 {
diff --git a/apps/code/src/renderer/sagas/task/task-creation.test.ts b/apps/code/src/renderer/sagas/task/task-creation.test.ts
index faa63d59d..79961b7a1 100644
--- a/apps/code/src/renderer/sagas/task/task-creation.test.ts
+++ b/apps/code/src/renderer/sagas/task/task-creation.test.ts
@@ -51,10 +51,6 @@ vi.mock("@features/sessions/service/service", () => ({
}),
}));
-vi.mock("@renderer/utils/generateTitle", () => ({
- createFileTagRegex: () => /
/g,
-}));
-
vi.mock("@utils/logger", () => ({
logger: {
scope: () => ({
@@ -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);
@@ -430,7 +426,6 @@ describe("TaskCreationSaga", () => {
await saga.run({
taskDescription: '
',
- content: '
',
repository: "posthog/posthog",
workspaceMode: "cloud",
branch: "main",
@@ -438,12 +433,77 @@ describe("TaskCreationSaga", () => {
expect(createTaskMock).toHaveBeenCalledWith(
expect.objectContaining({
- title: "Reading attachment\u2026",
+ title: "@tmp/code.ts",
description: '
',
}),
);
});
+ 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
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();
diff --git a/apps/code/src/renderer/sagas/task/task-creation.ts b/apps/code/src/renderer/sagas/task/task-creation.ts
index 789b91ea9..582c0b94a 100644
--- a/apps/code/src/renderer/sagas/task/task-creation.ts
+++ b/apps/code/src/renderer/sagas/task/task-creation.ts
@@ -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";
@@ -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,
@@ -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:
diff --git a/apps/code/src/renderer/utils/generateTitle.test.ts b/apps/code/src/renderer/utils/generateTitle.test.ts
index 6ecd85605..6f0582226 100644
--- a/apps/code/src/renderer/utils/generateTitle.test.ts
+++ b/apps/code/src/renderer/utils/generateTitle.test.ts
@@ -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 = '
';
+ 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 =
+ '
';
+ 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 = "
hello world
";
+ const result = await enrichDescriptionWithFileContent(description);
+ expect(result).toBe(description);
+ expect(mockReadAbsoluteFile).not.toHaveBeenCalled();
+ });
});
describe("generateTitleAndSummary", () => {
diff --git a/apps/code/src/renderer/utils/generateTitle.ts b/apps/code/src/renderer/utils/generateTitle.ts
index 3a3b3911a..c2f3b0fa9 100644
--- a/apps/code/src/renderer/utils/generateTitle.ts
+++ b/apps/code/src/renderer/utils/generateTitle.ts
@@ -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 = () => /
/g;
const ATTACHED_FILES_REGEX = /^\[?Attached files:.*]?$/gm;
const PASTED_TEXT_SNIPPET_LIMIT = 500;
@@ -55,18 +55,20 @@ export async function enrichDescriptionWithFileContent(
description: string,
filePaths: string[] = [],
): Promise
{
- 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;
@@ -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 {