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;
Comment on lines +4426 to +4438
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Remove the hard requirement on modelId/laneId before AI suggestion.

Line 4426 currently forces fallback unless prompt, modelId, and laneId are all present. That bypasses your own model fallback chain and cwd fallback path, so AI suggestion can be unintentionally disabled.

💡 Suggested fix
-    if (!prompt.length || !requestedModelId.length || !sourceLaneId.length) {
+    if (!prompt.length) {
       return fallback();
     }

     let cwd = projectRoot;
-    try {
-      ({ laneWorktreePath: cwd } = resolveLaneLaunchContext({
-        laneService,
-        laneId: sourceLaneId,
-        purpose: "name a lane from prompt",
-      }));
-    } catch {
-      cwd = projectRoot;
-    }
+    if (sourceLaneId.length) {
+      try {
+        ({ laneWorktreePath: cwd } = resolveLaneLaunchContext({
+          laneService,
+          laneId: sourceLaneId,
+          purpose: "name a lane from prompt",
+        }));
+      } catch {
+        cwd = projectRoot;
+      }
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/services/chat/agentChatService.ts` around lines 4426 -
4438, The current guard returns fallback() when requestedModelId or sourceLaneId
are empty, disabling AI suggestions; change the initial check to only require
prompt (i.e., remove requestedModelId/sourceLaneId from the if that calls
fallback()), keep the existing try/catch that attempts resolveLaneLaunchContext
to set cwd (laneWorktreePath) and fall back to projectRoot on error, and ensure
any later code that actually needs a model or lane uses the existing
model-fallback chain rather than this early return; refer to prompt,
requestedModelId, sourceLaneId, fallback(), resolveLaneLaunchContext and the
cwd/laneWorktreePath assignment when making the change.

}

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) {
Comment on lines +4476 to +4479
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Enforce lane-safe normalization on model output before returning.

sanitizeAutoTitle(...) does not guarantee lane-safe characters across the full string (e.g., internal /, :), so this can return values that violate the lane naming contract while fallback returns a strict slug.

💡 Suggested fix
+      const normalizeLaneBase = (value: string): string | null => {
+        const normalized = value
+          .toLowerCase()
+          .replace(/[^a-z0-9 -]+/g, " ")
+          .replace(/\s+/g, " ")
+          .trim();
+        if (!normalized.length) return null;
+        const slug = normalized.replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
+        return slug.length ? slug.slice(0, 56) : null;
+      };
+
       const sanitized = sanitizeAutoTitle(result.text.trim(), 56);
-      if (!sanitized) return fallback();
-      return sanitized;
+      const laneBase = sanitized ? normalizeLaneBase(sanitized) : null;
+      if (!laneBase) return fallback();
+      return laneBase;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/services/chat/agentChatService.ts` around lines 4476 -
4479, The returned auto-title must be run through the lane-safe normalizer and
validated before returning: after calling sanitizeAutoTitle(result.text.trim(),
56) run the project’s lane-safe normalizer (e.g., normalizeLaneName or
ensureLaneSafe) on the sanitized value and verify it produces a non-empty, valid
lane string; if the normalized result is empty/invalid, call fallback() instead;
update the return path in the function containing sanitizeAutoTitle(...) and the
existing fallback() usage to perform this post-normalization and validation.

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
Original file line number Diff line number Diff line change
Expand Up @@ -3930,7 +3930,7 @@ export function createConflictService({
];
const status: PrepareResolverSessionResult["status"] = contextGaps.length > 0 ? "blocked" : "ready";

const prompt = buildExternalResolverPrompt({
let prompt = buildExternalResolverPrompt({
targetLaneId,
sourceLaneIds,
contexts,
Expand All @@ -3939,6 +3939,10 @@ export function createConflictService({
integrationLaneId: integrationLane?.id ?? null,
scenario
});
const extra = typeof args.additionalInstructions === "string" ? args.additionalInstructions.trim() : "";
if (extra.length > 0) {
prompt += `\n\n---\n\n## Operator instructions\n\n${extra}\n`;
}
const promptPath = path.join(runDir, "prompt.md");
fs.writeFileSync(promptPath, prompt, "utf8");

Expand Down
10 changes: 10 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);
});
Comment on lines +3800 to +3803
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 1) Confirm the new IPC handler currently forwards payload directly.
rg -n -C3 'IPC\.agentChatSuggestLaneName|suggestLaneNameFromPrompt\('

# 2) Inspect whether service-level validation exists for this arg type.
fd -i 'agentChatService.ts' --exec rg -n -C5 'suggestLaneNameFromPrompt|AgentChatSuggestLaneNameArgs' {}

Repository: arul28/ADE

Length of output: 2780


🏁 Script executed:

# Find AgentChatSuggestLaneNameArgs type definition
rg -n 'AgentChatSuggestLaneNameArgs\s*=' --type ts --type tsx

Repository: arul28/ADE

Length of output: 81


🏁 Script executed:

# Check if validation patterns exist at IPC boundary by looking at other handlers
rg -n 'ipcMain\.handle' apps/desktop/src/main/services/ipc/registerIpc.ts | head -20

Repository: arul28/ADE

Length of output: 1730


🏁 Script executed:

# Look for requireRecord or parseRecord patterns to see if validation is a standard pattern
rg -n 'requireRecord|parseRecord' apps/desktop/src/main/services/ipc/registerIpc.ts | head -10

Repository: arul28/ADE

Length of output: 677


🏁 Script executed:

# Check the service implementation more thoroughly to understand validation approach
sed -n '4414,4450p' apps/desktop/src/main/services/ipc/registerIpc.ts

Repository: arul28/ADE

Length of output: 1465


🏁 Script executed:

# View requireRecord implementation
sed -n '3494,3510p' apps/desktop/src/main/services/ipc/registerIpc.ts

Repository: arul28/ADE

Length of output: 690


🏁 Script executed:

# View full suggestLaneNameFromPrompt service method
sed -n '4414,4470p' apps/desktop/src/main/services/ipc/registerIpc.ts

Repository: arul28/ADE

Length of output: 2163


🏁 Script executed:

# Find AgentChatSuggestLaneNameArgs type definition
rg -n 'AgentChatSuggestLaneNameArgs' -A 3 --type ts

Repository: arul28/ADE

Length of output: 3567


🏁 Script executed:

# Get the full AgentChatSuggestLaneNameArgs type definition
sed -n '615,625p' apps/desktop/src/shared/types/chat.ts

Repository: arul28/ADE

Length of output: 479


🏁 Script executed:

# Check if there are similar agent chat IPC handlers and how they're validated
rg -n 'ipcMain\.handle\(IPC\.agentChat' apps/desktop/src/main/services/ipc/registerIpc.ts -A 3 | head -40

Repository: arul28/ADE

Length of output: 2037


🏁 Script executed:

# Look at patterns: how many IPC handlers use requireRecord vs direct typing
rg -n 'const parse.*Args.*=|ipcMain\.handle' apps/desktop/src/main/services/ipc/registerIpc.ts | grep -A1 'parse.*Args' | head -20

Repository: arul28/ADE

Length of output: 427


Add runtime validation for IPC.agentChatSuggestLaneName payloads at the IPC boundary.

The handler accepts arg: AgentChatSuggestLaneNameArgs with static typing only. While the service layer performs String coercion on the received fields, runtime validation should occur at the IPC boundary before dispatch. This aligns with the established pattern in this file (e.g., agentChatCancelSteer uses parseAgentChatCancelSteerArgs) and hardens against malformed renderer payloads.

Suggested fix
+  const parseAgentChatSuggestLaneNameArgs = (value: unknown): AgentChatSuggestLaneNameArgs => {
+    const record = requireRecord(value, "Agent chat suggest lane name request");
+    if (typeof record.prompt !== "string" || !record.prompt.trim()) {
+      throw new Error("Agent chat suggest lane name prompt must be a non-empty string");
+    }
+    if (typeof record.modelId !== "string" || !record.modelId.trim()) {
+      throw new Error("Agent chat suggest lane name model ID must be a non-empty string");
+    }
+    if (typeof record.laneId !== "string" || !record.laneId.trim()) {
+      throw new Error("Agent chat suggest lane name lane ID must be a non-empty string");
+    }
+    return {
+      prompt: record.prompt.trim(),
+      modelId: record.modelId.trim(),
+      laneId: record.laneId.trim(),
+    } as AgentChatSuggestLaneNameArgs;
+  };
+
-  ipcMain.handle(IPC.agentChatSuggestLaneName, async (_event, arg: AgentChatSuggestLaneNameArgs): Promise<string> => {
+  ipcMain.handle(IPC.agentChatSuggestLaneName, async (_event, arg: unknown): Promise<string> => {
     const ctx = getCtx();
-    return await ctx.agentChatService.suggestLaneNameFromPrompt(arg);
+    return await ctx.agentChatService.suggestLaneNameFromPrompt(parseAgentChatSuggestLaneNameArgs(arg));
   });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ipcMain.handle(IPC.agentChatSuggestLaneName, async (_event, arg: AgentChatSuggestLaneNameArgs): Promise<string> => {
const ctx = getCtx();
return await ctx.agentChatService.suggestLaneNameFromPrompt(arg);
});
const parseAgentChatSuggestLaneNameArgs = (value: unknown): AgentChatSuggestLaneNameArgs => {
const record = requireRecord(value, "Agent chat suggest lane name request");
if (typeof record.prompt !== "string" || !record.prompt.trim()) {
throw new Error("Agent chat suggest lane name prompt must be a non-empty string");
}
if (typeof record.modelId !== "string" || !record.modelId.trim()) {
throw new Error("Agent chat suggest lane name model ID must be a non-empty string");
}
if (typeof record.laneId !== "string" || !record.laneId.trim()) {
throw new Error("Agent chat suggest lane name lane ID must be a non-empty string");
}
return {
prompt: record.prompt.trim(),
modelId: record.modelId.trim(),
laneId: record.laneId.trim(),
} as AgentChatSuggestLaneNameArgs;
};
ipcMain.handle(IPC.agentChatSuggestLaneName, async (_event, arg: unknown): Promise<string> => {
const ctx = getCtx();
return await ctx.agentChatService.suggestLaneNameFromPrompt(parseAgentChatSuggestLaneNameArgs(arg));
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/services/ipc/registerIpc.ts` around lines 3800 - 3803,
Add runtime validation at the IPC boundary for IPC.agentChatSuggestLaneName by
parsing the incoming payload before calling the service: implement or reuse a
validator named parseAgentChatSuggestLaneNameArgs that accepts the raw arg and
throws on invalid shape, invoke it inside the handler to produce a typed value,
then pass that validated result to
ctx.agentChatService.suggestLaneNameFromPrompt; update the handler signature to
accept the raw arg and remove direct reliance on static typing (keep references
to IPC.agentChatSuggestLaneName, AgentChatSuggestLaneNameArgs,
parseAgentChatSuggestLaneNameArgs, and suggestLaneNameFromPrompt to locate the
change).


ipcMain.handle(IPC.agentChatHandoff, async (_event, arg: AgentChatHandoffArgs): Promise<AgentChatHandoffResult> => {
const ctx = getCtx();
return await ctx.agentChatService.handoffSession(arg);
Expand Down Expand Up @@ -4779,6 +4785,9 @@ export function registerIpc({
const reasoning = typeof arg?.reasoning === "string" && arg.reasoning.trim().length > 0
? arg.reasoning.trim()
: null;
const additionalInstructions = typeof arg?.additionalInstructions === "string" && arg.additionalInstructions.trim().length > 0
? arg.additionalInstructions.trim()
: null;
let runId = "";

if (!model) {
Expand Down Expand Up @@ -4835,6 +4844,7 @@ export function registerIpc({
model,
reasoningEffort: reasoning,
permissionMode,
additionalInstructions,
originSurface: context.sourceTab === "integration"
? "integration"
: context.sourceTab === "rebase"
Expand Down
Loading
Loading