Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
58 changes: 38 additions & 20 deletions apps/desktop/src/main/services/ipc/registerIpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
} from "../projects/projectIconResolver";
import { launchAgentChatCli } from "../chat/agentChatCliLaunch";
import { runGit } from "../git/git";
import type { AdeCleanupResult, AdeProjectSnapshot, IosSimulatorWindowState } from "../../../shared/types";
import type { AdeCleanupResult, AdeProjectSnapshot, IosSimulatorStatus, IosSimulatorWindowState } from "../../../shared/types";
import { toShallowRecentProjectSummary } from "../projects/recentProjectSummary";
import type {
ApplyConflictProposalArgs,
Expand Down Expand Up @@ -1980,6 +1980,11 @@ export function registerIpc({
const value = (arg as { projectRoot?: unknown }).projectRoot;
return typeof value === "string" && value.trim() ? value.trim() : null;
};
const isIosSimulatorStatus = (value: unknown): value is IosSimulatorStatus => {
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
const record = value as Partial<IosSimulatorStatus>;
return typeof record.platform === "string" && typeof record.supported === "boolean";
};
Comment thread
arul28 marked this conversation as resolved.
Outdated
const getIosSimulatorContextForEvent = (event: IpcMainInvokeEvent, arg?: unknown): AppContext | null => {
const windowId = BrowserWindow.fromWebContents(event.sender)?.id ?? null;
const session = getWindowSession?.(windowId) ?? null;
Expand All @@ -1999,30 +2004,43 @@ export function registerIpc({
if (sessionRoot) return resolveProjectContext(sessionRoot);
return getCtx();
};
const ensureIosSimulatorForEvent = (
const throwIosSimulatorUnavailableForEvent = (ctx: AppContext | null, arg?: unknown, channel = IPC.iosSimulatorListWindowSources): never => {
const requestedProjectRoot = readProjectRootArg(arg);
const projectRoot = requestedProjectRoot ?? ctx?.project?.rootPath ?? null;
const logger = ctx?.logger ?? getCtx().logger;
logger.warn("ios_simulator.service_unavailable", {
channel,
requestedProjectRoot,
contextProjectRoot: ctx?.project?.rootPath ?? null,
hasUserSelectedProject: ctx?.hasUserSelectedProject ?? false,
});
throw new Error(
projectRoot
? `iOS Simulator service is not available for ${projectRoot}.`
: "iOS Simulator service is not available because no local project is bound to this window.",
);
};
const getIosSimulatorStatusForEvent = async (
event: IpcMainInvokeEvent,
arg?: unknown,
channel = IPC.iosSimulatorListWindowSources,
): NonNullable<AppContext["iosSimulatorService"]> => {
): Promise<IosSimulatorStatus> => {
const ctx = getIosSimulatorContextForEvent(event, arg);
const service = ctx?.iosSimulatorService;
if (!service) {
const requestedProjectRoot = readProjectRootArg(arg);
const projectRoot = requestedProjectRoot ?? ctx?.project?.rootPath ?? null;
const logger = ctx?.logger ?? getCtx().logger;
logger.warn("ios_simulator.service_unavailable", {
channel,
requestedProjectRoot,
contextProjectRoot: ctx?.project?.rootPath ?? null,
hasUserSelectedProject: ctx?.hasUserSelectedProject ?? false,
if (service) return await service.getStatus();

const requestedProjectRoot = readProjectRootArg(arg);
const runtimeStatus = await tryLocalRuntimeSync(event, async (pool, rootPath) => {
if (requestedProjectRoot && rootPath !== requestedProjectRoot) return null;
Comment thread
arul28 marked this conversation as resolved.
Outdated
const response = await pool.callActionForRoot(rootPath, {
domain: "ios_simulator",
action: "getStatus",
args: {},
});
throw new Error(
projectRoot
? `iOS Simulator service is not available for ${projectRoot}.`
: "iOS Simulator service is not available because no local project is bound to this window.",
);
}
return service;
return isIosSimulatorStatus(response.result) ? response.result : null;
});
if (runtimeStatus) return runtimeStatus;
return throwIosSimulatorUnavailableForEvent(ctx, arg, channel);
};

const ensureAppControl = (): NonNullable<AppContext["appControlService"]> => {
Expand Down Expand Up @@ -7074,7 +7092,7 @@ export function registerIpc({
ipcMain.handle(IPC.iosSimulatorGetWindowState, async () => getSimulatorWindowState());

ipcMain.handle(IPC.iosSimulatorListWindowSources, async (event, arg = {}) => {
const status = await ensureIosSimulatorForEvent(event, arg, IPC.iosSimulatorListWindowSources).getStatus();
const status = await getIosSimulatorStatusForEvent(event, arg, IPC.iosSimulatorListWindowSources);
if (!status.supported) return [];
const readSources = async () => desktopCapturer.getSources({
types: ["window"],
Expand Down
52 changes: 52 additions & 0 deletions apps/desktop/src/main/services/ipc/runtimeBridge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -884,6 +884,58 @@ describe("registerIpc sync bridge", () => {
expect(otherGetStatus).not.toHaveBeenCalled();
});

it("uses the bound local runtime for iOS Simulator window sources when no in-process project context is loaded", async () => {
const getProjectContext = vi.fn(() => null);
const localRuntimeConnectionPool = {
callActionForRoot: vi.fn(async () => ({
domain: "ios_simulator",
action: "getStatus",
result: {
platform: "darwin",
supported: false,
tools: [],
activeDevice: null,
activeSession: null,
},
statusHints: {},
})),
};
registerIpc({
getCtx: () => ({
project: { rootPath: "/fallback" },
logger: { warn: vi.fn(), info: vi.fn(), error: vi.fn() },
}) as any,
getWindowSession: () => ({
windowId: 7,
project: { rootPath: "/repo", displayName: "Repo" } as any,
binding: localBinding("/repo"),
}),
getProjectContext,
localRuntimeConnectionPool: localRuntimeConnectionPool as any,
switchProjectFromDialog: vi.fn(),
closeCurrentProject: vi.fn(),
closeProjectByPath: vi.fn(),
globalStatePath: "/tmp/ade-state.json",
});

await expect(
ipcHandlers.get(IPC.iosSimulatorListWindowSources)?.(
eventForSender(),
{ projectRoot: "/repo" },
),
).resolves.toEqual([]);

expect(getProjectContext).toHaveBeenCalledWith("/repo");
expect(localRuntimeConnectionPool.callActionForRoot).toHaveBeenCalledWith(
"/repo",
{
domain: "ios_simulator",
action: "getStatus",
args: {},
},
);
});

it("rejects iOS Simulator window-source requests for an unbound project root", async () => {
const getProjectContext = vi.fn(() => ({
project: { rootPath: "/other" },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2225,10 +2225,11 @@ export function ChatIosSimulatorPanel({
}
}, [armWindowCaptureRecoveryAfterInput, liveInputBlocked, liveInputBlockedMessage, projectRoot, selectedDeviceUdid]);

const snapshotElementCount = snapshot?.elements.length ?? 0;
const providerSummary = snapshot
? snapshot.elements.length > 0
? `Snapshot ready with ${snapshot.elements.length} selectable item${snapshot.elements.length === 1 ? "" : "s"}.`
: "Snapshot ready."
? snapshotElementCount > 0
? `Snapshot ready with ${snapshotElementCount} selectable item${snapshotElementCount === 1 ? "" : "s"}.`
: "Snapshot ready with no selectable elements. Click a point or use Screenshot to attach visual context."
: null;
const hoverSource = hoveredElement?.source ?? null;
let previewModeHint: string | null = null;
Expand Down Expand Up @@ -2262,6 +2263,8 @@ export function ChatIosSimulatorPanel({
? "Drag a region on the simulator snapshot to attach a screenshot crop and screen context."
: selectedElement
? `Selected element: ${elementLabel(selectedElement)}. Click another outline to swap, or Alt-click to select a parent.`
: snapshot && snapshotElementCount === 0
? "No inspectable elements found. Click a point to attach coordinate context, or use Screenshot to capture a region."
: "Click a UI element outline to insert it as context. Alt-click to select a larger container.";
}
const simulatorWindowWarning = mode === "interact"
Expand Down