Skip to content
This repository was archived by the owner on Aug 6, 2026. It is now read-only.

Commit 959ea07

Browse files
authored
fix(sessions): keep auto task titles pinned to the original prompt (#3384)
1 parent 3950b23 commit 959ea07

4 files changed

Lines changed: 99 additions & 0 deletions

File tree

packages/core/src/sessions/chatTitle.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { Task } from "@posthog/shared/domain-types";
22
import { describe, expect, it } from "vitest";
33
import {
4+
canApplyTitleFromPrompts,
45
decideTitleGeneration,
56
formatPromptsForTitleInput,
67
getFallbackTaskTitle,
@@ -182,6 +183,32 @@ describe("decideTitleGeneration", () => {
182183
);
183184
});
184185

186+
describe("canApplyTitleFromPrompts", () => {
187+
it("allows the first-prompt fire to write the title", () => {
188+
expect(
189+
canApplyTitleFromPrompts(1, { title: "Custom", description: "d" }),
190+
).toBe(true);
191+
});
192+
193+
it("blocks later fires from rewriting a real title", () => {
194+
expect(
195+
canApplyTitleFromPrompts(1 + REGENERATE_INTERVAL, {
196+
title: "Fix login bug",
197+
description: "the login page 500s",
198+
}),
199+
).toBe(false);
200+
});
201+
202+
it("allows later fires to replace a placeholder title", () => {
203+
expect(
204+
canApplyTitleFromPrompts(1 + REGENERATE_INTERVAL, {
205+
title: "Fix login",
206+
description: "Fix login",
207+
}),
208+
).toBe(true);
209+
});
210+
});
211+
185212
describe("selectPromptsForTitle", () => {
186213
it("returns all prompts on the first prompt", () => {
187214
expect(selectPromptsForTitle(["a"], 1)).toEqual(["a"]);

packages/core/src/sessions/chatTitle.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,18 @@ export function decideTitleGeneration(input: {
7777
return { shouldGenerateFromPrompts, shouldGenerateFromTaskDescription };
7878
}
7979

80+
// Prompt-window fires past the first prompt describe the recent conversation,
81+
// not the task, so the title must stay pinned to the original prompt context.
82+
// Later fires may only fill in a title that is still the raw-description
83+
// placeholder (e.g. an earlier generation failed); the summary always
84+
// refreshes regardless.
85+
export function canApplyTitleFromPrompts(
86+
promptCount: number,
87+
task: Pick<Task, "title" | "description">,
88+
): boolean {
89+
return promptCount <= 1 || isPlaceholderTaskTitle(task);
90+
}
91+
8092
export function selectPromptsForTitle(
8193
prompts: string[],
8294
promptCount: number,

packages/ui/src/features/sessions/hooks/useChatTitleGenerator.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,54 @@ describe("useChatTitleGenerator", () => {
449449
expect(mockGenerateTitle).toHaveBeenCalledTimes(1);
450450
});
451451

452+
it("does not rewrite an unlocked real title from later prompts, but still refreshes the summary", async () => {
453+
// Auto-generated at creation: real title, title_manually_set false.
454+
const unlockedTask = createTask({
455+
title: "Fix login bug",
456+
description: "the login page 500s for SSO users",
457+
});
458+
cacheTask(unlockedTask);
459+
mockGenerateTitle.mockResolvedValue({
460+
title: "Discuss deploy schedule",
461+
summary: "User is coordinating a deploy",
462+
});
463+
mockPrompts.value = Array.from({ length: 8 }, (_, i) => `prompt ${i}`);
464+
465+
renderHook(() => useChatTitleGenerator(unlockedTask));
466+
467+
await waitFor(() => {
468+
expect(mockGenerateTitle).toHaveBeenCalledTimes(1);
469+
});
470+
await waitFor(() => {
471+
expect(mockSessionStoreSetters.updateSession).toHaveBeenCalledWith(
472+
"run-1",
473+
{ conversationSummary: "User is coordinating a deploy" },
474+
);
475+
});
476+
expect(mockUpdateTask).not.toHaveBeenCalled();
477+
});
478+
479+
it("replaces a placeholder title from later prompts", async () => {
480+
const placeholderTask = createTask({
481+
title: "Attached files: pasted-text.txt",
482+
description: "Attached files: pasted-text.txt",
483+
});
484+
cacheTask(placeholderTask);
485+
mockGenerateTitle.mockResolvedValue({
486+
title: "Refactor auth flow",
487+
summary: "",
488+
});
489+
mockPrompts.value = Array.from({ length: 8 }, (_, i) => `prompt ${i}`);
490+
491+
renderHook(() => useChatTitleGenerator(placeholderTask));
492+
493+
await waitFor(() => {
494+
expect(mockUpdateTask).toHaveBeenCalledWith(TASK_ID, {
495+
title: "Refactor auth flow",
496+
});
497+
});
498+
});
499+
452500
it("skips catch-up generation when the title is locked and a summary exists", async () => {
453501
const lockedTask = createTask({
454502
title: "Custom auth title",

packages/ui/src/features/sessions/hooks/useChatTitleGenerator.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { Schemas } from "@posthog/api-client";
22
import {
3+
canApplyTitleFromPrompts,
34
decideTitleGeneration,
45
formatPromptsForTitleInput,
56
isAutoTitleLocked,
@@ -120,6 +121,17 @@ export function useChatTitleGenerator(task: Task): void {
120121

121122
if (title && isTitleLocked()) {
122123
log.debug("Skipping auto-title, user renamed task", { taskId });
124+
} else if (
125+
title &&
126+
!canApplyTitleFromPrompts(
127+
promptCount,
128+
getCachedTask(queryClient, taskId) ?? task,
129+
)
130+
) {
131+
log.debug("Skipping auto-title, keeping original-context title", {
132+
taskId,
133+
promptCount,
134+
});
123135
} else if (title) {
124136
if (client) {
125137
await client.updateTask(taskId, { title });

0 commit comments

Comments
 (0)