Skip to content

Commit d1b3b41

Browse files
arul28cursoragentclaude
authored
ADE-29: Reorganize lane stack from Manage Lane (#311)
* feat(lanes): manage lane stack position and optional reparent base branch - Extend lanes.reparent with stackBaseBranchRef (resolved in repo, prefers origin). - Skip no-op reparent when parent link and base_ref are unchanged. - Manage Lane dialog: parent lane selector, optional base branch, apply runs reparent. - Parse stackBaseBranchRef for remote lanes.reparent in ade-cli sync. ADE-29 Co-authored-by: Arul Sharma <arul28@users.noreply.github.com> * docs(ui): disclose git rebase when applying stack change in Manage Lane Co-authored-by: Arul Sharma <arul28@users.noreply.github.com> * fix(ui): responsive lane modals and richer Manage Lane UX - LaneDialogShell: viewport-centered, max-height, scrollable body (all lane modals). - laneDialogTokens: hero and accent section styles for lane dialogs. - Manage Lane: shell description, per-section guidance, batch vs single callouts, adopt/archive/delete copy, stack accent panel and select styling. ADE-29 Co-authored-by: Arul Sharma <arul28@users.noreply.github.com> * ship: continuation of ADE-29 lane management work Picks up uncommitted lane work across desktop, ade-cli, ios, and docs: ManageLaneDialog refactor (singleLane), chat pane turn-active error handling, iOS sync command helpers (lanes.reparent), sync host / remote command updates, project transition state, PR detail/route state cleanup, and broad doc refreshes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ship: iter 2 — address 12 review comments (Greptile/CodeRabbit/Copilot) Greptile P1/P2 + CodeRabbit 10 + Copilot review-only on commit 56a8469. - ManageLaneDialog + LaneManageSheet (iOS): normalize baseChanged compare (strip refs/heads/, origin/), and treat empty input as a change when an existing override is set so users can clear the base override. - ptyService: zero out recentOutputTail in closeEntry + dispose to release up-to-2MB tail buffer when a session ends. - ptyService.test: add coverage for disk-only readTranscriptTail and stripAnsi invocation on merged tail. - ade-cli app.tsx: require trimmed summary length > 0; reject ambiguous partial lane reference in resolveLaneReference; clear interrupted state when sessionFound === false. - registerIpc: keep in-flight projectGetDetail entries non-expiring until the promise settles, then start TTL from resolve time. - preload: do not null rememberProjectBinding inside clearAround's twice- running cleanup for openRepo / switchToPath. - chatExecutionSummary: preserve merged taskId on subagent_progress / subagent_result so timeline doesn't collapse to the latest entry. - PrsRootScreen (iOS): guard concurrent external history fetches behind isLoadingExternalHistory flag to remove race on filter change. addressed: 3246905124 3246905210 3246917318 3246917328 3246917331 3246917349 3246917363 3246917372 3246917378 3246917382 3246917386 3246917389 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ship: iter 3 — fix Apply-on-open regression (Greptile P1) Iter 2's baseChanged/reparentBaseChanged change made Apply enable on initial dialog open whenever lane.baseRef was non-empty, because lane.baseRef is a non-nullable string. That could fire an unintended rebase on an unsuspecting click. Now the empty-override branch flips true only when lane.baseRef actually diverges from the selected parent's default branch (compared with the existing normalizeBranchRefForCompare helper). Three states: - empty override, lane.baseRef == default → baseChanged false - empty override, lane.baseRef != default → baseChanged true (clear) - non-empty override differing from existing → baseChanged true Same fix applied to apps/ios/ADE/Views/Lanes/LaneManageSheet.swift. addressed: 3247162053 3247162141 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ship: iter 4 — exempt read-only chat from transition guard (CodeRabbit Major) callProjectRuntimeActionIfBound previously threw for every chat-domain call when projectRuntimeTransitionDepth > 0, including reads like list, getSummary, getAvailableModels, and getEventHistory. That blocked the UI from rendering chat summaries/history during a project switch. Now only state-changing chat actions (MUTATING_CHAT_ACTIONS allowlist: sendMessage, respondToInput, approveToolUse, interrupt, steer family, session lifecycle, ensureCtoSession, ensureAgentIdentitySession, etc.) throw the transition error. Read-only chat actions return {handled:false} so the caller falls through to the IPC-backed read APIs. Added preload.test coverage for read-only chat falling through to IPC during an in-flight switchToPath. addressed: 3247309433 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Arul Sharma <arul28@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 9eccaab commit d1b3b41

58 files changed

Lines changed: 3576 additions & 918 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/ade-cli/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,7 @@ ade init
229229
ade lanes list --text
230230
ade lanes create "fix-checkout-flow" --parent main
231231
ade lanes create "lin-123" --linear-issue-json '{"id":"...","identifier":"LIN-123","title":"...","projectId":"...","projectSlug":"...","teamId":"...","teamKey":"...","stateId":"...","stateName":"Todo","stateType":"unstarted","priority":2,"priorityLabel":"high","labels":[],"assigneeId":null,"assigneeName":null,"createdAt":"...","updatedAt":"..."}'
232+
ade lanes reparent lane-child --parent lane-parent --stack-base-branch main
232233
ade --role cto linear quick-view --text
233234
ade --role cto linear search-issues --query "auth" --state-type started,unstarted --first 50
234235
ade git commit --lane lane-id
@@ -239,6 +240,7 @@ ade diff patch --lane lane-id --path src/file.ts --text
239240
ade prs create --lane lane-id --base main --title "Fix checkout flow"
240241
ade prs create --lane lane-id --base main --close-linear-issue-on-merge
241242
ade prs list-open --text
243+
ade prs github-snapshot --include-external-closed
242244
ade prs path-to-merge --pr pr-id --model gpt-5.5 --max-rounds 3 --no-auto-merge
243245
ade prs path-to-merge --pr pr-id --model gpt-5.5 --conflict-strategy auto --force-finalize conditional
244246
ade prs pipeline pr-id save --conflict-strategy rebase --no-early-merge-on-green

apps/ade-cli/src/adeRpcServer.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,7 @@ function createRuntime() {
337337
void data;
338338
return true;
339339
}),
340+
readTranscriptTail: vi.fn(async () => ""),
340341
enrichSessions: vi.fn((sessions: unknown[]) => sessions),
341342
},
342343
testService: {

apps/ade-cli/src/adeRpcServer.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3592,9 +3592,11 @@ async function waitForSessionCompletion(args: {
35923592
while (Date.now() <= deadline) {
35933593
const session = runtime.sessionService.get(sessionId);
35943594
if (session && session.status !== "running") {
3595-
const logTail = runtime.sessionService.readTranscriptTail(session.transcriptPath, maxLogBytes, {
3595+
const logTail = await runtime.ptyService.readTranscriptTail({
3596+
sessionId,
3597+
maxBytes: maxLogBytes,
35963598
raw: true,
3597-
alignToLineBoundary: true
3599+
alignToLineBoundary: true,
35983600
});
35993601
return {
36003602
session,
@@ -3610,9 +3612,11 @@ async function waitForSessionCompletion(args: {
36103612
session,
36113613
timedOut: true,
36123614
logTail: session
3613-
? runtime.sessionService.readTranscriptTail(session.transcriptPath, maxLogBytes, {
3615+
? await runtime.ptyService.readTranscriptTail({
3616+
sessionId,
3617+
maxBytes: maxLogBytes,
36143618
raw: true,
3615-
alignToLineBoundary: true
3619+
alignToLineBoundary: true,
36163620
})
36173621
: ""
36183622
};

apps/ade-cli/src/cli.test.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1502,6 +1502,74 @@ describe("ADE CLI", () => {
15021502
});
15031503
});
15041504

1505+
it("forwards lane reparent stack base branch override to the runtime action", () => {
1506+
const reparent = buildCliPlan([
1507+
"lanes",
1508+
"reparent",
1509+
"lane-child",
1510+
"--parent",
1511+
"lane-parent",
1512+
"--stack-base-branch",
1513+
"develop",
1514+
]);
1515+
expect(reparent.kind).toBe("execute");
1516+
if (reparent.kind !== "execute") return;
1517+
expect(reparent.steps[0]?.params).toEqual({
1518+
name: "run_ade_action",
1519+
arguments: {
1520+
domain: "lane",
1521+
action: "reparent",
1522+
args: {
1523+
laneId: "lane-child",
1524+
newParentLaneId: "lane-parent",
1525+
stackBaseBranchRef: "develop",
1526+
},
1527+
},
1528+
});
1529+
1530+
const reparentDefault = buildCliPlan([
1531+
"lanes",
1532+
"reparent",
1533+
"lane-child",
1534+
"--parent",
1535+
"lane-parent",
1536+
]);
1537+
expect(reparentDefault.kind).toBe("execute");
1538+
if (reparentDefault.kind !== "execute") return;
1539+
expect(reparentDefault.steps[0]?.params).toEqual({
1540+
name: "run_ade_action",
1541+
arguments: {
1542+
domain: "lane",
1543+
action: "reparent",
1544+
args: {
1545+
laneId: "lane-child",
1546+
newParentLaneId: "lane-parent",
1547+
},
1548+
},
1549+
});
1550+
});
1551+
1552+
it("forwards PR GitHub snapshot full-history flag to the runtime action", () => {
1553+
const snapshot = buildCliPlan([
1554+
"prs",
1555+
"github-snapshot",
1556+
"--include-external-closed",
1557+
]);
1558+
expect(snapshot.kind).toBe("execute");
1559+
if (snapshot.kind !== "execute") return;
1560+
expect(snapshot.steps[0]?.params).toEqual({
1561+
name: "run_ade_action",
1562+
arguments: {
1563+
domain: "pr",
1564+
action: "getGithubSnapshot",
1565+
args: {
1566+
force: false,
1567+
includeExternalClosed: true,
1568+
},
1569+
},
1570+
});
1571+
});
1572+
15051573
it("maps discoverable git status, sync, and conflict helpers to existing actions", () => {
15061574
const fullStatus = buildCliPlan([
15071575
"git",
@@ -1854,6 +1922,16 @@ describe("ADE CLI", () => {
18541922
// Regression: --text as output flag must not swallow --help.
18551923
const lanesHelp = buildCliPlan(["lanes", "list", "--text", "--help"]);
18561924
expect(lanesHelp.kind).toBe("help");
1925+
1926+
const reparentHelp = buildCliPlan([
1927+
"lanes",
1928+
"reparent",
1929+
"lane-child",
1930+
"--stack-base-branch",
1931+
"develop",
1932+
"--help",
1933+
]);
1934+
expect(reparentHelp.kind).toBe("help");
18571935
});
18581936

18591937
it("maps PR create Linear close flag to the typed RPC tool", () => {

apps/ade-cli/src/cli.ts

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -882,6 +882,9 @@ const HELP_BY_COMMAND: Record<string, string> = {
882882
$ ade lanes archive <lane> Archive a lane in ADE
883883
$ ade lanes unarchive <lane> Restore an archived lane
884884
$ ade lanes attach --path <worktree> --name <n> Attach an external worktree
885+
$ ade lanes reparent <lane> --parent <parent> Move lane onto a new parent (runs git rebase)
886+
$ ade lanes reparent <lane> --parent <parent> --stack-base-branch <branch>
887+
Reparent and stack onto a specific branch (e.g. origin/main)
885888
$ ade lanes actions --text List callable lane service methods
886889
`,
887890
git: `${ADE_BANNER}
@@ -947,6 +950,8 @@ const HELP_BY_COMMAND: Record<string, string> = {
947950
$ ade prs link --lane <lane> --url <pr-url> Map an existing GitHub PR to a lane
948951
$ ade prs checks <pr> --text Show check status
949952
$ ade prs comments <pr> --text Show unresolved review work
953+
$ ade prs github-snapshot --include-external-closed
954+
Include closed external PR history in the GitHub snapshot
950955
$ ade prs inventory <pr> Refresh ADE issue inventory
951956
$ ade prs path-to-merge <pr> --model <model> --max-rounds 3 --no-auto-merge
952957
$ ade prs path-to-merge <pr> --model <model> --conflict-strategy auto --force-finalize conditional
@@ -2478,6 +2483,23 @@ function buildLanePlan(args: string[]): CliPlan {
24782483
readLaneId(args) ?? firstPositional(args),
24792484
"laneId",
24802485
);
2486+
const reparentArgs: JsonObject = {
2487+
laneId,
2488+
newParentLaneId:
2489+
readValue(args, [
2490+
"--parent",
2491+
"--parent-lane",
2492+
"--parent-lane-id",
2493+
]) ?? firstPositional(args),
2494+
};
2495+
const stackBaseBranchRef = readValue(args, [
2496+
"--stack-base-branch",
2497+
"--stack-base",
2498+
"--base-branch-ref",
2499+
]);
2500+
if (stackBaseBranchRef != null) {
2501+
reparentArgs.stackBaseBranchRef = stackBaseBranchRef;
2502+
}
24812503
return {
24822504
kind: "execute",
24832505
label: "lane reparent",
@@ -2486,15 +2508,7 @@ function buildLanePlan(args: string[]): CliPlan {
24862508
"result",
24872509
"lane",
24882510
"reparent",
2489-
collectGenericObjectArgs(args, {
2490-
laneId,
2491-
newParentLaneId:
2492-
readValue(args, [
2493-
"--parent",
2494-
"--parent-lane",
2495-
"--parent-lane-id",
2496-
]) ?? firstPositional(args),
2497-
}),
2511+
collectGenericObjectArgs(args, reparentArgs),
24982512
),
24992513
],
25002514
};
@@ -3628,7 +3642,13 @@ function buildPrPlan(args: string[]): CliPlan {
36283642
label: "PR mobile snapshot",
36293643
steps: [actionArgsListStep("result", "pr", "getMobileSnapshot", [])],
36303644
};
3631-
if (sub === "github-snapshot")
3645+
if (sub === "github-snapshot") {
3646+
const snapshotArgs: JsonObject = {
3647+
force: readFlag(args, ["--force"]),
3648+
};
3649+
if (readFlag(args, ["--include-external-closed", "--include-closed-external"])) {
3650+
snapshotArgs.includeExternalClosed = true;
3651+
}
36323652
return {
36333653
kind: "execute",
36343654
label: "PR GitHub snapshot",
@@ -3637,12 +3657,11 @@ function buildPrPlan(args: string[]): CliPlan {
36373657
"result",
36383658
"pr",
36393659
"getGithubSnapshot",
3640-
collectGenericObjectArgs(args, {
3641-
force: readFlag(args, ["--force"]),
3642-
}),
3660+
collectGenericObjectArgs(args, snapshotArgs),
36433661
),
36443662
],
36453663
};
3664+
}
36463665
if (sub === "conflicts") {
36473666
const mode = firstPositional(args) ?? "list";
36483667
if (mode === "list")
@@ -8312,6 +8331,7 @@ const VALUE_CARRIER_FLAGS: ReadonlySet<string> = new Set([
83128331
"--backend",
83138332
"--base",
83148333
"--base-branch",
8334+
"--base-branch-ref",
83158335
"--base-ref",
83168336
"--body",
83178337
"--branch",
@@ -8449,6 +8469,8 @@ const VALUE_CARRIER_FLAGS: ReadonlySet<string> = new Set([
84498469
"--source",
84508470
"--source-lane",
84518471
"--stack",
8472+
"--stack-base",
8473+
"--stack-base-branch",
84528474
"--stack-id",
84538475
"--scheme",
84548476
"--start-point",

apps/ade-cli/src/services/sync/syncHostService.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,7 @@ function createHostArgs(projectRoot: string, projects: SyncMobileProjectSummary[
292292
},
293293
ptyService: {
294294
create: vi.fn(),
295+
readTranscriptTail: vi.fn(async () => ""),
295296
enrichSessions: (rows: unknown[]) => rows,
296297
},
297298
computerUseArtifactBrokerService: {

apps/ade-cli/src/services/sync/syncHostService.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2830,11 +2830,12 @@ export function createSyncHostService(args: SyncHostServiceArgs) {
28302830
peer.subscribedSessionIds.add(sessionId);
28312831
const session = args.sessionService.get(sessionId);
28322832
const transcript = session
2833-
? await args.sessionService.readTranscriptTail(
2834-
session.transcriptPath,
2835-
Math.max(1_024, Math.min(2_000_000, Math.floor(payload?.maxBytes ?? DEFAULT_TERMINAL_SNAPSHOT_BYTES))),
2836-
{ raw: true, alignToLineBoundary: true },
2837-
)
2833+
? await args.ptyService.readTranscriptTail({
2834+
sessionId,
2835+
maxBytes: Math.max(1_024, Math.min(2_000_000, Math.floor(payload?.maxBytes ?? DEFAULT_TERMINAL_SNAPSHOT_BYTES))),
2836+
raw: true,
2837+
alignToLineBoundary: true,
2838+
})
28382839
: "";
28392840
const snapshot: SyncTerminalSnapshotPayload = {
28402841
sessionId,

apps/ade-cli/src/services/sync/syncRemoteCommandService.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -404,9 +404,11 @@ function parseRenameLaneArgs(value: Record<string, unknown>): RenameLaneArgs {
404404
}
405405

406406
function parseReparentLaneArgs(value: Record<string, unknown>): ReparentLaneArgs {
407+
const stackBaseBranchRef = asTrimmedString(value.stackBaseBranchRef);
407408
return {
408409
laneId: requireString(value.laneId, "lanes.reparent requires laneId."),
409410
newParentLaneId: requireString(value.newParentLaneId, "lanes.reparent requires newParentLaneId."),
411+
...(stackBaseBranchRef ? { stackBaseBranchRef } : {}),
410412
};
411413
}
412414

@@ -2333,7 +2335,10 @@ export function createSyncRemoteCommandService(args: SyncRemoteCommandServiceArg
23332335
register("prs.getComments", { viewerAllowed: true }, async (payload) => args.prService.getComments(requirePrId(payload, "prs.getComments")));
23342336
register("prs.getFiles", { viewerAllowed: true }, async (payload) => args.prService.getFiles(requirePrId(payload, "prs.getFiles")));
23352337
register("prs.getGitHubSnapshot", { viewerAllowed: true }, async (payload) =>
2336-
args.prService.getGithubSnapshot({ force: payload.force === true }));
2338+
args.prService.getGithubSnapshot({
2339+
force: payload.force === true,
2340+
includeExternalClosed: payload.includeExternalClosed === true,
2341+
}));
23372342
register("prs.getReviewThreads", { viewerAllowed: true }, async (payload) => args.prService.getReviewThreads(requirePrId(payload, "prs.getReviewThreads")));
23382343
register("prs.getActionRuns", { viewerAllowed: true }, async (payload) => args.prService.getActionRuns(requirePrId(payload, "prs.getActionRuns")));
23392344
register("prs.getActivity", { viewerAllowed: true }, async (payload) => args.prService.getActivity(requirePrId(payload, "prs.getActivity")));

apps/ade-cli/src/tuiClient/__tests__/appInput.test.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,85 @@ describe("subagentSnapshotsFromEvents", () => {
174174
summary: "done",
175175
});
176176
});
177+
178+
it("adopts the resolved task id when a runtime placeholder has the same parent tool id", () => {
179+
const snapshots = subagentSnapshotsFromEvents([
180+
{
181+
sessionId: "s1",
182+
timestamp: "2026-01-01T12:00:00.000Z",
183+
sequence: 1,
184+
event: {
185+
type: "subagent_started",
186+
taskId: "spawn-1",
187+
parentToolUseId: "spawn-1",
188+
description: "Parallel agent",
189+
},
190+
},
191+
{
192+
sessionId: "s1",
193+
timestamp: "2026-01-01T12:00:01.000Z",
194+
sequence: 2,
195+
event: {
196+
type: "subagent_progress",
197+
taskId: "thread-1",
198+
parentToolUseId: "spawn-1",
199+
summary: "working",
200+
},
201+
},
202+
]);
203+
204+
expect(snapshots).toHaveLength(1);
205+
expect(snapshots[0]).toMatchObject({
206+
id: "thread-1",
207+
name: "Parallel agent",
208+
parentToolUseId: "spawn-1",
209+
status: "running",
210+
summary: "working",
211+
});
212+
});
213+
214+
it("stops foreground subagents when their parent turn has ended", () => {
215+
const snapshots = subagentSnapshotsFromEvents([
216+
{
217+
sessionId: "s1",
218+
timestamp: "2026-01-01T12:00:00.000Z",
219+
sequence: 1,
220+
event: {
221+
type: "subagent_started",
222+
taskId: "agent-1",
223+
description: "Foreground agent",
224+
turnId: "turn-1",
225+
},
226+
},
227+
{
228+
sessionId: "s1",
229+
timestamp: "2026-01-01T12:00:01.000Z",
230+
sequence: 2,
231+
event: {
232+
type: "subagent_started",
233+
taskId: "agent-bg",
234+
description: "Background agent",
235+
background: true,
236+
turnId: "turn-1",
237+
},
238+
},
239+
{
240+
sessionId: "s1",
241+
timestamp: "2026-01-01T12:00:02.000Z",
242+
sequence: 3,
243+
event: { type: "done", turnId: "turn-1", status: "completed" },
244+
},
245+
]);
246+
247+
expect(snapshots.find((snapshot) => snapshot.id === "agent-1")).toMatchObject({
248+
status: "stopped",
249+
summary: "Parent turn ended before ADE received a final subagent status",
250+
});
251+
expect(snapshots.find((snapshot) => snapshot.id === "agent-bg")).toMatchObject({
252+
status: "running",
253+
background: true,
254+
});
255+
});
177256
});
178257

179258
describe("clampChatScrollOffsetRows", () => {

0 commit comments

Comments
 (0)