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
44 changes: 44 additions & 0 deletions apps/desktop/src/main/services/pty/ptyService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -731,6 +731,50 @@ describe("ptyService", () => {
expect(sessionService.create).toHaveBeenCalledTimes(createCallsBeforeResume);
});

it("backfills a targetless Claude resume command before launching the resumed PTY", async () => {
(mocks.extractResumeCommandFromOutput as any).mockReturnValueOnce("claude --resume claude-session-123");
const { service, sessionService, mockPty } = createHarness();
sessionService.create({
sessionId: "session-claude-picker",
laneId: "lane-1",
ptyId: null,
tracked: true,
title: "Claude CLI",
startedAt: "2026-04-09T12:00:00.000Z",
transcriptPath: "/tmp/transcripts/session-claude-picker.log",
toolType: "claude",
resumeCommand: "claude --permission-mode default --resume",
resumeMetadata: {
provider: "claude",
targetKind: "session",
targetId: null,
launch: { permissionMode: "default" },
},
});
sessionService.end({
sessionId: "session-claude-picker",
endedAt: "2026-04-09T12:30:00.000Z",
exitCode: 0,
status: "completed",
});

await service.create({
sessionId: "session-claude-picker",
laneId: "lane-1",
title: "Claude CLI",
cols: 80,
rows: 24,
toolType: "claude",
startupCommand: "claude --permission-mode default --resume",
});

expect(sessionService.setResumeCommand).toHaveBeenCalledWith(
"session-claude-picker",
"claude --resume claude-session-123",
);
expect(mockPty.write).toHaveBeenCalledWith("claude --resume claude-session-123\r");
});

