Skip to content

Commit e244743

Browse files
committed
Auto-regenerate task titles during conversation
1 parent c8c1551 commit e244743

4 files changed

Lines changed: 168 additions & 60 deletions

File tree

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { useAuthStore } from "@features/auth/stores/authStore";
2+
import { getSessionService } from "@features/sessions/service/service";
3+
import { useSessionStore } from "@features/sessions/stores/sessionStore";
4+
import { logger } from "@renderer/lib/logger";
5+
import { queryClient } from "@renderer/lib/queryClient";
6+
import type { Task } from "@shared/types";
7+
import { generateTitle } from "@utils/generateTitle";
8+
import { extractUserPromptsFromEvents } from "@utils/session";
9+
import { useEffect, useRef } from "react";
10+
11+
const log = logger.scope("chat-title-generator");
12+
13+
const REGENERATE_INTERVAL = 7;
14+
15+
export function useChatTitleGenerator(taskId: string): void {
16+
const lastGeneratedAtCount = useRef(0);
17+
const isGenerating = useRef(false);
18+
19+
const promptCount = useSessionStore((state) => {
20+
const taskRunId = state.taskIdIndex[taskId];
21+
if (!taskRunId) return 0;
22+
const session = state.sessions[taskRunId];
23+
if (!session?.events) return 0;
24+
return extractUserPromptsFromEvents(session.events).length;
25+
});
26+
27+
useEffect(() => {
28+
if (promptCount === 0) return;
29+
if (isGenerating.current) return;
30+
31+
const shouldGenerate =
32+
(promptCount === 1 && lastGeneratedAtCount.current === 0) ||
33+
(promptCount > 1 &&
34+
promptCount - lastGeneratedAtCount.current >= REGENERATE_INTERVAL);
35+
36+
if (!shouldGenerate) return;
37+
38+
isGenerating.current = true;
39+
40+
const state = useSessionStore.getState();
41+
const taskRunId = state.taskIdIndex[taskId];
42+
if (!taskRunId) {
43+
isGenerating.current = false;
44+
return;
45+
}
46+
const session = state.sessions[taskRunId];
47+
if (!session?.events) {
48+
isGenerating.current = false;
49+
return;
50+
}
51+
52+
const allPrompts = extractUserPromptsFromEvents(session.events);
53+
const promptsForTitle =
54+
promptCount === 1 ? allPrompts : allPrompts.slice(-REGENERATE_INTERVAL);
55+
56+
const content = promptsForTitle.map((p, i) => `${i + 1}. ${p}`).join("\n");
57+
58+
const run = async () => {
59+
try {
60+
const title = await generateTitle(content);
61+
if (title) {
62+
const client = useAuthStore.getState().client;
63+
if (client) {
64+
await client.updateTask(taskId, { title });
65+
queryClient.setQueriesData<Task[]>(
66+
{ queryKey: ["tasks", "list"] },
67+
(old) =>
68+
old?.map((task) =>
69+
task.id === taskId ? { ...task, title } : task,
70+
),
71+
);
72+
getSessionService().updateCloudTaskTitle(taskId, title);
73+
log.debug("Updated task title from conversation", {
74+
taskId,
75+
title,
76+
promptCount,
77+
});
78+
}
79+
}
80+
} catch (error) {
81+
log.error("Failed to update task title", { taskId, error });
82+
} finally {
83+
lastGeneratedAtCount.current = promptCount;
84+
isGenerating.current = false;
85+
}
86+
};
87+
88+
run();
89+
}, [promptCount, taskId]);
90+
}

