Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
14 changes: 5 additions & 9 deletions apps/ade-cli/src/adeRpcServer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2459,21 +2459,17 @@ describe("adeRpcServer", () => {
closeLinearIssueOnMerge: true,
});

const drafted = await callTool(handler, "create_pr_from_lane", {
const defaulted = await callTool(handler, "create_pr_from_lane", {
laneId: "lane-1",
baseBranch: "main",
});
expect(drafted?.isError).toBeUndefined();
expect(fixture.runtime.prService.draftDescription).toHaveBeenCalledWith({
laneId: "lane-1",
baseBranch: "main",
closeLinearIssueOnMerge: true,
});
expect(defaulted?.isError).toBeUndefined();
expect(fixture.runtime.prService.draftDescription).not.toHaveBeenCalled();
expect(fixture.runtime.prService.createFromLane).toHaveBeenLastCalledWith({
laneId: "lane-1",
baseBranch: "main",
title: "Drafted PR",
body: "Drafted body",
title: "Lane 1 -> main",
body: "",
draft: false,
closeLinearIssueOnMerge: true,
});
Expand Down
42 changes: 32 additions & 10 deletions apps/ade-cli/src/adeRpcServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -979,7 +979,7 @@ const TOOL_SPECS: ToolSpec[] = [
},
{
name: "create_pr_from_lane",
description: "Create a PR from a lane branch. Drafts a title/body from ADE context when omitted. Returns GitHub and ADE PR URLs when available.",
description: "Create a PR from a lane branch. When omitted, the title defaults to \"source lane -> target lane\" and the body is empty. Returns GitHub and ADE PR URLs when available.",
inputSchema: {
type: "object",
required: ["laneId"],
Expand Down Expand Up @@ -2019,6 +2019,35 @@ function resolveLaneWorktreePath(runtime: AdeRuntime, laneId: string | null | un
return null;
}

function branchNameForPrTitle(ref: string | null | undefined): string {
let value = (ref ?? "").trim();
value = value.replace(/^refs\/heads\//, "");
value = value.replace(/^refs\/remotes\//, "");
value = value.replace(/^origin\//, "");
return value;
}

async function defaultPrTitleForLane(runtime: AdeRuntime, laneId: string, baseBranch?: string | null): Promise<string> {
const lanes = await runtime.laneService.list({ includeArchived: false, includeStatus: false }).catch(() => []);
const sourceLane = lanes.find((lane) => lane.id === laneId) ?? null;
const laneInfo = (() => {
try {
return typeof runtime.laneService.getLaneBaseAndBranch === "function"
? runtime.laneService.getLaneBaseAndBranch(laneId)
: null;
} catch {
return null;
}
})();
const sourceName = asOptionalTrimmedString(sourceLane?.name) || laneId;
const targetBranch = branchNameForPrTitle(baseBranch || sourceLane?.baseRef || laneInfo?.baseRef || runtime.project?.baseRef || "main");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Resolve stacked-lane targets before titling PRs

When create_pr_from_lane is called without an explicit baseBranch for a stacked lane, this defaults the title from sourceLane.baseRef before considering the parent lane. The actual PR target later defaults in prService.createFromLane via the parent-aware resolveStableLaneBaseBranch, so a stacked child whose stored baseRef is still the primary branch gets a title like Child -> Primary while the PR is created into the parent lane. Use the same parent-aware target resolution here before passing the default title to PR creation.

Useful? React with 👍 / 👎.

const targetLane = targetBranch
? lanes.find((lane) => lane.id !== laneId && branchNameForPrTitle(lane.branchRef) === targetBranch)
: null;
const targetName = asOptionalTrimmedString(targetLane?.name) || targetBranch || "target";
return `${sourceName} -> ${targetName}`;
}

function buildAdeInlineGuidanceForLane(laneWorktreePath: string | null | undefined): string {
return buildAdeCliInlineGuidance(getAdeAgentSkillRootsForPrompt({ cwd: laneWorktreePath ?? undefined }));
}
Expand Down Expand Up @@ -4469,15 +4498,8 @@ async function runTool(args: {
let title = asOptionalTrimmedString(toolArgs.title);
let body = typeof toolArgs.body === "string" ? toolArgs.body : null;
const closeLinearIssueOnMerge = asBoolean(toolArgs.closeLinearIssueOnMerge, true);
if (!title || body == null) {
const draft = await prSvc.draftDescription({
laneId,
...(baseBranch ? { baseBranch } : {}),
...(closeLinearIssueOnMerge ? { closeLinearIssueOnMerge } : {}),
});
title = title || asOptionalTrimmedString(draft.title) || `PR for ${laneId}`;
body = body ?? asOptionalTrimmedString(draft.body) ?? "";
}
if (!title) title = await defaultPrTitleForLane(runtime, laneId, baseBranch);
if (body == null) body = "";
const draft = asBoolean(toolArgs.draft, false);
const pr = await prSvc.createFromLane({
laneId,
Expand Down
11 changes: 11 additions & 0 deletions apps/ade-cli/src/tuiClient/__tests__/ChatView.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
renderChatVisibleSelectionRows,
renderChatVisibleSelectionRowsFromRows,
selectedTextFromChatRows,
workFileDiffKey,
workGroupExpandKey,
} from "../components/ChatView";
import { aggregateChatBlocks } from "../aggregate";
Expand Down Expand Up @@ -1240,6 +1241,16 @@ describe("ChatView", () => {
const expanded = renderEvents(events, { width: 120, expanded: true });
expect(expanded).toContain("early.ts");
expect(expanded).toContain("recent.ts");
expect(expanded).toContain("diff");

const rows = renderChatVisibleSelectionRows({
events,
notices: [],
activeSession: session,
width: 120,
expandedLineIds: new Set([workGroupExpandKey(chatEventLineId(events[0]!, 0))]),
});
expect(rows.some((row) => row.actionId === workFileDiffKey(chatEventLineId(events[0]!, 0), "f1"))).toBe(true);
});

it("tags a collapsed work-group header with an expandable click-target id", () => {
Expand Down
1 change: 1 addition & 0 deletions apps/ade-cli/src/tuiClient/__tests__/aggregate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ describe("aggregateChatBlocks typed groups", () => {
kind: "modify",
additions: 1,
deletions: 1,
diff: "+added line\n-removed line",
status: "ok",
});
expect(fileGroup!.entries[1]).toMatchObject({
Expand Down
3 changes: 3 additions & 0 deletions apps/ade-cli/src/tuiClient/aggregate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export type FileChangeEntry = {
status: WorkToolStatus;
additions: number;
deletions: number;
diff: string;
deleted?: boolean;
};

Expand Down Expand Up @@ -282,6 +283,7 @@ function appendFileChangeEvent(
existing.kind = event.kind;
existing.additions = additions;
existing.deletions = deletions;
existing.diff = event.diff;
if (deleted) existing.deleted = true;
return;
}
Expand All @@ -292,6 +294,7 @@ function appendFileChangeEvent(
status,
additions,
deletions,
diff: event.diff,
};
if (deleted) entry.deleted = true;
block.entries.push(entry);
Expand Down
78 changes: 67 additions & 11 deletions apps/ade-cli/src/tuiClient/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ import {
} from "./newLaneForm";
import {
ChatView,
workFileDiffKey,
chatScrollMaxOffsetFromSelectableRows,
hasConversationContent,
renderChatSelectableRows,
Expand Down Expand Up @@ -1132,6 +1133,24 @@ function reparentTargetsForLane(lane: LaneSummary, lanes: LaneSummary[]): LaneSu
});
}

function prBranchNameFromRef(ref: string | null | undefined): string {
let value = (ref ?? "").trim();
value = value.replace(/^refs\/heads\//, "");
value = value.replace(/^refs\/remotes\//, "");
value = value.replace(/^origin\//, "");
return value;
}

function defaultPrTitleForLane(sourceLane: LaneSummary | null | undefined, lanes: LaneSummary[]): string {
const sourceName = sourceLane?.name?.trim() || "Source lane";
const targetBranch = prBranchNameFromRef(sourceLane?.baseRef);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Use the stable PR base for TUI default titles

For stacked lanes, sourceLane.baseRef can still be the primary base (for example main) while PR creation targets the parent lane branch via resolveStableLaneBaseBranch in prService.createFromLane. Because /pr open now pre-fills and submits this required title, opening a PR from a child lane can create a correctly based PR with a misleading child -> main title instead of child -> parent. Derive this from the same stable base/parent-lane logic used by PR creation.

Useful? React with 👍 / 👎.

const targetLane = targetBranch
? lanes.find((lane) => lane.id !== sourceLane?.id && prBranchNameFromRef(lane.branchRef) === targetBranch)
: null;
const targetName = targetLane?.name?.trim() || targetBranch || "target";
return `${sourceName} -> ${targetName}`;
}

function resolveLaneReference(lanes: LaneSummary[], reference: string): LaneSummary | null {
const normalized = reference.trim().toLowerCase();
if (!normalized) return null;
Expand Down Expand Up @@ -4555,6 +4574,10 @@ export function AdeCodeApp({ project, forceEmbedded, requireSocket, socketPath,
}),
[activeSession, displayEvents, displayNotices, displayPendingSteers, expandedLineIds],
);
const displayBlocksRef = useRef<AggregatedBlock[]>([]);
useEffect(() => {
displayBlocksRef.current = displayBlocks;
}, [displayBlocks]);
const displayStreaming = selectedAgentSnapshot ? selectedAgentSnapshot.status === "running" : streaming;
const displayInterrupted = selectedAgentSnapshot ? false : interrupted && !displayStreaming;
useEffect(() => {
Expand Down Expand Up @@ -9203,7 +9226,7 @@ export function AdeCodeApp({ project, forceEmbedded, requireSocket, socketPath,
setRightPane({
kind: "details",
title: "PR",
body: `No PR is linked to this lane yet.\n${ahead > 0 ? `${ahead} commit${ahead === 1 ? "" : "s"} ahead of base.\n` : ""}Run /pr open <title> to create a draft.`,
body: `No PR is linked to this lane yet.\n${ahead > 0 ? `${ahead} commit${ahead === 1 ? "" : "s"} ahead of base.\n` : ""}Run /pr open to create a pull request.`,
});
return;
}
Expand Down Expand Up @@ -9241,12 +9264,13 @@ export function AdeCodeApp({ project, forceEmbedded, requireSocket, socketPath,
return;
}
if (!args) {
const defaultTitle = defaultPrTitleForLane(activeLane, lanes);
openForm({
kind: "form",
title: "Open PR",
command: "pr-open",
fields: [
{ name: "title", label: "Title", required: true, placeholder: activeLane?.name ?? "Draft PR" },
{ name: "title", label: "Title", required: true, placeholder: defaultTitle, initialValue: defaultTitle },
{ name: "body", label: "Body", placeholder: "Optional" },
],
});
Expand All @@ -9256,7 +9280,7 @@ export function AdeCodeApp({ project, forceEmbedded, requireSocket, socketPath,
laneId,
title: args,
body: "",
draft: true,
draft: false,
});
setRightPane({ kind: "details", title: "PR open", body: formatPrSummary(created) });
return;
Expand Down Expand Up @@ -10059,10 +10083,10 @@ export function AdeCodeApp({ project, forceEmbedded, requireSocket, socketPath,
laneId,
title,
body,
draft: true,
draft: false,
});
setRightPane({ kind: "details", title: "PR open", body: renderObject(created, 24) });
addNotice("Created draft PR.", "success");
addNotice("Created PR.", "success");
await refreshState();
}

Expand Down Expand Up @@ -11645,10 +11669,10 @@ export function AdeCodeApp({ project, forceEmbedded, requireSocket, socketPath,
// file changes), if the click landed on one. Mirrors chatPointFromMouse's
// viewport math but returns the row's expandableId instead of a text point so
// a plain click can toggle the group's collapse state.
const expandableGroupIdFromMouse = useCallback((
const chatRowTargetFromMouse = useCallback((
x: number | null,
y: number | null,
): string | null => {
): { expandableId: string | null; actionId: string | null } | null => {
if (x == null || y == null) return null;
const drawerWidth = resolveDrawerPaneWidth(columns, drawerOpen);
const textStartColumn = drawerWidth + 2;
Expand All @@ -11657,9 +11681,35 @@ export function AdeCodeApp({ project, forceEmbedded, requireSocket, socketPath,
const bottomRow = topRow + Math.max(1, chatRowBudget) - 1;
if (x < textStartColumn || x > textEndColumn || y < topRow || y > bottomRow) return null;
const visibleRow = Math.max(0, Math.min(y - topRow, Math.max(0, chatRowBudget - 1)));
return visibleChatSelectionRows[visibleRow]?.expandableId ?? null;
const row = visibleChatSelectionRows[visibleRow];
if (!row) return null;
return {
expandableId: row.expandableId ?? null,
actionId: row.actionId ?? null,
};
}, [addModeRows, chatRowBudget, chatWrapWidth, columns, drawerOpen, goalBannerRows, visibleChatSelectionRows]);

const openFileChangeDiffAction = useCallback((actionId: string): boolean => {
for (const block of displayBlocksRef.current) {
if (block.kind !== "files-changed-group") continue;
const selected = block.entries.find((entry) => workFileDiffKey(block.id, entry.itemId) === actionId);
if (!selected) continue;
const files = block.entries.map((entry) => ({
path: entry.path,
additions: entry.additions,
deletions: entry.deletions,
body: entry.diff,
}));
const title = block.entries.length === 1 ? selected.path : "This turn";
setRightPane({ kind: "diff", title, files });
setRightOpen(true);
lastUserOpenedPaneRef.current = "diff";
focusDetailsOnly();
return true;
}
return false;
}, [focusDetailsOnly]);

const toggleExpandedLineId = useCallback((lineId: string) => {
setExpandedLineIds((prev) => {
const next = new Set(prev);
Expand Down Expand Up @@ -11944,13 +11994,19 @@ export function AdeCodeApp({ project, forceEmbedded, requireSocket, socketPath,
// ▸ Files changed (N)) toggles it open/closed instead of starting a text
// selection. Shift-click still extends a selection across the header.
if (!mouse.shift) {
const groupId = expandableGroupIdFromMouse(mouse.x, mouse.y);
if (groupId) {
const chatRowTarget = chatRowTargetFromMouse(mouse.x, mouse.y);
if (chatRowTarget?.actionId && openFileChangeDiffAction(chatRowTarget.actionId)) {
stopChatSelectionEdgeScroll();
chatSelectionAnchorRef.current = null;
if (activeSelection) updateChatMouseSelection(null);
return;
}
if (chatRowTarget?.expandableId) {
stopChatSelectionEdgeScroll();
chatSelectionAnchorRef.current = null;
if (activeSelection) updateChatMouseSelection(null);
focusChat();
toggleExpandedLineId(groupId);
toggleExpandedLineId(chatRowTarget.expandableId);
return;
}
}
Expand Down
13 changes: 13 additions & 0 deletions apps/ade-cli/src/tuiClient/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ type RenderedChatRow = {
* this id and toggles it in `expandedLineIds` to collapse/expand the group.
*/
expandableGroupId?: string;
/** Click action for rows that open another pane instead of toggling in place. */
actionId?: string;
};

export type ChatTextSelection = {
Expand All @@ -92,6 +94,7 @@ export type ChatVisibleSelectionRow = {
* RenderedChatRow.expandableGroupId). Lets the click handler toggle the
* group without re-deriving block layout. */
expandableId?: string | null;
actionId?: string | null;
};

// Expansion keys for collapsible work-log groups (tool calls / file changes)
Expand All @@ -102,6 +105,11 @@ export function workGroupExpandKey(blockId: string): string {
return `${WORK_GROUP_EXPAND_PREFIX}${blockId}`;
}

export const WORK_FILE_DIFF_PREFIX = "workfilediff:";
export function workFileDiffKey(blockId: string, itemId: string): string {
return `${WORK_FILE_DIFF_PREFIX}${encodeURIComponent(blockId)}:${encodeURIComponent(itemId)}`;
}

function textWidth(value: string): number {
return terminalDisplayWidth(value);
}
Expand Down Expand Up @@ -823,13 +831,16 @@ function fileChangeEntryRow(
{ text: trimmedPath, color: theme.color.t1 },
{ text: " " },
{ text: stats, color: statsColor },
{ text: " " },
{ text: "diff", color: theme.color.t4 },
];
return {
id: `${blockId}:${entry.itemId}`,
tone: "work",
text: runsPlainText(runs),
runs,
rail: null,
actionId: workFileDiffKey(blockId, entry.itemId),
};
}

Expand Down Expand Up @@ -1551,6 +1562,7 @@ export function renderChatVisibleSelectionRowsFromRows({
sourceRow: typeof row.sourceRowIndex === "number" ? row.sourceRowIndex : null,
text: renderedRowText(row),
expandableId: row.expandableGroupId ?? null,
actionId: row.actionId ?? null,
}));
}

Expand Down Expand Up @@ -1690,6 +1702,7 @@ export function renderChatVisibleSelectionRows({
sourceRow: typeof row.sourceRowIndex === "number" ? row.sourceRowIndex : null,
text: renderedRowText(row),
expandableId: row.expandableGroupId ?? null,
actionId: row.actionId ?? null,
}));
}

Expand Down
2 changes: 1 addition & 1 deletion apps/ade-cli/src/tuiClient/components/RightPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ export const LANE_DETAIL_ACTIONS: ReadonlyArray<{
intent?: "rescue-unstaged";
}> = [
{ k: "n", label: "new chat", slashCommand: "/new chat", glyph: "✦", glyphColorKind: "additive" },
{ k: "o", label: "open / create PR", slashCommand: "/pr open", detail: "draft when missing", glyph: "↗", glyphColorKind: "navigation" },
{ k: "o", label: "open / create PR", slashCommand: "/pr open", detail: "create when missing", glyph: "↗", glyphColorKind: "navigation" },
{ k: "a", label: "stage all", slashCommand: "/stage all", glyph: "+", glyphColorKind: "additive" },
{
k: "u",
Expand Down
Loading