Skip to content
Closed
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
4 changes: 2 additions & 2 deletions apps/desktop/src/main/main.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { app, BrowserWindow, nativeImage, protocol, shell } from "electron";
import path from "node:path";
type NodePtyType = typeof import("node-pty");

Check warning on line 3 in apps/desktop/src/main/main.ts

View workflow job for this annotation

GitHub Actions / lint-desktop

`import()` type annotations are forbidden
import { registerIpc } from "./services/ipc/registerIpc";
import { createFileLogger } from "./services/logging/logger";
import { openKvDb } from "./services/state/kvDb";
Expand Down Expand Up @@ -666,8 +666,8 @@
const agentToolsService = createAgentToolsService({ logger });
const devToolsService = createDevToolsService({ logger });

const project = toProjectInfo(projectRoot, baseRef);
const { projectId } = upsertProjectRow({ db, repoRoot: projectRoot, displayName: project.displayName, baseRef });
const { projectId } = upsertProjectRow({ db, repoRoot: projectRoot, displayName: path.basename(projectRoot), baseRef });
const project = toProjectInfo(projectRoot, baseRef, projectId);

const operationService = createOperationService({ db, projectId });

Expand Down
80 changes: 79 additions & 1 deletion apps/desktop/src/main/services/chat/agentChatService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ import type { EpisodicSummaryService } from "../memory/episodicSummaryService";
import {
createDefaultComputerUsePolicy,
normalizeComputerUsePolicy,
ADE_LOCAL_EXECUTION_TARGET_ID,
} from "../../../shared/types";
import type {
AgentChatApprovalDecision,
Expand Down Expand Up @@ -247,6 +248,8 @@ type PersistedChatState = {
lastLaneDirectiveKey?: string | null;
manuallyNamed?: boolean;
requestedCwd?: string | null;
executionTargetId?: string | null;
executionTargetLabel?: string | null;
/** Persisted "Allow for Session" tool approval overrides (Claude runtime). */
approvalOverrides?: string[];
updatedAt: string;
Expand Down Expand Up @@ -4566,6 +4569,12 @@ export function createAgentChatService(args: {
...(managed.session.requestedCwd != null && String(managed.session.requestedCwd).trim().length
? { requestedCwd: String(managed.session.requestedCwd).trim() }
: {}),
...(managed.session.executionTargetId != null && String(managed.session.executionTargetId).trim().length
? { executionTargetId: String(managed.session.executionTargetId).trim() }
: {}),
...(managed.session.executionTargetLabel != null && String(managed.session.executionTargetLabel).trim().length
? { executionTargetLabel: String(managed.session.executionTargetLabel).trim() }
: {}),
updatedAt: nowIso()
};

Expand Down Expand Up @@ -4706,6 +4715,12 @@ export function createAgentChatService(args: {
...(typeof record.requestedCwd === "string" && record.requestedCwd.trim().length
? { requestedCwd: record.requestedCwd.trim() }
: {}),
...(typeof record.executionTargetId === "string" && record.executionTargetId.trim().length
? { executionTargetId: record.executionTargetId.trim() }
: {}),
...(typeof record.executionTargetLabel === "string" && record.executionTargetLabel.trim().length
? { executionTargetLabel: record.executionTargetLabel.trim() }
: {}),
updatedAt: typeof record.updatedAt === "string" && record.updatedAt.trim().length ? record.updatedAt : nowIso()
};
hydrateNativePermissionControls(hydrated as Parameters<typeof hydrateNativePermissionControls>[0]);
Expand Down Expand Up @@ -5625,6 +5640,14 @@ export function createAgentChatService(args: {
...(persisted?.requestedCwd != null && String(persisted.requestedCwd).trim().length
? { requestedCwd: String(persisted.requestedCwd).trim() }
: {}),
...(persisted?.executionTargetId != null && String(persisted.executionTargetId).trim().length
? {
executionTargetId: String(persisted.executionTargetId).trim(),
...(persisted?.executionTargetLabel != null && String(persisted.executionTargetLabel).trim().length
? { executionTargetLabel: String(persisted.executionTargetLabel).trim() }
: {}),
}
: {}),
Comment on lines +5643 to +5650
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Normalize upgraded chats to the local target when rebuilding the managed session.

Pre-feature chat metadata will not have these fields, so resumed sessions can keep managed.session.executionTargetId unset here while summarizeSessionRow() later reports "local". That split state will make upgraded chats behave differently depending on which API the UI reads.

💡 Suggested fix
-        ...(persisted?.executionTargetId != null && String(persisted.executionTargetId).trim().length
-          ? {
-              executionTargetId: String(persisted.executionTargetId).trim(),
-              ...(persisted?.executionTargetLabel != null && String(persisted.executionTargetLabel).trim().length
-                ? { executionTargetLabel: String(persisted.executionTargetLabel).trim() }
-                : {}),
-            }
-          : {}),
+        ...(() => {
+          const explicitExecutionTargetId =
+            typeof persisted?.executionTargetId === "string" && persisted.executionTargetId.trim().length
+              ? persisted.executionTargetId.trim()
+              : null;
+          const executionTargetId = explicitExecutionTargetId ?? ADE_LOCAL_EXECUTION_TARGET_ID;
+          const executionTargetLabel =
+            explicitExecutionTargetId && typeof persisted?.executionTargetLabel === "string" && persisted.executionTargetLabel.trim().length
+              ? persisted.executionTargetLabel.trim()
+              : executionTargetId === ADE_LOCAL_EXECUTION_TARGET_ID
+                ? "This computer"
+                : executionTargetId;
+          return { executionTargetId, executionTargetLabel };
+        })(),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/services/chat/agentChatService.ts` around lines 5643 -
5650, The rebuilt managed session can end up with executionTargetId unset for
pre-feature chats causing divergence vs summarizeSessionRow(); when
reconstructing the managed.session from persisted (using
persisted.executionTargetId and persisted.executionTargetLabel) ensure you
normalize missing/empty executionTargetId to the local target (e.g., set
executionTargetId to "local") so upgraded chats get a consistent target; update
the logic around persisted.executionTargetId / persisted.executionTargetLabel in
the managed session rebuild code (the block referencing
persisted.executionTargetId and executionTargetLabel) to substitute the local
identifier when executionTargetId is null/empty.

createdAt: row.startedAt,
lastActivityAt: persisted?.updatedAt ?? row.endedAt ?? row.startedAt
},
Expand Down Expand Up @@ -10145,6 +10168,8 @@ export function createAgentChatService(args: {
automationRunId,
computerUse,
requestedCwd,
executionTargetId: requestedExecutionTargetId,
executionTargetLabel: requestedExecutionTargetLabel,
}: AgentChatCreateArgs): Promise<AgentChatSession> => {
const launchContext = resolveLaneLaunchContext({
laneService,
Expand Down Expand Up @@ -10220,6 +10245,15 @@ export function createAgentChatService(args: {
: requestedPermMode;
const chatConfig = resolveChatConfig();

const rawExecTargetId = typeof requestedExecutionTargetId === "string" ? requestedExecutionTargetId.trim() : "";
const normalizedExecTargetId = rawExecTargetId.length ? rawExecTargetId : ADE_LOCAL_EXECUTION_TARGET_ID;
const normalizedExecTargetLabel =
typeof requestedExecutionTargetLabel === "string" && requestedExecutionTargetLabel.trim().length
? requestedExecutionTargetLabel.trim()
: normalizedExecTargetId === ADE_LOCAL_EXECUTION_TARGET_ID
? "This computer"
: normalizedExecTargetId;

const nativePermissionFields = (() => {
if (effectiveProvider === "claude") {
const interactionMode = requestedInteractionMode
Expand Down Expand Up @@ -10309,6 +10343,8 @@ export function createAgentChatService(args: {
...(typeof requestedCwd === "string" && requestedCwd.trim().length
? { requestedCwd: requestedCwd.trim() }
: {}),
executionTargetId: normalizedExecTargetId,
executionTargetLabel: normalizedExecTargetLabel,
},
transcriptPath,
transcriptBytesWritten: fileSizeOrZero(transcriptPath),
Expand Down Expand Up @@ -10445,6 +10481,8 @@ export function createAgentChatService(args: {
permissionMode: managed.session.permissionMode,
surface: managed.session.surface,
computerUse: managed.session.computerUse,
executionTargetId: managed.session.executionTargetId ?? null,
executionTargetLabel: managed.session.executionTargetLabel ?? null,
});

const createdManaged = ensureManagedSession(created.id);
Expand Down Expand Up @@ -12239,7 +12277,19 @@ export function createAgentChatService(args: {
...(hasLivePendingInput(liveManaged) ? { awaitingInput: true } : {}),
...(liveSession?.threadId || persisted?.threadId
? { threadId: liveSession?.threadId ?? persisted?.threadId }
: {})
: {}),
...(() => {
const idRaw = liveSession?.executionTargetId ?? persisted?.executionTargetId;
const id = typeof idRaw === "string" && idRaw.trim().length ? idRaw.trim() : ADE_LOCAL_EXECUTION_TARGET_ID;
const labelRaw = liveSession?.executionTargetLabel ?? persisted?.executionTargetLabel;
const label =
typeof labelRaw === "string" && labelRaw.trim().length
? labelRaw.trim()
: id === ADE_LOCAL_EXECUTION_TARGET_ID
? "This computer"
: id;
return { executionTargetId: id, executionTargetLabel: label };
})()
} satisfies AgentChatSessionSummary;
};

Expand Down Expand Up @@ -12772,6 +12822,8 @@ export function createAgentChatService(args: {
cursorConfigValues,
permissionMode,
computerUse,
executionTargetId,
executionTargetLabel,
}: AgentChatUpdateSessionArgs): Promise<AgentChatSession> => {
const managed = ensureManagedSession(sessionId);
const chatConfig = resolveChatConfig();
Expand Down Expand Up @@ -12986,6 +13038,32 @@ export function createAgentChatService(args: {
}
}

if (executionTargetId !== undefined) {
const nextId = typeof executionTargetId === "string" && executionTargetId.trim().length
? executionTargetId.trim()
: ADE_LOCAL_EXECUTION_TARGET_ID;
managed.session.executionTargetId = nextId;
if (executionTargetLabel !== undefined) {
const nextLabel = typeof executionTargetLabel === "string" && executionTargetLabel.trim().length
? executionTargetLabel.trim()
: nextId === ADE_LOCAL_EXECUTION_TARGET_ID
? "This computer"
: nextId;
managed.session.executionTargetLabel = nextLabel;
} else if (nextId === ADE_LOCAL_EXECUTION_TARGET_ID) {
managed.session.executionTargetLabel = "This computer";
} else if (!managed.session.executionTargetLabel?.trim()) {
managed.session.executionTargetLabel = nextId;
}
} else if (executionTargetLabel !== undefined) {
const nextLabel = typeof executionTargetLabel === "string" && executionTargetLabel.trim().length
? executionTargetLabel.trim()
: managed.session.executionTargetId === ADE_LOCAL_EXECUTION_TARGET_ID
? "This computer"
: (managed.session.executionTargetId ?? "This computer");
managed.session.executionTargetLabel = nextLabel;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

if (computerUse !== undefined) {
const nextComputerUse = normalizeComputerUsePolicy(computerUse, createDefaultComputerUsePolicy());
const prevComputerUse = managed.session.computerUse;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { AdeDb } from "../state/kvDb";
import type { AdeExecutionTargetsState } from "../../../shared/types";
import { defaultExecutionTargetsState, normalizeExecutionTargetsState } from "../../../shared/types";

function keyForProject(projectId: string): string {
return `ade_execution_targets:${projectId}`;
}

export function getExecutionTargetsState(db: AdeDb | null, projectId: string): AdeExecutionTargetsState {
const pid = projectId.trim();
if (!db || !pid.length) return defaultExecutionTargetsState();
const raw = db.getJson<unknown>(keyForProject(pid));
return normalizeExecutionTargetsState(raw);
}

export function setExecutionTargetsState(
db: AdeDb | null,
projectId: string,
next: AdeExecutionTargetsState,
): AdeExecutionTargetsState {
const pid = projectId.trim();
if (!db || !pid.length) return defaultExecutionTargetsState();
const normalized = normalizeExecutionTargetsState(next);
db.setJson(keyForProject(pid), normalized);
Comment on lines +23 to +24
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Reject duplicate execution target ids before saving.

setExecutionTargetsState() relies on normalizeExecutionTargetsState(), but the normalizer in apps/desktop/src/shared/types/executionTargets.ts currently preserves duplicate SSH ids. That lets the store hold multiple profiles behind the same foreign key even though activeTargetId and chat tags resolve by id, so later lookups or rename/delete flows can hit the wrong target.

💡 Local fix at the persistence boundary
-import type { AdeExecutionTargetsState } from "../../../shared/types";
-import { defaultExecutionTargetsState, normalizeExecutionTargetsState } from "../../../shared/types";
+import type { AdeExecutionTargetsState } from "../../../shared/types";
+import {
+  ADE_LOCAL_EXECUTION_TARGET_ID,
+  defaultExecutionTargetsState,
+  normalizeExecutionTargetsState,
+} from "../../../shared/types";
...
 export function setExecutionTargetsState(
   db: AdeDb | null,
   projectId: string,
   next: AdeExecutionTargetsState,
 ): AdeExecutionTargetsState {
   const pid = projectId.trim();
   if (!db || !pid.length) return defaultExecutionTargetsState();
   const normalized = normalizeExecutionTargetsState(next);
-  db.setJson(keyForProject(pid), normalized);
-  return normalized;
+  const profiles = Array.from(
+    new Map(normalized.profiles.map((profile) => [profile.id, profile])).values(),
+  );
+  const safe = {
+    ...normalized,
+    profiles,
+    activeTargetId: profiles.some((profile) => profile.id === normalized.activeTargetId)
+      ? normalized.activeTargetId
+      : ADE_LOCAL_EXECUTION_TARGET_ID,
+  };
+  db.setJson(keyForProject(pid), safe);
+  return safe;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/main/services/executionTargets/executionTargetsStateService.ts`
around lines 23 - 24, The normalized executionTargets state may contain
duplicate execution target ids, so update setExecutionTargetsState (the code
that calls normalizeExecutionTargetsState and db.setJson(keyForProject(pid),
normalized)) to validate the normalized object before persisting: scan
normalized.executionTargets (or the structure produced by
normalizeExecutionTargetsState) for duplicate id values, and if any duplicates
are found reject the update (throw or return an error) or deduplicate
deterministically (e.g., keep first occurrence) and only then call db.setJson;
ensure the check references normalizeExecutionTargetsState and
keyForProject(pid) so the validation lives at the persistence boundary.

return normalized;
}
42 changes: 36 additions & 6 deletions apps/desktop/src/main/services/ipc/registerIpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ import type {
ProjectConfigTrust,
ProjectConfigValidationResult,
ProjectInfo,
AdeExecutionTargetsState,
RecentProjectSummary,
PtyCreateArgs,
PtyCreateResult,
Expand Down Expand Up @@ -3216,6 +3217,21 @@ export function registerIpc({
ctx.db.setJson(key, arg.state);
});

ipcMain.handle(IPC.executionTargetsGet, async (): Promise<AdeExecutionTargetsState> => {
const ctx = getCtx();
const { getExecutionTargetsState } = await import("../executionTargets/executionTargetsStateService");
return getExecutionTargetsState(ctx.db, ctx.projectId);
});

ipcMain.handle(
IPC.executionTargetsSet,
async (_event, arg: AdeExecutionTargetsState): Promise<AdeExecutionTargetsState> => {
const ctx = getCtx();
const { setExecutionTargetsState } = await import("../executionTargets/executionTargetsStateService");
return setExecutionTargetsState(ctx.db, ctx.projectId, arg);
},
);

ipcMain.handle(IPC.lanesList, async (_event, arg: ListLanesArgs): Promise<LaneSummary[]> => {
const ctx = getCtx();
return await withIpcTiming(
Expand Down Expand Up @@ -3879,13 +3895,27 @@ export function registerIpc({
const chatSummaryBySessionId = new Map(chats.map((chat) => [chat.sessionId, chat] as const));
return sessions.map((session) => {
if (!isChatToolType(session.toolType)) return session;
if (session.status !== "running") return session;
const chat = chatSummaryBySessionId.get(session.id);
if (!chat) return session;
if (chat.awaitingInput) return { ...session, runtimeState: "waiting-input" as const };
if (chat.status === "active") return { ...session, runtimeState: "running" as const };
if (chat.status === "idle") return { ...session, runtimeState: "idle" as const };
return session;
const withTarget =
chat
&& (chat.executionTargetId != null && String(chat.executionTargetId).trim().length > 0
|| chat.executionTargetLabel != null && String(chat.executionTargetLabel).trim().length > 0)
? {
...session,
...(chat.executionTargetId != null && String(chat.executionTargetId).trim().length
? { executionTargetId: String(chat.executionTargetId).trim() }
: {}),
...(chat.executionTargetLabel != null && String(chat.executionTargetLabel).trim().length
? { executionTargetLabel: String(chat.executionTargetLabel).trim() }
: {}),
}
: session;
if (session.status !== "running") return withTarget;
if (!chat) return withTarget;
if (chat.awaitingInput) return { ...withTarget, runtimeState: "waiting-input" as const };
if (chat.status === "active") return { ...withTarget, runtimeState: "running" as const };
if (chat.status === "idle") return { ...withTarget, runtimeState: "idle" as const };
return withTarget;
});
},
{
Expand Down
9 changes: 7 additions & 2 deletions apps/desktop/src/main/services/projects/projectService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ export function upsertProjectRow({
return { projectId: id };
}

export function toProjectInfo(repoRoot: string, baseRef: string): ProjectInfo {
return { rootPath: repoRoot, displayName: path.basename(repoRoot), baseRef };
export function toProjectInfo(repoRoot: string, baseRef: string, projectId?: string): ProjectInfo {
return {
rootPath: repoRoot,
displayName: path.basename(repoRoot),
baseRef,
...(projectId?.trim() ? { projectId: projectId.trim() } : {}),
};
}
5 changes: 5 additions & 0 deletions apps/desktop/src/preload/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,7 @@ import type {
ProjectConfigTrust,
ProjectConfigValidationResult,
ProjectInfo,
AdeExecutionTargetsState,
RecentProjectSummary,
PtyCreateArgs,
PtyCreateResult,
Expand Down Expand Up @@ -593,6 +594,10 @@ declare global {
onMissing: (cb: (data: { rootPath: string }) => void) => () => void;
onStateEvent: (cb: (event: AdeProjectEvent) => void) => () => void;
};
executionTargets: {
get: () => Promise<AdeExecutionTargetsState>;
set: (state: AdeExecutionTargetsState) => Promise<AdeExecutionTargetsState>;
};
keybindings: {
get: () => Promise<KeybindingsSnapshot>;
set: (overrides: KeybindingOverride[]) => Promise<KeybindingsSnapshot>;
Expand Down
6 changes: 6 additions & 0 deletions apps/desktop/src/preload/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,7 @@ import type {
ProjectConfigTrust,
ProjectConfigValidationResult,
ProjectInfo,
AdeExecutionTargetsState,
RecentProjectSummary,
PtyCreateArgs,
PtyCreateResult,
Expand Down Expand Up @@ -599,6 +600,11 @@ contextBridge.exposeInMainWorld("ade", {
return () => ipcRenderer.removeListener(IPC.projectStateEvent, listener);
}
},
executionTargets: {
get: async (): Promise<AdeExecutionTargetsState> => ipcRenderer.invoke(IPC.executionTargetsGet),
set: async (state: AdeExecutionTargetsState): Promise<AdeExecutionTargetsState> =>
ipcRenderer.invoke(IPC.executionTargetsSet, state),
},
keybindings: {
get: async (): Promise<KeybindingsSnapshot> => ipcRenderer.invoke(IPC.keybindingsGet),
set: async (overrides: KeybindingOverride[]): Promise<KeybindingsSnapshot> =>
Expand Down
8 changes: 8 additions & 0 deletions apps/desktop/src/renderer/browserMock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -860,6 +860,14 @@ if (typeof window !== "undefined" && !(window as any).ade) {
onMissing: noop,
onStateEvent: noop,
},
executionTargets: {
get: resolved({
version: 1,
profiles: [{ id: "local", kind: "local" as const, label: "This computer" }],
activeTargetId: "local",
}),
set: async (state: any) => state,
},
Comment on lines +863 to +870
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Make browser mock executionTargets.set() stateful to match IPC semantics.

Line 837 currently ignores the incoming state and always returns the same static payload, so get() on Line 832 never reflects user updates. This breaks execution-target flows in browser-preview mode and drifts from the real IPC contract.

🔧 Proposed fix
 import { getDefaultModelDescriptor } from "../shared/modelRegistry";
+import {
+  defaultExecutionTargetsState,
+  normalizeExecutionTargetsState,
+  type AdeExecutionTargetsState,
+} from "../shared/types/executionTargets";
@@
 const DEFAULT_BROWSER_MOCK_CODEX_MODEL = getDefaultModelDescriptor("codex")?.id ?? "openai/gpt-5.4-codex";
+let mockExecutionTargetsState: AdeExecutionTargetsState = defaultExecutionTargetsState();
@@
     executionTargets: {
-      get: resolved({
-        version: 1,
-        profiles: [{ id: "local", kind: "local" as const, label: "This computer" }],
-        activeTargetId: "local",
-      }),
-      set: resolvedArg({
-        version: 1,
-        profiles: [{ id: "local", kind: "local" as const, label: "This computer" }],
-        activeTargetId: "local",
-      }),
+      get: async () => mockExecutionTargetsState,
+      set: async (next: AdeExecutionTargetsState) => {
+        mockExecutionTargetsState = normalizeExecutionTargetsState(next);
+        return mockExecutionTargetsState;
+      },
     },

As per coding guidelines, "Keep IPC contracts, preload types, shared types, and renderer usage in sync whenever an interface changes."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/browserMock.ts` around lines 831 - 842, The mock's
executionTargets.set() currently ignores its input and always returns a static
payload, so executionTargets.get() never reflects updates; make the mock
stateful by introducing an internal variable initialized to the same payload
used by executionTargets.get (version:1, profiles [{id:"local", kind:"local",
label:"This computer"}], activeTargetId:"local"), then change
executionTargets.get to return that variable (via resolved) and change
executionTargets.set to update that variable with the incoming argument (and
return it via resolvedArg) so caller updates persist and mirror real IPC
semantics (referencing executionTargets.get and executionTargets.set in the
diff).

keybindings: {
get: resolved({ definitions: [], overrides: [] }),
set: resolvedArg({ definitions: [], overrides: [] }),
Expand Down
3 changes: 3 additions & 0 deletions apps/desktop/src/renderer/components/app/TopBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
getStoredZoomLevel,
} from "../../lib/zoom";
import { cn } from "../ui/cn";
import { TopBarExecutionTargetSelect } from "../executionTargets/TopBarExecutionTargetSelect";
import type { ProcessRuntime, RecentProjectSummary, SyncRoleSnapshot } from "../../../shared/types";
import { AutoUpdateControl } from "./AutoUpdateControl";

Expand Down Expand Up @@ -417,6 +418,8 @@ export function TopBar() {
>
<Plus size={12} weight="regular" />
</button>

<TopBarExecutionTargetSelect projectRoot={project?.rootPath ?? null} />
</div>

{syncSnapshot && syncLabel ? (
Expand Down
Loading
Loading