Skip to content
Merged
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
3 changes: 3 additions & 0 deletions apps/ade-cli/src/adeRpcServer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2435,6 +2435,9 @@ describe("adeRpcServer", () => {
rows: 24,
command: "codex",
startupCommand: expect.stringContaining("codex --no-alt-screen"),
env: expect.objectContaining({
ADE_AGENT_SKILLS_DIRS: expect.stringContaining(path.join("lane-1", "apps", "desktop", "resources", "agent-skills")),
}),
}),
);
expect(fixture.runtime.ptyService.writeBySessionId).toHaveBeenCalledWith("session-1", "fix failing tests\r");
Expand Down
18 changes: 15 additions & 3 deletions apps/ade-cli/src/adeRpcServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,12 @@ import { launchPrIssueResolutionChat, previewPrIssueResolutionPrompt } from "../
import { runGit } from "../../desktop/src/main/services/git/git";
import { resolvePathWithinRoot } from "../../desktop/src/main/services/shared/utils";
import { getDefaultModelDescriptor } from "../../desktop/src/shared/modelRegistry";
import { ADE_CLI_INLINE_GUIDANCE } from "../../desktop/src/shared/adeCliGuidance";
import { buildAdeCliInlineGuidance } from "../../desktop/src/shared/adeCliGuidance";
import {
ADE_AGENT_SKILLS_DIRS_ENV,
getAdeAgentSkillRootsForPrompt,
joinAdeAgentSkillRoots,
} from "../../desktop/src/shared/agentSkillRoots";
import {
getPrIssueResolutionAvailability,
isActionablePrIssueComment,
Expand Down Expand Up @@ -2981,6 +2986,10 @@ function resolveLaneWorktreePath(runtime: AdeRuntime, laneId: string | null | un
return null;
}

function buildAdeInlineGuidanceForLane(laneWorktreePath: string | null | undefined): string {
return buildAdeCliInlineGuidance(getAdeAgentSkillRootsForPrompt({ cwd: laneWorktreePath ?? undefined }));
}

function resolveRunContextLaneId(runtime: AdeRuntime, callerCtx: CallerContext): string | null {
const runId = asOptionalTrimmedString(callerCtx.runId);
if (!runId) return null;
Expand Down Expand Up @@ -5106,10 +5115,11 @@ async function runTool(args: {
const model = asOptionalTrimmedString(toolArgs.model) ?? asOptionalTrimmedString(toolArgs.modelId);
const ptyService = runtime.ptyService;
const preassignedSessionId = provider === "claude" ? randomUUID() : undefined;
const laneWorktreePath = resolveLaneWorktreePath(runtime, laneId);

const launchFields: { startupCommand?: string; command?: string; args?: string[]; env?: Record<string, string> } = (() => {
if (!isCliProvider(provider)) return {};
return buildTrackedCliLaunchCommand({ provider, permissionMode, sessionId: preassignedSessionId, model });
return buildTrackedCliLaunchCommand({ provider, permissionMode, sessionId: preassignedSessionId, model, laneWorktreePath });
})();

const created = await ptyService.create({
Expand Down Expand Up @@ -6812,7 +6822,7 @@ async function runTool(args: {
});

const promptSegments: string[] = [];
promptSegments.push(ADE_CLI_INLINE_GUIDANCE);
promptSegments.push(buildAdeInlineGuidanceForLane(laneWorktreePath));
if (promptRunId || promptStepId || promptAttemptId) {
promptSegments.push(
`Mission context: run=${promptRunId ?? "n/a"} step=${promptStepId ?? "n/a"} attempt=${promptAttemptId ?? "n/a"}.`
Expand Down Expand Up @@ -6881,6 +6891,8 @@ async function runTool(args: {
// command remains a display/resume preview only; the actual launch uses
// command/args/env so it works on Windows without POSIX inline assignment.
const workerEnv: Record<string, string> = {};
const skillRootsEnv = joinAdeAgentSkillRoots(getAdeAgentSkillRootsForPrompt({ cwd: laneWorktreePath }));
if (skillRootsEnv) workerEnv[ADE_AGENT_SKILLS_DIRS_ENV] = skillRootsEnv;
const envPrefixParts: string[] = [];
const addWorkerEnv = (key: string, value: string | null | undefined) => {
if (!value) return;
Expand Down
12 changes: 12 additions & 0 deletions apps/ade-cli/src/services/sync/syncRemoteCommandService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1440,6 +1440,17 @@ async function resolvePrimaryLaneIdOnlyForSync(args: SyncRemoteCommandServiceArg
return lanes.find((lane) => lane.laneType === "primary")?.id ?? "";
}

function resolveLaneWorktreePathForSync(args: SyncRemoteCommandServiceArgs, laneId: string): string | null {
try {
const lane = args.laneService.getLaneBaseAndBranch(laneId);
const trimmed = typeof lane?.worktreePath === "string" ? lane.worktreePath.trim() : "";
if (trimmed.length) return trimmed;
} catch {
// Ignore and let the caller fall back to process/app skill roots.
}
return null;
}

async function resolveLaneOverlayContext(args: SyncRemoteCommandServiceArgs, laneId: string) {
const projectConfigService = requireService(args.projectConfigService, "Project config service not available.");
const lanes = await args.laneService.list({ includeStatus: false });
Expand Down Expand Up @@ -1792,6 +1803,7 @@ export function createSyncRemoteCommandService(args: SyncRemoteCommandServiceArg
permissionMode,
sessionId: preassignedSessionId,
model: parsed.modelId ?? parsed.model ?? undefined,
laneWorktreePath: resolveLaneWorktreePathForSync(args, parsed.laneId),
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,12 @@ describe("buildCodingAgentSystemPrompt", () => {
expect(result).toContain("## User-Facing Progress");
expect(result).toContain("## Mission");
});

it("uses the active cwd when describing ADE skill roots", () => {
const result = buildCodingAgentSystemPrompt({ cwd: "/repo/.ade/worktrees/chat-lane" });

expect(result).toContain("/repo/.ade/worktrees/chat-lane/apps/desktop/resources/agent-skills");
});
});

describe("composeSystemPrompt", () => {
Expand Down
7 changes: 5 additions & 2 deletions apps/desktop/src/main/services/ai/tools/systemPrompt.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ADE_CLI_AGENT_GUIDANCE } from "../../../../shared/adeCliGuidance";
import { buildAdeCliAgentGuidance } from "../../../../shared/adeCliGuidance";
import { getAdeAgentSkillRootsForPrompt } from "../../../../shared/agentSkillRoots";

type HarnessMode = "chat" | "coding" | "planning";
type HarnessPermissionMode = "plan" | "edit" | "full-auto";
Expand Down Expand Up @@ -82,6 +83,7 @@ export function buildCodingAgentSystemPrompt(args: {
toolNames?: string[];
interactive?: boolean;
runtime?: AdeRuntimeKind;
adeSkillRoots?: readonly string[];
}): string {
const mode = args.mode ?? "coding";
const permissionMode = args.permissionMode ?? "edit";
Expand All @@ -103,6 +105,7 @@ export function buildCodingAgentSystemPrompt(args: {
const hasTodoTools = toolNames.includes("TodoWrite") || toolNames.includes("TodoRead");
const hasWorkflowTools = hasCreateLane || hasCreatePr || hasCaptureScreenshot || hasReportCompletion;
const guardedLocalReadOnly = permissionMode === "plan";
const adeSkillRoots = args.adeSkillRoots ?? getAdeAgentSkillRootsForPrompt({ cwd: args.cwd });
const PR_ISSUE_TOOL_NAMES = new Set([
"prGetChecks",
"prGetReviewComments",
Expand Down Expand Up @@ -181,7 +184,7 @@ export function buildCodingAgentSystemPrompt(args: {
: "If requirements are unclear, make the safest reasonable assumption and continue. State the assumption in the final answer.",
"If tool results fail or contradict the current plan, synthesize the finding and adapt rather than repeating the same failing action.",
"",
ADE_CLI_AGENT_GUIDANCE,
buildAdeCliAgentGuidance(adeSkillRoots),
...(hasMemoryTools
? [
"",
Expand Down
17 changes: 12 additions & 5 deletions apps/desktop/src/main/services/chat/agentChatService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,8 @@ import {
reportProviderRuntimeReady,
} from "../ai/providerRuntimeHealth";
import { resolveAdeLayout } from "../../../shared/adeLayout";
import { ADE_CLI_AGENT_GUIDANCE } from "../../../shared/adeCliGuidance";
import { buildAdeCliAgentGuidance } from "../../../shared/adeCliGuidance";
import { getAdeAgentSkillRootsForPrompt } from "../../../shared/agentSkillRoots";
import { parseAgentChatTranscript } from "../../../shared/chatTranscript";
import { extractLeadingSlashCommand, isProviderSlashCommandInput } from "../../../shared/chatSlashCommands";
import { stripAnsi } from "../../utils/ansiStrip";
Expand Down Expand Up @@ -1599,6 +1600,7 @@ const DEFAULT_TRANSCRIPT_READ_CHARS = 8_000;
const MAX_TRANSCRIPT_READ_CHARS = 40_000;
const AUTOMATIC_MACOS_VM_CONTEXT_HEADER = "ADE macOS VM capability for this lane (automatic context).";
const AUTOMATIC_MACOS_VM_CONTEXT_ENDINGS = [
"- Tools: macos_vm_status, macos_vm_start, macos_vm_screenshot, macos_vm_click, macos_vm_type.",
"- This lane uses a sanitized mirror for the VM share; ADE syncs code while excluding secrets, runtime databases, caches, transcripts, generated local history, and .git.",
"- Keep VM-side edits inside the mounted guest lane path so the host lane and guest stay aligned.",
] as const;
Expand Down Expand Up @@ -3544,6 +3546,10 @@ function toHarnessPermissionMode(
return "edit";
}

function buildAdeGuidanceForLane(laneWorktreePath: string): string {
return buildAdeCliAgentGuidance(getAdeAgentSkillRootsForPrompt({ cwd: laneWorktreePath }));
}

function buildCodexDeveloperInstructions(args: {
laneWorktreePath: string;
session: Pick<AgentChatSession, "permissionMode" | "interactionMode">;
Expand All @@ -3558,6 +3564,7 @@ function buildCodexDeveloperInstructions(args: {
permissionMode: toHarnessPermissionMode(args.session.permissionMode),
interactive: true,
runtime: "codex-app-server",
adeSkillRoots: getAdeAgentSkillRootsForPrompt({ cwd: args.laneWorktreePath }),
});
}

Expand Down Expand Up @@ -10243,7 +10250,7 @@ export function createAgentChatService(args: {
"DO NOT save: file paths, raw error messages without lessons, task progress updates, information derivable from git log or the code itself, obvious patterns already visible in the codebase.",
...slashCommandsSection,
"",
ADE_CLI_AGENT_GUIDANCE,
buildAdeGuidanceForLane(managed.laneWorktreePath),
].join("\n");
};

Expand Down Expand Up @@ -13735,7 +13742,7 @@ export function createAgentChatService(args: {
"DO NOT save: file paths, raw error messages without lessons, task progress updates, information derivable from git log or the code itself, obvious patterns already visible in the codebase.",
...slashCommandsSection,
"",
ADE_CLI_AGENT_GUIDANCE,
buildAdeGuidanceForLane(managed.laneWorktreePath),
].join("\n"),
};
opts.settingSources = ["user", "project", "local"];
Expand Down Expand Up @@ -15112,7 +15119,7 @@ export function createAgentChatService(args: {
const laneDirectiveKey = executionContext.laneDirectiveKey;
const shouldInjectLaneDirective = laneDirectiveKey != null && managed.lastLaneDirectiveKey !== laneDirectiveKey;
// Guidance injection is capability-based, not session-state-based:
// Claude sessions receive ADE_CLI_AGENT_GUIDANCE in their persistent system
// Claude sessions receive lane-scoped ADE guidance in their persistent system
// prompt, and native Codex app-server sessions receive ADE guidance through
// developerInstructions/collaborationMode settings. Providers without a
// trusted instruction channel still need the guidance in the user prompt,
Expand Down Expand Up @@ -15162,7 +15169,7 @@ export function createAgentChatService(args: {
: null,
buildExecutionModeDirective(executionMode, managed.session.provider),
buildClaudeInteractionModeDirective(managed.session.interactionMode, managed.session.provider),
shouldInjectGuidance ? ADE_CLI_AGENT_GUIDANCE : null,
shouldInjectGuidance ? buildAdeGuidanceForLane(managed.laneWorktreePath) : null,
buildComputerUseDirective(
computerUseArtifactBrokerRef?.getBackendStatus() ?? null,
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4084,6 +4084,7 @@ describe("aiOrchestratorService", () => {
});

it("recovers running attempts with tracked sessions that go silent", async () => {
vi.useRealTimers();
const fixture = await createFixture({
aiIntegrationService: createStagnationRecoveryAiIntegrationService()
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3701,6 +3701,7 @@ export function AgentChatMessageList({
// coalesce every source (ResizeObserver, stick-flip effect, jump button)
// into at most one scrollTop assignment per frame.
const scrollRafRef = useRef<number | null>(null);
const scrollFollowFramesRef = useRef(0);
// Each programmatic scroll write increments this counter; the matching
// scroll event then decrements it and skips stick-state updates. Keeps
// user gestures as the only thing that toggles auto-follow off.
Expand Down Expand Up @@ -3837,33 +3838,45 @@ export function AgentChatMessageList({
// - A ResizeObserver on the content wrapper picks up every size change —
// new rows appearing *and* streaming tokens extending existing rows —
// without the old MutationObserver's characterData firehose.
const scrollToBottomSoon = useCallback(() => {
const scrollToBottomSoon = useCallback((followUpFrames = 1) => {
scrollFollowFramesRef.current = Math.max(scrollFollowFramesRef.current, followUpFrames);
if (scrollRafRef.current !== null) return;
scrollRafRef.current = requestAnimationFrame(() => {
const run = () => {
scrollRafRef.current = null;
const el = scrollRef.current;
if (!el || !stickToBottomRef.current) return;
const target = el.scrollHeight - el.clientHeight;
if (target <= 0) return;
if (!el || !stickToBottomRef.current) {
scrollFollowFramesRef.current = 0;
return;
}
const target = Math.max(0, el.scrollHeight - el.clientHeight);
const before = el.scrollTop;
if (Math.abs(before - target) < 1) return;
el.scrollTop = target;
// Only register a pending programmatic scroll event if the assignment
// actually moved the element. Otherwise (clamped to the same value,
// hidden element, etc.) no scroll event will fire and the counter
// would stay positive forever, misclassifying the next real user
// scroll as programmatic.
if (el.scrollTop !== before) {
programmaticScrollCountRef.current += 1;
if (Math.abs(before - target) >= 1) {
el.scrollTop = target;
setScrollTop(el.scrollTop);
// Only register a pending programmatic scroll event if the assignment
// actually moved the element. Otherwise (clamped to the same value,
// hidden element, etc.) no scroll event will fire and the counter
// would stay positive forever, misclassifying the next real user
// scroll as programmatic.
if (el.scrollTop !== before) {
programmaticScrollCountRef.current += 1;
}
}
});
const remaining = scrollFollowFramesRef.current;
if (remaining > 0) {
scrollFollowFramesRef.current = remaining - 1;
scrollRafRef.current = requestAnimationFrame(run);
}
};
scrollRafRef.current = requestAnimationFrame(run);
}, []);

useEffect(() => () => {
if (scrollRafRef.current !== null) {
cancelAnimationFrame(scrollRafRef.current);
scrollRafRef.current = null;
}
scrollFollowFramesRef.current = 0;
}, []);

// When the user re-enters the sticky zone (or on first mount), snap to bottom.
Expand All @@ -3884,7 +3897,7 @@ export function AgentChatMessageList({
return;
}
const ro = new ResizeObserver(() => {
if (stickToBottomRef.current) scrollToBottomSoon();
if (stickToBottomRef.current) scrollToBottomSoon(2);
});
ro.observe(wrapper);
return () => ro.disconnect();
Expand Down Expand Up @@ -3945,14 +3958,18 @@ export function AgentChatMessageList({
}
// Debounce measurement tick updates to batch rapid height changes
// into a single re-render instead of one per row.
if (!measureFlushTimer.current) {
measureFlushTimer.current = setTimeout(() => {
measureFlushTimer.current = null;
setMeasurementTick((value) => value + 1);
}, 80);
const isFollowingBottom = stickToBottomRef.current;
if (measureFlushTimer.current) {
if (!isFollowingBottom) return;
clearTimeout(measureFlushTimer.current);
}
measureFlushTimer.current = setTimeout(() => {
measureFlushTimer.current = null;
setMeasurementTick((value) => value + 1);
if (isFollowingBottom) scrollToBottomSoon(2);
}, isFollowingBottom ? 16 : 80);
}
}, [rowHeight, shouldVirtualize, timelineRowGapPx]);
}, [rowHeight, scrollToBottomSoon, shouldVirtualize, timelineRowGapPx]);

// Compute the visible window of rows when virtualization is active.
// measurementTick forces recomputation when row heights are measured so
Expand All @@ -3973,6 +3990,10 @@ export function AgentChatMessageList({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [shouldVirtualize, groupedRows.length, scrollTop, containerHeight, rowHeight, measurementTick, timelineRowGapPx]);

useLayoutEffect(() => {
if (stickToBottomRef.current) scrollToBottomSoon(2);
}, [containerHeight, groupedRows.length, measurementTick, scrollToBottomSoon, shouldVirtualize, totalHeight]);

const handleScroll = useCallback((event: React.UIEvent<HTMLDivElement>) => {
const target = event.currentTarget;
// Absorb scroll events produced by our own programmatic scroll-to-bottom
Expand Down
Loading
Loading