Skip to content

Commit 92e55ac

Browse files
committed
feat: add Antigravity provider, home scope, and welcome overlay redesign
- Add Antigravity agent provider (supervisor adapter + renderer UI + icon) - Introduce `homeScope` shared module and "Home" project support across store, actions, sidebar, and IPC - Redesign welcome overlay with animated code wallpaper and entrance animations - Add `browserMcp` config scope, composer add menu, and browser scope indicator in AttachmentBar - Extend Browser MCP tool registry with `open` tool and streamline ingress/server logic - Improve thread draft view layout, compact header polish, and add comprehensive tests - Add `newThreadMode` and `browserMcpScope` shared settings with UI controls - Register Antigravity in the agent registry; update all provider browser MCP builders for home scope - Wire `getHomeScopeLocation` IPC procedure for cross-process project location queries - Refactor session ref watcher polling and fix session resume for Claude/ACP sessions
1 parent e4abf50 commit 92e55ac

77 files changed

Lines changed: 2473 additions & 344 deletions

File tree

Some content is hidden

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

src/main/browser/BrowserMcpIngress.ts

Lines changed: 2 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,7 @@ type JsonRpcResponse = JsonRpcResponseOk | JsonRpcResponseErr;
4343
/**
4444
* Single in-process MCP server. Speaks Streamable-HTTP MCP at `POST /mcp`
4545
* (JSON-RPC body, single JSON response). All five agent providers connect
46-
* here by URL — no per-thread Node child process. Also exposes legacy
47-
* `POST /<tool_name>` REST routes for backward compatibility with the old
48-
* stdio bridge until the v2 ship lands.
46+
* here by URL — no per-thread Node child process.
4947
*/
5048
export class BrowserMcpIngress {
5149
private server: Server | null = null;
@@ -189,18 +187,6 @@ export class BrowserMcpIngress {
189187
return;
190188
}
191189

192-
// Legacy REST: POST /<tool_name>. Retained so any older client still
193-
// pointed at this endpoint continues to work during the transition.
194-
if (req.method === "POST") {
195-
const tool = path.replace(/^\/+/, "").replace(/\/+$/, "");
196-
if (!tool) {
197-
this.sendJson(res, 404, { error: "not found" });
198-
return;
199-
}
200-
await this.handleLegacyRest(req, res, tool);
201-
return;
202-
}
203-
204190
this.sendJson(res, 404, { error: "not found" });
205191
} catch (err) {
206192
this.sendJson(res, 500, { error: (err as Error).message ?? "internal" });
@@ -261,7 +247,7 @@ export class BrowserMcpIngress {
261247
result: {
262248
protocolVersion: MCP_PROTOCOL_VERSION,
263249
capabilities: { tools: {} },
264-
serverInfo: { name: "lightcode_browser", version: "2.0.0" },
250+
serverInfo: { name: "browser", version: "2.0.0" },
265251
},
266252
};
267253
}
@@ -329,40 +315,6 @@ export class BrowserMcpIngress {
329315
};
330316
}
331317
}
332-
333-
private async handleLegacyRest(
334-
req: IncomingMessage,
335-
res: ServerResponse,
336-
tool: string,
337-
): Promise<void> {
338-
if (!isKnownToolName(tool)) {
339-
this.sendJson(res, 404, { error: `unknown tool: ${tool}` });
340-
return;
341-
}
342-
const ctx = this.buildContext();
343-
if (!ctx) {
344-
this.sendJson(res, 503, { error: "browser not ready" });
345-
return;
346-
}
347-
ctx.manager.revealPanel();
348-
const raw = await this.readBody(req);
349-
let args: Record<string, unknown> = {};
350-
if (raw) {
351-
try {
352-
const parsed = JSON.parse(raw);
353-
if (parsed && typeof parsed === "object") args = parsed as Record<string, unknown>;
354-
} catch {
355-
this.sendJson(res, 400, { error: "invalid json" });
356-
return;
357-
}
358-
}
359-
try {
360-
const result = await dispatchTool(tool, args, ctx);
361-
this.sendJson(res, 200, result);
362-
} catch (err) {
363-
this.sendJson(res, 500, { error: (err as Error).message ?? "internal" });
364-
}
365-
}
366318
}
367319

