From 1d7307f4c6d19e834053ae653e9535030c46c092 Mon Sep 17 00:00:00 2001 From: neriousy Date: Fri, 13 Mar 2026 18:11:10 +0100 Subject: [PATCH] refactor: shorten file mentions in thread titles --- apps/web/src/components/ChatView.tsx | 6 ++-- apps/web/src/threadTitle.test.ts | 53 ++++++++++++++++++++++++++++ apps/web/src/threadTitle.ts | 17 +++++++++ apps/web/src/truncateTitle.test.ts | 17 --------- apps/web/src/truncateTitle.ts | 7 ---- 5 files changed, 73 insertions(+), 27 deletions(-) create mode 100644 apps/web/src/threadTitle.test.ts create mode 100644 apps/web/src/threadTitle.ts delete mode 100644 apps/web/src/truncateTitle.test.ts delete mode 100644 apps/web/src/truncateTitle.ts diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 9f625762c..56398ef52 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -73,7 +73,7 @@ import { proposedPlanTitle, resolvePlanFollowUpSubmission, } from "../proposedPlan"; -import { truncateTitle } from "../truncateTitle"; +import { buildThreadTitle } from "../threadTitle"; import { DEFAULT_INTERACTION_MODE, DEFAULT_RUNTIME_MODE, @@ -2341,7 +2341,7 @@ export default function ChatView({ threadId }: ChatViewProps) { titleSeed = "New thread"; } } - const title = truncateTitle(titleSeed); + const title = buildThreadTitle(titleSeed); let threadCreateModel: ModelSlug = selectedModel || (activeProject.model as ModelSlug) || DEFAULT_MODEL_BY_PROVIDER.codex; @@ -2768,7 +2768,7 @@ export default function ChatView({ threadId }: ChatViewProps) { const nextThreadId = newThreadId(); const planMarkdown = activeProposedPlan.planMarkdown; const implementationPrompt = buildPlanImplementationPrompt(planMarkdown); - const nextThreadTitle = truncateTitle(buildPlanImplementationThreadTitle(planMarkdown)); + const nextThreadTitle = buildThreadTitle(buildPlanImplementationThreadTitle(planMarkdown)); const nextThreadModel: ModelSlug = selectedModel || (activeThread.model as ModelSlug) || diff --git a/apps/web/src/threadTitle.test.ts b/apps/web/src/threadTitle.test.ts new file mode 100644 index 000000000..5b31c6f1c --- /dev/null +++ b/apps/web/src/threadTitle.test.ts @@ -0,0 +1,53 @@ +import { describe, expect, it } from "vitest"; + +import { buildThreadTitle } from "./threadTitle"; + +describe("buildThreadTitle", () => { + it("trims surrounding whitespace", () => { + expect(buildThreadTitle(" hello world ")).toBe("hello world"); + }); + + it("returns trimmed text when within max length", () => { + expect(buildThreadTitle("alpha", 10)).toBe("alpha"); + }); + + it("appends ellipsis when text exceeds max length", () => { + expect(buildThreadTitle("abcdefghij", 5)).toBe("abcde..."); + }); + + it("shortens a single file mention to its basename while keeping the marker", () => { + expect(buildThreadTitle("Inspect @apps/web/src/components/ChatView.tsx please")).toBe( + "Inspect @ChatView.tsx please", + ); + }); + + it("shortens multiple file mentions independently", () => { + expect(buildThreadTitle("Compare @apps/web/src/a.ts with @packages/shared/src/b.ts now")).toBe( + "Compare @a.ts with @b.ts now", + ); + }); + + it("shortens mentions before truncating the title", () => { + const title = buildThreadTitle( + "@apps/web/src/components/ChatView.tsx investigate header layout", + 20, + ); + + expect(title).toBe("@ChatView.tsx invest..."); + expect(title).not.toContain("apps/web/src/components"); + }); + + it("leaves incomplete trailing mentions unchanged", () => { + expect(buildThreadTitle("Inspect @apps/web/src/components/ChatView.tsx")).toBe( + "Inspect @apps/web/src/components/ChatView.tsx", + ); + }); + + it("preserves punctuation and whitespace around mention segments", () => { + expect( + buildThreadTitle( + "Review @apps/web/src/components/ChatView.tsx ; then ping @README.md please", + ), + ).toBe("Review @ChatView.tsx ; then ping @README.md please"); + }); +}); diff --git a/apps/web/src/threadTitle.ts b/apps/web/src/threadTitle.ts new file mode 100644 index 000000000..1666c3e27 --- /dev/null +++ b/apps/web/src/threadTitle.ts @@ -0,0 +1,17 @@ +import { splitPromptIntoComposerSegments } from "./composer-editor-mentions"; +import { basenameOfPath } from "./vscode-icons"; + +export function buildThreadTitle(text: string, maxLength = 50): string { + const normalized = splitPromptIntoComposerSegments(text) + .map((segment) => + segment.type === "mention" ? `@${basenameOfPath(segment.path)}` : segment.text, + ) + .join("") + .trim(); + + if (normalized.length <= maxLength) { + return normalized; + } + + return `${normalized.slice(0, maxLength)}...`; +} diff --git a/apps/web/src/truncateTitle.test.ts b/apps/web/src/truncateTitle.test.ts deleted file mode 100644 index d7d61c5da..000000000 --- a/apps/web/src/truncateTitle.test.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { truncateTitle } from "./truncateTitle"; - -describe("truncateTitle", () => { - it("trims surrounding whitespace", () => { - expect(truncateTitle(" hello world ")).toBe("hello world"); - }); - - it("returns trimmed text when within max length", () => { - expect(truncateTitle("alpha", 10)).toBe("alpha"); - }); - - it("appends ellipsis when text exceeds max length", () => { - expect(truncateTitle("abcdefghij", 5)).toBe("abcde..."); - }); -}); diff --git a/apps/web/src/truncateTitle.ts b/apps/web/src/truncateTitle.ts deleted file mode 100644 index bce554528..000000000 --- a/apps/web/src/truncateTitle.ts +++ /dev/null @@ -1,7 +0,0 @@ -export function truncateTitle(text: string, maxLength = 50): string { - const trimmed = text.trim(); - if (trimmed.length <= maxLength) { - return trimmed; - } - return `${trimmed.slice(0, maxLength)}...`; -}