it("preserves the strict resume path when a requested session id does not exist", async () => {
const { service } = createHarness();

Expand Down
35 changes: 18 additions & 17 deletions apps/desktop/src/main/services/pty/ptyService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -766,7 +766,7 @@ export function createPtyService({
const tryBackfillResumeTarget = async (
sessionId: string,
preferredToolType: TerminalToolType | null,
reason: "close" | "dispose" | "orphan-dispose" | "session-list",
reason: "close" | "dispose" | "orphan-dispose" | "session-list" | "resume-launch",
sessionCwd?: string | null,
): Promise<boolean> => {
const session = sessionService.get(sessionId);
Expand Down Expand Up @@ -1104,15 +1104,15 @@ export function createPtyService({
const tracked = existingSession?.tracked ?? (args.tracked !== false);
const toolTypeHint = normalizeToolType(args.toolType ?? existingSession?.toolType ?? null);
const requestedStartupCommand = typeof args.startupCommand === "string" ? args.startupCommand.trim() : "";
const initialResumeCommand = existingSession?.resumeCommand ?? defaultResumeCommandForTool(toolTypeHint);
const initialResumeMetadata = existingSession?.resumeMetadata ?? buildInitialResumeMetadata({
let initialResumeCommand = existingSession?.resumeCommand ?? defaultResumeCommandForTool(toolTypeHint);
let initialResumeMetadata = existingSession?.resumeMetadata ?? buildInitialResumeMetadata({
toolType: toolTypeHint,
startupCommand: requestedStartupCommand,
});
const transcriptPath = tracked
? (existingSession?.transcriptPath?.trim() || safeTranscriptPathFor(sessionId))
: "";
const startupCommand = requestedStartupCommand.trim();
let startupCommand = requestedStartupCommand.trim();
const cleanupPaths: string[] = [];

let transcriptStream: fs.WriteStream | null = null;
Expand Down Expand Up @@ -1157,6 +1157,20 @@ export function createPtyService({
...(args.env ?? {})
};
const launchEnv = getAdeCliAgentEnv?.(baseLaunchEnv) ?? baseLaunchEnv;
const shouldBackfillResumeTarget =
existingSession
&& isTrackedCliToolType(toolTypeHint)
&& !existingSession.resumeMetadata?.targetId?.trim();
if (shouldBackfillResumeTarget) {
const backfilled = await tryBackfillResumeTarget(sessionId, toolTypeHint, "resume-launch", cwd);
const updatedSession = backfilled ? sessionService.get(sessionId) : null;
if (updatedSession?.resumeCommand?.trim()) {
initialResumeCommand = updatedSession.resumeCommand.trim();
initialResumeMetadata = updatedSession.resumeMetadata ?? initialResumeMetadata;
startupCommand = initialResumeCommand;
}
}
Comment on lines +1160 to +1172
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 Backfilled command drops original launch flags

When tryBackfillResumeTarget succeeds, startupCommand is overwritten with the backfilled command (e.g. claude --resume <id>). However, the original requestedStartupCommand may contain flags such as --permission-mode default that are not preserved in the backfilled command. The PTY therefore launches without those flags.

This may be intentional if permission mode is meant to come from session metadata at the tool level, but the test's expected invocation ("claude --resume claude-session-123\r") silently drops --permission-mode default that was in the startup command. Worth verifying the tool CLI picks up the correct permission mode without the explicit flag on resume.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/main/services/pty/ptyService.ts
Line: 1160-1172

Comment:
**Backfilled command drops original launch flags**

When `tryBackfillResumeTarget` succeeds, `startupCommand` is overwritten with the backfilled command (e.g. `claude --resume <id>`). However, the original `requestedStartupCommand` may contain flags such as `--permission-mode default` that are not preserved in the backfilled command. The PTY therefore launches without those flags.

This may be intentional if permission mode is meant to come from session metadata at the tool level, but the test's expected invocation (`"claude --resume claude-session-123\r"`) silently drops `--permission-mode default` that was in the startup command. Worth verifying the tool CLI picks up the correct permission mode without the explicit flag on resume.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code


const shellCandidates = resolveShellCandidates();
let pty: IPty;
let selectedShell: ShellSpec | null = null;
Expand Down Expand Up @@ -1262,19 +1276,6 @@ export function createPtyService({
.catch(() => {});
}

if (
existingSession
&& isTrackedCliToolType(toolTypeHint)
&& !existingSession.resumeMetadata?.targetId?.trim()
) {
logger.warn("pty.resume_target_missing", {
sessionId,
ptyId,
toolType: toolTypeHint,
reason: "resume-launch",
});
}

const entry: PtyEntry = {
pty,
laneId,
Expand Down
69 changes: 66 additions & 3 deletions apps/desktop/src/renderer/components/app/AppShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,52 @@ function primaryTabPath(pathname: string): string {
return roots.find((root) => pathname === root || pathname.startsWith(`${root}/`)) ?? pathname;
}

const PROJECT_ROUTE_STORAGE_PREFIX = "ade:project-route:";

function projectRouteStorageKey(projectRoot: string): string {
return `${PROJECT_ROUTE_STORAGE_PREFIX}${projectRoot}`;
}

function serializeLocationRoute(location: ReturnType<typeof useLocation>): string | null {
const pathname = location.pathname || "/work";
const route = `${pathname}${location.search ?? ""}${location.hash ?? ""}`;
const allowedRoots = [
"/project",
"/lanes",
"/files",
"/work",
"/graph",
"/prs",
"/review",
"/history",
"/automations",
"/missions",
"/cto",
"/settings",
];
if (!allowedRoots.some((root) => pathname === root || pathname.startsWith(`${root}/`))) {
return null;
}
return route;
Comment on lines +75 to +92
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 allowedRoots diverges from primaryTabPath roots

serializeLocationRoute adds /review and /cto to its allow-list, but primaryTabPath (defined just above) does not include them. Routes under those paths will therefore be persisted and later restored, but the sidebar tab won't be activated (since primaryTabPath won't match them). If /review and /cto are real top-level pages that should be restored, they should also be added to primaryTabPath; otherwise they should be dropped from allowedRoots.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/components/app/AppShell.tsx
Line: 75-92

Comment:
**`allowedRoots` diverges from `primaryTabPath` roots**

`serializeLocationRoute` adds `/review` and `/cto` to its allow-list, but `primaryTabPath` (defined just above) does not include them. Routes under those paths will therefore be persisted and later restored, but the sidebar tab won't be activated (since `primaryTabPath` won't match them). If `/review` and `/cto` are real top-level pages that should be restored, they should also be added to `primaryTabPath`; otherwise they should be dropped from `allowedRoots`.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code

}

function readStoredProjectRoute(projectRoot: string): string | null {
try {
const value = window.localStorage.getItem(projectRouteStorageKey(projectRoot));
return value?.startsWith("/") ? value : null;
} catch {
return null;
}
}

function writeStoredProjectRoute(projectRoot: string, route: string): void {
try {
window.localStorage.setItem(projectRouteStorageKey(projectRoot), route);
} catch {
// localStorage can be unavailable in private/test environments.
}
}

type AiBannerState = {
laneId: string | null;
jobId: string | null;
Expand Down Expand Up @@ -206,6 +252,7 @@ export function AppShell({ children }: { children: React.ReactNode }) {
const [projectMissing, setProjectMissing] = useState(false);
const [feedbackGenerating, setFeedbackGenerating] = useState(false);
const previousProjectRootRef = useRef<string | null | undefined>(undefined);
const lastRouteSaveProjectRootRef = useRef<string | null | undefined>(undefined);
const isOnboardingRoute = location.pathname === "/onboarding";
const isLanesRoute = location.pathname.startsWith("/lanes");
const shouldTrackTerminalAttention =
Expand Down Expand Up @@ -582,10 +629,26 @@ export function AppShell({ children }: { children: React.ReactNode }) {

if (previousProjectRoot === undefined) return;
if (!nextProjectRoot || showWelcome) return;
if (location.pathname !== "/project") return;
if (previousProjectRoot === nextProjectRoot) return;
navigate("/work", { replace: true });
}, [location.pathname, navigate, project?.rootPath, showWelcome]);
if (previousProjectRoot) {
const previousRoute = serializeLocationRoute(location);
if (previousRoute) writeStoredProjectRoute(previousProjectRoot, previousRoute);
}
navigate(readStoredProjectRoute(nextProjectRoot) ?? "/work", { replace: true });
}, [location, navigate, project?.rootPath, showWelcome]);

useEffect(() => {
const projectRoot = project?.rootPath ?? null;
if (!projectRoot || showWelcome) return;

if (lastRouteSaveProjectRootRef.current !== projectRoot) {
lastRouteSaveProjectRootRef.current = projectRoot;
return;
}

const route = serializeLocationRoute(location);
if (route) writeStoredProjectRoute(projectRoot, route);
}, [location, project?.rootPath, showWelcome]);

useEffect(() => {
let cancelled = false;
Expand Down
Loading