368320
function isJsonRpcRequest(value: unknown): value is JsonRpcRequest {

src/main/browser/cdp/dialogController.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ type Disposition = { action: "accept" | "dismiss"; promptText?: string };
2424
/**
2525
* Owns the CDP-level Page.javascriptDialogOpening lifecycle. By default, any
2626
* dialog is auto-dismissed (preserves the existing UX where alerts don't pop
27-
* a modal over the embedded view). The agent can call browser_dialog to set
27+
* a modal over the embedded view). The agent can call the dialog tool to set
2828
* a one-shot disposition for the next dialog, allowing accept/dismiss/answer
2929
* flows for confirm/prompt.
3030
*/

src/main/browser/mcp/toolRegistry.ts

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,15 @@ export const TOOLS: ToolSpec[] = [
6767
},
6868
},
6969
},
70+
{
71+
name: "open",
72+
description: "Open a URL in the active Lightcode browser tab, creating a tab if needed.",
73+
inputSchema: {
74+
type: "object",
75+
required: ["url"],
76+
properties: { tabId: { type: "string" }, url: { type: "string" } },
77+
},
78+
},
7079
{
7180
name: "activate_tab",
7281
description: "Make the given tab the active visible tab.",
@@ -200,6 +209,19 @@ export const TOOLS: ToolSpec[] = [
200209
},
201210
},
202211
},
212+
{
213+
name: "inspect",
214+
description:
215+
"Inspect the page with a structured snapshot of visible interactive elements, roles, text, refs, and bounds.",
216+
inputSchema: {
217+
type: "object",
218+
properties: {
219+
tabId: { type: "string" },
220+
maxNodes: { type: "number" },
221+
includeHidden: { type: "boolean" },
222+
},
223+
},
224+
},
203225
{
204226
name: "get",
205227
description:
@@ -459,13 +481,13 @@ export const TOOLS: ToolSpec[] = [
459481

460482
export const TOOL_NAMES = new Set(TOOLS.map((t) => t.name));
461483

484+
const TOOL_ALIASES = new Map([
485+
["open", "navigate"],
486+
["inspect", "snapshot"],
487+
]);
488+
462489
export function normalizeToolName(name: string): string {
463-
if (TOOL_NAMES.has(name)) return name;
464-
if (name.startsWith("browser_")) {
465-
const stripped = name.slice("browser_".length);
466-
if (TOOL_NAMES.has(stripped)) return stripped;
467-
}
468-
return name;
490+
return TOOL_ALIASES.get(name) ?? name;
469491
}
470492

471493
export function isKnownToolName(name: string): boolean {

src/main/ipc/localHandlers.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { homedir } from "node:os";
12
import { clipboard, dialog, nativeImage, shell, type BrowserWindow } from "electron";
23
import type { BrowserPanelManager } from "../browser";
34
import {
@@ -101,6 +102,10 @@ export function createLocalIpcHandlers(
101102
}
102103
win.focus();
103104
},
105+
getHomeScopeLocation: () =>
106+
process.platform === "win32"
107+
? { kind: "windows", path: homedir() }
108+
: { kind: "posix", path: homedir() },
104109
getKeybindings: () => readKeybindingsFile(options.requireLightcodePaths().keybindingsPath),
105110
revealProjectEntry: async (payload) => {
106111
shell.showItemInFolder(resolveProjectFsPath(payload));

src/main/sharedSettingsFile.test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ describe("sharedSettingsFile", () => {
6666
closeToTray: true,
6767
threadRemoveAction: "archive",
6868
newThreadMode: "page",
69+
homeScopeEnabled: true,
6970
autoShowTerminalPanel: true,
7071
gitReviewMode: "panel",
7172
providerConfigs: {},
@@ -82,7 +83,7 @@ describe("sharedSettingsFile", () => {
8283
favoriteModels: [],
8384
recentModels: [],
8485
agentHookSupport: {},
85-
browser: { mcpEnabled: true, allowEval: false, allowDataAccess: false },
86+
browser: { allowEval: false, allowDataAccess: false },
8687
});
8788

8889
expect(readSharedSettingsFile(settingsPath)).toEqual({
@@ -125,6 +126,7 @@ describe("sharedSettingsFile", () => {
125126
closeToTray: true,
126127
threadRemoveAction: "archive",
127128
newThreadMode: "page",
129+
homeScopeEnabled: true,
128130
autoShowTerminalPanel: true,
129131
gitReviewMode: "panel",
130132
providerConfigs: {},
@@ -141,7 +143,7 @@ describe("sharedSettingsFile", () => {
141143
favoriteModels: [],
142144
recentModels: [],
143145
agentHookSupport: {},
144-
browser: { mcpEnabled: true, allowEval: false, allowDataAccess: false },
146+
browser: { allowEval: false, allowDataAccess: false },
145147
});
146148
expect(readFileSync(settingsPath, "utf8")).toContain('"themeMode": "dark"');
147149
});

src/renderer/actions/projectActions.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,33 @@
1+
import type { Project, ProjectLocation } from "@/shared/contracts";
12
import { readBridge } from "@/renderer/bridge";
23
import { useAppStore } from "@/renderer/state/appStore";
34
import { useDevTerminalStore } from "@/renderer/state/devTerminalStore";
45
import { useFileEditorStore } from "@/renderer/state/fileEditorStore";
56
import { useGitStore } from "@/renderer/state/gitStore";
67
import { usePanelStore } from "@/renderer/state/panelStore";
78

9+
// The home dir doesn't change at runtime, so cache the single IPC roundtrip
10+
// and reuse it across callers (MainView mount effect + WelcomeOverlay
11+
// "Ask Question" path).
12+
let homeScopeLocationPromise: Promise<ProjectLocation> | null = null;
13+
14+
export function loadHomeScopeLocation(): Promise<ProjectLocation> {
15+
if (!homeScopeLocationPromise) {
16+
homeScopeLocationPromise = readBridge()
17+
.getHomeScopeLocation()
18+
.catch((err) => {
19+
homeScopeLocationPromise = null;
20+
throw err;
21+
});
22+
}
23+
return homeScopeLocationPromise;
24+
}
25+
26+
export async function ensureHomeScopeProject(): Promise<Project> {
27+
const location = await loadHomeScopeLocation();
28+
return useAppStore.getState().ensureHomeProject(location);
29+
}
30+
831
export function setProjectDisabled(projectId: string, disabled: boolean): void {
932
const store = useAppStore.getState();
1033
const project = store.projects.find((p) => p.id === projectId);

src/renderer/actions/threadActions.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { startTransition } from "react";
2+
import { isHomeProject } from "@/shared/homeScope";
23
import { isDraftPaneId } from "@/shared/paneId";
34
import { readBridge } from "@/renderer/bridge";
45
import { useAppStore } from "@/renderer/state/appStore";
@@ -20,7 +21,13 @@ let openThreadRequestId = 0;
2021
export function openNewThread(projectId?: string): void {
2122
openThreadRequestId += 1;
2223
const store = useAppStore.getState();
23-
const targetProjectId = projectId ?? getCurrentProjectId() ?? store.projects[0]?.id;
24+
const targetProjectId =
25+
projectId ??
26+
getCurrentProjectId() ??
27+
(useSharedSettings.getState().homeScopeEnabled
28+
? store.projects.find(isHomeProject)?.id
29+
: undefined) ??
30+
store.projects.find((project) => !project.disabled && !isHomeProject(project))?.id;
2431
startTransition(() => {
2532
if (!targetProjectId) {
2633
useAppStore.getState().openHome();

src/renderer/app.test.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ const { bridge } = vi.hoisted(() => ({
1414
.fn<() => Promise<{ windows: unknown[]; wsl: unknown[]; fromCache: boolean }>>()
1515
.mockResolvedValue({ windows: [], wsl: [], fromCache: false }),
1616
getThreadSnapshots: vi.fn<() => Promise<unknown[]>>().mockResolvedValue([]),
17+
getHomeScopeLocation: vi
18+
.fn<() => Promise<{ kind: "windows"; path: string }>>()
19+
.mockResolvedValue({ kind: "windows", path: "C:\\Users\\demo" }),
1720
dbGetThreadRuntimeItems: vi
1821
.fn<(threadId: string) => Promise<unknown[]>>()
1922
.mockResolvedValue([]),

src/renderer/components/common/ProviderModelMenu/ProviderModelMenu.tsx

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,11 @@ export interface ProviderModelMenuProps {
5858
hideLabelOnWrap?: boolean;
5959
forceHideLabel?: boolean;
6060
openSignal?: number;
61-
onChange: (next: { agentKind: string; model: string }) => void;
61+
onChange: (next: {
62+
agentKind: string;
63+
model: string;
64+
presentationMode?: ThreadPresentationMode;
65+
}) => void;
6266
onOpenChange?: (open: boolean) => void;
6367
}
6468

@@ -320,7 +324,11 @@ export function ProviderModelMenu(props: ProviderModelMenuProps) {
320324
// context/fast resolution doesn't block the close animation.
321325
handleOpenChange(false);
322326
startTransition(() => {
323-
onChange({ agentKind: selected.providerKind, model: selected.modelId });
327+
onChange({
328+
agentKind: selected.providerKind,
329+
model: selected.modelId,
330+
...(selected.presentationMode ? { presentationMode: selected.presentationMode } : {}),
331+
});
324332
});
325333
}
326334

@@ -409,8 +417,12 @@ export function ProviderModelMenu(props: ProviderModelMenuProps) {
409417
items={items}
410418
selectedKeys={selectedKeys}
411419
scrollRef={windowedListRef}
412-
toggleFavorite={(providerKind, modelId) =>
413-
toggleFavoriteModel(providerKind, modelId, presentationMode ?? "terminal")
420+
toggleFavorite={(providerKind, modelId, rowPresentationMode) =>
421+
toggleFavoriteModel(
422+
providerKind,
423+
modelId,
424+
rowPresentationMode ?? presentationMode ?? "terminal",
425+
)
414426
}
415427
onSelect={handleSelect}
416428
/>
@@ -426,7 +438,11 @@ function WindowedProviderModelList(props: {
426438
items: ProviderModelItem[];
427439
selectedKeys: Set<string>;
428440
scrollRef: RefObject<HTMLDivElement | null>;
429-
toggleFavorite: (providerKind: string, modelId: string) => void;
441+
toggleFavorite: (
442+
providerKind: string,
443+
modelId: string,
444+
presentationMode: ThreadPresentationMode | undefined,
445+
) => void;
430446
onSelect: (itemId: string) => void;
431447
}) {
432448
const { domIdPrefix, items, selectedKeys, scrollRef, toggleFavorite, onSelect } = props;
@@ -748,7 +764,7 @@ function WindowedProviderModelList(props: {
748764
onPointerUp={(event) => event.stopPropagation()}
749765
onClick={(event) => {
750766
event.stopPropagation();
751-
toggleFavorite(item.providerKind, item.modelId);
767+
toggleFavorite(item.providerKind, item.modelId, item.presentationMode);
752768
}}
753769
>
754770
<Star className="size-3.5" fill={item.isFavorite ? "currentColor" : "none"} />

src/renderer/components/common/ProviderModelMenu/parts/buildItems.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export interface ProviderModelMenuProvider {
66
kind: string;
77
label: string;
88
icon?: string;
9+
presentationMode?: ThreadPresentationMode;
910
capabilities: AgentCapability;
1011
}
1112

@@ -58,6 +59,7 @@ const PROVIDER_ORDER: readonly string[] = [
5859
"claude",
5960
"codex",
6061
"gemini",
62+
"antigravity",
6163
"opencode",
6264
"cursor",
6365
"copilot",
@@ -357,6 +359,7 @@ export function buildProviderModelItems(input: BuildProviderModelItemsInput): Pr
357359
providerKind: m.ref.agentKind,
358360
modelId: m.ref.modelId,
359361
label: m.label,
362+
...(m.ref.presentationMode ? { presentationMode: m.ref.presentationMode } : {}),
360363
...(providerIcon ? { providerIcon } : {}),
361364
...(m.subProviderLabel ? { subProviderLabel: m.subProviderLabel } : {}),
362365
...modelHintProps(m),
@@ -419,6 +422,7 @@ export function buildProviderModelItems(input: BuildProviderModelItemsInput): Pr
419422
providerKind: provider.kind,
420423
modelId: m.id,
421424
label: m.label,
425+
...(provider.presentationMode ? { presentationMode: provider.presentationMode } : {}),
422426
...(provider.icon ? { providerIcon: provider.icon } : {}),
423427
...(m.subLabel ? { subProviderLabel: m.subLabel } : {}),
424428
...modelHintProps(m),
@@ -452,6 +456,7 @@ export function buildProviderModelItems(input: BuildProviderModelItemsInput): Pr
452456
providerKind: provider.kind,
453457
modelId: m.id,
454458
label: m.label,
459+
...(provider.presentationMode ? { presentationMode: provider.presentationMode } : {}),
455460
...(provider.icon ? { providerIcon: provider.icon } : {}),
456461
...modelHintProps(m),
457462
...(m.tooltipDescription ? { tooltipDescription: m.tooltipDescription } : {}),
@@ -478,6 +483,7 @@ export function buildProviderModelItems(input: BuildProviderModelItemsInput): Pr
478483
providerKind: provider.kind,
479484
modelId: m.id,
480485
label: m.label,
486+
...(provider.presentationMode ? { presentationMode: provider.presentationMode } : {}),
481487
...(provider.icon ? { providerIcon: provider.icon } : {}),
482488
...modelHintProps(m),
483489
...(m.tooltipDescription ? { tooltipDescription: m.tooltipDescription } : {}),

0 commit comments

Comments
 (0)