From 3ced06331515c9a648393de37ab5b83d2305dd78 Mon Sep 17 00:00:00 2001 From: Charles Vien Date: Wed, 20 May 2026 15:20:41 -0700 Subject: [PATCH 1/2] Render at-mentioned chips as @labels in task titles --- .../message-editor/utils/content.test.ts | 25 +++++++++++ .../features/message-editor/utils/content.ts | 4 ++ .../renderer/sagas/task/task-creation.test.ts | 42 ++++++++++++++++--- .../src/renderer/sagas/task/task-creation.ts | 4 +- 4 files changed, 67 insertions(+), 8 deletions(-) diff --git a/apps/code/src/renderer/features/message-editor/utils/content.test.ts b/apps/code/src/renderer/features/message-editor/utils/content.test.ts index 825dd20efd..8db8a9a480 100644 --- a/apps/code/src/renderer/features/message-editor/utils/content.test.ts +++ b/apps/code/src/renderer/features/message-editor/utils/content.test.ts @@ -4,6 +4,7 @@ import { type EditorContent, extractFilePaths, xmlToContent, + xmlToPlainText, } from "./content"; describe("xmlToContent", () => { @@ -196,6 +197,30 @@ describe("xmlToContent", () => { expect(extractFilePaths(content)).toEqual(["src/sub", "src/a.ts"]); }); + it("xmlToPlainText renders folder mentions as @mentions", () => { + expect( + xmlToPlainText('look at please'), + ).toBe("look at @products/agentic_tests please"); + }); + + it("xmlToPlainText renders file mentions as @mentions", () => { + expect( + xmlToPlainText('see for details'), + ).toBe("see @foo/bar.ts for details"); + }); + + it("xmlToPlainText renders structured chip types as @label", () => { + expect( + xmlToPlainText( + 'investigate and ', + ), + ).toBe("investigate @err-1 and @flag-2"); + }); + + it("xmlToPlainText leaves plain text untouched", () => { + expect(xmlToPlainText("ship the fix")).toBe("ship the fix"); + }); + 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 5207e55473..5b0e195c57 100644 --- a/apps/code/src/renderer/features/message-editor/utils/content.ts +++ b/apps/code/src/renderer/features/message-editor/utils/content.ts @@ -142,6 +142,10 @@ function chipFromTag(tag: string, rawAttrs: string): MentionChip | null { } } +export function xmlToPlainText(xml: string): string { + return contentToPlainText(xmlToContent(xml)); +} + export function xmlToContent(xml: string): EditorContent { const segments: EditorContent["segments"] = []; let lastIndex = 0; 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 faa63d59df..826b9594dc 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); @@ -438,12 +434,46 @@ describe("TaskCreationSaga", () => { expect(createTaskMock).toHaveBeenCalledWith( expect.objectContaining({ - title: "Reading attachment\u2026", + title: "@tmp/code.ts", description: '', }), ); }); + 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 789b91ea91..9a3818e3ed 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,7 +401,7 @@ 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), description, From 4dbc9e104d2ceb3d91336ffd1294e183b2b0ae4b Mon Sep 17 00:00:00 2001 From: Charles Vien Date: Wed, 20 May 2026 15:46:42 -0700 Subject: [PATCH 2/2] ok --- .../sessions/components/ConversationView.tsx | 65 ++++++++++--------- .../session-update/AgentMessage.tsx | 6 +- .../session-update/FileMentionChip.tsx | 4 +- .../sessions/hooks/useSessionTaskId.tsx | 21 ++++++ 4 files changed, 60 insertions(+), 36 deletions(-) create mode 100644 apps/code/src/renderer/features/sessions/hooks/useSessionTaskId.tsx diff --git a/apps/code/src/renderer/features/sessions/components/ConversationView.tsx b/apps/code/src/renderer/features/sessions/components/ConversationView.tsx index 6d0773c56d..4afb50fd67 100644 --- a/apps/code/src/renderer/features/sessions/components/ConversationView.tsx +++ b/apps/code/src/renderer/features/sessions/components/ConversationView.tsx @@ -1,6 +1,7 @@ import { CHAT_CONTENT_MAX_WIDTH } from "@features/sessions/constants"; import { useContextUsage } from "@features/sessions/hooks/useContextUsage"; import { useConversationSearch } from "@features/sessions/hooks/useConversationSearch"; +import { SessionTaskIdProvider } from "@features/sessions/hooks/useSessionTaskId"; import { sessionStoreSetters, useOptimisticItemsForTask, @@ -267,37 +268,39 @@ export function ConversationView({ /> )} - - 0} - pausedDurationMs={pausedDurationMs} - isCompacting={isCompacting} - usage={contextUsage} - /> - - } - /> + + + 0} + pausedDurationMs={pausedDurationMs} + isCompacting={isCompacting} + usage={contextUsage} + /> + + } + /> + {showScrollButton && (