Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
6 changes: 5 additions & 1 deletion apps/desktop/src/main/services/conflicts/conflictService.ts
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
192 changes: 169 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,7 @@ describe("kvDb mission schema migration", () => {
"integration_lane_name",
"status",
"integration_lane_id",
"preferred_integration_lane_id",
"resolution_state_json",
"pairwise_results_json",
"lane_summaries_json"
Expand Down
1 change: 1 addition & 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,7 @@ 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 {}

// Queue landing state table (crash recovery for sequential landing)
db.run(`
Expand Down
53 changes: 53 additions & 0 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,6 +792,7 @@ 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...");
Expand Down Expand Up @@ -812,6 +821,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 +1521,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