Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions apps/desktop/src/main/services/chat/agentChatService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ import type {
AgentChatSurface,
AgentChatSteerArgs,
AgentChatSendArgs,
AgentChatSuggestLaneNameArgs,
AgentChatCursorConfigOption,
AgentChatCursorConfigValue,
AgentChatCursorModeSnapshot,
Expand Down Expand Up @@ -837,6 +838,13 @@ Return only the title text.
- No quotes.
- No emoji.
- No trailing punctuation.`;

const LANE_NAME_FROM_PROMPT_SYSTEM_PROMPT = `You name git worktree lanes for a software project.
Return only the base name text (no model suffixes).
- Use 2 to 5 words, lowercase except proper nouns if needed.
- Slug-friendly: letters, numbers, spaces, and hyphens only (no slashes).
- Describe the task or feature from the user's message.
- No quotes, no emoji, no trailing punctuation.`;
const CODEX_REASONING_EFFORTS: Array<{ effort: string; description: string }> = [
{ effort: "low", description: "Fastest turn-around with shallow reasoning." },
{ effort: "medium", description: "Balanced reasoning depth and speed." },
Expand Down Expand Up @@ -4403,6 +4411,80 @@ export function createAgentChatService(args: {
};
};

const suggestLaneNameFromPrompt = async (args: AgentChatSuggestLaneNameArgs): Promise<string> => {
const prompt = String(args.prompt ?? "").trim();
const requestedModelId = String(args.modelId ?? "").trim();
const sourceLaneId = String(args.laneId ?? "").trim();
const fallback = (): string => {
const collapsed = prompt.replace(/\s+/g, " ").trim();
if (!collapsed.length) return "parallel-task";
const words = collapsed.split(/\s+/).filter(Boolean).slice(0, 4);
const slug = words.join("-").toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
return slug.length ? slug.slice(0, 48) : "parallel-task";
};

if (!prompt.length || !requestedModelId.length || !sourceLaneId.length) {
return fallback();
}

let cwd = projectRoot;
try {
({ laneWorktreePath: cwd } = resolveLaneLaunchContext({
laneService,
laneId: sourceLaneId,
purpose: "name a lane from prompt",
}));
} catch {
cwd = projectRoot;
}

try {
const auth = await detectAuth();
const availableModels = getRegistryModels(auth).filter((descriptor) => !descriptor.deprecated);
if (!availableModels.length) return fallback();

const config = resolveChatConfig();
const preferredModelId =
[
requestedModelId,
config.autoTitleModelId,
DEFAULT_AUTO_TITLE_MODEL_ID,
"anthropic/claude-haiku-4-5",
"openai/gpt-5.4-mini",
"openai/gpt-5.2",
"openai/gpt-5.4",
availableModels[0]?.id,
].find((candidate) => {
const modelId = typeof candidate === "string" ? candidate.trim() : "";
return modelId.length > 0 && availableModels.some((descriptor) => descriptor.id === modelId);
}) ?? null;

if (!preferredModelId) return fallback();

const descriptor = getModelById(preferredModelId);
if (!descriptor) return fallback();

const resolvedModel = await providerResolver.resolveModel(descriptor.id, auth, {
cwd,
middleware: false,
});
const result = await generateText({
model: resolvedModel,
system: LANE_NAME_FROM_PROMPT_SYSTEM_PROMPT,
prompt: `User message to parallelize across models:\n${prompt.slice(0, 2000)}`,
});
const sanitized = sanitizeAutoTitle(result.text.trim(), 56);
if (!sanitized) return fallback();
return sanitized;
} catch (error) {
logger.warn("agent_chat.suggest_lane_name_failed", {
modelId: requestedModelId,
error: error instanceof Error ? error.message : String(error),
});
return fallback();
}
};

const computeHeadShaBestEffort = async (laneId: string): Promise<string | null> => {
let cwd: string;
try {
Expand Down Expand Up @@ -13458,6 +13540,7 @@ export function createAgentChatService(args: {

return {
createSession,
suggestLaneNameFromPrompt,
handoffSession,
sendMessage,
runSessionTurn,
Expand Down
6 changes: 6 additions & 0 deletions apps/desktop/src/main/services/ipc/registerIpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ import type {
AgentChatRespondToInputArgs,
AgentChatResumeArgs,
AgentChatSendArgs,
AgentChatSuggestLaneNameArgs,
AgentChatSession,
AgentChatSessionSummary,
AgentChatSubagentSnapshot,
Expand Down Expand Up @@ -3796,6 +3797,11 @@ export function registerIpc({
return await ctx.agentChatService.createSession(arg);
});

ipcMain.handle(IPC.agentChatSuggestLaneName, async (_event, arg: AgentChatSuggestLaneNameArgs): Promise<string> => {
const ctx = getCtx();
return await ctx.agentChatService.suggestLaneNameFromPrompt(arg);
});

ipcMain.handle(IPC.agentChatHandoff, async (_event, arg: AgentChatHandoffArgs): Promise<AgentChatHandoffResult> => {
const ctx = getCtx();
return await ctx.agentChatService.handoffSession(arg);
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/src/preload/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ import type {
AgentTool,
AgentChatApproveArgs,
AgentChatCreateArgs,
AgentChatSuggestLaneNameArgs,
AgentChatDisposeArgs,
AgentChatEventEnvelope,
AgentChatGetSummaryArgs,
Expand Down Expand Up @@ -831,6 +832,7 @@ declare global {
list: (args?: AgentChatListArgs) => Promise<AgentChatSessionSummary[]>;
getSummary: (args: AgentChatGetSummaryArgs) => Promise<AgentChatSessionSummary | null>;
create: (args: AgentChatCreateArgs) => Promise<AgentChatSession>;
suggestLaneName: (args: AgentChatSuggestLaneNameArgs) => Promise<string>;
handoff: (args: AgentChatHandoffArgs) => Promise<AgentChatHandoffResult>;
send: (args: AgentChatSendArgs) => Promise<void>;
steer: (args: AgentChatSteerArgs) => Promise<void>;
Expand Down
3 changes: 3 additions & 0 deletions apps/desktop/src/preload/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ import type {
AgentTool,
AgentChatApproveArgs,
AgentChatCreateArgs,
AgentChatSuggestLaneNameArgs,
AgentChatDisposeArgs,
AgentChatEventEnvelope,
AgentChatGetSummaryArgs,
Expand Down Expand Up @@ -1123,6 +1124,8 @@ contextBridge.exposeInMainWorld("ade", {
ipcRenderer.invoke(IPC.agentChatGetSummary, args),
create: async (args: AgentChatCreateArgs): Promise<AgentChatSession> =>
ipcRenderer.invoke(IPC.agentChatCreate, args),
suggestLaneName: async (args: AgentChatSuggestLaneNameArgs): Promise<string> =>
ipcRenderer.invoke(IPC.agentChatSuggestLaneName, args),
handoff: async (args: AgentChatHandoffArgs): Promise<AgentChatHandoffResult> =>
ipcRenderer.invoke(IPC.agentChatHandoff, args),
send: async (args: AgentChatSendArgs): Promise<void> =>
Expand Down
Loading
Loading