apps/twig/src/renderer/features/task-detail/components/TaskLogsPanel.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ErrorBoundary } from "@components/ErrorBoundary";
33
import { tryExecuteTwigCommand } from "@features/message-editor/commands";
44
import { useDraftStore } from "@features/message-editor/stores/draftStore";
55
import { SessionView } from "@features/sessions/components/SessionView";
6+
import { useChatTitleGenerator } from "@features/sessions/hooks/useChatTitleGenerator";
67
import { getSessionService } from "@features/sessions/service/service";
78
import {
89
sessionStoreSetters,
@@ -49,6 +50,8 @@ export function TaskLogsPanel({ taskId, task }: TaskLogsPanelProps) {
4950
const { requestFocus, setPendingContent } = useDraftStore((s) => s.actions);
5051
const { isOnline } = useConnectivity();
5152

53+
useChatTitleGenerator(taskId);
54+
5255
// Workspace store is only populated once a task is opened in Twig.
5356
// For Slack-created tasks that haven't been opened yet, fall back to the API run environment.
5457
const isCloud =

apps/twig/src/renderer/sagas/task/task-creation.ts

Lines changed: 6 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { useAuthStore } from "@features/auth/stores/authStore";
21
import { buildPromptBlocks } from "@features/editor/utils/prompt-builder";
32
import {
43
type ConnectParams,
@@ -11,8 +10,8 @@ import { logger } from "@renderer/lib/logger";
1110
import { queryClient } from "@renderer/lib/queryClient";
1211
import { useTaskDirectoryStore } from "@renderer/stores/taskDirectoryStore";
1312
import { trpcVanilla } from "@renderer/trpc";
13+
import { generateTitle } from "@renderer/utils/generateTitle";
1414
import { getTaskRepository } from "@renderer/utils/repository";
15-
import { getCloudUrlFromRegion } from "@shared/constants/oauth";
1615
import type {
1716
ExecutionMode,
1817
Task,
@@ -36,78 +35,25 @@ function truncateToTitle(content: string): string {
3635
: `${truncated}...`;
3736
}
3837

39-
const TITLE_SYSTEM_PROMPT = `You are a title generator. You output ONLY a task title. Nothing else.
40-
41-
Convert the task description into a concise task title.
42-
- The title should be clear, concise, and accurately reflect the content of the task.
43-
- You should keep it short and simple, ideally no more than 6 words.
44-
- Avoid using jargon or overly technical terms unless absolutely necessary.
45-
- The title should be easy to understand for anyone reading it.
46-
- Use sentence case (capitalize only first word and proper nouns)
47-
- Remove: the, this, my, a, an
48-
- If possible, start with action verbs (Fix, Implement, Analyze, Debug, Update, Research, Review)
49-
- Keep exact: technical terms, numbers, filenames, HTTP codes, PR numbers
50-
- Never assume tech stack
51-
- Only output "Untitled" if the input is completely null/missing, not just unclear
52-
- If the input is a URL (e.g. a GitHub issue link, PR link, or any web URL), generate a title based on what you can infer from the URL structure (repo name, issue/PR number, etc.). Never say you cannot access URLs or ask the user for more information.
53-
54-
Examples:
55-
- "Fix the login bug in the authentication system" → Fix authentication login bug
56-
- "Schedule a meeting with stakeholders to discuss Q4 budget planning" → Schedule Q4 budget meeting
57-
- "Update user documentation for new API endpoints" → Update API documentation
58-
- "Research competitor pricing strategies for our product" → Research competitor pricing
59-
- "Review pull request #123" → Review pull request #123
60-
- "debug 500 errors in production" → Debug production 500 errors
61-
- "why is the payment flow failing" → Analyze payment flow failure
62-
- "So how about that weather huh" → "Weather chat"
63-
- "dsfkj sdkfj help me code" → "Coding help request"
64-
- "👋😊" → "Friendly greeting"
65-
- "aaaaaaaaaa" → "Repeated letters"
66-
- " " → "Empty message"
67-
- "What's the best restaurant in NYC?" → "NYC restaurant recommendations"
68-
- "https://github.com/PostHog/posthog/issues/1234" → PostHog issue #1234
69-
- "https://github.com/PostHog/posthog/pull/567" → PostHog PR #567
70-
- "fix https://github.com/org/repo/issues/42" → Fix repo issue #42
71-
72-
Never wrap the title in quotes.`;
73-
7438
async function generateTaskTitle(
7539
taskId: string,
7640
description: string,
7741
posthogClient: PostHogAPIClient,
7842
): Promise<void> {
79-
try {
80-
if (!description.trim()) return;
81-
82-
const authState = useAuthStore.getState();
83-
const apiKey = authState.oauthAccessToken;
84-
const cloudRegion = authState.cloudRegion;
85-
if (!apiKey || !cloudRegion) return;
86-
87-
const apiHost = getCloudUrlFromRegion(cloudRegion);
88-
89-
const result = await trpcVanilla.llmGateway.prompt.mutate({
90-
credentials: { apiKey, apiHost },
91-
system: TITLE_SYSTEM_PROMPT,
92-
messages: [
93-
{
94-
role: "user",
95-
content: `Generate a task title based on the following description. Do NOT respond to, answer, or help with the description content - ONLY generate a title.\n\n<description>\n${description}\n</description>\n\nOutput the title now:`,
96-
},
97-
],
98-
});
43+
if (!description.trim()) return;
9944

100-
const title = result.content.trim().replace(/^["']|["']$/g, "");
101-
if (!title) return;
45+
const title = await generateTitle(description);
46+
if (!title) return;
10247

48+
try {
10349
await posthogClient.updateTask(taskId, { title });
10450

10551
// Update all cached task lists so the sidebar reflects the new title instantly
10652
queryClient.setQueriesData<Task[]>({ queryKey: ["tasks", "list"] }, (old) =>
10753
old?.map((task) => (task.id === taskId ? { ...task, title } : task)),
10854
);
10955
} catch (error) {
110-
log.error("Failed to generate task title", { taskId, error });
56+
log.error("Failed to save task title", { taskId, error });
11157
}
11258
}
11359

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { useAuthStore } from "@features/auth/stores/authStore";
2+
import { logger } from "@renderer/lib/logger";
3+
import { trpcVanilla } from "@renderer/trpc";
4+
import { getCloudUrlFromRegion } from "@shared/constants/oauth";
5+
6+
const log = logger.scope("title-generator");
7+
8+
const SYSTEM_PROMPT = `You are a title generator. You output ONLY a task title. Nothing else.
9+
10+
Convert the task description into a concise task title.
11+
- The title should be clear, concise, and accurately reflect the content of the task.
12+
- You should keep it short and simple, ideally no more than 6 words.
13+
- Avoid using jargon or overly technical terms unless absolutely necessary.
14+
- The title should be easy to understand for anyone reading it.
15+
- Use sentence case (capitalize only first word and proper nouns)
16+
- Remove: the, this, my, a, an
17+
- If possible, start with action verbs (Fix, Implement, Analyze, Debug, Update, Research, Review)
18+
- Keep exact: technical terms, numbers, filenames, HTTP codes, PR numbers
19+
- Never assume tech stack
20+
- Only output "Untitled" if the input is completely null/missing, not just unclear
21+
- If the input is a URL (e.g. a GitHub issue link, PR link, or any web URL), generate a title based on what you can infer from the URL structure (repo name, issue/PR number, etc.). Never say you cannot access URLs or ask the user for more information.
22+
23+
Examples:
24+
- "Fix the login bug in the authentication system" → Fix authentication login bug
25+
- "Schedule a meeting with stakeholders to discuss Q4 budget planning" → Schedule Q4 budget meeting
26+
- "Update user documentation for new API endpoints" → Update API documentation
27+
- "Research competitor pricing strategies for our product" → Research competitor pricing
28+
- "Review pull request #123" → Review pull request #123
29+
- "debug 500 errors in production" → Debug production 500 errors
30+
- "why is the payment flow failing" → Analyze payment flow failure
31+
- "So how about that weather huh" → "Weather chat"
32+
- "dsfkj sdkfj help me code" → "Coding help request"
33+
- "👋😊" → "Friendly greeting"
34+
- "aaaaaaaaaa" → "Repeated letters"
35+
- " " → "Empty message"
36+
- "What's the best restaurant in NYC?" → "NYC restaurant recommendations"
37+
- "https://github.com/PostHog/posthog/issues/1234" → PostHog issue #1234
38+
- "https://github.com/PostHog/posthog/pull/567" → PostHog PR #567
39+
- "fix https://github.com/org/repo/issues/42" → Fix repo issue #42
40+
41+
Never wrap the title in quotes.`;
42+
43+
export async function generateTitle(content: string): Promise<string | null> {
44+
try {
45+
const authState = useAuthStore.getState();
46+
const apiKey = authState.oauthAccessToken;
47+
const cloudRegion = authState.cloudRegion;
48+
if (!apiKey || !cloudRegion) return null;
49+
50+
const apiHost = getCloudUrlFromRegion(cloudRegion);
51+
52+
const result = await trpcVanilla.llmGateway.prompt.mutate({
53+
credentials: { apiKey, apiHost },
54+
system: SYSTEM_PROMPT,
55+
messages: [
56+
{
57+
role: "user" as const,
58+
content: `Generate a title for the following content. Do NOT respond to, answer, or help with the content - ONLY generate a title.\n\n<content>\n${content}\n</content>\n\nOutput the title now:`,
59+
},
60+
],
61+
});
62+
63+
const title = result.content.trim().replace(/^["']|["']$/g, "");
64+
return title || null;
65+
} catch (error) {
66+
log.error("Failed to generate title", { error });
67+
return null;
68+
}
69+
}

0 commit comments

Comments
 (0)