Skip to content

Commit 8eeeb7b

Browse files
committed
Add LLM task title generation and improve prompt timing
1 parent d191d23 commit 8eeeb7b

File tree

4 files changed

+102
-50
lines changed

4 files changed

+102
-50
lines changed

apps/twig/src/main/services/agent/service.ts

Lines changed: 0 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ import { logger } from "../../lib/logger.js";
2828
import { createTimingCollector } from "../../lib/timing.js";
2929
import { TypedEventEmitter } from "../../lib/typed-event-emitter.js";
3030
import type { FsService } from "../fs/service.js";
31-
import { getCurrentUserId, getPostHogClient } from "../posthog-analytics.js";
3231
import type { ProcessTrackingService } from "../process-tracking/service.js";
3332
import type { SleepService } from "../sleep/service.js";
3433
import {
@@ -468,32 +467,12 @@ export class AgentService extends TypedEventEmitter<AgentServiceEvents> {
468467
// Preview sessions don't persist logs — no real task exists
469468
const isPreview = taskId === "__preview__";
470469

471-
// OTEL log pipeline or legacy S3 writer if FF false
472-
const useOtelPipeline = isPreview
473-
? false
474-
: await tc.time("featureFlag", () =>
475-
this.isFeatureFlagEnabled("twig-agent-logs-pipeline"),
476-
);
477-
478-
log.info("Agent log transport", {
479-
transport: isPreview ? "none" : useOtelPipeline ? "otel" : "s3",
480-
taskId,
481-
taskRunId,
482-
});
483-
484470
const agent = new Agent({
485471
posthog: {
486472
apiUrl: credentials.apiHost,
487473
getApiKey: () => this.getToken(credentials.apiKey),
488474
projectId: credentials.projectId,
489475
},
490-
otelTransport: useOtelPipeline
491-
? {
492-
host: credentials.apiHost,
493-
apiKey: this.getToken(credentials.apiKey),
494-
logsPath: "/i/v1/agent-logs",
495-
}
496-
: undefined,
497476
skipLogPersistence: isPreview,
498477
debug: !app.isPackaged,
499478
onLog: onAgentLog,
@@ -1011,20 +990,6 @@ For git operations while detached:
1011990
return mockNodeDir;
1012991
}
1013992

1014-
private async isFeatureFlagEnabled(flagKey: string): Promise<boolean> {
1015-
try {
1016-
const client = getPostHogClient();
1017-
const userId = getCurrentUserId();
1018-
if (!client || !userId) {
1019-
return false;
1020-
}
1021-
return (await client.isFeatureEnabled(flagKey, userId)) ?? false;
1022-
} catch (error) {
1023-
log.warn(`Error checking feature flag "${flagKey}":`, error);
1024-
return false;
1025-
}
1026-
}
1027-
1028993
private cleanupMockNodeEnvironment(mockNodeDir: string): void {
1029994
try {
1030995
rmSync(mockNodeDir, { recursive: true, force: true });

apps/twig/src/renderer/features/sessions/service/service.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -611,6 +611,11 @@ export class SessionService {
611611
}
612612

613613
if (initialPrompt?.length) {
614+
if (submittedAt) {
615+
sessionStoreSetters.updateSession(taskRun.id, {
616+
promptStartedAt: submittedAt,
617+
});
618+
}
614619
await tc.time("sendPrompt", () => this.sendPrompt(taskId, initialPrompt));
615620
}
616621

@@ -816,9 +821,10 @@ export class SessionService {
816821
const msg = acpMsg.message;
817822

818823
if (isJsonRpcRequest(msg) && msg.method === "session/prompt") {
824+
// Preserve existing promptStartedAt if already set (e.g., submittedAt for initial prompts)
819825
sessionStoreSetters.updateSession(taskRunId, {
820826
isPromptPending: true,
821-
promptStartedAt: acpMsg.ts,
827+
promptStartedAt: session.promptStartedAt ?? acpMsg.ts,
822828
});
823829
}
824830

@@ -1079,9 +1085,11 @@ export class SessionService {
10791085
session: AgentSession,
10801086
blocks: ContentBlock[],
10811087
): Promise<{ stopReason: string }> {
1088+
// Preserve existing promptStartedAt if set (e.g., from submittedAt for initial prompts)
1089+
const currentSession = sessionStoreSetters.getSessions()[session.taskRunId];
10821090
sessionStoreSetters.updateSession(session.taskRunId, {
10831091
isPromptPending: true,
1084-
promptStartedAt: Date.now(),
1092+
promptStartedAt: currentSession?.promptStartedAt ?? Date.now(),
10851093
});
10861094

10871095
try {

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

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1+
import { useAuthStore } from "@features/auth/stores/authStore";
12
import { buildPromptBlocks } from "@features/editor/utils/prompt-builder";
23
import { getSessionService } from "@features/sessions/service/service";
34
import { useWorkspaceStore } from "@features/workspace/stores/workspaceStore";
45
import { Saga, type SagaLogger } from "@posthog/shared";
56
import type { PostHogAPIClient } from "@renderer/api/posthogClient";
67
import { logger } from "@renderer/lib/logger";
8+
import { queryClient } from "@renderer/lib/queryClient";
79
import { useTaskDirectoryStore } from "@renderer/stores/taskDirectoryStore";
810
import { trpcVanilla } from "@renderer/trpc";
911
import { getTaskRepository } from "@renderer/utils/repository";
12+
import { getCloudUrlFromRegion } from "@shared/constants/oauth";
1013
import type {
1114
ExecutionMode,
1215
Task,
@@ -16,6 +19,81 @@ import type {
1619

1720
const log = logger.scope("task-creation-saga");
1821

22+
function truncateToTitle(content: string): string {
23+
// Strip XML/HTML tags
24+
const stripped = content.replace(/<[^>]*>/g, "").trim();
25+
if (!stripped) return "Untitled";
26+
if (stripped.length <= 80) return stripped;
27+
// Truncate at word boundary
28+
const truncated = stripped.slice(0, 80);
29+
const lastSpace = truncated.lastIndexOf(" ");
30+
return lastSpace > 20
31+
? `${truncated.slice(0, lastSpace)}...`
32+
: `${truncated}...`;
33+
}
34+
35+
const TITLE_SYSTEM_PROMPT = `You are a title generator. You output ONLY a task title. Nothing else.
36+
37+
Convert the task description into a concise task title.
38+
- The title should be clear, concise, and accurately reflect the content of the task.
39+
- You should keep it short and simple, ideally no more than 6 words.
40+
- Avoid using jargon or overly technical terms unless absolutely necessary.
41+
- The title should be easy to understand for anyone reading it.
42+
- Use sentence case (capitalize only first word and proper nouns)
43+
-Remove: the, this, my, a, an
44+
- If possible, start with action verbs (Fix, Implement, Analyze, Debug, Update, Research, Review)
45+
- Keep exact: technical terms, numbers, filenames, HTTP codes, PR numbers
46+
- Never assume tech stack
47+
- Only output "Untitled" if the input is completely null/missing, not just unclear
48+
49+
Examples:
50+
- "Fix the login bug in the authentication system" → Fix authentication login bug
51+
- "Schedule a meeting with stakeholders to discuss Q4 budget planning" → Schedule Q4 budget meeting
52+
- "Update user documentation for new API endpoints" → Update API documentation
53+
- "Research competitor pricing strategies for our product" → Research competitor pricing
54+
- "Review pull request #123" → Review pull request #123
55+
- "debug 500 errors in production" → Debug production 500 errors
56+
- "why is the payment flow failing" → Analyze payment flow failure
57+
- "So how about that weather huh" → "Weather chat"
58+
- "dsfkj sdkfj help me code" → "Coding help request"
59+
- "👋😊" → "Friendly greeting"
60+
- "aaaaaaaaaa" → "Repeated letters"
61+
- " " → "Empty message"
62+
- "What's the best restaurant in NYC?" → "NYC restaurant recommendations"`;
63+
64+
async function generateTaskTitle(
65+
taskId: string,
66+
description: string,
67+
posthogClient: PostHogAPIClient,
68+
): Promise<void> {
69+
try {
70+
const authState = useAuthStore.getState();
71+
const apiKey = authState.oauthAccessToken;
72+
const cloudRegion = authState.cloudRegion;
73+
if (!apiKey || !cloudRegion) return;
74+
75+
const apiHost = getCloudUrlFromRegion(cloudRegion);
76+
77+
const result = await trpcVanilla.llmGateway.prompt.mutate({
78+
credentials: { apiKey, apiHost },
79+
system: TITLE_SYSTEM_PROMPT,
80+
messages: [{ role: "user", content: description }],
81+
});
82+
83+
const title = result.content.trim();
84+
if (!title) return;
85+
86+
await posthogClient.updateTask(taskId, { title });
87+
88+
// Update all cached task lists so the sidebar reflects the new title instantly
89+
queryClient.setQueriesData<Task[]>({ queryKey: ["tasks", "list"] }, (old) =>
90+
old?.map((task) => (task.id === taskId ? { ...task, title } : task)),
91+
);
92+
} catch (error) {
93+
log.error("Failed to generate task title", { taskId, error });
94+
}
95+
}
96+
1997
// Adapt our logger to SagaLogger interface
2098
const sagaLogger: SagaLogger = {
2199
info: (message, data) => log.info(message, data),
@@ -78,6 +156,11 @@ export class TaskCreationSaga extends Saga<
78156
)
79157
: await this.createTask(input);
80158

159+
// Fire-and-forget: generate a proper LLM title for new tasks
160+
if (!taskId) {
161+
generateTaskTitle(task.id, input.content ?? "", this.deps.posthogClient);
162+
}
163+
81164
// Step 2: Resolve repoPath - input takes precedence, then stored mappings
82165
// Wait for workspace store to load first (it loads async on init)
83166
await this.readOnlyStep("wait_workspaces_loaded", () =>
@@ -282,6 +365,7 @@ export class TaskCreationSaga extends Saga<
282365
name: "task_creation",
283366
execute: async () => {
284367
const result = await this.deps.posthogClient.createTask({
368+
title: truncateToTitle(input.content ?? ""),
285369
description: input.content ?? "",
286370
repository: repository ?? undefined,
287371
github_integration:

packages/agent/src/adapters/claude/claude-agent.ts

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
170170

171171
// Fire off MCP metadata fetch early — it populates a module-level cache
172172
// used later during permission checks, not needed by buildSessionOptions or query()
173-
const mcpMetadataPromise = fetchMcpToolMetadata(mcpServers, this.logger);
173+
fetchMcpToolMetadata(mcpServers, this.logger);
174174

175175
const options = timeSync("buildSessionOptions", () =>
176176
buildSessionOptions({
@@ -222,17 +222,14 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
222222
// Slash commands: send update to client when ready (not blocking)
223223
getAvailableSlashCommands(q)
224224
.then((slashCommands) => {
225-
this.sendAvailableCommandsUpdate(sessionId, slashCommands);
225+
if (this.sessionId === sessionId) {
226+
this.sendAvailableCommandsUpdate(sessionId, slashCommands);
227+
}
226228
})
227229
.catch((err) => {
228230
this.logger.warn("Failed to fetch slash commands", { err });
229231
});
230232

231-
// MCP metadata: already running, just ensure errors are handled
232-
mcpMetadataPromise.catch((err) => {
233-
this.logger.warn("Failed to fetch MCP tool metadata", { err });
234-
});
235-
236233
session.modelId = modelOptions.currentModelId;
237234

238235
// Fire-and-forget — trySetModel already swallows errors
@@ -272,7 +269,7 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
272269
const mcpServers = parseMcpServers(params);
273270

274271
// Fire off MCP metadata fetch early — populates cache for permission checks
275-
const mcpMetadataPromise = fetchMcpToolMetadata(mcpServers, this.logger);
272+
fetchMcpToolMetadata(mcpServers, this.logger);
276273

277274
const permissionMode: TwigExecutionMode =
278275
meta?.permissionMode &&
@@ -298,16 +295,14 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
298295
// Defer slash commands and MCP metadata — not needed to return configOptions
299296
getAvailableSlashCommands(q)
300297
.then((slashCommands) => {
301-
this.sendAvailableCommandsUpdate(sessionId, slashCommands);
298+
if (this.sessionId === sessionId) {
299+
this.sendAvailableCommandsUpdate(sessionId, slashCommands);
300+
}
302301
})
303302
.catch((err) => {
304303
this.logger.warn("Failed to fetch slash commands on resume", { err });
305304
});
306305

307-
mcpMetadataPromise.catch((err) => {
308-
this.logger.warn("Failed to fetch MCP tool metadata on resume", { err });
309-
});
310-
311306
return {
312307
configOptions: await this.buildConfigOptions(),
313308
};

0 commit comments

Comments
 (0)