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
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
4 changes: 4 additions & 0 deletions apps/desktop/src/main/services/ipc/registerIpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4785,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 @@ -4841,6 +4844,7 @@ export function registerIpc({
model,
reasoningEffort: reasoning,
permissionMode,
additionalInstructions,
originSurface: context.sourceTab === "integration"
? "integration"
: context.sourceTab === "rebase"
Expand Down
225 changes: 202 additions & 23 deletions apps/desktop/src/main/services/prs/prService.ts

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ describe("kvDb mission schema migration", () => {
"integration_lane_name",
"status",
"integration_lane_id",
"preferred_integration_lane_id",
"merge_into_head_sha",
"resolution_state_json",
"pairwise_results_json",
"lane_summaries_json"
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/src/main/services/state/kvDb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1161,6 +1161,8 @@ function migrate(db: { run: (sql: string, params?: SqlValue[]) => void }) {
try { db.run("alter table integration_proposals add column completed_at text"); } catch {}
try { db.run("alter table integration_proposals add column cleanup_declined_at text"); } catch {}
try { db.run("alter table integration_proposals add column cleanup_completed_at text"); } catch {}
try { db.run("alter table integration_proposals add column preferred_integration_lane_id text"); } catch {}
try { db.run("alter table integration_proposals add column merge_into_head_sha text"); } catch {}

// Queue landing state table (crash recovery for sequential landing)
db.run(`
Expand Down
61 changes: 59 additions & 2 deletions apps/desktop/src/renderer/components/prs/CreatePrModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,7 @@ export function CreatePrModal({
const [integrationTitle, setIntegrationTitle] = React.useState("");
const [integrationBody, setIntegrationBody] = React.useState("");
const [integrationDraft, setIntegrationDraft] = React.useState(false);
const [integrationMergeIntoLaneId, setIntegrationMergeIntoLaneId] = React.useState("");
const [proposal, setProposal] = React.useState<IntegrationProposal | null>(null);
const [simulating, setSimulating] = React.useState(false);
const [laneSyncStatusById, setLaneSyncStatusById] = React.useState<Record<string, GitUpstreamSyncStatus | null>>({});
Expand Down Expand Up @@ -622,6 +623,7 @@ export function CreatePrModal({
setDraftError(null);
setIntegrationSources([]);
setIntegrationBaseBranch("");
setIntegrationMergeIntoLaneId("");
setIntegrationName("");
setIntegrationTitle("");
setIntegrationBody("");
Expand Down Expand Up @@ -715,9 +717,15 @@ export function CreatePrModal({
try {
const trimmedIntegrationBaseBranch = integrationBaseBranch.trim();
const baseBranch = trimmedIntegrationBaseBranch || branchNameFromRef(primaryLane?.branchRef ?? "main");
const mergeInto = integrationMergeIntoLaneId.trim();
if (mergeInto && integrationSources.includes(mergeInto)) {
setExecError("Merge-into lane cannot be one of the source lanes.");
return;
}
const result = await window.ade.prs.simulateIntegration({
sourceLaneIds: integrationSources,
baseBranch,
mergeIntoLaneId: mergeInto || null,
});
setProposal(result);
// Auto-generate a name if empty
Expand Down Expand Up @@ -784,11 +792,16 @@ export function CreatePrModal({
body: integrationBody,
draft: integrationDraft,
integrationLaneName: integrationName || `integration/${Date.now().toString(36)}`,
preferredIntegrationLaneId: integrationMergeIntoLaneId.trim() || null,
});
lastProgressLabel = "Creating integration lane";
setIntegrationProgress("Creating integration lane...");
await window.ade.prs.createIntegrationLaneForProposal({
proposalId: proposal.proposalId,
await runWithDirtyWorktreeConfirmation({
confirmMessage: "Continue and prepare the integration lane anyway?",
run: async (allowDirtyWorktree) => window.ade.prs.createIntegrationLaneForProposal({
proposalId: proposal.proposalId,
...(allowDirtyWorktree ? { allowDirtyWorktree: true } : {}),
}),
});
setIntegrationProgress(null);
// No PR created — proposal saved for later commit from Integration tab
Expand All @@ -812,6 +825,19 @@ export function CreatePrModal({

const nonPrimaryLanes = React.useMemo(() => lanes.filter((l) => l.laneType !== "primary"), [lanes]);

const integrationMergeIntoOptions = React.useMemo(
() => [...nonPrimaryLanes].sort((a, b) => a.name.localeCompare(b.name)),
[nonPrimaryLanes],
);

React.useEffect(() => {
if (!integrationMergeIntoLaneId) return;
if (integrationSources.includes(integrationMergeIntoLaneId)) {
setIntegrationMergeIntoLaneId("");
setProposal(null);
}
}, [integrationMergeIntoLaneId, integrationSources]);

const toggleQueueLane = (laneId: string) => {
setQueueLaneIds((prev) =>
prev.includes(laneId) ? prev.filter((id) => id !== laneId) : [...prev, laneId]
Expand Down Expand Up @@ -1499,6 +1525,37 @@ export function CreatePrModal({
/>
</div>

<div>
<span style={labelStyle}>MERGE INTO (OPTIONAL)</span>
<select
aria-label="Merge integration into existing lane"
value={integrationMergeIntoLaneId}
onChange={(e) => {
setIntegrationMergeIntoLaneId(e.target.value);
setProposal(null);
}}
style={selectStyle}
onFocus={(ev) => { ev.currentTarget.style.borderColor = C.accent; }}
onBlur={(ev) => { ev.currentTarget.style.borderColor = C.borderSubtle; }}
>
<option value="">New integration branch</option>
{integrationMergeIntoOptions.map((lane) => (
<option key={lane.id} value={lane.id} disabled={integrationSources.includes(lane.id)}>
{lane.name}{integrationSources.includes(lane.id) ? " (source)" : ""}
</option>
))}
</select>
<div style={{
marginTop: 6,
fontSize: 10,
fontFamily: "var(--font-sans)",
color: C.textMuted,
lineHeight: "14px",
}}>
When set, simulation includes conflicts against that lane&apos;s current HEAD. Commit prepares merges there instead of creating a new lane.
</div>
</div>

{/* Simulate button */}
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
<button
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ type PrAiResolverPanelProps = {
startLabel?: string;
defaultExpanded?: boolean;
sessionShellClassName?: string;
/** Optional notes appended to the generated resolver prompt (e.g. “keep UI from lane A”). */
showResolverInstructions?: boolean;
};

function normalizeReasoning(value: string): string | null {
Expand Down Expand Up @@ -91,12 +93,14 @@ export function PrAiResolverPanel({
startLabel = "Start AI Resolver",
defaultExpanded = true,
sessionShellClassName,
showResolverInstructions = false,
}: PrAiResolverPanelProps) {
const { resolverSessionsByContextKey, upsertResolverSession } = usePrs();
const [launching, setLaunching] = React.useState(false);
const [status, setStatus] = React.useState<"idle" | "starting" | "running" | "completed" | "failed" | "cancelled">("idle");
const [message, setMessage] = React.useState<string | null>(null);
const [expanded, setExpanded] = React.useState(defaultExpanded);
const [resolverInstructions, setResolverInstructions] = React.useState("");
const terminalStatusRef = React.useRef<PrAiResolverCompletion["status"] | null>(null);
const contextKey = React.useMemo(() => (context ? buildPrAiResolutionContextKey(context) : null), [context]);
const activeSession = React.useMemo(
Expand All @@ -117,6 +121,10 @@ export function PrAiResolverPanel({
}
}, [activeSession]);

React.useEffect(() => {
setResolverInstructions("");
}, [contextKey]);

React.useEffect(() => {
if (!context || !contextKey || activeSession) return;
let cancelled = false;
Expand Down Expand Up @@ -173,11 +181,13 @@ export function PrAiResolverPanel({
setStatus("starting");
terminalStatusRef.current = null;
try {
const trimmedInstructions = resolverInstructions.trim();
const result = await window.ade.prs.aiResolutionStart({
model: displayModelId,
reasoning: normalizeReasoning(displayReasoning),
permissionMode: displayPermissionMode,
context,
...(trimmedInstructions ? { additionalInstructions: trimmedInstructions } : {}),
});
if (result.status !== "started") {
setStatus("failed");
Expand Down Expand Up @@ -208,6 +218,7 @@ export function PrAiResolverPanel({
displayReasoning,
launching,
onStarted,
resolverInstructions,
upsertResolverSession,
]);

Expand Down Expand Up @@ -262,6 +273,21 @@ export function PrAiResolverPanel({

{expanded ? (!sessionId ? (
<div className="space-y-4 px-4 py-4">
{showResolverInstructions ? (
<div className="space-y-1.5">
<label className="block font-mono text-[9px] font-bold uppercase tracking-wider text-fg/50">
Resolver instructions (optional)
</label>
<textarea
value={resolverInstructions}
onChange={(e) => setResolverInstructions(e.target.value)}
placeholder="e.g. Prefer UI from the first lane; keep server logic from the second."
rows={3}
disabled={launching}
className="w-full resize-y border border-border/20 bg-bg/60 px-3 py-2 font-mono text-[11px] leading-relaxed text-fg/85 placeholder:text-fg/35 focus:border-border/40 focus:outline-none"
/>
</div>
) : null}
<div className="flex flex-wrap items-center gap-3">
<PrResolverLaunchControls
modelId={displayModelId}
Expand Down
Loading