fix(bridge): rawRPC direct polling + SDK analysis docs + trial-and-error log
- Root cause: getDiagnostics.lastStepIndex is stale, SDK EventMonitor cannot detect real-time step changes
- Fix: Direct rawRPC('GetCascadeTrajectorySteps') polling every 5s
- Relay: PLANNER_RESPONSE, NOTIFY_USER, TASK_BOUNDARY, WAITING steps
- Added: docs/discord-bridge-analysis.md (full SDK architecture analysis)
- Added: docs/devlog/entries/20260308-003.md (trial-and-error history)
- Added: antigravity-sdk-main/ source reference
- Vikunja: #252 done, #253 created, #251 commented
This commit is contained in:
527
antigravity-sdk-main/src/cascade/cascade-manager.ts
Normal file
527
antigravity-sdk-main/src/cascade/cascade-manager.ts
Normal file
@@ -0,0 +1,527 @@
|
||||
/**
|
||||
* Cascade Manager — Session listing, creation, and monitoring.
|
||||
*
|
||||
* Provides high-level API to interact with Cascade conversations
|
||||
* using verified transport layer (CommandBridge + StateBridge).
|
||||
*
|
||||
* VERIFIED 2026-02-28: getDiagnostics.recentTrajectories returns clean JSON
|
||||
* with { googleAgentId, trajectoryId, summary, lastStepIndex, lastModifiedTime }.
|
||||
*
|
||||
* @module cascade/cascade-manager
|
||||
*/
|
||||
|
||||
import { IDisposable, DisposableStore } from '../core/disposable';
|
||||
import { EventEmitter, Event } from '../core/events';
|
||||
import { Logger } from '../core/logger';
|
||||
import type {
|
||||
ITrajectoryEntry,
|
||||
IAgentPreferences,
|
||||
IDiagnosticsInfo,
|
||||
ICreateSessionOptions,
|
||||
} from '../core/types';
|
||||
import { CommandBridge, AntigravityCommands } from '../transport/command-bridge';
|
||||
import { StateBridge } from '../transport/state-bridge';
|
||||
|
||||
const log = new Logger('CascadeManager');
|
||||
|
||||
/**
|
||||
* Manages Cascade conversations.
|
||||
*
|
||||
* Primary data source: `antigravity.getDiagnostics` → `recentTrajectories`
|
||||
* Fallback: `antigravityUnifiedStateSync.trajectorySummaries` protobuf parsing
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const manager = new CascadeManager(commands, state);
|
||||
* await manager.initialize();
|
||||
*
|
||||
* // List sessions (real titles from getDiagnostics)
|
||||
* const sessions = await manager.getSessions();
|
||||
* sessions.forEach(s => console.log(`${s.title} (step ${s.stepCount})`));
|
||||
*
|
||||
* // Read preferences (all 16 sentinel values)
|
||||
* const prefs = await manager.getPreferences();
|
||||
*
|
||||
* // Create & send
|
||||
* await manager.createSession({ task: 'Analyze coverage', background: true });
|
||||
* ```
|
||||
*/
|
||||
export class CascadeManager implements IDisposable {
|
||||
private readonly _disposables = new DisposableStore();
|
||||
private _sessions: ITrajectoryEntry[] = [];
|
||||
private _initialized = false;
|
||||
|
||||
// Events
|
||||
private readonly _onSessionsChanged = this._disposables.add(new EventEmitter<ITrajectoryEntry[]>());
|
||||
/** Fires when the session list changes */
|
||||
public readonly onSessionsChanged: Event<ITrajectoryEntry[]> = this._onSessionsChanged.event;
|
||||
|
||||
constructor(
|
||||
private readonly _commands: CommandBridge,
|
||||
private readonly _state: StateBridge,
|
||||
) { }
|
||||
|
||||
/**
|
||||
* Initialize the cascade manager.
|
||||
* Loads the initial session list from getDiagnostics.
|
||||
*/
|
||||
async initialize(): Promise<void> {
|
||||
if (this._initialized) return;
|
||||
|
||||
await this._loadSessions();
|
||||
this._initialized = true;
|
||||
log.info(`Initialized with ${this._sessions.length} sessions`);
|
||||
}
|
||||
|
||||
// ─── Read API ───────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Get all known Cascade sessions.
|
||||
*
|
||||
* Uses `getDiagnostics.recentTrajectories` (clean JSON with titles).
|
||||
*
|
||||
* @returns List of trajectory entries sorted by recency
|
||||
*/
|
||||
async getSessions(): Promise<ITrajectoryEntry[]> {
|
||||
if (!this._initialized) {
|
||||
await this._loadSessions();
|
||||
}
|
||||
return [...this._sessions];
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the session list.
|
||||
*
|
||||
* @returns Updated session list
|
||||
*/
|
||||
async refreshSessions(): Promise<ITrajectoryEntry[]> {
|
||||
await this._loadSessions();
|
||||
this._onSessionsChanged.fire(this._sessions);
|
||||
return [...this._sessions];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get agent preferences (all 16 sentinel values).
|
||||
*/
|
||||
async getPreferences(): Promise<IAgentPreferences> {
|
||||
return this._state.getAgentPreferences();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get IDE diagnostics (176KB JSON with system info, logs, trajectories).
|
||||
*
|
||||
* Structure (verified):
|
||||
* - isRemote, systemInfo (OS, user, email)
|
||||
* - extensionLogs (Array[375])
|
||||
* - rendererLogs, mainThreadLogs, agentWindowConsoleLogs
|
||||
* - languageServerLogs
|
||||
* - recentTrajectories (Array[10])
|
||||
*
|
||||
* @returns Parsed diagnostics information
|
||||
*/
|
||||
async getDiagnostics(): Promise<IDiagnosticsInfo> {
|
||||
const raw = await this._commands.execute<string>(AntigravityCommands.GET_DIAGNOSTICS);
|
||||
|
||||
if (!raw || typeof raw !== 'string') {
|
||||
throw new Error('getDiagnostics returned unexpected type');
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(raw);
|
||||
|
||||
return {
|
||||
isRemote: parsed.isRemote ?? false,
|
||||
systemInfo: {
|
||||
operatingSystem: parsed.systemInfo?.operatingSystem ?? 'unknown',
|
||||
timestamp: parsed.systemInfo?.timestamp ?? '',
|
||||
userEmail: parsed.systemInfo?.userEmail ?? '',
|
||||
userName: parsed.systemInfo?.userName ?? '',
|
||||
},
|
||||
raw: parsed,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Chrome DevTools MCP URL.
|
||||
*
|
||||
* Verified: returns `http://127.0.0.1:{port}/mcp`
|
||||
*
|
||||
* @returns MCP URL string
|
||||
*/
|
||||
async getMcpUrl(): Promise<string> {
|
||||
const result = await this._commands.execute<string>('antigravity.getChromeDevtoolsMcpUrl');
|
||||
return result ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a file is gitignored.
|
||||
*
|
||||
* @param filePath - Relative or absolute file path
|
||||
* @returns true if gitignored, false/null otherwise
|
||||
*/
|
||||
async isFileGitIgnored(filePath: string): Promise<boolean> {
|
||||
const result = await this._commands.execute<boolean | null>('antigravity.isFileGitIgnored', filePath);
|
||||
return result === true;
|
||||
}
|
||||
|
||||
// ─── Write API ──────────────────────────────────────────────────────────
|
||||
//
|
||||
// Two-layer architecture (VERIFIED 2026-02-28):
|
||||
//
|
||||
// Layer 1 -- HEADLESS LS API (RECOMMENDED):
|
||||
// Access: sdk.ls (LSBridge from antigravity-sdk)
|
||||
// Method: Preact VNode tree -> component.props.lsClient -> 148 LS methods
|
||||
// Creates cascade WITHOUT opening panel or switching UI.
|
||||
// Usage: await sdk.ls.createCascade({ text: 'prompt' })
|
||||
//
|
||||
// Layer 2 — COMMAND API (FALLBACK, this file):
|
||||
// Access: vscode.commands.executeCommand (extension host)
|
||||
// Method: startNewConversation → sendPromptToAgentPanel → restore
|
||||
// PROBLEM: Always switches UI, causes flickering, race conditions.
|
||||
// Use only when renderer integration is not available.
|
||||
//
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Create a new Cascade conversation via VS Code commands.
|
||||
*
|
||||
* ⚠️ **FALLBACK APPROACH** — causes UI flickering.
|
||||
* For true headless creation, use `sdk.ls.createCascade()`
|
||||
* from the SDK's LS bridge (see LSBridge module).
|
||||
*
|
||||
* VERIFIED 2026-02-28:
|
||||
* - `startNewConversation` ✅ creates new chat (but switches UI)
|
||||
* - `prioritized.chat.openNewConversation` ❌ does NOT create new
|
||||
* - `sendPromptToAgentPanel` ✅ sends to currently visible chat (always opens panel)
|
||||
* - `sendTextToChat` ❌ does not visibly work
|
||||
*
|
||||
* @param options - Session creation options
|
||||
* @returns Session ID (googleAgentId) or empty string if not detected
|
||||
*/
|
||||
async createSession(options: ICreateSessionOptions): Promise<string> {
|
||||
log.info(`Creating session (command fallback): "${options.task.substring(0, 50)}..."`);
|
||||
|
||||
// Snapshot current sessions to detect the new one
|
||||
const beforeIds = new Set(this._sessions.map(s => s.id));
|
||||
|
||||
// Remember current active session (for background restore)
|
||||
let previousActiveId = '';
|
||||
if (options.background) {
|
||||
try {
|
||||
const raw = await this._commands.execute<string>(AntigravityCommands.GET_DIAGNOSTICS);
|
||||
if (raw && typeof raw === 'string') {
|
||||
const diag = JSON.parse(raw);
|
||||
if (Array.isArray(diag.recentTrajectories) && diag.recentTrajectories.length > 0) {
|
||||
previousActiveId = diag.recentTrajectories[0].googleAgentId ?? '';
|
||||
}
|
||||
}
|
||||
} catch { }
|
||||
}
|
||||
|
||||
// Create new conversation (VERIFIED: startNewConversation works)
|
||||
await this._commands.execute(AntigravityCommands.START_NEW_CONVERSATION);
|
||||
await this._delay(1500); // Wait for UI to initialize
|
||||
|
||||
// Send initial prompt
|
||||
if (options.task) {
|
||||
await this._commands.execute(AntigravityCommands.SEND_PROMPT_TO_AGENT, options.task);
|
||||
}
|
||||
|
||||
// Mark as background if requested
|
||||
if (options.background) {
|
||||
await this._commands.execute(AntigravityCommands.TRACK_BACKGROUND_CONVERSATION);
|
||||
}
|
||||
|
||||
// Wait for new session to appear in getDiagnostics
|
||||
const newId = await this._waitForNewSession(beforeIds, 8000);
|
||||
|
||||
// If background: switch back to original conversation
|
||||
if (options.background && previousActiveId) {
|
||||
await this._delay(500);
|
||||
await this._commands.execute(AntigravityCommands.SET_VISIBLE_CONVERSATION, previousActiveId);
|
||||
log.info(`Background session created, restored to ${previousActiveId}`);
|
||||
}
|
||||
|
||||
if (newId) {
|
||||
log.info(`Session created: ${newId}`);
|
||||
} else {
|
||||
log.warn('Session created but ID not detected within timeout');
|
||||
}
|
||||
|
||||
return newId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a background Cascade conversation via commands.
|
||||
*
|
||||
* ⚠️ **FALLBACK** — Uses quick-switch approach (UI flickers briefly).
|
||||
* For true headless background sessions, use the SDK's LS bridge:
|
||||
* ```typescript
|
||||
* // Using LSBridge:
|
||||
* const cascadeId = await sdk.ls.createCascade({ text: 'task', modelId: 1018 });
|
||||
* ```
|
||||
*
|
||||
* @param task - Initial task/prompt to send
|
||||
* @returns Session ID or empty string
|
||||
*/
|
||||
async createBackgroundSession(task: string): Promise<string> {
|
||||
return this.createSession({ task, background: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message to the active Cascade conversation.
|
||||
*
|
||||
* Uses `antigravity.sendTextToChat` — the primary text sending command.
|
||||
*/
|
||||
async sendMessage(text: string): Promise<void> {
|
||||
await this._commands.execute(AntigravityCommands.SEND_TEXT_TO_CHAT, text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a prompt directly to the agent panel.
|
||||
*
|
||||
* Uses `antigravity.sendPromptToAgentPanel` — focuses the agent panel.
|
||||
*/
|
||||
async sendPrompt(text: string): Promise<void> {
|
||||
await this._commands.execute(AntigravityCommands.SEND_PROMPT_TO_AGENT, text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a chat action message (e.g., typing indicator, feedback).
|
||||
*
|
||||
* Uses `antigravity.sendChatActionMessage`.
|
||||
*/
|
||||
async sendChatAction(action: string): Promise<void> {
|
||||
await this._commands.execute(AntigravityCommands.SEND_CHAT_ACTION, action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch to a specific conversation.
|
||||
*
|
||||
* @param sessionId - Conversation UUID (googleAgentId)
|
||||
*/
|
||||
async focusSession(sessionId: string): Promise<void> {
|
||||
await this._commands.execute(AntigravityCommands.SET_VISIBLE_CONVERSATION, sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a new conversation in the agent panel (prioritized command).
|
||||
*
|
||||
* Uses `antigravity.prioritized.chat.openNewConversation` which both
|
||||
* opens the panel AND creates a fresh conversation.
|
||||
*/
|
||||
async openNewConversation(): Promise<void> {
|
||||
await this._commands.execute(AntigravityCommands.OPEN_NEW_CONVERSATION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a Cascade action.
|
||||
*
|
||||
* Uses `antigravity.executeCascadeAction`.
|
||||
*
|
||||
* @param action - Action data to execute
|
||||
*/
|
||||
async executeCascadeAction(action: unknown): Promise<void> {
|
||||
await this._commands.execute(AntigravityCommands.EXECUTE_CASCADE_ACTION, action);
|
||||
}
|
||||
|
||||
// ─── Step Control ───────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Accept the current agent step (code edit, file write, etc.).
|
||||
*
|
||||
* Uses `antigravity.agent.acceptAgentStep`.
|
||||
*/
|
||||
async acceptStep(): Promise<void> {
|
||||
await this._commands.execute(AntigravityCommands.ACCEPT_AGENT_STEP);
|
||||
}
|
||||
|
||||
/** Reject the current agent step. */
|
||||
async rejectStep(): Promise<void> {
|
||||
await this._commands.execute(AntigravityCommands.REJECT_AGENT_STEP);
|
||||
}
|
||||
|
||||
/**
|
||||
* Accept a pending command (non-terminal, e.g. file edit confirmation).
|
||||
*
|
||||
* Uses `antigravity.command.accept`.
|
||||
* This is DIFFERENT from terminalCommand.accept.
|
||||
*/
|
||||
async acceptCommand(): Promise<void> {
|
||||
await this._commands.execute(AntigravityCommands.COMMAND_ACCEPT);
|
||||
}
|
||||
|
||||
/** Reject a pending command (non-terminal). */
|
||||
async rejectCommand(): Promise<void> {
|
||||
await this._commands.execute(AntigravityCommands.COMMAND_REJECT);
|
||||
}
|
||||
|
||||
// ─── Terminal Control ───────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Accept a pending terminal command.
|
||||
*
|
||||
* Uses `antigravity.terminalCommand.accept`.
|
||||
*/
|
||||
async acceptTerminalCommand(): Promise<void> {
|
||||
await this._commands.execute(AntigravityCommands.TERMINAL_ACCEPT);
|
||||
}
|
||||
|
||||
/** Reject a pending terminal command. */
|
||||
async rejectTerminalCommand(): Promise<void> {
|
||||
await this._commands.execute(AntigravityCommands.TERMINAL_REJECT);
|
||||
}
|
||||
|
||||
/** Run a pending terminal command. */
|
||||
async runTerminalCommand(): Promise<void> {
|
||||
await this._commands.execute(AntigravityCommands.TERMINAL_RUN);
|
||||
}
|
||||
|
||||
// ─── Panel Control ──────────────────────────────────────────────────────
|
||||
|
||||
/** Open the Cascade agent panel */
|
||||
async openPanel(): Promise<void> {
|
||||
await this._commands.execute(AntigravityCommands.OPEN_AGENT_PANEL);
|
||||
}
|
||||
|
||||
/** Focus the Cascade agent panel */
|
||||
async focusPanel(): Promise<void> {
|
||||
await this._commands.execute(AntigravityCommands.FOCUS_AGENT_PANEL);
|
||||
}
|
||||
|
||||
/** Open the agent side panel */
|
||||
async openSidePanel(): Promise<void> {
|
||||
await this._commands.execute(AntigravityCommands.OPEN_AGENT_SIDE_PANEL);
|
||||
}
|
||||
|
||||
/** Focus the agent side panel */
|
||||
async focusSidePanel(): Promise<void> {
|
||||
await this._commands.execute(AntigravityCommands.FOCUS_AGENT_SIDE_PANEL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the browser integration port (e.g., 57401).
|
||||
*/
|
||||
async getBrowserPort(): Promise<number> {
|
||||
return this._commands.execute<number>(AntigravityCommands.GET_BROWSER_PORT);
|
||||
}
|
||||
|
||||
// ─── Private ────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Load sessions from getDiagnostics.recentTrajectories (clean JSON).
|
||||
*
|
||||
* VERIFIED structure per entry:
|
||||
* {
|
||||
* googleAgentId: "uuid", ← conversation ID
|
||||
* trajectoryId: "uuid", ← internal trajectory ID
|
||||
* summary: "title", ← human-readable title
|
||||
* lastStepIndex: 992, ← step count
|
||||
* lastModifiedTime: "ISO" ← last activity
|
||||
* }
|
||||
*/
|
||||
private async _loadSessions(): Promise<void> {
|
||||
try {
|
||||
// Primary: getDiagnostics.recentTrajectories (10 most recent, with titles)
|
||||
const raw = await this._commands.execute<string>(AntigravityCommands.GET_DIAGNOSTICS);
|
||||
if (raw && typeof raw === 'string') {
|
||||
const diag = JSON.parse(raw);
|
||||
if (Array.isArray(diag.recentTrajectories)) {
|
||||
this._sessions = diag.recentTrajectories.map((entry: any) => ({
|
||||
id: entry.googleAgentId ?? '',
|
||||
title: entry.summary ?? 'Untitled',
|
||||
stepCount: entry.lastStepIndex ?? 0,
|
||||
workspaceUri: '',
|
||||
lastModifiedTime: entry.lastModifiedTime ?? '',
|
||||
trajectoryId: entry.trajectoryId ?? '',
|
||||
}));
|
||||
log.debug(`Loaded ${this._sessions.length} sessions from getDiagnostics`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
log.warn('getDiagnostics failed, falling back to USS', error);
|
||||
}
|
||||
|
||||
// Fallback: parse trajectory summaries protobuf
|
||||
try {
|
||||
await this._loadSessionsFromUSS();
|
||||
} catch (error) {
|
||||
log.error('Failed to load sessions from USS', error);
|
||||
this._sessions = [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback: extract sessions from USS trajectory summaries protobuf.
|
||||
*/
|
||||
private async _loadSessionsFromUSS(): Promise<void> {
|
||||
const raw = await this._state.getRawValue('antigravityUnifiedStateSync.trajectorySummaries');
|
||||
if (!raw) {
|
||||
this._sessions = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const buffer = Buffer.from(raw, 'base64');
|
||||
const text = buffer.toString('utf8');
|
||||
|
||||
// Extract UUIDs
|
||||
const uuids = [...new Set(text.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/g) || [])];
|
||||
|
||||
this._sessions = uuids.map((id, i) => ({
|
||||
id,
|
||||
title: `Conversation ${i + 1}`,
|
||||
stepCount: 0,
|
||||
workspaceUri: '',
|
||||
}));
|
||||
|
||||
log.debug(`Loaded ${this._sessions.length} sessions from USS (fallback)`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for a new session to appear in getDiagnostics.
|
||||
* Polls every 500ms up to timeoutMs.
|
||||
*
|
||||
* @returns New session ID or empty string if timeout
|
||||
*/
|
||||
private async _waitForNewSession(beforeIds: Set<string>, timeoutMs: number): Promise<string> {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
const pollInterval = 500;
|
||||
|
||||
while (Date.now() < deadline) {
|
||||
await this._delay(pollInterval);
|
||||
|
||||
try {
|
||||
const raw = await this._commands.execute<string>(AntigravityCommands.GET_DIAGNOSTICS);
|
||||
if (!raw || typeof raw !== 'string') continue;
|
||||
|
||||
const diag = JSON.parse(raw);
|
||||
if (!Array.isArray(diag.recentTrajectories)) continue;
|
||||
|
||||
for (const entry of diag.recentTrajectories) {
|
||||
const id = entry.googleAgentId;
|
||||
if (id && !beforeIds.has(id)) {
|
||||
// Update local session list
|
||||
await this._loadSessions();
|
||||
return id;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore, retry
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple delay utility.
|
||||
*/
|
||||
private _delay(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._disposables.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
6
antigravity-sdk-main/src/cascade/index.ts
Normal file
6
antigravity-sdk-main/src/cascade/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Cascade module re-exports.
|
||||
* @module cascade
|
||||
*/
|
||||
|
||||
export { CascadeManager } from './cascade-manager';
|
||||
73
antigravity-sdk-main/src/core/disposable.ts
Normal file
73
antigravity-sdk-main/src/core/disposable.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* Disposable pattern for resource cleanup.
|
||||
*
|
||||
* @module disposable
|
||||
*/
|
||||
|
||||
/**
|
||||
* An object that can release resources when no longer needed.
|
||||
*/
|
||||
export interface IDisposable {
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects multiple disposables and disposes them all at once.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const store = new DisposableStore();
|
||||
* store.add(someEventSub);
|
||||
* store.add(anotherSub);
|
||||
* // Later:
|
||||
* store.dispose(); // cleans up everything
|
||||
* ```
|
||||
*/
|
||||
export class DisposableStore implements IDisposable {
|
||||
private readonly _disposables: IDisposable[] = [];
|
||||
private _disposed = false;
|
||||
|
||||
/**
|
||||
* Add a disposable to the store.
|
||||
*
|
||||
* @param disposable - The disposable to track
|
||||
* @returns The same disposable (for chaining)
|
||||
*/
|
||||
add<T extends IDisposable>(disposable: T): T {
|
||||
if (this._disposed) {
|
||||
disposable.dispose();
|
||||
console.warn('[AntigravitySDK] Adding disposable to already disposed store');
|
||||
} else {
|
||||
this._disposables.push(disposable);
|
||||
}
|
||||
return disposable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose all tracked disposables.
|
||||
*/
|
||||
dispose(): void {
|
||||
if (this._disposed) {
|
||||
return;
|
||||
}
|
||||
this._disposed = true;
|
||||
|
||||
for (const d of this._disposables) {
|
||||
try {
|
||||
d.dispose();
|
||||
} catch (error) {
|
||||
console.error('[AntigravitySDK] Dispose error:', error);
|
||||
}
|
||||
}
|
||||
this._disposables.length = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a disposable from a cleanup function.
|
||||
*
|
||||
* @param fn - Cleanup function to call on dispose
|
||||
*/
|
||||
export function toDisposable(fn: () => void): IDisposable {
|
||||
return { dispose: fn };
|
||||
}
|
||||
61
antigravity-sdk-main/src/core/errors.ts
Normal file
61
antigravity-sdk-main/src/core/errors.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* SDK-specific error classes.
|
||||
*
|
||||
* @module errors
|
||||
*/
|
||||
|
||||
/**
|
||||
* Base error for all Antigravity SDK errors.
|
||||
*/
|
||||
export class AntigravitySDKError extends Error {
|
||||
constructor(message: string) {
|
||||
super(`[AntigravitySDK] ${message}`);
|
||||
this.name = 'AntigravitySDKError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Thrown when Antigravity IDE is not detected or not running.
|
||||
*/
|
||||
export class AntigravityNotFoundError extends AntigravitySDKError {
|
||||
constructor() {
|
||||
super('Antigravity IDE not detected. Make sure this extension is running inside Antigravity.');
|
||||
this.name = 'AntigravityNotFoundError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Thrown when a command fails to execute.
|
||||
*/
|
||||
export class CommandExecutionError extends AntigravitySDKError {
|
||||
constructor(
|
||||
public readonly command: string,
|
||||
public readonly reason: string,
|
||||
) {
|
||||
super(`Command "${command}" failed: ${reason}`);
|
||||
this.name = 'CommandExecutionError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Thrown when the state database cannot be read.
|
||||
*/
|
||||
export class StateReadError extends AntigravitySDKError {
|
||||
constructor(
|
||||
public readonly key: string,
|
||||
public readonly reason: string,
|
||||
) {
|
||||
super(`Failed to read state key "${key}": ${reason}`);
|
||||
this.name = 'StateReadError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Thrown when a session/conversation is not found.
|
||||
*/
|
||||
export class SessionNotFoundError extends AntigravitySDKError {
|
||||
constructor(public readonly sessionId: string) {
|
||||
super(`Session "${sessionId}" not found`);
|
||||
this.name = 'SessionNotFoundError';
|
||||
}
|
||||
}
|
||||
99
antigravity-sdk-main/src/core/events.ts
Normal file
99
antigravity-sdk-main/src/core/events.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* Lightweight event system for SDK.
|
||||
*
|
||||
* Follows VS Code's `Event<T>` / `EventEmitter<T>` pattern.
|
||||
* Supports subscription, disposal, and one-shot listeners.
|
||||
*
|
||||
* @module events
|
||||
*/
|
||||
|
||||
import type { IDisposable } from './disposable';
|
||||
|
||||
/**
|
||||
* A function that represents a subscription to an event.
|
||||
* Call the returned disposable to unsubscribe.
|
||||
*/
|
||||
export type Event<T> = (listener: (e: T) => void) => IDisposable;
|
||||
|
||||
/**
|
||||
* Emits events to registered listeners.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const emitter = new EventEmitter<string>();
|
||||
*
|
||||
* const sub = emitter.event((msg) => console.log(msg));
|
||||
* emitter.fire('hello'); // logs: hello
|
||||
* sub.dispose();
|
||||
* emitter.fire('world'); // nothing happens
|
||||
* ```
|
||||
*/
|
||||
export class EventEmitter<T> implements IDisposable {
|
||||
private _listeners: Set<(e: T) => void> = new Set();
|
||||
private _disposed = false;
|
||||
|
||||
/**
|
||||
* The event that listeners can subscribe to.
|
||||
*/
|
||||
readonly event: Event<T> = (listener: (e: T) => void): IDisposable => {
|
||||
if (this._disposed) {
|
||||
throw new Error('EventEmitter has been disposed');
|
||||
}
|
||||
|
||||
this._listeners.add(listener);
|
||||
|
||||
return {
|
||||
dispose: () => {
|
||||
this._listeners.delete(listener);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Fire the event, notifying all listeners.
|
||||
*
|
||||
* @param data - The event data to send to listeners
|
||||
*/
|
||||
fire(data: T): void {
|
||||
if (this._disposed) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const listener of this._listeners) {
|
||||
try {
|
||||
listener(data);
|
||||
} catch (error) {
|
||||
console.error('[AntigravitySDK] Event listener error:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to the event, but only fire once.
|
||||
*
|
||||
* @param listener - Callback to invoke once
|
||||
* @returns Disposable to cancel before the event fires
|
||||
*/
|
||||
once(listener: (e: T) => void): IDisposable {
|
||||
const sub = this.event((data) => {
|
||||
sub.dispose();
|
||||
listener(data);
|
||||
});
|
||||
return sub;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current number of listeners.
|
||||
*/
|
||||
get listenerCount(): number {
|
||||
return this._listeners.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose of the emitter and all listeners.
|
||||
*/
|
||||
dispose(): void {
|
||||
this._disposed = true;
|
||||
this._listeners.clear();
|
||||
}
|
||||
}
|
||||
11
antigravity-sdk-main/src/core/index.ts
Normal file
11
antigravity-sdk-main/src/core/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Core module — types, events, disposables, errors, logging.
|
||||
*
|
||||
* @module core
|
||||
*/
|
||||
|
||||
export * from './types';
|
||||
export * from './events';
|
||||
export * from './disposable';
|
||||
export * from './errors';
|
||||
export { Logger, LogLevel } from './logger';
|
||||
84
antigravity-sdk-main/src/core/logger.ts
Normal file
84
antigravity-sdk-main/src/core/logger.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* Debug logger for SDK internals.
|
||||
*
|
||||
* Respects the `antigravitySDK.debug` setting.
|
||||
*
|
||||
* @module logger
|
||||
*/
|
||||
|
||||
/**
|
||||
* Log levels for SDK logging.
|
||||
*/
|
||||
export enum LogLevel {
|
||||
Debug = 0,
|
||||
Info = 1,
|
||||
Warn = 2,
|
||||
Error = 3,
|
||||
Off = 4,
|
||||
}
|
||||
|
||||
/**
|
||||
* SDK logger with level-based filtering.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const log = new Logger('CascadeManager');
|
||||
* log.debug('Loading sessions...');
|
||||
* log.info('Found 5 sessions');
|
||||
* log.error('Failed to load', err);
|
||||
* ```
|
||||
*/
|
||||
export class Logger {
|
||||
private static _globalLevel: LogLevel = LogLevel.Warn;
|
||||
|
||||
/**
|
||||
* Set the global log level for all SDK loggers.
|
||||
*
|
||||
* @param level - Minimum level to output
|
||||
*/
|
||||
static setLevel(level: LogLevel): void {
|
||||
Logger._globalLevel = level;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a logger for a specific module.
|
||||
*
|
||||
* @param module - Module name (shown in log prefix)
|
||||
*/
|
||||
constructor(private readonly module: string) { }
|
||||
|
||||
/** Log a debug message. */
|
||||
debug(message: string, ...args: unknown[]): void {
|
||||
this._log(LogLevel.Debug, message, args);
|
||||
}
|
||||
|
||||
/** Log an informational message. */
|
||||
info(message: string, ...args: unknown[]): void {
|
||||
this._log(LogLevel.Info, message, args);
|
||||
}
|
||||
|
||||
/** Log a warning. */
|
||||
warn(message: string, ...args: unknown[]): void {
|
||||
this._log(LogLevel.Warn, message, args);
|
||||
}
|
||||
|
||||
/** Log an error. */
|
||||
error(message: string, ...args: unknown[]): void {
|
||||
this._log(LogLevel.Error, message, args);
|
||||
}
|
||||
|
||||
private _log(level: LogLevel, message: string, args: unknown[]): void {
|
||||
if (level < Logger._globalLevel) {
|
||||
return;
|
||||
}
|
||||
|
||||
const prefix = `[AntigravitySDK:${this.module}]`;
|
||||
const fn =
|
||||
level === LogLevel.Error ? console.error
|
||||
: level === LogLevel.Warn ? console.warn
|
||||
: level === LogLevel.Info ? console.info
|
||||
: console.debug;
|
||||
|
||||
fn(prefix, message, ...args);
|
||||
}
|
||||
}
|
||||
381
antigravity-sdk-main/src/core/types.ts
Normal file
381
antigravity-sdk-main/src/core/types.ts
Normal file
@@ -0,0 +1,381 @@
|
||||
/**
|
||||
* Core type definitions for Antigravity SDK.
|
||||
*
|
||||
* These types mirror the internal protobuf schemas used by Antigravity's
|
||||
* Language Server, extracted via reverse engineering of the minified source.
|
||||
*
|
||||
* @module types
|
||||
*/
|
||||
|
||||
// ─── Enums ──────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Terminal command auto-execution policy.
|
||||
*
|
||||
* Controls how terminal commands are handled when the agent requests execution.
|
||||
*/
|
||||
export enum TerminalExecutionPolicy {
|
||||
/** Always ask user before running */
|
||||
OFF = 1,
|
||||
/** Auto-run safe commands, ask for potentially dangerous ones */
|
||||
AUTO = 2,
|
||||
/** Always auto-run without asking */
|
||||
EAGER = 3,
|
||||
}
|
||||
|
||||
/**
|
||||
* Artifact review policy for code changes.
|
||||
*/
|
||||
export enum ArtifactReviewPolicy {
|
||||
/** Always show diff review */
|
||||
ALWAYS = 1,
|
||||
/** Skip review for simple changes */
|
||||
TURBO = 2,
|
||||
/** Automatically decide based on change complexity */
|
||||
AUTO = 3,
|
||||
}
|
||||
|
||||
/**
|
||||
* Type of a Cortex step (tool call) in a trajectory.
|
||||
*/
|
||||
export enum CortexStepType {
|
||||
RunCommand = 'RunCommand',
|
||||
WriteToFile = 'WriteToFile',
|
||||
ViewFile = 'ViewFile',
|
||||
ViewFileOutline = 'ViewFileOutline',
|
||||
ViewCodeItem = 'ViewCodeItem',
|
||||
SearchWeb = 'SearchWeb',
|
||||
ReadUrlContent = 'ReadUrlContent',
|
||||
OpenBrowserUrl = 'OpenBrowserUrl',
|
||||
ReadBrowserPage = 'ReadBrowserPage',
|
||||
ListBrowserPages = 'ListBrowserPages',
|
||||
ListDirectory = 'ListDirectory',
|
||||
FindByName = 'FindByName',
|
||||
CodebaseSearch = 'CodebaseSearch',
|
||||
GrepSearch = 'GrepSearch',
|
||||
SendCommandInput = 'SendCommandInput',
|
||||
ReadTerminal = 'ReadTerminal',
|
||||
ShellExec = 'ShellExec',
|
||||
McpTool = 'McpTool',
|
||||
InvokeSubagent = 'InvokeSubagent',
|
||||
Memory = 'Memory',
|
||||
KnowledgeGeneration = 'KnowledgeGeneration',
|
||||
UserInput = 'UserInput',
|
||||
SystemMessage = 'SystemMessage',
|
||||
PlannerResponse = 'PlannerResponse',
|
||||
Wait = 'Wait',
|
||||
ProposeCode = 'ProposeCode',
|
||||
WriteCascadeEdit = 'WriteCascadeEdit',
|
||||
}
|
||||
|
||||
/**
|
||||
* Status of a Cortex step.
|
||||
*/
|
||||
export enum StepStatus {
|
||||
/** Step is being processed */
|
||||
Running = 'running',
|
||||
/** Step completed successfully */
|
||||
Completed = 'completed',
|
||||
/** Step failed */
|
||||
Failed = 'failed',
|
||||
/** Step is waiting for user interaction */
|
||||
WaitingForUser = 'waiting_for_user',
|
||||
/** Step was cancelled */
|
||||
Cancelled = 'cancelled',
|
||||
}
|
||||
|
||||
/**
|
||||
* Type of trajectory (conversation).
|
||||
*/
|
||||
export enum TrajectoryType {
|
||||
/** Standard chat conversation */
|
||||
Chat = 'chat',
|
||||
/** Agent mode (Cascade) */
|
||||
Cascade = 'cascade',
|
||||
}
|
||||
|
||||
// ─── Interfaces ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* A single step (tool call) in a Cascade trajectory.
|
||||
*/
|
||||
export interface ICortexStep {
|
||||
/** Unique step identifier */
|
||||
readonly id: string;
|
||||
|
||||
/** Step index within the trajectory */
|
||||
readonly index: number;
|
||||
|
||||
/** Type of tool call */
|
||||
readonly type: CortexStepType;
|
||||
|
||||
/** Current status */
|
||||
readonly status: StepStatus;
|
||||
|
||||
/** Human-readable summary of what this step does */
|
||||
readonly summary: string;
|
||||
|
||||
/** Step-specific data (command line, file path, etc.) */
|
||||
readonly data: Record<string, unknown>;
|
||||
|
||||
/** Internal metadata not shown in UI */
|
||||
readonly metadata: IStepMetadata;
|
||||
|
||||
/** Timestamp when step was created */
|
||||
readonly createdAt: Date;
|
||||
|
||||
/** Timestamp when step completed (if completed) */
|
||||
readonly completedAt?: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal metadata attached to each step.
|
||||
*/
|
||||
export interface IStepMetadata {
|
||||
/** Raw protobuf fields from the server response */
|
||||
readonly rawFields: Record<string, unknown>;
|
||||
|
||||
/** Token count for this step's input */
|
||||
readonly inputTokens?: number;
|
||||
|
||||
/** Token count for this step's output */
|
||||
readonly outputTokens?: number;
|
||||
|
||||
/** Model used for this step */
|
||||
readonly model?: string;
|
||||
|
||||
/** Whether this step was auto-approved */
|
||||
readonly autoApproved?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* A chat message in a conversation.
|
||||
*/
|
||||
export interface IChatMessage {
|
||||
/** Message role */
|
||||
readonly role: 'user' | 'assistant' | 'system';
|
||||
|
||||
/** Message content */
|
||||
readonly content: string;
|
||||
|
||||
/** Message ID */
|
||||
readonly id: string;
|
||||
|
||||
/** Timestamp */
|
||||
readonly createdAt: Date;
|
||||
|
||||
/** Hidden metadata */
|
||||
readonly metadata: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Information about the current context window usage.
|
||||
*/
|
||||
export interface IContextInfo {
|
||||
/** Total tokens currently in context */
|
||||
readonly totalTokens: number;
|
||||
|
||||
/** Maximum context window size */
|
||||
readonly maxTokens: number;
|
||||
|
||||
/** Usage as percentage (0-100) */
|
||||
readonly usagePercent: number;
|
||||
|
||||
/** Token breakdown by category */
|
||||
readonly breakdown: ITokenBreakdown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Token usage breakdown.
|
||||
*/
|
||||
export interface ITokenBreakdown {
|
||||
/** System prompt tokens */
|
||||
readonly system: number;
|
||||
/** User message tokens */
|
||||
readonly userMessages: number;
|
||||
/** Assistant response tokens */
|
||||
readonly assistantMessages: number;
|
||||
/** Tool call input tokens */
|
||||
readonly toolCalls: number;
|
||||
/** Tool result tokens */
|
||||
readonly toolResults: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* A Cascade session (conversation/trajectory).
|
||||
*/
|
||||
export interface ISessionInfo {
|
||||
/** Unique session/cascade ID */
|
||||
readonly id: string;
|
||||
|
||||
/** Session title (auto-generated or user-set) */
|
||||
readonly title: string;
|
||||
|
||||
/** When the session was created */
|
||||
readonly createdAt: Date;
|
||||
|
||||
/** When the session was last active */
|
||||
readonly lastActiveAt: Date;
|
||||
|
||||
/** Type of trajectory */
|
||||
readonly type: TrajectoryType;
|
||||
|
||||
/** Whether the session is currently active */
|
||||
readonly isActive: boolean;
|
||||
|
||||
/** Tags applied to this session */
|
||||
readonly tags: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Agent preferences from USS (Unified State Sync).
|
||||
*
|
||||
* All 16 sentinel keys verified from live state.vscdb on 2026-02-28.
|
||||
*/
|
||||
export interface IAgentPreferences {
|
||||
/** Terminal command auto-execution policy (terminalAutoExecutionPolicySentinelKey) */
|
||||
readonly terminalExecutionPolicy: TerminalExecutionPolicy;
|
||||
|
||||
/** Code change review policy (artifactReviewPolicySentinelKey) */
|
||||
readonly artifactReviewPolicy: ArtifactReviewPolicy;
|
||||
|
||||
/** Planning mode (planningModeSentinelKey) */
|
||||
readonly planningMode: number;
|
||||
|
||||
/** Whether strict/secure mode is enabled (secureModeSentinelKey) */
|
||||
readonly secureModeEnabled: boolean;
|
||||
|
||||
/** Whether terminal sandbox is enabled (enableTerminalSandboxSentinelKey) */
|
||||
readonly terminalSandboxEnabled: boolean;
|
||||
|
||||
/** Whether sandbox allows network access (sandboxAllowNetworkSentinelKey) */
|
||||
readonly sandboxAllowNetwork: boolean;
|
||||
|
||||
/** Whether shell integration is enabled (enableShellIntegrationSentinelKey) */
|
||||
readonly shellIntegrationEnabled: boolean;
|
||||
|
||||
/** Allow agent to access files outside workspace (allowAgentAccessNonWorkspaceFilesSentinelKey) */
|
||||
readonly allowNonWorkspaceFiles: boolean;
|
||||
|
||||
/** Allow Cascade to read .gitignore files (allowCascadeAccessGitignoreFilesSentinelKey) */
|
||||
readonly allowGitignoreAccess: boolean;
|
||||
|
||||
/** Explain and fix in current conversation (explainAndFixInCurrentConversationSentinelKey) */
|
||||
readonly explainFixInCurrentConvo: boolean;
|
||||
|
||||
/** Auto-continue on max generator invocations (autoContinueOnMaxGeneratorInvocationsSentinelKey) */
|
||||
readonly autoContinueOnMax: number;
|
||||
|
||||
/** Disable auto-open of edited files (disableAutoOpenEditedFilesSentinelKey) */
|
||||
readonly disableAutoOpenEdited: boolean;
|
||||
|
||||
/** Enable sounds for special events (enableSoundsForSpecialEventsSentinelKey) */
|
||||
readonly enableSounds: boolean;
|
||||
|
||||
/** Disable Cascade auto-fix for lint errors (disableCascadeAutoFixLintsSentinelKey) */
|
||||
readonly disableAutoFixLints: boolean;
|
||||
|
||||
/** Explicitly allowed terminal commands (terminalAllowedCommandsSentinelKey) */
|
||||
readonly allowedCommands: string[];
|
||||
|
||||
/** Explicitly denied terminal commands (terminalDeniedCommandsSentinelKey) */
|
||||
readonly deniedCommands: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Model configuration.
|
||||
*/
|
||||
export interface IModelConfig {
|
||||
/** Model identifier */
|
||||
readonly id: string;
|
||||
|
||||
/** Human-readable model name */
|
||||
readonly name: string;
|
||||
|
||||
/** Whether this model is currently selected */
|
||||
readonly isActive: boolean;
|
||||
|
||||
/** Maximum context window size in tokens */
|
||||
readonly maxContextTokens: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for creating a new Cascade session.
|
||||
*/
|
||||
export interface ICreateSessionOptions {
|
||||
/** Initial task/message to send */
|
||||
readonly task: string;
|
||||
|
||||
/** Whether to run in background (don't focus the panel) */
|
||||
readonly background?: boolean;
|
||||
|
||||
/** Model to use (defaults to current) */
|
||||
readonly model?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Agent state from the Agent Manager.
|
||||
*/
|
||||
export interface IAgentState {
|
||||
/** Whether the agent manager is enabled */
|
||||
readonly isEnabled: boolean;
|
||||
|
||||
/** Whether the agent is currently processing */
|
||||
readonly isProcessing: boolean;
|
||||
|
||||
/** Active cascade/conversation ID */
|
||||
readonly activeCascadeId: string | null;
|
||||
|
||||
/** Current model in use */
|
||||
readonly currentModel: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Trajectory entry from getDiagnostics.recentTrajectories.
|
||||
*
|
||||
* VERIFIED 2026-02-28: getDiagnostics returns clean JSON array with:
|
||||
* { googleAgentId, trajectoryId, summary, lastStepIndex, lastModifiedTime }
|
||||
*/
|
||||
export interface ITrajectoryEntry {
|
||||
/** Conversation UUID = googleAgentId */
|
||||
readonly id: string;
|
||||
|
||||
/** Human-readable title = summary field */
|
||||
readonly title: string;
|
||||
|
||||
/** Current step index in this conversation */
|
||||
readonly stepCount: number;
|
||||
|
||||
/** Workspace URI (from USS protobuf fallback) */
|
||||
readonly workspaceUri: string;
|
||||
|
||||
/** Internal trajectory UUID (from getDiagnostics) */
|
||||
readonly trajectoryId?: string;
|
||||
|
||||
/** ISO timestamp of last modification (from getDiagnostics) */
|
||||
readonly lastModifiedTime?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Diagnostics info from `antigravity.getDiagnostics`.
|
||||
*
|
||||
* VERIFIED: returns 176KB JSON string with 8 top-level keys:
|
||||
* isRemote, systemInfo, extensionLogs, rendererLogs,
|
||||
* mainThreadLogs, agentWindowConsoleLogs, languageServerLogs,
|
||||
* recentTrajectories.
|
||||
*/
|
||||
export interface IDiagnosticsInfo {
|
||||
/** Whether IDE is running remotely (SSH) */
|
||||
readonly isRemote: boolean;
|
||||
|
||||
/** System info */
|
||||
readonly systemInfo: {
|
||||
readonly operatingSystem: string;
|
||||
readonly timestamp: string;
|
||||
readonly userEmail: string;
|
||||
readonly userName: string;
|
||||
};
|
||||
|
||||
/** Raw JSON for fields not yet typed */
|
||||
readonly raw: Record<string, unknown>;
|
||||
}
|
||||
87
antigravity-sdk-main/src/index.ts
Normal file
87
antigravity-sdk-main/src/index.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* Antigravity SDK — Community SDK for Antigravity IDE.
|
||||
*
|
||||
* @packageDocumentation
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { AntigravitySDK } from 'antigravity-sdk';
|
||||
*
|
||||
* export function activate(context: vscode.ExtensionContext) {
|
||||
* const sdk = new AntigravitySDK(context);
|
||||
* await sdk.initialize();
|
||||
*
|
||||
* // Read preferences
|
||||
* const prefs = await sdk.cascade.getPreferences();
|
||||
* console.log('Terminal policy:', prefs.terminalExecutionPolicy);
|
||||
*
|
||||
* // List sessions
|
||||
* const sessions = await sdk.cascade.getSessions();
|
||||
* console.log(`${sessions.length} conversations`);
|
||||
*
|
||||
* // Get diagnostics
|
||||
* const diag = await sdk.cascade.getDiagnostics();
|
||||
* console.log(`User: ${diag.systemInfo.userName}`);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
|
||||
// Core
|
||||
export {
|
||||
// Types
|
||||
TerminalExecutionPolicy,
|
||||
ArtifactReviewPolicy,
|
||||
CortexStepType,
|
||||
StepStatus,
|
||||
TrajectoryType,
|
||||
// Interfaces
|
||||
type ICortexStep,
|
||||
type IStepMetadata,
|
||||
type IChatMessage,
|
||||
type IContextInfo,
|
||||
type ITokenBreakdown,
|
||||
type ISessionInfo,
|
||||
type IAgentPreferences,
|
||||
type IModelConfig,
|
||||
type ICreateSessionOptions,
|
||||
type IAgentState,
|
||||
type ITrajectoryEntry,
|
||||
type IDiagnosticsInfo,
|
||||
} from './core/types';
|
||||
|
||||
export { Event, EventEmitter } from './core/events';
|
||||
export { IDisposable, DisposableStore, toDisposable } from './core/disposable';
|
||||
export {
|
||||
AntigravitySDKError,
|
||||
AntigravityNotFoundError,
|
||||
CommandExecutionError,
|
||||
StateReadError,
|
||||
SessionNotFoundError,
|
||||
} from './core/errors';
|
||||
export { Logger, LogLevel } from './core/logger';
|
||||
|
||||
// Transport
|
||||
export { CommandBridge, AntigravityCommands } from './transport/command-bridge';
|
||||
export { StateBridge, USSKeys } from './transport/state-bridge';
|
||||
export { EventMonitor, type IStateChange, type IStepCountChange, type IActiveSessionChange } from './transport/event-monitor';
|
||||
export { LSBridge, Models, type ModelId, type IHeadlessCascadeOptions, type ISendMessageOptions, type IConversationAnnotations } from './transport/ls-bridge';
|
||||
|
||||
// Cascade
|
||||
export { CascadeManager } from './cascade/cascade-manager';
|
||||
|
||||
// Integration
|
||||
export { IntegrationManager, IntegrityManager, TitleManager, IntegrationPoint } from './integration';
|
||||
export type {
|
||||
IntegrationConfig,
|
||||
IButtonIntegration,
|
||||
ITurnMetaIntegration,
|
||||
IUserBadgeIntegration,
|
||||
IBotActionIntegration,
|
||||
IDropdownIntegration,
|
||||
ITitleIntegration,
|
||||
IToastConfig,
|
||||
TurnMetric,
|
||||
} from './integration';
|
||||
|
||||
// SDK
|
||||
export { AntigravitySDK, type ISDKOptions } from './sdk';
|
||||
21
antigravity-sdk-main/src/integration/index.ts
Normal file
21
antigravity-sdk-main/src/integration/index.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Integration module — re-exports.
|
||||
* @module integration
|
||||
*/
|
||||
export { IntegrationManager } from './integration-manager';
|
||||
export { IntegrityManager } from './integrity-manager';
|
||||
export { TitleManager } from './title-manager';
|
||||
export { IntegrationPoint } from './types';
|
||||
export type {
|
||||
IntegrationConfig,
|
||||
IIntegrationManager,
|
||||
IButtonIntegration,
|
||||
ITurnMetaIntegration,
|
||||
IUserBadgeIntegration,
|
||||
IBotActionIntegration,
|
||||
IDropdownIntegration,
|
||||
ITitleIntegration,
|
||||
IToastConfig,
|
||||
IToastRow,
|
||||
TurnMetric,
|
||||
} from './types';
|
||||
704
antigravity-sdk-main/src/integration/integration-manager.ts
Normal file
704
antigravity-sdk-main/src/integration/integration-manager.ts
Normal file
@@ -0,0 +1,704 @@
|
||||
/**
|
||||
* Integration Manager — Public API for UI integration into Agent View.
|
||||
*
|
||||
* Orchestrates ScriptGenerator and WorkbenchPatcher to provide
|
||||
* a clean, developer-friendly API.
|
||||
*
|
||||
* @module integration/integration-manager
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { IntegrationManager, IntegrationPoint } from 'antigravity-sdk';
|
||||
*
|
||||
* const integrator = new IntegrationManager();
|
||||
*
|
||||
* integrator.register({
|
||||
* id: 'myStats',
|
||||
* point: IntegrationPoint.TOP_BAR,
|
||||
* icon: '📊',
|
||||
* tooltip: 'Show Stats',
|
||||
* toast: {
|
||||
* title: 'My Extension Stats',
|
||||
* rows: [{ key: 'turns:', value: 'Dynamic data here' }],
|
||||
* },
|
||||
* });
|
||||
*
|
||||
* integrator.register({
|
||||
* id: 'turnInfo',
|
||||
* point: IntegrationPoint.TURN_METADATA,
|
||||
* metrics: ['turnNumber', 'userCharCount', 'separator', 'aiCharCount', 'codeBlocks'],
|
||||
* });
|
||||
*
|
||||
* await integrator.install();
|
||||
* // Restart Antigravity to see changes
|
||||
* ```
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import { IDisposable } from '../core/disposable';
|
||||
import { Logger } from '../core/logger';
|
||||
import {
|
||||
IntegrationConfig,
|
||||
IntegrationPoint,
|
||||
IIntegrationManager,
|
||||
IButtonIntegration,
|
||||
ITurnMetaIntegration,
|
||||
IUserBadgeIntegration,
|
||||
IBotActionIntegration,
|
||||
IDropdownIntegration,
|
||||
ITitleIntegration,
|
||||
IToastConfig,
|
||||
} from './types';
|
||||
import { ScriptGenerator } from './script-generator';
|
||||
import { WorkbenchPatcher } from './workbench-patcher';
|
||||
import { IntegrityManager } from './integrity-manager';
|
||||
import { TitleManager } from './title-manager';
|
||||
import { generateTitleProxyCode } from './title-proxy';
|
||||
|
||||
const log = new Logger('IntegrationManager');
|
||||
|
||||
/**
|
||||
* Manages UI integrations into the Antigravity Agent View.
|
||||
*
|
||||
* Provides a declarative API to register integration points,
|
||||
* generates a self-contained JavaScript file, and installs it
|
||||
* into Antigravity's workbench.
|
||||
*
|
||||
* Features:
|
||||
* - **Theme-aware**: Adapts to dark/light mode automatically
|
||||
* - **Auto-repair**: Watches workbench.html and re-patches after updates
|
||||
* - **Dynamic update**: Re-generate script without re-patching workbench.html
|
||||
*/
|
||||
export class IntegrationManager implements IIntegrationManager, IDisposable {
|
||||
private readonly _configs: Map<string, IntegrationConfig> = new Map();
|
||||
private readonly _generator = new ScriptGenerator();
|
||||
private readonly _patcher: WorkbenchPatcher;
|
||||
private readonly _integrity: IntegrityManager;
|
||||
private readonly _titles = new TitleManager();
|
||||
private readonly _namespace: string;
|
||||
private _watcher: fs.FSWatcher | null = null;
|
||||
private _autoRepairDebounce: ReturnType<typeof setTimeout> | null = null;
|
||||
private _titleProxyEnabled = false;
|
||||
|
||||
/**
|
||||
* @param namespace - Unique slug that isolates this extension's files.
|
||||
* Derived automatically from `context.extension.id` when using AntigravitySDK.
|
||||
* Multiple SDK-based extensions can coexist without conflicts.
|
||||
*/
|
||||
constructor(namespace: string = 'default') {
|
||||
this._namespace = namespace;
|
||||
this._patcher = new WorkbenchPatcher(namespace);
|
||||
this._integrity = new IntegrityManager(
|
||||
this._patcher.getWorkbenchDir(),
|
||||
namespace,
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Registration ──────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Register a single integration point.
|
||||
*
|
||||
* @throws If an integration with the same ID already exists
|
||||
*/
|
||||
register(config: IntegrationConfig): void {
|
||||
if (this._configs.has(config.id)) {
|
||||
throw new Error(`Integration '${config.id}' is already registered`);
|
||||
}
|
||||
this._configs.set(config.id, config);
|
||||
log.debug(`Registered integration: ${config.id} (${config.point})`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register multiple integration points at once.
|
||||
*/
|
||||
registerMany(configs: IntegrationConfig[]): void {
|
||||
for (const c of configs) {
|
||||
this.register(c);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a registered integration by ID.
|
||||
*/
|
||||
unregister(id: string): void {
|
||||
this._configs.delete(id);
|
||||
log.debug(`Unregistered integration: ${id}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered integrations.
|
||||
*/
|
||||
getRegistered(): ReadonlyArray<IntegrationConfig> {
|
||||
return Array.from(this._configs.values());
|
||||
}
|
||||
|
||||
// ─── Convenience methods (fluent API) ──────────────────────────────
|
||||
|
||||
/**
|
||||
* Add a button to the top bar (near +, refresh icons).
|
||||
*/
|
||||
addTopBarButton(id: string, icon: string, tooltip?: string, toast?: IToastConfig): this {
|
||||
this.register({
|
||||
id,
|
||||
point: IntegrationPoint.TOP_BAR,
|
||||
icon,
|
||||
tooltip,
|
||||
toast,
|
||||
} as IButtonIntegration);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a button to the top-right corner (before X).
|
||||
*/
|
||||
addTopRightButton(id: string, icon: string, tooltip?: string, toast?: IToastConfig): this {
|
||||
this.register({
|
||||
id,
|
||||
point: IntegrationPoint.TOP_RIGHT,
|
||||
icon,
|
||||
tooltip,
|
||||
toast,
|
||||
} as IButtonIntegration);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a button next to the send/voice buttons.
|
||||
*/
|
||||
addInputButton(id: string, icon: string, tooltip?: string, toast?: IToastConfig): this {
|
||||
this.register({
|
||||
id,
|
||||
point: IntegrationPoint.INPUT_AREA,
|
||||
icon,
|
||||
tooltip,
|
||||
toast,
|
||||
} as IButtonIntegration);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an icon to the bottom icon row (file, terminal, etc.).
|
||||
*/
|
||||
addBottomIcon(id: string, icon: string, tooltip?: string, toast?: IToastConfig): this {
|
||||
this.register({
|
||||
id,
|
||||
point: IntegrationPoint.BOTTOM_ICONS,
|
||||
icon,
|
||||
tooltip,
|
||||
toast,
|
||||
} as IButtonIntegration);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable per-turn metadata display.
|
||||
*/
|
||||
addTurnMetadata(id: string, metrics: ITurnMetaIntegration['metrics'], clickable = true): this {
|
||||
this.register({
|
||||
id,
|
||||
point: IntegrationPoint.TURN_METADATA,
|
||||
metrics,
|
||||
clickable,
|
||||
} as ITurnMetaIntegration);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add character count badges to user messages.
|
||||
*/
|
||||
addUserBadges(id: string, display: IUserBadgeIntegration['display'] = 'charCount'): this {
|
||||
this.register({
|
||||
id,
|
||||
point: IntegrationPoint.USER_BADGE,
|
||||
display,
|
||||
} as IUserBadgeIntegration);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an action button next to Good/Bad feedback.
|
||||
*/
|
||||
addBotAction(id: string, icon: string, label: string, toast?: IToastConfig): this {
|
||||
this.register({
|
||||
id,
|
||||
point: IntegrationPoint.BOT_ACTION,
|
||||
icon,
|
||||
label,
|
||||
toast,
|
||||
} as IBotActionIntegration);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add item(s) to the 3-dot dropdown menu.
|
||||
*/
|
||||
addDropdownItem(id: string, label: string, icon?: string, toast?: IToastConfig, separator = false): this {
|
||||
this.register({
|
||||
id,
|
||||
point: IntegrationPoint.DROPDOWN_MENU,
|
||||
label,
|
||||
icon,
|
||||
toast,
|
||||
separator,
|
||||
} as IDropdownIntegration);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable chat title interaction.
|
||||
*/
|
||||
addTitleInteraction(id: string, interaction: ITitleIntegration['interaction'] = 'dblclick', hint?: string, toast?: IToastConfig): this {
|
||||
this.register({
|
||||
id,
|
||||
point: IntegrationPoint.CHAT_TITLE,
|
||||
interaction,
|
||||
hint,
|
||||
toast,
|
||||
} as ITitleIntegration);
|
||||
return this;
|
||||
}
|
||||
|
||||
// ─── Title Proxy ─────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Enable the title proxy feature.
|
||||
*
|
||||
* Adds renderer-side code that intercepts the summaries provider
|
||||
* and injects custom chat titles. Uses structural matching to find
|
||||
* the provider (obfuscation-safe).
|
||||
*
|
||||
* After enabling, call `install()` or `updateScript()` to apply.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const sdk = new AntigravitySDK(context);
|
||||
* await sdk.initialize();
|
||||
*
|
||||
* sdk.integration.enableTitleProxy();
|
||||
* await sdk.integration.install();
|
||||
*
|
||||
* // Now rename from extension host:
|
||||
* sdk.integration.titles.rename(cascadeId, 'My Custom Title');
|
||||
* ```
|
||||
*/
|
||||
enableTitleProxy(): this {
|
||||
this._titleProxyEnabled = true;
|
||||
if (this._patcher.isAvailable()) {
|
||||
this._titles.initialize(this._patcher.getWorkbenchDir(), this._namespace);
|
||||
}
|
||||
log.info('Title proxy enabled');
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Access the title manager for programmatic title control.
|
||||
*
|
||||
* Requires `enableTitleProxy()` to be called first.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* sdk.integration.titles.rename(cascadeId, 'My Title');
|
||||
* sdk.integration.titles.remove(cascadeId);
|
||||
* const all = sdk.integration.titles.getAll();
|
||||
* ```
|
||||
*/
|
||||
get titles(): TitleManager {
|
||||
if (!this._titleProxyEnabled) {
|
||||
log.warn('Title proxy not enabled. Call enableTitleProxy() first.');
|
||||
}
|
||||
return this._titles;
|
||||
}
|
||||
|
||||
// ─── Build & Install ───────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Generate the integration script from all registered configs.
|
||||
*
|
||||
* If title proxy is enabled, appends the title proxy renderer code.
|
||||
*
|
||||
* @returns Complete JavaScript code as a string
|
||||
*/
|
||||
build(): string {
|
||||
const configs = Array.from(this._configs.values());
|
||||
if (configs.length === 0 && !this._titleProxyEnabled) {
|
||||
throw new Error('No integration points registered and title proxy not enabled');
|
||||
}
|
||||
|
||||
let script = '';
|
||||
if (configs.length > 0) {
|
||||
log.info(`Building script for ${configs.length} integration(s)`);
|
||||
script = this._generator.generate(configs);
|
||||
}
|
||||
|
||||
if (this._titleProxyEnabled) {
|
||||
log.info('Appending title proxy code');
|
||||
script += '\n' + generateTitleProxyCode(this._namespace);
|
||||
}
|
||||
|
||||
return script;
|
||||
}
|
||||
|
||||
/**
|
||||
* Install the generated script into workbench.html.
|
||||
*
|
||||
* For seamless hot-reload behavior, use `installSeamless()` instead.
|
||||
*
|
||||
* @returns true if the script content actually changed on disk
|
||||
*/
|
||||
async install(): Promise<boolean> {
|
||||
if (!this._patcher.isAvailable()) {
|
||||
throw new Error('Antigravity workbench not found. Is Antigravity installed?');
|
||||
}
|
||||
|
||||
const script = this.build();
|
||||
|
||||
// Read existing script to detect changes
|
||||
const scriptPath = this._patcher.getScriptPath();
|
||||
let oldContent = '';
|
||||
try {
|
||||
if (fs.existsSync(scriptPath)) {
|
||||
oldContent = fs.readFileSync(scriptPath, 'utf8');
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
|
||||
this._patcher.install(script);
|
||||
this._integrity.suppressCheck();
|
||||
this._patcher.writeHeartbeat();
|
||||
|
||||
const changed = oldContent !== script;
|
||||
log.info(
|
||||
`Installed integration (${this._configs.size} points, titleProxy: ${this._titleProxyEnabled}) -> ${scriptPath} [${changed ? 'CHANGED' : 'unchanged'}]`,
|
||||
);
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Seamless install — handles everything automatically.
|
||||
*
|
||||
* This is the **recommended** install method for extension developers.
|
||||
* It handles the entire lifecycle:
|
||||
*
|
||||
* 1. **First install:** Writes script + patches HTML + prompts user to reload
|
||||
* 2. **Update:** Compares content, if changed → auto-reloads window (no prompt)
|
||||
* 3. **No change:** Does nothing
|
||||
*
|
||||
* The developer never needs to think about reload.
|
||||
*
|
||||
* @param executeCommand - Function to execute VS Code commands
|
||||
* (pass `vscode.commands.executeCommand` or equivalent)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const sdk = new AntigravitySDK(context);
|
||||
* await sdk.initialize();
|
||||
*
|
||||
* sdk.integration.enableTitleProxy();
|
||||
* // That's it. SDK handles install, reload, everything.
|
||||
* await sdk.integration.installSeamless(
|
||||
* (cmd) => vscode.commands.executeCommand(cmd),
|
||||
* (msg, ...items) => vscode.window.showInformationMessage(msg, ...items),
|
||||
* );
|
||||
* ```
|
||||
*/
|
||||
async installSeamless(
|
||||
executeCommand: (command: string) => Thenable<any>,
|
||||
showMessage?: (message: string, ...items: string[]) => Thenable<string | undefined>,
|
||||
): Promise<void> {
|
||||
const wasInstalled = this._patcher.isInstalled();
|
||||
|
||||
// Snapshot old content before install
|
||||
const scriptPath = this._patcher.getScriptPath();
|
||||
let oldContent = '';
|
||||
try {
|
||||
if (fs.existsSync(scriptPath)) {
|
||||
oldContent = fs.readFileSync(scriptPath, 'utf8');
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
|
||||
const changed = await this.install();
|
||||
|
||||
if (!wasInstalled) {
|
||||
// First install: prompt user
|
||||
log.info('First install. Prompting for reload.');
|
||||
if (showMessage) {
|
||||
const action = await showMessage(
|
||||
'Better Antigravity installed. Reload to activate.',
|
||||
'Reload Now',
|
||||
);
|
||||
if (action === 'Reload Now') {
|
||||
await executeCommand('workbench.action.reloadWindow');
|
||||
}
|
||||
}
|
||||
} else if (changed) {
|
||||
// Update: auto-reload (no prompt)
|
||||
log.info('Script changed on disk. Auto-reloading window...');
|
||||
// Small delay to let extension finish activation
|
||||
setTimeout(() => executeCommand('workbench.action.reloadWindow'), 500);
|
||||
} else {
|
||||
log.debug('Script unchanged. No reload needed.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the integration from workbench.html.
|
||||
*
|
||||
* ⚠️ Requires Antigravity restart to take effect.
|
||||
*/
|
||||
async uninstall(): Promise<void> {
|
||||
this._patcher.uninstall();
|
||||
this._integrity.releaseCheck();
|
||||
this._patcher.removeHeartbeat();
|
||||
this.disableAutoRepair();
|
||||
log.info('Uninstalled integration. Restart Antigravity to apply.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an integration is currently installed.
|
||||
*/
|
||||
isInstalled(): boolean {
|
||||
return this._patcher.isInstalled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Signal that the extension is active.
|
||||
*
|
||||
* Call this in your extension's `activate()` function.
|
||||
* The integration script checks for this heartbeat;
|
||||
* if it's missing or stale (>48h), the script won't start.
|
||||
*
|
||||
* This prevents orphaned integrations from running after
|
||||
* an extension is disabled or uninstalled.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* export function activate(context: vscode.ExtensionContext) {
|
||||
* const sdk = new AntigravitySDK(context);
|
||||
* sdk.integration.signalActive();
|
||||
* // ...
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
signalActive(): void {
|
||||
this._patcher.writeHeartbeat();
|
||||
log.debug('Heartbeat refreshed');
|
||||
}
|
||||
|
||||
// ─── Dynamic Update ─────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Re-generate and overwrite the integration script without re-patching workbench.html.
|
||||
*
|
||||
* Use this after registering/unregistering integration points at runtime.
|
||||
* The script file is updated in-place; the next Antigravity restart
|
||||
* will pick up the changes. workbench.html <script> tag is unchanged.
|
||||
*
|
||||
* @returns true if script was updated
|
||||
*/
|
||||
updateScript(): boolean {
|
||||
if (!this._patcher.isInstalled()) {
|
||||
log.warn('Cannot update script — integration is not installed');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const script = this.build();
|
||||
fs.writeFileSync(this._patcher.getScriptPath(), script, 'utf8');
|
||||
log.info(`Script updated (${this._configs.size} points)`);
|
||||
return true;
|
||||
} catch (err) {
|
||||
log.error('Failed to update script', err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Auto-Repair ────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Enable auto-repair: watches workbench.html for changes
|
||||
* and automatically re-applies the integration patch.
|
||||
*
|
||||
* This handles Antigravity updates that overwrite workbench.html.
|
||||
* The watcher detects when the file changes and re-patches it
|
||||
* if the integration marker is missing.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const integrator = new IntegrationManager();
|
||||
* integrator.useDemoPreset();
|
||||
* await integrator.install();
|
||||
* integrator.enableAutoRepair(); // Survive Antigravity updates
|
||||
* ```
|
||||
*/
|
||||
enableAutoRepair(): void {
|
||||
if (this._watcher) return;
|
||||
|
||||
const htmlPath = this._patcher.getWorkbenchDir() + '\\workbench.html';
|
||||
if (!fs.existsSync(htmlPath)) {
|
||||
log.warn('Cannot enable auto-repair — workbench.html not found');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this._watcher = fs.watch(htmlPath, (eventType) => {
|
||||
if (eventType !== 'change') return;
|
||||
|
||||
// Debounce — Antigravity may write multiple times
|
||||
if (this._autoRepairDebounce) clearTimeout(this._autoRepairDebounce);
|
||||
this._autoRepairDebounce = setTimeout(() => {
|
||||
this._tryRepair();
|
||||
}, 2000);
|
||||
});
|
||||
|
||||
log.info('Auto-repair enabled — watching workbench.html');
|
||||
} catch (err) {
|
||||
log.error('Failed to enable auto-repair', err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable auto-repair watcher.
|
||||
*/
|
||||
disableAutoRepair(): void {
|
||||
if (this._watcher) {
|
||||
this._watcher.close();
|
||||
this._watcher = null;
|
||||
log.info('Auto-repair disabled');
|
||||
}
|
||||
if (this._autoRepairDebounce) {
|
||||
clearTimeout(this._autoRepairDebounce);
|
||||
this._autoRepairDebounce = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether auto-repair is active.
|
||||
*/
|
||||
get isAutoRepairEnabled(): boolean {
|
||||
return this._watcher !== null;
|
||||
}
|
||||
|
||||
private _tryRepair(): void {
|
||||
try {
|
||||
if (this._patcher.isInstalled()) {
|
||||
log.debug('Auto-repair: integration still present, no action needed');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._configs.size === 0) {
|
||||
log.debug('Auto-repair: no configs registered, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
log.info('Auto-repair: integration lost (Antigravity update?), re-patching...');
|
||||
const script = this.build();
|
||||
this._patcher.install(script);
|
||||
this._integrity.repair();
|
||||
log.info('Auto-repair: re-patched successfully. Restart Antigravity.');
|
||||
} catch (err) {
|
||||
log.error('Auto-repair failed', err);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Preset ────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Register the Demo preset — a complete demo of all 9 integration points.
|
||||
* Useful for testing and as a reference implementation.
|
||||
*/
|
||||
useDemoPreset(): this {
|
||||
this.addTopBarButton('demo_overview', '\u{1F4E1}', 'SDK: Session Overview', {
|
||||
title: 'Session Overview',
|
||||
badge: { text: 'TOP_BAR', bgColor: 'rgba(79,195,247,.2)', textColor: '#4fc3f7' },
|
||||
rows: [
|
||||
{ key: 'location:', value: 'Header icon bar' },
|
||||
{ key: 'use case:', value: 'Session overview, navigation' },
|
||||
],
|
||||
});
|
||||
|
||||
this.addTopRightButton('demo_perf', '\u26A1', 'SDK: Performance', {
|
||||
title: 'Performance',
|
||||
badge: { text: 'TOP_RIGHT', bgColor: 'rgba(255,193,7,.2)', textColor: '#ffd54f' },
|
||||
rows: [
|
||||
{ key: 'location:', value: 'Top right, before close' },
|
||||
{ key: 'use case:', value: 'Status indicator' },
|
||||
],
|
||||
});
|
||||
|
||||
this.addInputButton('demo_stats', '\u{1F4CA}', 'SDK: Stats', {
|
||||
title: 'Input Stats',
|
||||
badge: { text: 'INPUT_AREA', bgColor: 'rgba(76,175,80,.2)', textColor: '#81c784' },
|
||||
rows: [
|
||||
{ key: 'location:', value: 'Next to send button' },
|
||||
{ key: 'use case:', value: 'Token counter, analytics' },
|
||||
],
|
||||
});
|
||||
|
||||
this.addBottomIcon('demo_actions', '\u2630', 'SDK: Quick Actions', {
|
||||
title: 'Quick Actions',
|
||||
badge: { text: 'BOTTOM_ICONS', bgColor: 'rgba(255,152,0,.2)', textColor: '#ffb74d' },
|
||||
rows: [
|
||||
{ key: 'location:', value: 'Bottom icon row' },
|
||||
{ key: 'use case:', value: 'Mode switches, quick actions' },
|
||||
],
|
||||
});
|
||||
|
||||
this.addTurnMetadata('demo_turns', [
|
||||
'turnNumber',
|
||||
'userCharCount',
|
||||
'separator',
|
||||
'aiCharCount',
|
||||
'codeBlocks',
|
||||
'thinkingIndicator',
|
||||
]);
|
||||
|
||||
this.addUserBadges('demo_ubadge', 'charCount');
|
||||
|
||||
this.addBotAction('demo_inspect', '\u{1F50D}', 'inspect', {
|
||||
title: 'Response Inspector',
|
||||
badge: { text: 'BOT_ACTION', bgColor: 'rgba(156,39,176,.2)', textColor: '#ce93d8' },
|
||||
rows: [
|
||||
{ key: 'location:', value: 'Next to Good/Bad' },
|
||||
{ key: 'use case:', value: 'Response analysis' },
|
||||
],
|
||||
});
|
||||
|
||||
this.addDropdownItem('demo_menu_stats', 'SDK Stats', '\u{1F4CA}', {
|
||||
title: 'Extended Stats',
|
||||
badge: { text: 'DROPDOWN', bgColor: 'rgba(233,30,99,.2)', textColor: '#f48fb1' },
|
||||
rows: [
|
||||
{ key: 'location:', value: '3-dot dropdown menu' },
|
||||
{ key: 'use case:', value: 'Extended actions' },
|
||||
],
|
||||
}, true);
|
||||
|
||||
this.addDropdownItem('demo_menu_debug', 'SDK Debug', '\u{1F9EA}', {
|
||||
title: 'Debug Info',
|
||||
badge: { text: 'DEBUG', bgColor: 'rgba(255,87,34,.2)', textColor: '#ff8a65' },
|
||||
rows: [
|
||||
{ key: 'location:', value: '3-dot dropdown menu' },
|
||||
{ key: 'use case:', value: 'Debug, diagnostics' },
|
||||
],
|
||||
});
|
||||
|
||||
this.addTitleInteraction('demo_title', 'dblclick', 'dblclick', {
|
||||
title: 'Chat Title',
|
||||
badge: { text: 'TITLE', bgColor: 'rgba(0,150,136,.2)', textColor: '#80cbc4' },
|
||||
rows: [
|
||||
{ key: 'location:', value: 'Conversation title' },
|
||||
{ key: 'use case:', value: 'Rename, bookmark' },
|
||||
],
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
// ─── Dispose ───────────────────────────────────────────────────────
|
||||
|
||||
dispose(): void {
|
||||
this.disableAutoRepair();
|
||||
this._configs.clear();
|
||||
this._titles.dispose();
|
||||
}
|
||||
}
|
||||
270
antigravity-sdk-main/src/integration/integrity-manager.ts
Normal file
270
antigravity-sdk-main/src/integration/integrity-manager.ts
Normal file
@@ -0,0 +1,270 @@
|
||||
/**
|
||||
* Integrity Manager — Suppress Antigravity's "corrupt installation" warnings.
|
||||
*
|
||||
* When the SDK patches workbench files, Antigravity's IntegrityService detects
|
||||
* checksum mismatches and shows two warnings:
|
||||
* 1. Console WARN ("Installation has been modified on disk")
|
||||
* 2. UI Notification ("Your Antigravity installation appears to be corrupt")
|
||||
*
|
||||
* This class updates ALL mismatched SHA256 hashes in product.json, so
|
||||
* IntegrityService sees isPure=true and produces no warnings at all.
|
||||
*
|
||||
* Handles not just workbench.html but also workbench.desktop.main.js (auto-run fix),
|
||||
* workbench-jetski-agent.html (agent manager patching), and any other modified files.
|
||||
*
|
||||
* Multi-extension coordination: a registry file (.ag-sdk-integrity.json)
|
||||
* in the workbench directory tracks active SDK namespaces and the original
|
||||
* hashes, so the last extension to uninstall restores the original state.
|
||||
*
|
||||
* @module integration/integrity-manager
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
|
||||
import * as crypto from 'crypto';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { Logger } from '../core/logger';
|
||||
|
||||
const log = new Logger('IntegrityManager');
|
||||
|
||||
/** Coordination registry stored in the workbench directory. */
|
||||
interface IIntegrityRegistry {
|
||||
/** Active SDK namespace slugs. */
|
||||
namespaces: string[];
|
||||
/** Original product.json hashes for ALL checksummed files (before any patching). */
|
||||
originalHashes: Record<string, string>;
|
||||
}
|
||||
|
||||
/** Registry filename — lives next to workbench.html. */
|
||||
const REGISTRY_FILENAME = '.ag-sdk-integrity.json';
|
||||
|
||||
/**
|
||||
* Manages integrity check suppression for Antigravity's IntegrityService.
|
||||
*
|
||||
* Call `suppressCheck()` after any file patching (workbench.html, main.js, etc.).
|
||||
* It scans ALL files listed in product.json checksums, recomputes hashes for
|
||||
* any that have changed, and updates product.json. IntegrityService will see
|
||||
* `isPure = true` on next restart, producing zero warnings.
|
||||
*/
|
||||
export class IntegrityManager {
|
||||
private readonly _productJsonPath: string;
|
||||
private readonly _appOutDir: string;
|
||||
private readonly _registryPath: string;
|
||||
private readonly _namespace: string;
|
||||
|
||||
/**
|
||||
* @param workbenchDir — Absolute path to the workbench directory
|
||||
* (e.g. `%LOCALAPPDATA%/Programs/Antigravity/resources/app/out/vs/code/electron-browser/workbench/`)
|
||||
* @param namespace — Unique slug for this extension (e.g. 'kanezal-better-antigravity')
|
||||
*/
|
||||
constructor(workbenchDir: string, namespace: string) {
|
||||
this._namespace = namespace;
|
||||
this._registryPath = path.join(workbenchDir, REGISTRY_FILENAME);
|
||||
|
||||
// product.json is at resources/app/product.json
|
||||
// workbenchDir is resources/app/out/vs/code/electron-browser/workbench/
|
||||
const appDir = path.resolve(workbenchDir, '..', '..', '..', '..', '..');
|
||||
this._productJsonPath = path.join(appDir, 'product.json');
|
||||
this._appOutDir = path.join(appDir, 'out');
|
||||
}
|
||||
|
||||
/**
|
||||
* Suppress the integrity check by updating ALL mismatched hashes in product.json.
|
||||
*
|
||||
* Scans every file listed in product.json checksums, recomputes SHA256 for each,
|
||||
* and updates any that have changed. This handles not just workbench.html but also
|
||||
* workbench.desktop.main.js (auto-run fix), jetskiAgent files, etc.
|
||||
*
|
||||
* Call this after any file patching. Safe to call multiple times.
|
||||
*/
|
||||
suppressCheck(): void {
|
||||
try {
|
||||
if (!fs.existsSync(this._productJsonPath)) {
|
||||
log.warn(`product.json not found at ${this._productJsonPath}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const productJson = JSON.parse(fs.readFileSync(this._productJsonPath, 'utf8'));
|
||||
if (!productJson.checksums) {
|
||||
log.debug('No checksums in product.json — nothing to update');
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. Load or create registry, register this namespace
|
||||
const registry = this._readRegistry();
|
||||
if (!registry.namespaces.includes(this._namespace)) {
|
||||
registry.namespaces.push(this._namespace);
|
||||
}
|
||||
|
||||
// 2. Scan ALL checksummed files, save originals & update mismatches
|
||||
let updatedCount = 0;
|
||||
for (const [relPath, storedHash] of Object.entries(productJson.checksums) as [string, string][]) {
|
||||
const filePath = path.join(this._appOutDir, relPath);
|
||||
|
||||
let actualHash: string;
|
||||
try {
|
||||
const content = fs.readFileSync(filePath);
|
||||
actualHash = this._computeHash(content);
|
||||
} catch {
|
||||
// File not found — skip (don't break other checks)
|
||||
continue;
|
||||
}
|
||||
|
||||
if (actualHash !== storedHash) {
|
||||
// Save original hash if we haven't already
|
||||
if (!(relPath in registry.originalHashes)) {
|
||||
registry.originalHashes[relPath] = storedHash;
|
||||
log.debug(`Saved original hash for ${relPath}`);
|
||||
}
|
||||
|
||||
productJson.checksums[relPath] = actualHash;
|
||||
updatedCount++;
|
||||
log.info(`Updated hash: ${relPath} (${storedHash.substring(0, 8)}... -> ${actualHash.substring(0, 8)}...)`);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Write registry
|
||||
this._writeRegistry(registry);
|
||||
|
||||
// 4. Write product.json if anything changed
|
||||
if (updatedCount > 0) {
|
||||
fs.writeFileSync(this._productJsonPath, JSON.stringify(productJson, null, '\t'), 'utf8');
|
||||
log.info(`Updated ${updatedCount} hash(es) in product.json`);
|
||||
} else {
|
||||
log.debug('All hashes already match — no update needed');
|
||||
}
|
||||
} catch (err) {
|
||||
log.error('Failed to suppress integrity check', err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Release the integrity check suppression.
|
||||
*
|
||||
* Call this when uninstalling the integration. If no other SDK namespaces
|
||||
* remain active, restores all original hashes in product.json.
|
||||
*/
|
||||
releaseCheck(): void {
|
||||
try {
|
||||
const registry = this._readRegistry();
|
||||
|
||||
// Remove this namespace
|
||||
registry.namespaces = registry.namespaces.filter(ns => ns !== this._namespace);
|
||||
this._writeRegistry(registry);
|
||||
|
||||
if (registry.namespaces.length > 0) {
|
||||
// Other SDK extensions still active — recompute all hashes
|
||||
log.debug(`${registry.namespaces.length} other namespace(s) still active, recomputing hashes`);
|
||||
this.suppressCheck();
|
||||
return;
|
||||
}
|
||||
|
||||
// Last extension uninstalling — restore ALL original hashes
|
||||
if (Object.keys(registry.originalHashes).length > 0) {
|
||||
this._restoreOriginalHashes(registry.originalHashes);
|
||||
log.info(`Restored ${Object.keys(registry.originalHashes).length} original hash(es)`);
|
||||
}
|
||||
|
||||
// Clean up registry file
|
||||
this._deleteRegistry();
|
||||
} catch (err) {
|
||||
log.error('Failed to release integrity check', err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-apply integrity suppression after auto-repair.
|
||||
*
|
||||
* Call this after auto-repair has re-patched files
|
||||
* (e.g. after an AG update that overwrote workbench files).
|
||||
*/
|
||||
repair(): void {
|
||||
log.info('Repairing integrity check suppression...');
|
||||
this.suppressCheck();
|
||||
}
|
||||
|
||||
// ── Private helpers ─────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Compute SHA256 hash matching Antigravity's ChecksumService format:
|
||||
* base64 WITHOUT trailing '=' padding.
|
||||
*/
|
||||
private _computeHash(content: Buffer): string {
|
||||
return crypto.createHash('sha256')
|
||||
.update(content)
|
||||
.digest('base64')
|
||||
.replace(/=+$/, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore all original hashes in product.json.
|
||||
*/
|
||||
private _restoreOriginalHashes(originalHashes: Record<string, string>): void {
|
||||
if (!fs.existsSync(this._productJsonPath)) return;
|
||||
|
||||
const productJson = JSON.parse(fs.readFileSync(this._productJsonPath, 'utf8'));
|
||||
if (!productJson.checksums) return;
|
||||
|
||||
for (const [relPath, hash] of Object.entries(originalHashes)) {
|
||||
if (relPath in productJson.checksums) {
|
||||
productJson.checksums[relPath] = hash;
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFileSync(this._productJsonPath, JSON.stringify(productJson, null, '\t'), 'utf8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the coordination registry from disk.
|
||||
*/
|
||||
private _readRegistry(): IIntegrityRegistry {
|
||||
try {
|
||||
if (fs.existsSync(this._registryPath)) {
|
||||
const raw = fs.readFileSync(this._registryPath, 'utf8');
|
||||
const data = JSON.parse(raw);
|
||||
|
||||
// Migrate from old format (single originalHash) to new (originalHashes map)
|
||||
let originalHashes: Record<string, string> = {};
|
||||
if (data.originalHashes && typeof data.originalHashes === 'object') {
|
||||
originalHashes = data.originalHashes;
|
||||
} else if (typeof data.originalHash === 'string') {
|
||||
// Legacy v1.5.0 format: single hash for workbench.html
|
||||
originalHashes['vs/code/electron-browser/workbench/workbench.html'] = data.originalHash;
|
||||
}
|
||||
|
||||
return {
|
||||
namespaces: Array.isArray(data.namespaces) ? data.namespaces : [],
|
||||
originalHashes,
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// Corrupt or inaccessible — start fresh
|
||||
}
|
||||
return { namespaces: [], originalHashes: {} };
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the coordination registry to disk.
|
||||
*/
|
||||
private _writeRegistry(registry: IIntegrityRegistry): void {
|
||||
try {
|
||||
fs.writeFileSync(this._registryPath, JSON.stringify(registry, null, 2), 'utf8');
|
||||
} catch (err) {
|
||||
log.error('Failed to write integrity registry', err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the coordination registry file.
|
||||
*/
|
||||
private _deleteRegistry(): void {
|
||||
try {
|
||||
if (fs.existsSync(this._registryPath)) {
|
||||
fs.unlinkSync(this._registryPath);
|
||||
}
|
||||
} catch {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
554
antigravity-sdk-main/src/integration/script-generator.ts
Normal file
554
antigravity-sdk-main/src/integration/script-generator.ts
Normal file
@@ -0,0 +1,554 @@
|
||||
/**
|
||||
* Script Generator — Builds self-contained JS from integration configs.
|
||||
*
|
||||
* Generates a Trusted Types-safe integration script that:
|
||||
* - Uses ONLY createElement/textContent (no innerHTML)
|
||||
* - Uses MutationObserver for dynamic content
|
||||
* - Is fully self-contained (runs in renderer, no Node.js APIs)
|
||||
*
|
||||
* @module integration/script-generator
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
|
||||
import { Selectors, AG_PREFIX, AG_DATA_ATTR } from './selectors';
|
||||
import {
|
||||
IntegrationConfig,
|
||||
IntegrationPoint,
|
||||
IToastConfig,
|
||||
IToastRow,
|
||||
TurnMetric,
|
||||
IButtonIntegration,
|
||||
ITurnMetaIntegration,
|
||||
IUserBadgeIntegration,
|
||||
IBotActionIntegration,
|
||||
IDropdownIntegration,
|
||||
ITitleIntegration,
|
||||
} from './types';
|
||||
|
||||
/**
|
||||
* Generates a self-contained JavaScript integration script
|
||||
* from an array of IntegrationConfig objects.
|
||||
*/
|
||||
export class ScriptGenerator {
|
||||
/**
|
||||
* Generate the complete integration script.
|
||||
*
|
||||
* @param configs — Registered integration configurations
|
||||
* @returns — Complete JS code as a string
|
||||
*/
|
||||
generate(configs: IntegrationConfig[]): string {
|
||||
const parts: string[] = [];
|
||||
|
||||
parts.push(this._header());
|
||||
parts.push(this._css(configs));
|
||||
parts.push(this._helpers());
|
||||
parts.push(this._toast());
|
||||
parts.push(this._stats());
|
||||
|
||||
// Generate code for each integration point
|
||||
const grouped = this._groupByPoint(configs);
|
||||
|
||||
for (const [point, cfgs] of Object.entries(grouped)) {
|
||||
parts.push(this._generatePoint(point as IntegrationPoint, cfgs));
|
||||
}
|
||||
|
||||
parts.push(this._mainLoop(Object.keys(grouped) as IntegrationPoint[]));
|
||||
parts.push(this._footer());
|
||||
|
||||
return parts.join('\n');
|
||||
}
|
||||
|
||||
// ─── Grouping ──────────────────────────────────────────────────────
|
||||
|
||||
private _groupByPoint(configs: IntegrationConfig[]): Record<string, IntegrationConfig[]> {
|
||||
const groups: Record<string, IntegrationConfig[]> = {};
|
||||
for (const c of configs) {
|
||||
if (c.enabled === false) continue;
|
||||
if (!groups[c.point]) groups[c.point] = [];
|
||||
groups[c.point].push(c);
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
// ─── Code Sections ────────────────────────────────────────────────
|
||||
|
||||
private _header(): string {
|
||||
return `(function agSDK(){
|
||||
'use strict';
|
||||
if(window.__agSDK)return;
|
||||
window.__agSDK=true;
|
||||
|
||||
// ─── Theme Detection ───
|
||||
var _isDark=document.body.classList.contains('vscode-dark')||document.body.classList.contains('vscode-high-contrast');
|
||||
var _theme={
|
||||
bg:_isDark?'rgba(25,25,30,.95)':'rgba(245,245,250,.95)',
|
||||
fg:_isDark?'#ccc':'#333',
|
||||
fgDim:_isDark?'rgba(200,200,200,.45)':'rgba(80,80,80,.5)',
|
||||
fgHover:_isDark?'rgba(200,200,200,.8)':'rgba(40,40,40,.9)',
|
||||
accent:_isDark?'#4fc3f7':'#0288d1',
|
||||
accentBg:_isDark?'rgba(79,195,247,.12)':'rgba(2,136,209,.08)',
|
||||
success:_isDark?'#81c784':'#388e3c',
|
||||
successBg:_isDark?'rgba(76,175,80,.1)':'rgba(56,142,60,.06)',
|
||||
warn:_isDark?'#ffb74d':'#e65100',
|
||||
border:_isDark?'rgba(79,195,247,.06)':'rgba(0,0,0,.06)',
|
||||
borderHover:_isDark?'rgba(79,195,247,.2)':'rgba(2,136,209,.15)',
|
||||
sep:_isDark?'rgba(255,255,255,.06)':'rgba(0,0,0,.06)',
|
||||
shadow:_isDark?'rgba(0,0,0,.5)':'rgba(0,0,0,.15)',
|
||||
metaBg:_isDark?'linear-gradient(135deg,rgba(79,195,247,.03),rgba(156,39,176,.02))':'linear-gradient(135deg,rgba(2,136,209,.03),rgba(123,31,162,.02))',
|
||||
metaBgHover:_isDark?'linear-gradient(135deg,rgba(79,195,247,.07),rgba(156,39,176,.05))':'linear-gradient(135deg,rgba(2,136,209,.07),rgba(123,31,162,.05))'
|
||||
};
|
||||
// Watch for theme changes (VS Code toggles body classes)
|
||||
new MutationObserver(function(){var newDark=document.body.classList.contains('vscode-dark');if(newDark!==_isDark){location.reload();}}).observe(document.body,{attributes:true,attributeFilter:['class']});
|
||||
`;
|
||||
}
|
||||
|
||||
private _footer(): string {
|
||||
// The heartbeat file is in the same directory as the script.
|
||||
// We use sync XHR (allowed in renderer since we're in a script tag,
|
||||
// not a module) to check the file before starting.
|
||||
// Max age: 48 hours (172800000ms) — enough to survive normal restarts
|
||||
// but catches disabled extensions reliably.
|
||||
return `
|
||||
var _heartbeatMaxAge=172800000;
|
||||
function checkHeartbeat(){
|
||||
try{
|
||||
var xhr=new XMLHttpRequest();
|
||||
xhr.open('GET','./ag-sdk-heartbeat?t='+Date.now(),false);
|
||||
xhr.send();
|
||||
if(xhr.status!==200)return false;
|
||||
var ts=parseInt(xhr.responseText,10);
|
||||
if(isNaN(ts))return false;
|
||||
return(Date.now()-ts)<_heartbeatMaxAge;
|
||||
}catch(e){return false;}
|
||||
}
|
||||
function boot(){
|
||||
if(!checkHeartbeat()){
|
||||
console.log('[AG SDK] Heartbeat missing or stale — extension disabled? Skipping.');
|
||||
return;
|
||||
}
|
||||
if(document.readyState==='complete')setTimeout(start,3000);
|
||||
else window.addEventListener('load',function(){setTimeout(start,3000);});
|
||||
}
|
||||
boot();
|
||||
})();`;
|
||||
}
|
||||
|
||||
private _css(configs: IntegrationConfig[]): string {
|
||||
// Only include CSS for points that are actually used
|
||||
const points = new Set(configs.map(c => c.point));
|
||||
|
||||
// All colors now use _theme variables for light/dark mode support
|
||||
// CSS is generated as a JS template that reads _theme at runtime
|
||||
return `
|
||||
// ─── Theme-Aware CSS ───
|
||||
var _cssRules=[
|
||||
'.${AG_PREFIX}meta{padding:3px 8px;background:'+_theme.metaBg+';border-top:1px solid '+_theme.border+';font-family:"Cascadia Code","Fira Code",monospace;font-size:9px;color:'+_theme.fgDim+';display:flex;align-items:center;gap:5px;flex-wrap:wrap;transition:all .2s;cursor:default;user-select:none;margin-top:2px;border-radius:0 0 6px 6px}',
|
||||
'.${AG_PREFIX}meta:hover{background:'+_theme.metaBgHover+';color:'+_theme.fgHover+'}',
|
||||
'.${AG_PREFIX}t{padding:1px 4px;border-radius:3px;font-size:8px;font-weight:700;letter-spacing:.3px}',
|
||||
'.${AG_PREFIX}u{background:'+_theme.successBg+';color:'+_theme.success+'}',
|
||||
'.${AG_PREFIX}b{background:'+_theme.accentBg+';color:'+_theme.accent+'}',
|
||||
'.${AG_PREFIX}k{color:'+_theme.fgDim+';font-size:8px}',
|
||||
'.${AG_PREFIX}v{color:'+_theme.fg+';font-size:8px;opacity:.55}',
|
||||
'.${AG_PREFIX}hi{color:'+_theme.accent+'}',
|
||||
'.${AG_PREFIX}w{color:'+_theme.warn+'}',
|
||||
'.${AG_PREFIX}s{color:'+_theme.sep+'}',
|
||||
// Toast
|
||||
'.${AG_PREFIX}toast{position:fixed;bottom:80px;right:20px;background:'+_theme.bg+';border:1px solid '+_theme.borderHover+';border-radius:8px;padding:10px 14px;font-family:"Cascadia Code",monospace;font-size:10px;color:'+_theme.fg+';z-index:99999;max-width:320px;backdrop-filter:blur(10px);box-shadow:0 4px 24px '+_theme.shadow+';animation:${AG_PREFIX}fade .25s ease}',
|
||||
'@keyframes ${AG_PREFIX}fade{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}',
|
||||
'.${AG_PREFIX}toast-t{color:'+_theme.accent+';font-weight:700;margin-bottom:5px;font-size:11px;display:flex;align-items:center;gap:6px}',
|
||||
'.${AG_PREFIX}toast-r{display:flex;gap:8px;margin:1px 0}',
|
||||
'.${AG_PREFIX}toast-k{color:'+_theme.fgDim+';min-width:70px}',
|
||||
'.${AG_PREFIX}toast-v{color:'+_theme.fg+'}',
|
||||
'.${AG_PREFIX}toast-badge{font-size:8px;padding:1px 5px;border-radius:3px;font-weight:700}',
|
||||
// Buttons
|
||||
'.${AG_PREFIX}hdr{display:inline-flex;align-items:center;gap:3px;padding:1px 6px;border-radius:4px;cursor:pointer;color:'+_theme.fgDim+';font-size:9px;font-family:"Cascadia Code",monospace;transition:all .15s;user-select:none}',
|
||||
'.${AG_PREFIX}hdr:hover{background:'+_theme.accentBg+';color:'+_theme.accent+'}',
|
||||
'.${AG_PREFIX}inp{display:inline-flex;align-items:center;justify-content:center;min-width:18px;height:18px;border-radius:4px;cursor:pointer;color:'+_theme.fgDim+';font-size:11px;transition:all .15s;flex-shrink:0;padding:0 4px;font-family:"Cascadia Code",monospace}',
|
||||
'.${AG_PREFIX}inp:hover{background:'+_theme.accentBg+';color:'+_theme.accent+'}',
|
||||
'.${AG_PREFIX}menu{padding:4px 8px;cursor:pointer;font-size:11px;color:'+_theme.fg+';opacity:.7;transition:all .12s;display:flex;align-items:center;gap:6px;white-space:nowrap}',
|
||||
'.${AG_PREFIX}menu:hover{background:'+_theme.accentBg+';color:'+_theme.accent+';opacity:1}',
|
||||
'.${AG_PREFIX}vote{display:inline-flex;align-items:center;gap:3px;padding:1px 6px;border-radius:3px;cursor:pointer;color:'+_theme.fgDim+';font-size:9px;font-family:"Cascadia Code",monospace;transition:all .15s;margin-left:4px}',
|
||||
'.${AG_PREFIX}vote:hover{background:'+_theme.accentBg+';color:'+_theme.accent+'}',
|
||||
'.${AG_PREFIX}ubadge{display:inline-flex;align-items:center;gap:2px;padding:1px 5px;border-radius:3px;background:'+_theme.successBg+';cursor:pointer;color:'+_theme.success+';opacity:.4;font-size:8px;font-family:"Cascadia Code",monospace;transition:all .15s;margin-left:3px}',
|
||||
'.${AG_PREFIX}ubadge:hover{background:'+_theme.successBg+';color:'+_theme.success+';opacity:1}',
|
||||
'.${AG_PREFIX}title-hint{position:absolute;right:0;top:50%;transform:translateY(-50%);font-size:8px;color:'+_theme.accent+';opacity:.3;pointer-events:none;font-family:"Cascadia Code",monospace;transition:opacity .2s}',
|
||||
'.${AG_PREFIX}title-wrap:hover .${AG_PREFIX}title-hint{opacity:1}'
|
||||
];
|
||||
var css=document.createElement('style');
|
||||
css.textContent=_cssRules.join('\\n');
|
||||
document.head.appendChild(css);
|
||||
`;
|
||||
}
|
||||
|
||||
private _helpers(): string {
|
||||
return `
|
||||
function mk(tag,cls,txt){var e=document.createElement(tag);if(cls)e.className=cls;if(txt!==undefined)e.textContent=txt;return e;}
|
||||
function fmt(n){return n>=1000?(n/1000).toFixed(1)+'k':''+n;}
|
||||
`;
|
||||
}
|
||||
|
||||
private _toast(): string {
|
||||
return `
|
||||
var _toastT=0;
|
||||
function toast(title,badge,rows){
|
||||
var old=document.querySelector('.${AG_PREFIX}toast');if(old)old.remove();
|
||||
var t=mk('div','${AG_PREFIX}toast');
|
||||
var hdr=mk('div','${AG_PREFIX}toast-t');
|
||||
hdr.appendChild(document.createTextNode(title));
|
||||
if(badge){var b=mk('span','${AG_PREFIX}toast-badge');b.textContent=badge[0];b.style.background=badge[1];b.style.color=badge[2];hdr.appendChild(b);}
|
||||
t.appendChild(hdr);
|
||||
rows.forEach(function(r){var row=mk('div','${AG_PREFIX}toast-r');row.appendChild(mk('span','${AG_PREFIX}toast-k',r[0]));row.appendChild(mk('span','${AG_PREFIX}toast-v',r[1]));t.appendChild(row);});
|
||||
document.body.appendChild(t);
|
||||
clearTimeout(_toastT);_toastT=setTimeout(function(){if(t.parentNode)t.remove();},6000);
|
||||
t.addEventListener('click',function(){t.remove();});
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
private _stats(): string {
|
||||
return `
|
||||
function getStats(){
|
||||
var c=document.querySelector(${JSON.stringify(Selectors.TURNS_CONTAINER)});
|
||||
if(!c)return null;
|
||||
var turns=0,uC=0,bC=0,code=0;
|
||||
Array.from(c.children).forEach(function(ch){
|
||||
if(ch.getAttribute('${AG_DATA_ATTR}')||ch.children.length<1)return;
|
||||
turns++;
|
||||
uC+=(ch.children[0]?.textContent?.trim()||'').length;
|
||||
bC+=(ch.children[1]?.textContent?.trim()||'').length;
|
||||
code+=(ch.children[1]?.querySelectorAll('pre')?.length||0);
|
||||
});
|
||||
return{turns:turns,u:uC,b:bC,code:code};
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
// ─── Point generators ─────────────────────────────────────────────
|
||||
|
||||
private _generatePoint(point: IntegrationPoint, configs: IntegrationConfig[]): string {
|
||||
switch (point) {
|
||||
case IntegrationPoint.TOP_BAR:
|
||||
return this._genTopBar(configs as IButtonIntegration[]);
|
||||
case IntegrationPoint.TOP_RIGHT:
|
||||
return this._genTopRight(configs as IButtonIntegration[]);
|
||||
case IntegrationPoint.INPUT_AREA:
|
||||
return this._genInputArea(configs as IButtonIntegration[]);
|
||||
case IntegrationPoint.BOTTOM_ICONS:
|
||||
return this._genBottomIcons(configs as IButtonIntegration[]);
|
||||
case IntegrationPoint.TURN_METADATA:
|
||||
return this._genTurnMeta(configs as ITurnMetaIntegration[]);
|
||||
case IntegrationPoint.USER_BADGE:
|
||||
return this._genUserBadge(configs as IUserBadgeIntegration[]);
|
||||
case IntegrationPoint.BOT_ACTION:
|
||||
return this._genBotAction(configs as IBotActionIntegration[]);
|
||||
case IntegrationPoint.DROPDOWN_MENU:
|
||||
return this._genDropdown(configs as IDropdownIntegration[]);
|
||||
case IntegrationPoint.CHAT_TITLE:
|
||||
return this._genTitle(configs as ITitleIntegration[]);
|
||||
default:
|
||||
return `// Unknown point: ${point}`;
|
||||
}
|
||||
}
|
||||
|
||||
private _genToastCall(toast?: IToastConfig): string {
|
||||
if (!toast) return '';
|
||||
const badge = toast.badge
|
||||
? `[${JSON.stringify(toast.badge.text)},${JSON.stringify(toast.badge.bgColor)},${JSON.stringify(toast.badge.textColor)}]`
|
||||
: 'null';
|
||||
const rows = toast.rows
|
||||
.map(r => {
|
||||
if (r.dynamic) {
|
||||
return `[${JSON.stringify(r.key)},${r.value}]`;
|
||||
}
|
||||
return `[${JSON.stringify(r.key)},${JSON.stringify(r.value)}]`;
|
||||
})
|
||||
.join(',');
|
||||
return `toast(${JSON.stringify(toast.title)},${badge},[${rows}]);`;
|
||||
}
|
||||
|
||||
private _genTopBar(configs: IButtonIntegration[]): string {
|
||||
const buttons = configs.map(c => {
|
||||
const toastCall = this._genToastCall(c.toast);
|
||||
return ` var btn_${c.id}=mk('a','${AG_PREFIX}hdr ${AG_PREFIX}${c.id}');
|
||||
btn_${c.id}.textContent=${JSON.stringify(c.icon)};
|
||||
btn_${c.id}.title=${JSON.stringify(c.tooltip || '')};
|
||||
btn_${c.id}.addEventListener('click',function(){${toastCall}});
|
||||
iconsArea.insertBefore(btn_${c.id},iconsArea.children[1]);`;
|
||||
});
|
||||
|
||||
return `
|
||||
function integrateTopBar(){
|
||||
var p=document.querySelector(${JSON.stringify(Selectors.PANEL)});if(!p)return;
|
||||
var topBar=p.querySelector(${JSON.stringify(Selectors.TOP_BAR)});if(!topBar)return;
|
||||
var iconsArea=topBar.querySelector(${JSON.stringify(Selectors.TOP_ICONS)});
|
||||
if(!iconsArea||iconsArea.querySelector('.${AG_PREFIX}${configs[0].id}'))return;
|
||||
${buttons.join('\n')}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
private _genTopRight(configs: IButtonIntegration[]): string {
|
||||
const buttons = configs.map(c => {
|
||||
const toastCall = this._genToastCall(c.toast);
|
||||
return ` var btn_${c.id}=mk('a','${AG_PREFIX}hdr ${AG_PREFIX}${c.id}');
|
||||
btn_${c.id}.textContent=${JSON.stringify(c.icon)};
|
||||
btn_${c.id}.title=${JSON.stringify(c.tooltip || '')};
|
||||
btn_${c.id}.addEventListener('click',function(){${toastCall}});
|
||||
iconsArea.insertBefore(btn_${c.id},iconsArea.lastElementChild);`;
|
||||
});
|
||||
|
||||
return `
|
||||
function integrateTopRight(){
|
||||
var p=document.querySelector(${JSON.stringify(Selectors.PANEL)});if(!p)return;
|
||||
var topBar=p.querySelector(${JSON.stringify(Selectors.TOP_BAR)});if(!topBar)return;
|
||||
var iconsArea=topBar.querySelector(${JSON.stringify(Selectors.TOP_ICONS)});
|
||||
if(!iconsArea||iconsArea.querySelector('.${AG_PREFIX}${configs[0].id}'))return;
|
||||
${buttons.join('\n')}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
private _genInputArea(configs: IButtonIntegration[]): string {
|
||||
const buttons = configs.map(c => {
|
||||
const toastCall = this._genToastCall(c.toast);
|
||||
return ` var btn=mk('div','${AG_PREFIX}inp ${AG_PREFIX}${c.id}');
|
||||
btn.textContent=${JSON.stringify(c.icon)};
|
||||
btn.title=${JSON.stringify(c.tooltip || '')};
|
||||
btn.addEventListener('click',function(){${toastCall}});
|
||||
btnRow.insertBefore(btn,btnRow.firstChild);`;
|
||||
});
|
||||
|
||||
return `
|
||||
function integrateInputArea(){
|
||||
var ib=document.querySelector(${JSON.stringify(Selectors.INPUT_BOX)});
|
||||
if(!ib||ib.querySelector('.${AG_PREFIX}${configs[0].id}'))return;
|
||||
var allBtns=ib.querySelectorAll('button,[role="button"]');
|
||||
if(allBtns.length===0)return;
|
||||
var btnRow=allBtns[allBtns.length-1].parentElement;if(!btnRow)return;
|
||||
${buttons.join('\n')}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
private _genBottomIcons(configs: IButtonIntegration[]): string {
|
||||
const buttons = configs.map(c => {
|
||||
const toastCall = this._genToastCall(c.toast);
|
||||
return ` var btn=mk('div','${AG_PREFIX}inp ${AG_PREFIX}${c.id}');
|
||||
btn.textContent=${JSON.stringify(c.icon)};
|
||||
btn.title=${JSON.stringify(c.tooltip || '')};
|
||||
btn.addEventListener('click',function(){${toastCall}});
|
||||
row.appendChild(btn);`;
|
||||
});
|
||||
|
||||
return `
|
||||
function integrateBottomIcons(){
|
||||
var ib=document.querySelector(${JSON.stringify(Selectors.INPUT_BOX)});
|
||||
if(!ib||ib.querySelector('.${AG_PREFIX}${configs[0].id}'))return;
|
||||
var rows=ib.querySelectorAll('.flex.items-center');
|
||||
var row=null;
|
||||
for(var i=0;i<rows.length;i++){if(rows[i].querySelectorAll('svg').length>=2){row=rows[i];}}
|
||||
if(!row)return;
|
||||
${buttons.join('\n')}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
private _genTurnMeta(configs: ITurnMetaIntegration[]): string {
|
||||
// Take first config for metrics (single turn metadata style)
|
||||
const cfg = configs[0];
|
||||
const metricParts: string[] = [];
|
||||
|
||||
for (const m of cfg.metrics) {
|
||||
switch (m) {
|
||||
case 'turnNumber':
|
||||
metricParts.push(`meta.appendChild(mk('span','${AG_PREFIX}t ${AG_PREFIX}b','T'+tI));`);
|
||||
break;
|
||||
case 'userCharCount':
|
||||
metricParts.push(`if(uL>0){meta.appendChild(mk('span','${AG_PREFIX}t ${AG_PREFIX}u','USER'));meta.appendChild(mk('span','${AG_PREFIX}k',fmt(uL)));}`);
|
||||
break;
|
||||
case 'separator':
|
||||
metricParts.push(`if(uL>0&&bL>0)meta.appendChild(mk('span','${AG_PREFIX}s','\\u2502'));`);
|
||||
break;
|
||||
case 'aiCharCount':
|
||||
metricParts.push(`if(bL>0){meta.appendChild(mk('span','${AG_PREFIX}t ${AG_PREFIX}b','AI'));meta.appendChild(mk('span','${AG_PREFIX}k',fmt(bL)));}`);
|
||||
break;
|
||||
case 'codeBlocks':
|
||||
metricParts.push(`if(codes>0){meta.appendChild(mk('span','${AG_PREFIX}k','code:'));meta.appendChild(mk('span','${AG_PREFIX}v ${AG_PREFIX}w',''+codes));}`);
|
||||
break;
|
||||
case 'thinkingIndicator':
|
||||
metricParts.push(`if(brain)meta.appendChild(mk('span','${AG_PREFIX}v','\\u{1F9E0}'));`);
|
||||
break;
|
||||
case 'ratio':
|
||||
metricParts.push(`if(uL>0&&bL>0){meta.appendChild(mk('span','${AG_PREFIX}k',(bL/uL).toFixed(1)+'x'));}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const clickHandler = cfg.clickable !== false
|
||||
? `meta.addEventListener('click',function(){toast('Turn '+tI,null,[['user:',fmt(uL)],['AI:',fmt(bL)],['ratio:',uL>0?(bL/uL).toFixed(1)+'x':'\\u2014']]);});`
|
||||
: '';
|
||||
|
||||
return `
|
||||
function integrateTurnMeta(){
|
||||
var c=document.querySelector(${JSON.stringify(Selectors.TURNS_CONTAINER)});if(!c)return;
|
||||
var tI=0;
|
||||
Array.from(c.children).forEach(function(turn){
|
||||
if(turn.getAttribute('${AG_DATA_ATTR}')||turn.children.length<1)return;
|
||||
turn.setAttribute('${AG_DATA_ATTR}','1');
|
||||
tI++;var uL=(turn.children[0]?.textContent?.trim()||'').length;
|
||||
var bL=(turn.children[1]?.textContent?.trim()||'').length;
|
||||
if(uL===0&&bL===0)return;
|
||||
var codes=turn.children[1]?.querySelectorAll('pre')?.length||0;
|
||||
var brain=(turn.children[1]?.textContent||'').includes('Thought');
|
||||
var meta=mk('div','${AG_PREFIX}meta');
|
||||
${metricParts.join('\n ')}
|
||||
${clickHandler}
|
||||
turn.appendChild(meta);
|
||||
});
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
private _genUserBadge(configs: IUserBadgeIntegration[]): string {
|
||||
const cfg = configs[0];
|
||||
let displayExpr = 'fmt(uLen)+" ch"';
|
||||
if (cfg.display === 'wordCount') {
|
||||
displayExpr = '(txt.split(/\\\\s+/).length)+" w"';
|
||||
} else if (cfg.display === 'custom' && cfg.customFormat) {
|
||||
displayExpr = cfg.customFormat;
|
||||
}
|
||||
|
||||
return `
|
||||
function integrateUserBadges(){
|
||||
var c=document.querySelector(${JSON.stringify(Selectors.TURNS_CONTAINER)});if(!c)return;
|
||||
Array.from(c.children).forEach(function(turn,i){
|
||||
if(turn.getAttribute('${AG_DATA_ATTR}u')||turn.children.length<1)return;
|
||||
var bubble=turn.children[0]?.querySelector(${JSON.stringify(Selectors.USER_BUBBLE)});
|
||||
if(!bubble)return;
|
||||
var txt=turn.children[0]?.textContent?.trim()||'';
|
||||
var uLen=txt.length;if(uLen<5)return;
|
||||
turn.setAttribute('${AG_DATA_ATTR}u','1');
|
||||
var row=turn.children[0]?.querySelector('.flex.w-full,.flex.flex-row')||turn.children[0];
|
||||
var badge=mk('span','${AG_PREFIX}ubadge');
|
||||
badge.textContent=${displayExpr};
|
||||
badge.title='SDK: User message';
|
||||
row.appendChild(badge);
|
||||
});
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
private _genBotAction(configs: IBotActionIntegration[]): string {
|
||||
const items = configs.map(c => {
|
||||
const toastCall = this._genToastCall(c.toast);
|
||||
return `var b=mk('span','${AG_PREFIX}vote');b.textContent=${JSON.stringify(c.icon + ' ' + c.label)};
|
||||
b.addEventListener('click',function(ev){ev.stopPropagation();${toastCall}});
|
||||
row.appendChild(b);`;
|
||||
});
|
||||
|
||||
return `
|
||||
function integrateBotAction(){
|
||||
var c=document.querySelector(${JSON.stringify(Selectors.TURNS_CONTAINER)});if(!c)return;
|
||||
c.querySelectorAll('span,button,a,div').forEach(function(el){
|
||||
if(el.getAttribute('${AG_DATA_ATTR}v'))return;
|
||||
var txt=el.textContent?.trim();
|
||||
if(txt==='Good'||txt==='Bad'){
|
||||
var row=el.parentElement;if(!row||row.querySelector('.${AG_PREFIX}vote'))return;
|
||||
el.setAttribute('${AG_DATA_ATTR}v','1');
|
||||
${items.join('\n ')}
|
||||
}
|
||||
});
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
private _genDropdown(configs: IDropdownIntegration[]): string {
|
||||
const markers = JSON.stringify(Selectors.DROPDOWN_MARKER_TEXT);
|
||||
const items = configs.map(c => {
|
||||
const toastCall = this._genToastCall(c.toast);
|
||||
const sep = c.separator
|
||||
? `var sep=mk('div','');sep.style.cssText='height:1px;background:rgba(255,255,255,.06);margin:4px 8px';dd.appendChild(sep);`
|
||||
: '';
|
||||
return `${sep}
|
||||
var mi=mk('div','${AG_PREFIX}menu');
|
||||
${c.icon ? `mi.appendChild(mk('span','',${JSON.stringify(c.icon)}));` : ''}
|
||||
mi.appendChild(document.createTextNode(${JSON.stringify(c.label)}));
|
||||
mi.addEventListener('click',function(){${toastCall}});
|
||||
dd.appendChild(mi);`;
|
||||
});
|
||||
|
||||
return `
|
||||
function integrateDropdown(){
|
||||
var dds=document.querySelectorAll('.rounded-bg.py-1,.rounded-lg.py-1');
|
||||
dds.forEach(function(dd){
|
||||
if(dd.getAttribute('${AG_DATA_ATTR}m'))return;
|
||||
var items=dd.querySelectorAll(${JSON.stringify(Selectors.DROPDOWN_ITEM)});
|
||||
var markers=${markers};
|
||||
var found=false;
|
||||
items.forEach(function(it){markers.forEach(function(m){if((it.textContent||'').includes(m))found=true;});});
|
||||
if(!found)return;
|
||||
dd.setAttribute('${AG_DATA_ATTR}m','1');
|
||||
${items.join('\n ')}
|
||||
});
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
private _genTitle(configs: ITitleIntegration[]): string {
|
||||
const cfg = configs[0];
|
||||
const toastCall = this._genToastCall(cfg.toast);
|
||||
const event = cfg.interaction || 'dblclick';
|
||||
|
||||
return `
|
||||
function integrateTitle(){
|
||||
var p=document.querySelector(${JSON.stringify(Selectors.PANEL)});if(!p)return;
|
||||
var el=p.querySelector(${JSON.stringify(Selectors.TITLE)});
|
||||
if(!el||el.getAttribute('${AG_DATA_ATTR}t'))return;
|
||||
el.setAttribute('${AG_DATA_ATTR}t','1');
|
||||
el.style.cursor='pointer';
|
||||
el.classList.add('${AG_PREFIX}title-wrap');
|
||||
el.style.position='relative';
|
||||
${cfg.hint ? `var hint=mk('span','${AG_PREFIX}title-hint',${JSON.stringify(cfg.hint)});el.appendChild(hint);` : ''}
|
||||
el.addEventListener(${JSON.stringify(event)},function(){
|
||||
var title=el.textContent?.replace(${JSON.stringify(cfg.hint || '')},'')?.trim()||'';
|
||||
${toastCall || `toast('Chat',null,[['title:',title],['chars:',''+title.length]]);`}
|
||||
});
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
// ─── Main loop ────────────────────────────────────────────────────
|
||||
|
||||
private _mainLoop(points: IntegrationPoint[]): string {
|
||||
const fnMap: Record<string, string> = {
|
||||
[IntegrationPoint.TOP_BAR]: 'integrateTopBar',
|
||||
[IntegrationPoint.TOP_RIGHT]: 'integrateTopRight',
|
||||
[IntegrationPoint.INPUT_AREA]: 'integrateInputArea',
|
||||
[IntegrationPoint.BOTTOM_ICONS]: 'integrateBottomIcons',
|
||||
[IntegrationPoint.TURN_METADATA]: 'integrateTurnMeta',
|
||||
[IntegrationPoint.USER_BADGE]: 'integrateUserBadges',
|
||||
[IntegrationPoint.BOT_ACTION]: 'integrateBotAction',
|
||||
[IntegrationPoint.DROPDOWN_MENU]: 'integrateDropdown',
|
||||
[IntegrationPoint.CHAT_TITLE]: 'integrateTitle',
|
||||
};
|
||||
|
||||
const calls = points.map(p => ` ${fnMap[p]}();`).join('\n');
|
||||
|
||||
return `
|
||||
function fullScan(){
|
||||
${calls}
|
||||
}
|
||||
var _timer=0;
|
||||
function debounced(){clearTimeout(_timer);_timer=setTimeout(function(){requestAnimationFrame(fullScan);},400);}
|
||||
function start(){
|
||||
var p=document.querySelector(${JSON.stringify(Selectors.PANEL)});
|
||||
if(!p){setTimeout(start,1000);return;}
|
||||
fullScan();
|
||||
new MutationObserver(debounced).observe(p,{childList:true,subtree:true});
|
||||
setInterval(fullScan,8000);
|
||||
console.log('[AG SDK] Active \\u2014 ${points.length} integration points');
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
56
antigravity-sdk-main/src/integration/selectors.ts
Normal file
56
antigravity-sdk-main/src/integration/selectors.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* DOM Selectors — Single source of truth for all Agent View selectors.
|
||||
*
|
||||
* Verified against Antigravity v1.107.0 DOM (2026-02-28).
|
||||
* If Antigravity updates break selectors, only THIS file needs updating.
|
||||
*
|
||||
* @module integration/selectors
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
|
||||
export const Selectors = {
|
||||
/** The entire agent side panel container */
|
||||
PANEL: '.antigravity-agent-side-panel',
|
||||
|
||||
/** Top bar with title and action icons */
|
||||
TOP_BAR: '.flex.items-center.justify-between',
|
||||
|
||||
/** Icons area in top bar (contains +, refresh, ..., X) */
|
||||
TOP_ICONS: '.flex.items-center.gap-2',
|
||||
|
||||
/** Chat title element */
|
||||
TITLE: '.flex.min-w-0.items-center.overflow-hidden',
|
||||
|
||||
/** Main conversation scroll area */
|
||||
CONVERSATION: '#conversation',
|
||||
|
||||
/** Message turns container (direct children are turns) */
|
||||
TURNS_CONTAINER: '#conversation .gap-y-3',
|
||||
|
||||
/** User message bubble (inside turn) */
|
||||
USER_BUBBLE: '.rounded-lg',
|
||||
|
||||
/** Input box container */
|
||||
INPUT_BOX: '#antigravity\\.agentSidePanelInputBox',
|
||||
|
||||
/** 3-dot dropdown menu (appears dynamically) */
|
||||
DROPDOWN_MARKER_TEXT: ['Customization', 'Export'],
|
||||
|
||||
/** Dropdown menu item class pattern */
|
||||
DROPDOWN_ITEM: '.cursor-pointer',
|
||||
|
||||
/** Good/Bad feedback text markers */
|
||||
FEEDBACK_MARKERS: ['Good', 'Bad'],
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* CSS class prefixes used by SDK integrations.
|
||||
* Used to identify and clean up integrated elements.
|
||||
*/
|
||||
export const AG_PREFIX = 'ag-';
|
||||
|
||||
/**
|
||||
* Data attribute used to mark processed elements.
|
||||
*/
|
||||
export const AG_DATA_ATTR = 'data-ag-sdk';
|
||||
171
antigravity-sdk-main/src/integration/title-manager.ts
Normal file
171
antigravity-sdk-main/src/integration/title-manager.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
/**
|
||||
* Title Manager — Extension-host API for managing chat titles.
|
||||
*
|
||||
* Allows extensions to programmatically rename conversations
|
||||
* by writing to a data file that the renderer-side title proxy reads.
|
||||
*
|
||||
* Also provides a direct localStorage synchronization mechanism
|
||||
* via the integration script's window.__agSDKTitles API.
|
||||
*
|
||||
* @module integration/title-manager
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const sdk = new AntigravitySDK(context);
|
||||
* await sdk.initialize();
|
||||
*
|
||||
* // Rename via extension host (writes data file, renderer picks up on next poll)
|
||||
* sdk.titles.rename('cascade-uuid', 'My Custom Title');
|
||||
*
|
||||
* // Get all custom titles
|
||||
* const titles = sdk.titles.getAll();
|
||||
*
|
||||
* // Remove a custom title (reverts to auto-generated summary)
|
||||
* sdk.titles.remove('cascade-uuid');
|
||||
* ```
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { Logger } from '../core/logger';
|
||||
import { IDisposable } from '../core/disposable';
|
||||
import { getTitlesDataFile } from './title-proxy';
|
||||
|
||||
const log = new Logger('TitleManager');
|
||||
|
||||
/**
|
||||
* Manages custom conversation titles from the extension host.
|
||||
*
|
||||
* Titles are persisted in a JSON file in the workbench directory.
|
||||
* The renderer-side title proxy reads this file and merges with localStorage.
|
||||
*/
|
||||
export class TitleManager implements IDisposable {
|
||||
private _titles: Record<string, string> = {};
|
||||
private _dataPath: string = '';
|
||||
private _initialized = false;
|
||||
|
||||
/**
|
||||
* Initialize with the workbench directory path.
|
||||
*
|
||||
* @param workbenchDir - Path to workbench directory where data file is stored
|
||||
* @param namespace - Extension namespace for file isolation
|
||||
*/
|
||||
initialize(workbenchDir: string, namespace: string = 'default'): void {
|
||||
this._dataPath = path.join(workbenchDir, getTitlesDataFile(namespace));
|
||||
this._load();
|
||||
this._initialized = true;
|
||||
log.info(`Initialized, ${Object.keys(this._titles).length} custom titles loaded`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the manager is initialized.
|
||||
*/
|
||||
get isInitialized(): boolean {
|
||||
return this._initialized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a custom title for a conversation.
|
||||
*
|
||||
* The title will be displayed in the Agent View title bar
|
||||
* and conversation list instead of the auto-generated summary.
|
||||
*
|
||||
* @param cascadeId - The conversation's cascade ID (UUID)
|
||||
* @param title - The custom title to display
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Rename the active conversation
|
||||
* const id = sdk.titles.getActiveCascadeId();
|
||||
* sdk.titles.rename(id, 'Project Alpha Discussion');
|
||||
* ```
|
||||
*/
|
||||
rename(cascadeId: string, title: string): void {
|
||||
if (!cascadeId) {
|
||||
log.warn('rename: cascadeId is required');
|
||||
return;
|
||||
}
|
||||
if (!title || !title.trim()) {
|
||||
log.warn('rename: title cannot be empty');
|
||||
return;
|
||||
}
|
||||
this._titles[cascadeId] = title.trim();
|
||||
this._save();
|
||||
log.debug(`Renamed ${cascadeId.substring(0, 8)}... -> "${title.trim()}"`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the custom title for a conversation.
|
||||
*
|
||||
* @param cascadeId - The conversation's cascade ID
|
||||
* @returns The custom title, or undefined if no custom title is set
|
||||
*/
|
||||
getTitle(cascadeId: string): string | undefined {
|
||||
return this._titles[cascadeId];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all custom titles.
|
||||
*
|
||||
* @returns A copy of the titles map (cascadeId -> title)
|
||||
*/
|
||||
getAll(): Readonly<Record<string, string>> {
|
||||
return { ...this._titles };
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a custom title, reverting to the auto-generated summary.
|
||||
*
|
||||
* @param cascadeId - The conversation's cascade ID
|
||||
*/
|
||||
remove(cascadeId: string): void {
|
||||
if (this._titles[cascadeId]) {
|
||||
delete this._titles[cascadeId];
|
||||
this._save();
|
||||
log.debug(`Removed title for ${cascadeId.substring(0, 8)}...`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all custom titles.
|
||||
*/
|
||||
clear(): void {
|
||||
this._titles = {};
|
||||
this._save();
|
||||
log.debug('Cleared all custom titles');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of custom titles.
|
||||
*/
|
||||
get count(): number {
|
||||
return Object.keys(this._titles).length;
|
||||
}
|
||||
|
||||
/** Load titles from the data file */
|
||||
private _load(): void {
|
||||
try {
|
||||
if (fs.existsSync(this._dataPath)) {
|
||||
const content = fs.readFileSync(this._dataPath, 'utf8');
|
||||
this._titles = JSON.parse(content) || {};
|
||||
}
|
||||
} catch (err) {
|
||||
log.warn(`Failed to load titles: ${err}`);
|
||||
this._titles = {};
|
||||
}
|
||||
}
|
||||
|
||||
/** Save titles to the data file */
|
||||
private _save(): void {
|
||||
if (!this._dataPath) return;
|
||||
try {
|
||||
fs.writeFileSync(this._dataPath, JSON.stringify(this._titles, null, 2), 'utf8');
|
||||
} catch (err) {
|
||||
log.warn(`Failed to save titles: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
// Nothing to clean up - titles persist on disk
|
||||
}
|
||||
}
|
||||
292
antigravity-sdk-main/src/integration/title-proxy.ts
Normal file
292
antigravity-sdk-main/src/integration/title-proxy.ts
Normal file
@@ -0,0 +1,292 @@
|
||||
/**
|
||||
* Title Proxy — Renderer-side code for intercepting chat summaries.
|
||||
*
|
||||
* Generates JavaScript that runs in the workbench renderer process.
|
||||
* Uses Preact VNode context walk to find the summaries provider,
|
||||
* wraps getState() to inject custom titles from localStorage,
|
||||
* and captures onDidChange listeners for forced re-renders.
|
||||
*
|
||||
* All identifiers used here are STRUCTURALLY MATCHED, not hardcoded
|
||||
* minified variable names — this survives obfuscation changes.
|
||||
*
|
||||
* @module integration/title-proxy
|
||||
* @internal
|
||||
*/
|
||||
|
||||
/** localStorage key prefix for custom titles */
|
||||
const TITLES_STORAGE_PREFIX = 'ag-sdk-titles';
|
||||
|
||||
/** Data file prefix for extension-host to set titles */
|
||||
const TITLES_DATA_PREFIX = 'ag-sdk-titles';
|
||||
|
||||
/**
|
||||
* Generate the renderer-side title proxy JavaScript.
|
||||
*
|
||||
* This code:
|
||||
* 1. BFS walks the Preact VNode tree (limit 3000, arrays not counted)
|
||||
* 2. Finds summaries provider via structural matching
|
||||
* 3. Wraps provider.getState() to inject custom titles
|
||||
* 4. Captures onDidChange listeners for forced re-renders
|
||||
* 5. Reads custom titles from localStorage + data file
|
||||
* 6. Exposes window.__agSDKTitles API for inline rename
|
||||
*
|
||||
* @param dataFilePath - Relative path to the JSON data file (for extension-host titles)
|
||||
* @returns JavaScript source code
|
||||
*/
|
||||
export function generateTitleProxyCode(namespace: string = 'default'): string {
|
||||
const slug = namespace.replace(/[^a-zA-Z0-9-]/g, '-');
|
||||
const storageKey = `${TITLES_STORAGE_PREFIX}-${slug}`;
|
||||
const dataFile = `./${TITLES_DATA_PREFIX}-${slug}.json`;
|
||||
return `
|
||||
// ── AG SDK: Title Proxy ──────────────────────────────────────────
|
||||
// Intercepts summaries provider to inject custom chat titles.
|
||||
// Uses structural matching (obfuscation-safe).
|
||||
|
||||
(function initTitleProxy(){
|
||||
var PANEL_SEL='.antigravity-agent-side-panel';
|
||||
var TITLE_SEL='.flex.min-w-0.items-center.overflow-hidden';
|
||||
var STORAGE_KEY='${storageKey}';
|
||||
var DATA_FILE='${dataFile}';
|
||||
|
||||
var _provider=null;
|
||||
var _origGetState=null;
|
||||
var _listeners=[];
|
||||
var _customTitles={};
|
||||
var _searchTime=0;
|
||||
|
||||
// ── Load / Save ────────────────────────────────────────────────
|
||||
|
||||
function loadTitles(){
|
||||
// Step 1: Load from localStorage (sync, fast)
|
||||
try{_customTitles=JSON.parse(localStorage.getItem(STORAGE_KEY)||'{}');}catch(e){_customTitles={};}
|
||||
// Step 2: Merge extension-host titles from data file (async fetch)
|
||||
fetch(DATA_FILE).then(function(r){
|
||||
if(!r.ok)return;
|
||||
return r.text();
|
||||
}).then(function(text){
|
||||
if(!text)return;
|
||||
try{
|
||||
var extTitles=JSON.parse(text);
|
||||
if(extTitles&&typeof extTitles==='object'){
|
||||
for(var k in extTitles){_customTitles[k]=extTitles[k];}
|
||||
saveTitles();
|
||||
notifyListeners();
|
||||
}
|
||||
}catch(e){}
|
||||
}).catch(function(){});
|
||||
}
|
||||
|
||||
function saveTitles(){
|
||||
try{localStorage.setItem(STORAGE_KEY,JSON.stringify(_customTitles));}catch(e){}
|
||||
}
|
||||
|
||||
// ── Notify ─────────────────────────────────────────────────────
|
||||
|
||||
function notifyListeners(){
|
||||
for(var i=0;i<_listeners.length;i++){try{_listeners[i]();}catch(e){}}
|
||||
}
|
||||
|
||||
// ── Provider Wrapping ──────────────────────────────────────────
|
||||
|
||||
function wrapProvider(provider){
|
||||
if(provider.__agSDKWrapped)return;
|
||||
provider.__agSDKWrapped=true;
|
||||
_provider=provider;
|
||||
var origFn=provider.getState;
|
||||
_origGetState=origFn;
|
||||
|
||||
// Wrap getState to inject custom titles
|
||||
provider.getState=function(){
|
||||
var state=origFn.call(provider);
|
||||
if(!state||!state.summaries)return state;
|
||||
var hasOverrides=false;
|
||||
for(var cid in _customTitles){if(state.summaries[cid]){hasOverrides=true;break;}}
|
||||
if(!hasOverrides)return state;
|
||||
var ns={};
|
||||
for(var k in state.summaries)ns[k]=state.summaries[k];
|
||||
for(var cid in _customTitles){
|
||||
if(ns[cid]){
|
||||
var copy={};for(var p in ns[cid])copy[p]=ns[cid][p];
|
||||
copy.summary=_customTitles[cid];
|
||||
ns[cid]=copy;
|
||||
}
|
||||
}
|
||||
var newState={};for(var sk in state)newState[sk]=state[sk];
|
||||
newState.summaries=ns;
|
||||
return newState;
|
||||
};
|
||||
|
||||
// Intercept onDidChange to capture listeners
|
||||
var origOnDidChange=provider.onDidChange;
|
||||
provider.onDidChange=function(callback){
|
||||
_listeners.push(callback);
|
||||
var origDispose=origOnDidChange.call(this,callback);
|
||||
return{dispose:function(){
|
||||
var idx=_listeners.indexOf(callback);
|
||||
if(idx>=0)_listeners.splice(idx,1);
|
||||
origDispose.dispose();
|
||||
}};
|
||||
};
|
||||
|
||||
console.log('[AG SDK] Title proxy active, custom titles:', Object.keys(_customTitles).length);
|
||||
|
||||
// Force re-render so custom titles appear immediately
|
||||
// (without waiting for next native summaries update)
|
||||
setTimeout(function(){notifyListeners();},50);
|
||||
}
|
||||
|
||||
// ── VNode BFS Walk ─────────────────────────────────────────────
|
||||
|
||||
function findProvider(){
|
||||
if(_provider)return;
|
||||
var panel=document.querySelector(PANEL_SEL);
|
||||
if(!panel||!panel.__k)return;
|
||||
// Throttle only AFTER confirming panel exists (don't block retries when panel isn't mounted)
|
||||
var now=Date.now();
|
||||
if(_searchTime&&now-_searchTime<30000)return;
|
||||
_searchTime=now;
|
||||
var queue=[panel.__k],visited=0;
|
||||
while(queue.length>0&&visited<3000){
|
||||
var node=queue.shift();
|
||||
if(!node)continue;
|
||||
if(Array.isArray(node)){
|
||||
for(var ai=0;ai<node.length;ai++){if(node[ai])queue.push(node[ai]);}
|
||||
continue;
|
||||
}
|
||||
visited++;
|
||||
var comp=node.__c;
|
||||
if(comp&&comp.context&&typeof comp.context==='object'){
|
||||
for(var key in comp.context){
|
||||
try{
|
||||
var ctx=comp.context[key];
|
||||
if(!ctx||!ctx.props||!ctx.props.value)continue;
|
||||
var val=ctx.props.value;
|
||||
// Structural match: {provider: {getState() -> {summaries}}}
|
||||
if(val.provider&&typeof val.provider.getState==='function'){
|
||||
var ts=val.provider.getState();
|
||||
if(ts&&ts.summaries){wrapProvider(val.provider);return;}
|
||||
}
|
||||
// Structural match: {trajectorySummariesProvider: {getState() -> {summaries}}}
|
||||
if(val.trajectorySummariesProvider&&typeof val.trajectorySummariesProvider.getState==='function'){
|
||||
var ts2=val.trajectorySummariesProvider.getState();
|
||||
if(ts2&&ts2.summaries){wrapProvider(val.trajectorySummariesProvider);return;}
|
||||
}
|
||||
}catch(e){}
|
||||
}
|
||||
}
|
||||
// Direct props match
|
||||
if(comp&&comp.props&&comp.props.trajectorySummariesProvider){
|
||||
var tsp=comp.props.trajectorySummariesProvider;
|
||||
if(typeof tsp.getState==='function'){
|
||||
try{var ts3=tsp.getState();
|
||||
if(ts3&&ts3.summaries){wrapProvider(tsp);return;}
|
||||
}catch(e){}
|
||||
}
|
||||
}
|
||||
if(node.__k){
|
||||
if(Array.isArray(node.__k)){for(var ki=0;ki<node.__k.length;ki++){if(node.__k[ki])queue.push(node.__k[ki]);}}
|
||||
else{queue.push(node.__k);}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── CascadeId Resolution ───────────────────────────────────────
|
||||
|
||||
function findCascadeIdByTitle(text){
|
||||
if(!_origGetState)return '';
|
||||
try{
|
||||
var state=_origGetState.call(_provider);
|
||||
if(!state||!state.summaries)return '';
|
||||
// Reverse lookup custom titles first
|
||||
for(var cid in _customTitles){if(_customTitles[cid]===text)return cid;}
|
||||
// Match original summaries
|
||||
var bestId='',bestTime=0;
|
||||
for(var cid in state.summaries){
|
||||
var e=state.summaries[cid];
|
||||
if(e&&e.summary===text){
|
||||
var t=0;try{t=new Date(e.lastModifiedTime).getTime();}catch(e){}
|
||||
if(!bestId||t>bestTime){bestId=cid;bestTime=t;}
|
||||
}
|
||||
}
|
||||
return bestId;
|
||||
}catch(e){return '';}
|
||||
}
|
||||
|
||||
// ── Public API ─────────────────────────────────────────────────
|
||||
|
||||
window.__agSDKTitles={
|
||||
rename:function(cascadeId,title){
|
||||
if(!cascadeId||!title)return false;
|
||||
_customTitles[cascadeId]=title;
|
||||
saveTitles();
|
||||
notifyListeners();
|
||||
return true;
|
||||
},
|
||||
renameByCurrentTitle:function(currentTitle,newTitle){
|
||||
var cid=findCascadeIdByTitle(currentTitle);
|
||||
if(!cid)return false;
|
||||
return this.rename(cid,newTitle);
|
||||
},
|
||||
remove:function(cascadeId){
|
||||
delete _customTitles[cascadeId];
|
||||
saveTitles();
|
||||
notifyListeners();
|
||||
},
|
||||
getTitle:function(cascadeId){return _customTitles[cascadeId]||null;},
|
||||
getAll:function(){var copy={};for(var k in _customTitles)copy[k]=_customTitles[k];return copy;},
|
||||
getActiveCascadeId:function(){
|
||||
var panel=document.querySelector(PANEL_SEL);
|
||||
if(!panel)return '';
|
||||
var titleEl=panel.querySelector(TITLE_SEL);
|
||||
if(!titleEl)return '';
|
||||
var text='';
|
||||
function findText(el){
|
||||
for(var i=0;i<el.childNodes.length;i++){
|
||||
var n=el.childNodes[i];
|
||||
if(n.nodeType===3&&n.textContent.trim().length>0)return n.textContent.trim();
|
||||
if(n.nodeType===1){var found=findText(n);if(found)return found;}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
text=findText(titleEl);
|
||||
return text?findCascadeIdByTitle(text):'';
|
||||
},
|
||||
isReady:function(){return !!_provider;},
|
||||
reload:function(){loadTitles();notifyListeners();}
|
||||
};
|
||||
|
||||
// ── Init ───────────────────────────────────────────────────────
|
||||
|
||||
loadTitles();
|
||||
|
||||
function poll(){
|
||||
findProvider();
|
||||
}
|
||||
|
||||
// Poll until provider found, then every 30s for recovery
|
||||
var pollTimer=setInterval(function(){poll();},2000);
|
||||
|
||||
// Initial attempt after DOM is ready
|
||||
if(document.querySelector(PANEL_SEL)){
|
||||
poll();
|
||||
}
|
||||
|
||||
})();
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the data file name for extension-host titles.
|
||||
*/
|
||||
export function getTitlesDataFile(namespace: string = 'default'): string {
|
||||
const slug = namespace.replace(/[^a-zA-Z0-9-]/g, '-');
|
||||
return `${TITLES_DATA_PREFIX}-${slug}.json`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the localStorage key used by the renderer.
|
||||
*/
|
||||
export function getTitlesStorageKey(namespace: string = 'default'): string {
|
||||
const slug = namespace.replace(/[^a-zA-Z0-9-]/g, '-');
|
||||
return `${TITLES_STORAGE_PREFIX}-${slug}`;
|
||||
}
|
||||
213
antigravity-sdk-main/src/integration/types.ts
Normal file
213
antigravity-sdk-main/src/integration/types.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
/**
|
||||
* Integration module types — standardized UI integration points
|
||||
* for the Antigravity Agent View.
|
||||
*
|
||||
* @module integration/types
|
||||
*/
|
||||
|
||||
// ─── Integration Points ──────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Standardized integration points in the Agent View UI.
|
||||
*
|
||||
* Each point corresponds to a specific DOM location in the
|
||||
* Antigravity chat interface (verified 2026-02-28).
|
||||
*/
|
||||
export enum IntegrationPoint {
|
||||
/** Top bar — next to +, refresh, ... icons */
|
||||
TOP_BAR = 'topBar',
|
||||
/** Top right corner — before the X (close) button */
|
||||
TOP_RIGHT = 'topRight',
|
||||
/** Input area — next to voice/send buttons */
|
||||
INPUT_AREA = 'inputArea',
|
||||
/** Bottom icon row — file, terminal, artifact, chrome icons */
|
||||
BOTTOM_ICONS = 'bottomIcons',
|
||||
/** Per-turn metadata — appended inside each conversation turn */
|
||||
TURN_METADATA = 'turnMeta',
|
||||
/** User message badge — small badge inside user message bubbles */
|
||||
USER_BADGE = 'userBadge',
|
||||
/** Bot response action — button next to Good/Bad feedback */
|
||||
BOT_ACTION = 'botAction',
|
||||
/** 3-dot dropdown menu — extra items in the overflow menu */
|
||||
DROPDOWN_MENU = 'dropdownMenu',
|
||||
/** Chat title bar — interaction on conversation title */
|
||||
CHAT_TITLE = 'chatTitle',
|
||||
}
|
||||
|
||||
// ─── Configuration Interfaces ──────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Base configuration for all integration points.
|
||||
*/
|
||||
export interface IIntegrationBase {
|
||||
/** Unique ID for this integration (prevents duplicates) */
|
||||
id: string;
|
||||
/** Which integration point to target */
|
||||
point: IntegrationPoint;
|
||||
/** Whether this integration is enabled (default: true) */
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for button-type integrations (top bar, input area, etc.).
|
||||
*/
|
||||
export interface IButtonIntegration extends IIntegrationBase {
|
||||
point:
|
||||
| IntegrationPoint.TOP_BAR
|
||||
| IntegrationPoint.TOP_RIGHT
|
||||
| IntegrationPoint.INPUT_AREA
|
||||
| IntegrationPoint.BOTTOM_ICONS;
|
||||
/** Icon (emoji or text glyph) */
|
||||
icon: string;
|
||||
/** Tooltip text */
|
||||
tooltip?: string;
|
||||
/** Toast to show on click */
|
||||
toast?: IToastConfig;
|
||||
/** CSS class override */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for turn-level metadata integration.
|
||||
*/
|
||||
export interface ITurnMetaIntegration extends IIntegrationBase {
|
||||
point: IntegrationPoint.TURN_METADATA;
|
||||
/** Which metrics to display */
|
||||
metrics: TurnMetric[];
|
||||
/** Whether turns are clickable to show details toast */
|
||||
clickable?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for user message badges.
|
||||
*/
|
||||
export interface IUserBadgeIntegration extends IIntegrationBase {
|
||||
point: IntegrationPoint.USER_BADGE;
|
||||
/** What to show in the badge */
|
||||
display: 'charCount' | 'wordCount' | 'custom';
|
||||
/** Custom formatter function body (receives `textLength` as arg) */
|
||||
customFormat?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for bot response action buttons.
|
||||
*/
|
||||
export interface IBotActionIntegration extends IIntegrationBase {
|
||||
point: IntegrationPoint.BOT_ACTION;
|
||||
/** Icon */
|
||||
icon: string;
|
||||
/** Label text */
|
||||
label: string;
|
||||
/** Toast config on click */
|
||||
toast?: IToastConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for dropdown menu items.
|
||||
*/
|
||||
export interface IDropdownIntegration extends IIntegrationBase {
|
||||
point: IntegrationPoint.DROPDOWN_MENU;
|
||||
/** Menu item icon */
|
||||
icon?: string;
|
||||
/** Menu item label */
|
||||
label: string;
|
||||
/** Add separator before this item */
|
||||
separator?: boolean;
|
||||
/** Toast config on click */
|
||||
toast?: IToastConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for chat title interaction.
|
||||
*/
|
||||
export interface ITitleIntegration extends IIntegrationBase {
|
||||
point: IntegrationPoint.CHAT_TITLE;
|
||||
/** Interaction type */
|
||||
interaction: 'click' | 'dblclick' | 'hover';
|
||||
/** Hint text shown on hover */
|
||||
hint?: string;
|
||||
/** Toast config on interaction */
|
||||
toast?: IToastConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toast popup configuration.
|
||||
*/
|
||||
export interface IToastConfig {
|
||||
/** Toast title */
|
||||
title: string;
|
||||
/** Badge label and colors */
|
||||
badge?: {
|
||||
text: string;
|
||||
bgColor: string;
|
||||
textColor: string;
|
||||
};
|
||||
/** Key-value rows to display */
|
||||
rows: IToastRow[];
|
||||
/** Auto-dismiss after N milliseconds (default: 6000) */
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* A row in a toast popup.
|
||||
*/
|
||||
export interface IToastRow {
|
||||
/** Label (left side) */
|
||||
key: string;
|
||||
/**
|
||||
* Value (right side).
|
||||
* Can be a static string or a dynamic expression.
|
||||
* Dynamic expressions are JS code that runs in the renderer,
|
||||
* with access to `getStats()` which returns conversation stats.
|
||||
*/
|
||||
value: string;
|
||||
/** If true, `value` is treated as a JS expression */
|
||||
dynamic?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Metrics available for turn metadata display.
|
||||
*/
|
||||
export type TurnMetric =
|
||||
| 'turnNumber'
|
||||
| 'userCharCount'
|
||||
| 'aiCharCount'
|
||||
| 'codeBlocks'
|
||||
| 'thinkingIndicator'
|
||||
| 'ratio'
|
||||
| 'separator';
|
||||
|
||||
/**
|
||||
* Union type of all integration configurations.
|
||||
*/
|
||||
export type IntegrationConfig =
|
||||
| IButtonIntegration
|
||||
| ITurnMetaIntegration
|
||||
| IUserBadgeIntegration
|
||||
| IBotActionIntegration
|
||||
| IDropdownIntegration
|
||||
| ITitleIntegration;
|
||||
|
||||
// ─── Manager Interface ────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Public interface for the Integration Manager.
|
||||
*/
|
||||
export interface IIntegrationManager {
|
||||
/** Register a single integration point */
|
||||
register(config: IntegrationConfig): void;
|
||||
/** Register multiple integration points at once */
|
||||
registerMany(configs: IntegrationConfig[]): void;
|
||||
/** Remove a registered integration by ID */
|
||||
unregister(id: string): void;
|
||||
/** Get all registered integrations */
|
||||
getRegistered(): ReadonlyArray<IntegrationConfig>;
|
||||
/** Generate the integration script from all registered configs */
|
||||
build(): string;
|
||||
/** Install the generated script into workbench.html. Returns true if content changed. */
|
||||
install(): Promise<boolean>;
|
||||
/** Remove the integration from workbench.html */
|
||||
uninstall(): Promise<void>;
|
||||
/** Check if an integration is currently installed */
|
||||
isInstalled(): boolean;
|
||||
}
|
||||
257
antigravity-sdk-main/src/integration/workbench-patcher.ts
Normal file
257
antigravity-sdk-main/src/integration/workbench-patcher.ts
Normal file
@@ -0,0 +1,257 @@
|
||||
/**
|
||||
* Workbench Patcher — Install/uninstall integration scripts into workbench.html.
|
||||
*
|
||||
* Handles the file-level modification of Antigravity's workbench.html
|
||||
* to include/remove custom script tags.
|
||||
*
|
||||
* @module integration/workbench-patcher
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/** Default prefix for generated files */
|
||||
const FILE_PREFIX = 'ag-sdk';
|
||||
|
||||
/**
|
||||
* Manages patching/unpatching of Antigravity's workbench.html.
|
||||
*/
|
||||
export class WorkbenchPatcher {
|
||||
private readonly _workbenchDir: string;
|
||||
private readonly _workbenchHtml: string;
|
||||
private readonly _scriptPath: string;
|
||||
private readonly _heartbeatPath: string;
|
||||
private readonly _slug: string;
|
||||
|
||||
private readonly _markerStart: string;
|
||||
private readonly _markerEnd: string;
|
||||
|
||||
/**
|
||||
* @param namespace - Unique slug for this extension (e.g. 'kanezal-better-antigravity').
|
||||
* Used to namespace all generated files and HTML markers so multiple
|
||||
* SDK-based extensions can coexist without conflicts.
|
||||
*/
|
||||
constructor(namespace: string = 'default') {
|
||||
// Resolve Antigravity install path
|
||||
const appData = process.env.LOCALAPPDATA || '';
|
||||
this._workbenchDir = path.join(
|
||||
appData,
|
||||
'Programs',
|
||||
'Antigravity',
|
||||
'resources',
|
||||
'app',
|
||||
'out',
|
||||
'vs',
|
||||
'code',
|
||||
'electron-browser',
|
||||
'workbench',
|
||||
);
|
||||
this._workbenchHtml = path.join(this._workbenchDir, 'workbench.html');
|
||||
|
||||
this._slug = namespace.replace(/[^a-zA-Z0-9-]/g, '-');
|
||||
this._scriptPath = path.join(this._workbenchDir, `${FILE_PREFIX}-${this._slug}.js`);
|
||||
this._heartbeatPath = path.join(this._workbenchDir, `${FILE_PREFIX}-${this._slug}-heartbeat`);
|
||||
this._markerStart = `<!-- AG SDK [${this._slug}] -->`;
|
||||
this._markerEnd = `<!-- /AG SDK [${this._slug}] -->`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if workbench.html exists and is accessible.
|
||||
*/
|
||||
isAvailable(): boolean {
|
||||
return fs.existsSync(this._workbenchHtml);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if our integration is currently installed.
|
||||
*/
|
||||
isInstalled(): boolean {
|
||||
if (!this.isAvailable()) return false;
|
||||
try {
|
||||
const content = fs.readFileSync(this._workbenchHtml, 'utf8');
|
||||
return content.includes(this._markerStart);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Install the integration script.
|
||||
*
|
||||
* 1. Writes the script file to the workbench directory
|
||||
* 2. Patches workbench.html to include a <script> tag
|
||||
*
|
||||
* @param scriptContent — The generated JavaScript code
|
||||
*/
|
||||
install(scriptContent: string): void {
|
||||
if (!this.isAvailable()) {
|
||||
throw new Error(`Workbench not found at: ${this._workbenchDir}`);
|
||||
}
|
||||
|
||||
// First uninstall any previous integration for THIS namespace
|
||||
if (this.isInstalled()) {
|
||||
this.uninstall();
|
||||
}
|
||||
|
||||
// Clean up legacy files from previous versions (non-namespaced)
|
||||
this._cleanupLegacyFiles();
|
||||
|
||||
// Write the script file
|
||||
fs.writeFileSync(this._scriptPath, scriptContent, 'utf8');
|
||||
|
||||
// Patch workbench.html
|
||||
let html = fs.readFileSync(this._workbenchHtml, 'utf8');
|
||||
|
||||
// Insert before </html>
|
||||
const scriptBasename = path.basename(this._scriptPath);
|
||||
const scriptTag = [
|
||||
this._markerStart,
|
||||
`<script src="./${scriptBasename}"></script>`,
|
||||
this._markerEnd,
|
||||
].join('\n');
|
||||
|
||||
html = html.replace('</html>', `${scriptTag}\n</html>`);
|
||||
fs.writeFileSync(this._workbenchHtml, html, 'utf8');
|
||||
|
||||
// Create empty titles JSON if it doesn't exist (prevents console 404)
|
||||
const titlesPath = path.join(this._workbenchDir, `ag-sdk-titles-${this._slug}.json`);
|
||||
if (!fs.existsSync(titlesPath)) {
|
||||
fs.writeFileSync(titlesPath, '{}', 'utf8');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the integration.
|
||||
*
|
||||
* 1. Removes the <script> tag from workbench.html
|
||||
* 2. Deletes the script file
|
||||
*/
|
||||
uninstall(): void {
|
||||
if (!this.isAvailable()) return;
|
||||
|
||||
// Remove from workbench.html
|
||||
try {
|
||||
let html = fs.readFileSync(this._workbenchHtml, 'utf8');
|
||||
const regex = new RegExp(
|
||||
`\\n?${escapeRegex(this._markerStart)}[\\s\\S]*?${escapeRegex(this._markerEnd)}\\n?`,
|
||||
'g',
|
||||
);
|
||||
html = html.replace(regex, '');
|
||||
fs.writeFileSync(this._workbenchHtml, html, 'utf8');
|
||||
} catch {
|
||||
// Ignore errors during cleanup
|
||||
}
|
||||
|
||||
// Remove script file
|
||||
try {
|
||||
if (fs.existsSync(this._scriptPath)) {
|
||||
fs.unlinkSync(this._scriptPath);
|
||||
}
|
||||
} catch {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write/refresh the heartbeat marker file.
|
||||
*
|
||||
* The generated script checks this file's modification time
|
||||
* to determine if the extension is still active. If the file
|
||||
* is missing or stale, the script will not start.
|
||||
*/
|
||||
writeHeartbeat(): void {
|
||||
try {
|
||||
fs.writeFileSync(this._heartbeatPath, Date.now().toString(), 'utf8');
|
||||
} catch {
|
||||
// Ignore — workbench dir may not be writable
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the heartbeat marker file.
|
||||
*/
|
||||
removeHeartbeat(): void {
|
||||
try {
|
||||
if (fs.existsSync(this._heartbeatPath)) {
|
||||
fs.unlinkSync(this._heartbeatPath);
|
||||
}
|
||||
} catch {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to the heartbeat file.
|
||||
*/
|
||||
getHeartbeatPath(): string {
|
||||
return this._heartbeatPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to the workbench directory.
|
||||
*/
|
||||
getWorkbenchDir(): string {
|
||||
return this._workbenchDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to the script file.
|
||||
*/
|
||||
getScriptPath(): string {
|
||||
return this._scriptPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up legacy files from previous SDK versions.
|
||||
*
|
||||
* Removes non-namespaced files (from before namespace support)
|
||||
* and files with wrong namespace (e.g. 'undefined').
|
||||
*/
|
||||
private _cleanupLegacyFiles(): void {
|
||||
// Legacy file names that may exist from older versions
|
||||
const legacyFiles = [
|
||||
'ag-sdk-integrate.js',
|
||||
'ag-sdk-heartbeat',
|
||||
'ag-sdk-titles.json',
|
||||
'ag-sdk-titles-undefined.json',
|
||||
'ag-sdk-titles-default.json',
|
||||
];
|
||||
|
||||
for (const name of legacyFiles) {
|
||||
const p = path.join(this._workbenchDir, name);
|
||||
try {
|
||||
if (fs.existsSync(p)) fs.unlinkSync(p);
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
// Remove legacy script tags from workbench.html
|
||||
try {
|
||||
let html = fs.readFileSync(this._workbenchHtml, 'utf8');
|
||||
let changed = false;
|
||||
|
||||
// Remove bare <script src="./ag-sdk-integrate.js"></script> lines
|
||||
const legacyTagRegex = /<script src="\.\/ag-sdk-integrate\.js"><\/script>\n?/g;
|
||||
if (legacyTagRegex.test(html)) {
|
||||
html = html.replace(legacyTagRegex, '');
|
||||
changed = true;
|
||||
}
|
||||
|
||||
// Remove old X-Ray SDK markers with no namespace
|
||||
const xrayRegex = /<!-- X-Ray SDK Integration -->\n?<script[^>]*ag-sdk-integrate[^>]*><\/script>\n?<!-- \/X-Ray SDK Integration -->\n?/g;
|
||||
if (xrayRegex.test(html)) {
|
||||
html = html.replace(xrayRegex, '');
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
fs.writeFileSync(this._workbenchHtml, html, 'utf8');
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
}
|
||||
|
||||
function escapeRegex(str: string): string {
|
||||
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
229
antigravity-sdk-main/src/sdk.ts
Normal file
229
antigravity-sdk-main/src/sdk.ts
Normal file
@@ -0,0 +1,229 @@
|
||||
/**
|
||||
* Main SDK entry point.
|
||||
*
|
||||
* Provides a unified interface to Antigravity's agent system
|
||||
* via verified transport layer (CommandBridge + StateBridge + EventMonitor).
|
||||
*
|
||||
* @module AntigravitySDK
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { AntigravitySDK } from 'antigravity-sdk';
|
||||
*
|
||||
* export function activate(context: vscode.ExtensionContext) {
|
||||
* const sdk = new AntigravitySDK(context);
|
||||
* await sdk.initialize();
|
||||
*
|
||||
* // List conversations
|
||||
* const sessions = await sdk.cascade.getSessions();
|
||||
* console.log(`${sessions.length} conversations`);
|
||||
*
|
||||
* // Read preferences (all 16 sentinel values)
|
||||
* const prefs = await sdk.cascade.getPreferences();
|
||||
* console.log('Terminal policy:', prefs.terminalExecutionPolicy);
|
||||
*
|
||||
* // Monitor for new conversations
|
||||
* sdk.monitor.onNewConversation(() => {
|
||||
* console.log('New conversation detected!');
|
||||
* });
|
||||
* sdk.monitor.start(3000);
|
||||
*
|
||||
* // Clean up
|
||||
* context.subscriptions.push(sdk);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { DisposableStore, IDisposable } from './core/disposable';
|
||||
import { Logger, LogLevel } from './core/logger';
|
||||
import { AntigravityNotFoundError } from './core/errors';
|
||||
import { CommandBridge } from './transport/command-bridge';
|
||||
import { StateBridge } from './transport/state-bridge';
|
||||
import { EventMonitor } from './transport/event-monitor';
|
||||
import { LSBridge } from './transport/ls-bridge';
|
||||
import { CascadeManager } from './cascade/cascade-manager';
|
||||
import { IntegrationManager } from './integration/integration-manager';
|
||||
|
||||
const log = new Logger('SDK');
|
||||
|
||||
/**
|
||||
* SDK initialization options.
|
||||
*/
|
||||
export interface ISDKOptions {
|
||||
/** Enable debug logging */
|
||||
debug?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* The main Antigravity SDK class.
|
||||
*
|
||||
* Provides access to:
|
||||
* - `commands` — Execute Antigravity internal commands
|
||||
* - `state` — Read agent preferences and state from USS
|
||||
* - `cascade` — Manage Cascade conversations, send messages, read preferences
|
||||
* - `monitor` — Watch for state changes (new conversations, preference updates)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const sdk = new AntigravitySDK(context);
|
||||
* await sdk.initialize();
|
||||
* const sessions = await sdk.cascade.getSessions();
|
||||
* ```
|
||||
*/
|
||||
export class AntigravitySDK implements IDisposable {
|
||||
private readonly _disposables = new DisposableStore();
|
||||
private _initialized = false;
|
||||
|
||||
/** Command bridge for executing Antigravity commands */
|
||||
public readonly commands: CommandBridge;
|
||||
|
||||
/** State bridge for reading USS data */
|
||||
public readonly state: StateBridge;
|
||||
|
||||
/** Cascade manager for conversations, preferences, diagnostics */
|
||||
public readonly cascade: CascadeManager;
|
||||
|
||||
/** Event monitor for watching state changes */
|
||||
public readonly monitor: EventMonitor;
|
||||
|
||||
/** Integration manager for Agent View UI customization */
|
||||
public readonly integration: IntegrationManager;
|
||||
|
||||
/**
|
||||
* Language Server bridge for headless cascade operations.
|
||||
* Use this for background cascade creation without UI switching.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const id = await sdk.ls.createCascade({ text: 'Analyze coverage' });
|
||||
* await sdk.ls.sendMessage({ cascadeId: id, text: 'Focus on tests' });
|
||||
* await sdk.ls.focusCascade(id); // Only when ready to show
|
||||
* ```
|
||||
*/
|
||||
public readonly ls: LSBridge;
|
||||
|
||||
/**
|
||||
* Create a new Antigravity SDK instance.
|
||||
*
|
||||
* @param context - VS Code extension context
|
||||
* @param options - SDK options
|
||||
*/
|
||||
constructor(
|
||||
private readonly _context: vscode.ExtensionContext,
|
||||
options?: ISDKOptions,
|
||||
) {
|
||||
if (options?.debug) {
|
||||
Logger.setLevel(LogLevel.Debug);
|
||||
}
|
||||
|
||||
// Derive namespace from extension ID for file isolation
|
||||
// e.g. 'kanezal.better-antigravity' -> 'kanezal-better-antigravity'
|
||||
const namespace = this._context.extension.id.replace(/\./g, '-');
|
||||
|
||||
this.commands = this._disposables.add(new CommandBridge());
|
||||
this.state = this._disposables.add(new StateBridge());
|
||||
this.cascade = this._disposables.add(new CascadeManager(this.commands, this.state));
|
||||
this.monitor = this._disposables.add(new EventMonitor(this.state));
|
||||
this.integration = this._disposables.add(new IntegrationManager(namespace));
|
||||
this.ls = new LSBridge(
|
||||
<T = any>(cmd: string, ...args: any[]) => Promise.resolve(vscode.commands.executeCommand<T>(cmd, ...args))
|
||||
);
|
||||
|
||||
log.info(`SDK created (namespace: ${namespace})`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the SDK and verify Antigravity is running.
|
||||
*
|
||||
* Call this before using any SDK features.
|
||||
*
|
||||
* @throws {AntigravityNotFoundError} If Antigravity is not detected
|
||||
*/
|
||||
async initialize(): Promise<void> {
|
||||
if (this._initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
log.info('Initializing SDK...');
|
||||
|
||||
// Verify we're running inside Antigravity
|
||||
const isAntigravity = await this._detectAntigravity();
|
||||
if (!isAntigravity) {
|
||||
throw new AntigravityNotFoundError();
|
||||
}
|
||||
|
||||
// Initialize state bridge (opens state.vscdb via sql.js)
|
||||
await this.state.initialize();
|
||||
|
||||
// Initialize cascade manager (loads session list)
|
||||
await this.cascade.initialize();
|
||||
|
||||
// Initialize LS bridge (discovers Language Server port + CSRF token)
|
||||
const lsOk = await this.ls.initialize();
|
||||
if (lsOk) {
|
||||
log.info(`LS bridge ready on port ${this.ls.port} (csrf: ${this.ls.hasCsrfToken ? 'ok' : 'missing'})`);
|
||||
} else {
|
||||
log.warn('LS bridge not available — use sdk.ls.setConnection(port, csrfToken) or command fallback');
|
||||
}
|
||||
|
||||
// Refresh integration heartbeat (so renderer script knows extension is active)
|
||||
this.integration.signalActive();
|
||||
|
||||
this._initialized = true;
|
||||
log.info('SDK initialized successfully');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the SDK has been initialized.
|
||||
*/
|
||||
get isInitialized(): boolean {
|
||||
return this._initialized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the SDK version.
|
||||
*/
|
||||
get version(): string {
|
||||
try {
|
||||
return require('../package.json').version;
|
||||
} catch {
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect if we're running inside Antigravity IDE.
|
||||
*/
|
||||
private async _detectAntigravity(): Promise<boolean> {
|
||||
try {
|
||||
// Check for Antigravity-specific commands (VERIFIED naming)
|
||||
const commands = await this.commands.getAntigravityCommands();
|
||||
const hasAgentPanel = commands.includes('antigravity.agentPanel.open');
|
||||
|
||||
if (hasAgentPanel) {
|
||||
log.debug(`Detected Antigravity (${commands.length} commands)`);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Fallback: check env
|
||||
const appName = vscode.env.appName;
|
||||
if (appName?.toLowerCase().includes('antigravity')) {
|
||||
log.debug(`Detected Antigravity via appName: ${appName}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose of the SDK and all its resources.
|
||||
*/
|
||||
dispose(): void {
|
||||
log.info('Disposing SDK');
|
||||
this._disposables.dispose();
|
||||
}
|
||||
}
|
||||
342
antigravity-sdk-main/src/transport/command-bridge.ts
Normal file
342
antigravity-sdk-main/src/transport/command-bridge.ts
Normal file
@@ -0,0 +1,342 @@
|
||||
/**
|
||||
* Command Bridge — executes Antigravity internal commands via VS Code API.
|
||||
*
|
||||
* All commands go through `vscode.commands.executeCommand()` which is the
|
||||
* safe, official way to interact with Antigravity from extensions.
|
||||
*
|
||||
* VERIFIED: All commands listed below were confirmed to exist in
|
||||
* Antigravity v1.107.0 workbench.desktop.main.js and extension.js
|
||||
* on 2026-02-28.
|
||||
*
|
||||
* @module transport/command-bridge
|
||||
*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { IDisposable } from '../core/disposable';
|
||||
import { CommandExecutionError } from '../core/errors';
|
||||
import { Logger } from '../core/logger';
|
||||
|
||||
const log = new Logger('CommandBridge');
|
||||
|
||||
/**
|
||||
* All known Antigravity commands, organized by category.
|
||||
*
|
||||
* Sources: workbench.desktop.main.js (160+ commands) + extension.js (45 commands)
|
||||
*/
|
||||
export const AntigravityCommands = {
|
||||
|
||||
// ─── Agent Panel & UI (VERIFIED: .open/.focus suffix required) ────────
|
||||
|
||||
/** Open the Cascade agent panel */
|
||||
OPEN_AGENT_PANEL: 'antigravity.agentPanel.open',
|
||||
/** Focus the Cascade agent panel */
|
||||
FOCUS_AGENT_PANEL: 'antigravity.agentPanel.focus',
|
||||
/** Open the agent side panel */
|
||||
OPEN_AGENT_SIDE_PANEL: 'antigravity.agentSidePanel.open',
|
||||
/** Focus the agent side panel */
|
||||
FOCUS_AGENT_SIDE_PANEL: 'antigravity.agentSidePanel.focus',
|
||||
/** Toggle side panel visibility */
|
||||
TOGGLE_SIDE_PANEL: 'antigravity.agentSidePanel.toggleVisibility',
|
||||
/** Open agent (generic) */
|
||||
OPEN_AGENT: 'antigravity.openAgent',
|
||||
/** Toggle chat focus */
|
||||
TOGGLE_CHAT_FOCUS: 'antigravity.toggleChatFocus',
|
||||
/** Switch between workspace editor and agent view */
|
||||
SWITCH_WORKSPACE_AGENT: 'antigravity.switchBetweenWorkspaceAndAgent',
|
||||
|
||||
// ─── Conversation Management (Critical for SDK) ──────────────────────
|
||||
|
||||
/** Start a new conversation */
|
||||
START_NEW_CONVERSATION: 'antigravity.startNewConversation',
|
||||
/** Send a prompt to the agent panel */
|
||||
SEND_PROMPT_TO_AGENT: 'antigravity.sendPromptToAgentPanel',
|
||||
/** Send text to chat */
|
||||
SEND_TEXT_TO_CHAT: 'antigravity.sendTextToChat',
|
||||
/** Send a chat action message */
|
||||
SEND_CHAT_ACTION: 'antigravity.sendChatActionMessage',
|
||||
/** Set which conversation is visible */
|
||||
SET_VISIBLE_CONVERSATION: 'antigravity.setVisibleConversation',
|
||||
/** Execute a cascade action */
|
||||
EXECUTE_CASCADE_ACTION: 'antigravity.executeCascadeAction',
|
||||
/** Broadcast conversation deletion to all windows */
|
||||
BROADCAST_CONVERSATION_DELETION: 'antigravity.broadcastConversationDeletion',
|
||||
/** Track that a background conversation was created */
|
||||
TRACK_BACKGROUND_CONVERSATION: 'antigravity.trackBackgroundConversationCreated',
|
||||
|
||||
// ─── Agent Step Control (VERIFIED) ────────────────────────────────────
|
||||
|
||||
/** Accept the current agent step */
|
||||
ACCEPT_AGENT_STEP: 'antigravity.agent.acceptAgentStep',
|
||||
/** Reject the current agent step */
|
||||
REJECT_AGENT_STEP: 'antigravity.agent.rejectAgentStep',
|
||||
/** Accept a pending command */
|
||||
COMMAND_ACCEPT: 'antigravity.command.accept',
|
||||
/** Reject a pending command */
|
||||
COMMAND_REJECT: 'antigravity.command.reject',
|
||||
/** Accept a terminal command */
|
||||
TERMINAL_ACCEPT: 'antigravity.terminalCommand.accept',
|
||||
/** Reject a terminal command */
|
||||
TERMINAL_REJECT: 'antigravity.terminalCommand.reject',
|
||||
/** Run a terminal command */
|
||||
TERMINAL_RUN: 'antigravity.terminalCommand.run',
|
||||
/** Open new conversation (prioritized) */
|
||||
OPEN_NEW_CONVERSATION: 'antigravity.prioritized.chat.openNewConversation',
|
||||
|
||||
// ─── Terminal Integration ─────────────────────────────────────────────
|
||||
|
||||
/** Notify terminal command started */
|
||||
TERMINAL_COMMAND_START: 'antigravity.onManagerTerminalCommandStart',
|
||||
/** Notify terminal command data */
|
||||
TERMINAL_COMMAND_DATA: 'antigravity.onManagerTerminalCommandData',
|
||||
/** Notify terminal command finished */
|
||||
TERMINAL_COMMAND_FINISH: 'antigravity.onManagerTerminalCommandFinish',
|
||||
/** Update last terminal command */
|
||||
UPDATE_TERMINAL_LAST_COMMAND: 'antigravity.updateTerminalLastCommand',
|
||||
/** Notify shell command completion */
|
||||
ON_SHELL_COMPLETION: 'antigravity.onShellCommandCompletion',
|
||||
/** Show managed terminal */
|
||||
SHOW_MANAGED_TERMINAL: 'antigravity.showManagedTerminal',
|
||||
/** Send terminal output to chat */
|
||||
SEND_TERMINAL_TO_CHAT: 'antigravity.sendTerminalToChat',
|
||||
/** Send terminal output to side panel */
|
||||
SEND_TERMINAL_TO_SIDE_PANEL: 'antigravity.sendTerminalToSidePanel',
|
||||
|
||||
// ─── Agent & Mode ─────────────────────────────────────────────────────
|
||||
|
||||
/** Initialize the agent */
|
||||
INITIALIZE_AGENT: 'antigravity.initializeAgent',
|
||||
|
||||
// ─── Conversation Picker & Workspace ──────────────────────────────────
|
||||
|
||||
/** Open conversation workspace picker */
|
||||
OPEN_CONVERSATION_PICKER: 'antigravity.openConversationWorkspaceQuickPick',
|
||||
/** Open conversation picker (alternative) */
|
||||
OPEN_CONV_PICKER_ALT: 'antigravity.openConversationPicker',
|
||||
/** Set working directories */
|
||||
SET_WORKING_DIRS: 'antigravity.setWorkingDirectories',
|
||||
|
||||
// ─── Review & Diff ────────────────────────────────────────────────────
|
||||
|
||||
/** Open review changes view */
|
||||
OPEN_REVIEW_CHANGES: 'antigravity.openReviewChanges',
|
||||
/** Open diff view */
|
||||
OPEN_DIFF_VIEW: 'antigravity.openDiffView',
|
||||
/** Open diff zones */
|
||||
OPEN_DIFF_ZONES: 'antigravity.openDiffZones',
|
||||
/** Close all diff zones */
|
||||
CLOSE_ALL_DIFF_ZONES: 'antigravity.closeAllDiffZones',
|
||||
|
||||
// ─── Rules & Workflows ────────────────────────────────────────────────
|
||||
|
||||
/** Create a new rule */
|
||||
CREATE_RULE: 'antigravity.createRule',
|
||||
/** Create a new workflow */
|
||||
CREATE_WORKFLOW: 'antigravity.createWorkflow',
|
||||
/** Create a global workflow */
|
||||
CREATE_GLOBAL_WORKFLOW: 'antigravity.createGlobalWorkflow',
|
||||
/** Open global rules */
|
||||
OPEN_GLOBAL_RULES: 'antigravity.openGlobalRules',
|
||||
/** Open workspace rules */
|
||||
OPEN_WORKSPACE_RULES: 'antigravity.openWorkspaceRules',
|
||||
|
||||
// ─── Plugins & MCP ────────────────────────────────────────────────────
|
||||
|
||||
/** Open configure plugins page */
|
||||
OPEN_CONFIGURE_PLUGINS: 'antigravity.openConfigurePluginsPage',
|
||||
/** Get Cascade plugin template */
|
||||
GET_PLUGIN_TEMPLATE: 'antigravity.getCascadePluginTemplate',
|
||||
/** Poll MCP server states */
|
||||
POLL_MCP_SERVERS: 'antigravity.pollMcpServerStates',
|
||||
/** Open MCP config file */
|
||||
OPEN_MCP_CONFIG: 'antigravity.openMcpConfigFile',
|
||||
/** Open MCP docs page */
|
||||
OPEN_MCP_DOCS: 'antigravity.openMcpDocsPage',
|
||||
/** Update plugin installation count */
|
||||
UPDATE_PLUGIN_COUNT: 'antigravity.updatePluginInstallationCount',
|
||||
|
||||
// ─── Autocomplete ─────────────────────────────────────────────────────
|
||||
|
||||
/** Enable autocomplete */
|
||||
ENABLE_AUTOCOMPLETE: 'antigravity.enableAutocomplete',
|
||||
/** Disable autocomplete */
|
||||
DISABLE_AUTOCOMPLETE: 'antigravity.disableAutocomplete',
|
||||
/** Accept completion */
|
||||
ACCEPT_COMPLETION: 'antigravity.acceptCompletion',
|
||||
/** Force supercomplete */
|
||||
FORCE_SUPERCOMPLETE: 'antigravity.forceSupercomplete',
|
||||
/** Snooze autocomplete temporarily */
|
||||
SNOOZE_AUTOCOMPLETE: 'antigravity.snoozeAutocomplete',
|
||||
/** Cancel snooze */
|
||||
CANCEL_SNOOZE: 'antigravity.cancelSnoozeAutocomplete',
|
||||
|
||||
// ─── Auth & Account ───────────────────────────────────────────────────
|
||||
|
||||
/** Login to Antigravity */
|
||||
LOGIN: 'antigravity.login',
|
||||
/** Cancel login */
|
||||
CANCEL_LOGIN: 'antigravity.cancelLogin',
|
||||
/** Handle auth refresh */
|
||||
HANDLE_AUTH_REFRESH: 'antigravity.handleAuthRefresh',
|
||||
/** Sign in to Antigravity */
|
||||
SIGN_IN: 'antigravity.SignInToAntigravity',
|
||||
|
||||
// ─── Diagnostics & Debug ──────────────────────────────────────────────
|
||||
|
||||
/** Get diagnostics info */
|
||||
GET_DIAGNOSTICS: 'antigravity.getDiagnostics',
|
||||
/** Download diagnostics bundle */
|
||||
DOWNLOAD_DIAGNOSTICS: 'antigravity.downloadDiagnostics',
|
||||
/** Capture traces */
|
||||
CAPTURE_TRACES: 'antigravity.captureTraces',
|
||||
/** Enable tracing */
|
||||
ENABLE_TRACING: 'antigravity.enableTracing',
|
||||
/** Clear and disable tracing */
|
||||
CLEAR_TRACING: 'antigravity.clearAndDisableTracing',
|
||||
/** Get manager trace */
|
||||
GET_MANAGER_TRACE: 'antigravity.getManagerTrace',
|
||||
/** Get workbench trace */
|
||||
GET_WORKBENCH_TRACE: 'antigravity.getWorkbenchTrace',
|
||||
/** Toggle debug info widget */
|
||||
TOGGLE_DEBUG_INFO: 'antigravity.toggleDebugInfoWidget',
|
||||
/** Open troubleshooting */
|
||||
OPEN_TROUBLESHOOTING: 'antigravity.openTroubleshooting',
|
||||
/** Open issue reporter */
|
||||
OPEN_ISSUE_REPORTER: 'antigravity.openIssueReporter',
|
||||
|
||||
// ─── Language Server ──────────────────────────────────────────────────
|
||||
|
||||
/** Restart the language server */
|
||||
RESTART_LANGUAGE_SERVER: 'antigravity.restartLanguageServer',
|
||||
/** Kill language server and reload window */
|
||||
KILL_LS_AND_RELOAD: 'antigravity.killLanguageServerAndReloadWindow',
|
||||
|
||||
// ─── Git & Commit ─────────────────────────────────────────────────────
|
||||
|
||||
/** Generate commit message via AI */
|
||||
GENERATE_COMMIT_MESSAGE: 'antigravity.generateCommitMessage',
|
||||
/** Cancel commit message generation */
|
||||
CANCEL_COMMIT_MESSAGE: 'antigravity.cancelGenerateCommitMessage',
|
||||
|
||||
// ─── Browser ──────────────────────────────────────────────────────────
|
||||
|
||||
/** Open browser */
|
||||
OPEN_BROWSER: 'antigravity.openBrowser',
|
||||
/** Get browser onboarding port (returns number, e.g. 57401) */
|
||||
GET_BROWSER_PORT: 'antigravity.getBrowserOnboardingPort',
|
||||
|
||||
// ─── Settings & Import ────────────────────────────────────────────────
|
||||
|
||||
/** Open quick settings panel */
|
||||
OPEN_QUICK_SETTINGS: 'antigravity.openQuickSettingsPanel',
|
||||
/** Open customizations tab */
|
||||
OPEN_CUSTOMIZATIONS: 'antigravity.openCustomizationsTab',
|
||||
/** Import VS Code settings */
|
||||
IMPORT_VSCODE_SETTINGS: 'antigravity.importVSCodeSettings',
|
||||
/** Import VS Code extensions */
|
||||
IMPORT_VSCODE_EXTENSIONS: 'antigravity.importVSCodeExtensions',
|
||||
/** Import Cursor settings */
|
||||
IMPORT_CURSOR_SETTINGS: 'antigravity.importCursorSettings',
|
||||
/** Import Cursor extensions */
|
||||
IMPORT_CURSOR_EXTENSIONS: 'antigravity.importCursorExtensions',
|
||||
|
||||
// ─── Misc ─────────────────────────────────────────────────────────────
|
||||
|
||||
/** Reload window */
|
||||
RELOAD_WINDOW: 'antigravity.reloadWindow',
|
||||
/** Open documentation */
|
||||
OPEN_DOCS: 'antigravity.openDocs',
|
||||
/** Open changelog */
|
||||
OPEN_CHANGELOG: 'antigravity.openChangeLog',
|
||||
/** Explain and fix problem (from diagnostics) */
|
||||
EXPLAIN_AND_FIX: 'antigravity.explainAndFixProblem',
|
||||
/** Open a URL */
|
||||
OPEN_URL: 'antigravity.openGenericUrl',
|
||||
/** Editor mode settings */
|
||||
EDITOR_MODE_SETTINGS: 'antigravity.editorModeSettings',
|
||||
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Bridges between the SDK and Antigravity's command system.
|
||||
*
|
||||
* All interactions with Antigravity go through registered VS Code commands,
|
||||
* ensuring we never bypass the official extension API.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const bridge = new CommandBridge();
|
||||
*
|
||||
* // Open the agent panel
|
||||
* await bridge.execute(AntigravityCommands.OPEN_AGENT_PANEL);
|
||||
*
|
||||
* // Start a new conversation
|
||||
* await bridge.execute(AntigravityCommands.START_NEW_CONVERSATION);
|
||||
*
|
||||
* // Send a prompt
|
||||
* await bridge.execute(AntigravityCommands.SEND_PROMPT_TO_AGENT, 'Hello!');
|
||||
* ```
|
||||
*/
|
||||
export class CommandBridge implements IDisposable {
|
||||
private _disposed = false;
|
||||
|
||||
/**
|
||||
* Execute an Antigravity command.
|
||||
*
|
||||
* @param command - The command ID to execute
|
||||
* @param args - Arguments to pass to the command
|
||||
* @returns The command's return value
|
||||
* @throws {CommandExecutionError} If the command fails
|
||||
*/
|
||||
async execute<T = unknown>(command: string, ...args: unknown[]): Promise<T> {
|
||||
if (this._disposed) {
|
||||
throw new CommandExecutionError(command, 'CommandBridge has been disposed');
|
||||
}
|
||||
|
||||
log.debug(`Executing: ${command}`, args.length > 0 ? args : '');
|
||||
|
||||
try {
|
||||
const result = await vscode.commands.executeCommand<T>(command, ...args);
|
||||
return result;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
log.error(`Command failed: ${command}`, message);
|
||||
throw new CommandExecutionError(command, message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a command is registered and available.
|
||||
*
|
||||
* @param command - Command ID to check
|
||||
* @returns true if the command exists
|
||||
*/
|
||||
async isAvailable(command: string): Promise<boolean> {
|
||||
const commands = await vscode.commands.getCommands(true);
|
||||
return commands.includes(command);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered Antigravity commands.
|
||||
*
|
||||
* @returns List of command IDs starting with 'antigravity.'
|
||||
*/
|
||||
async getAntigravityCommands(): Promise<string[]> {
|
||||
const commands = await vscode.commands.getCommands(true);
|
||||
return commands.filter((cmd) => cmd.startsWith('antigravity.'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a command handler.
|
||||
*
|
||||
* @param command - Command ID to register
|
||||
* @param handler - Function to handle the command
|
||||
* @returns Disposable to unregister the command
|
||||
*/
|
||||
register(command: string, handler: (...args: unknown[]) => unknown): IDisposable {
|
||||
return vscode.commands.registerCommand(command, handler);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._disposed = true;
|
||||
}
|
||||
}
|
||||
307
antigravity-sdk-main/src/transport/event-monitor.ts
Normal file
307
antigravity-sdk-main/src/transport/event-monitor.ts
Normal file
@@ -0,0 +1,307 @@
|
||||
/**
|
||||
* Event Monitor — polls state.vscdb and getDiagnostics for changes.
|
||||
*
|
||||
* Detects:
|
||||
* - USS key changes (trajectory summaries, preferences, etc.)
|
||||
* - Step count changes per session (via getDiagnostics.recentTrajectories)
|
||||
* - Active session switches
|
||||
* - New conversations
|
||||
*
|
||||
* @module transport/event-monitor
|
||||
*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { IDisposable, DisposableStore } from '../core/disposable';
|
||||
import { EventEmitter, Event } from '../core/events';
|
||||
import { Logger } from '../core/logger';
|
||||
import { StateBridge, USSKeys } from './state-bridge';
|
||||
|
||||
const log = new Logger('EventMonitor');
|
||||
|
||||
/**
|
||||
* USS key change event.
|
||||
*/
|
||||
export interface IStateChange {
|
||||
/** Which USS key changed */
|
||||
readonly key: string;
|
||||
/** New data size */
|
||||
readonly newSize: number;
|
||||
/** Previous data size */
|
||||
readonly previousSize: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Step count change event — fired when the agent adds/processes steps.
|
||||
*/
|
||||
export interface IStepCountChange {
|
||||
/** Conversation UUID (googleAgentId) */
|
||||
readonly sessionId: string;
|
||||
/** Conversation title */
|
||||
readonly title: string;
|
||||
/** Previous step count */
|
||||
readonly previousCount: number;
|
||||
/** New step count */
|
||||
readonly newCount: number;
|
||||
/** Number of new steps added */
|
||||
readonly delta: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Active session change event.
|
||||
*/
|
||||
export interface IActiveSessionChange {
|
||||
/** New active session ID */
|
||||
readonly sessionId: string;
|
||||
/** New active session title */
|
||||
readonly title: string;
|
||||
/** Previous active session ID (empty if first detection) */
|
||||
readonly previousSessionId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Snapshot of a trajectory from getDiagnostics.
|
||||
*/
|
||||
interface ITrajectorySnapshot {
|
||||
id: string;
|
||||
title: string;
|
||||
stepCount: number;
|
||||
lastModified: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Monitors Antigravity state for changes.
|
||||
*
|
||||
* Two polling modes:
|
||||
* 1. **USS polling** — watches state.vscdb keys for size changes (lightweight)
|
||||
* 2. **Trajectory polling** — watches getDiagnostics for step count changes (heavier, optional)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const monitor = new EventMonitor(stateBridge);
|
||||
*
|
||||
* // React to step changes (agent is working)
|
||||
* monitor.onStepCountChanged((e) => {
|
||||
* console.log(`${e.title}: +${e.delta} steps (now ${e.newCount})`);
|
||||
* });
|
||||
*
|
||||
* // React to conversation switches
|
||||
* monitor.onActiveSessionChanged((e) => {
|
||||
* console.log(`Switched to: ${e.title}`);
|
||||
* });
|
||||
*
|
||||
* monitor.start(3000);
|
||||
* ```
|
||||
*/
|
||||
export class EventMonitor implements IDisposable {
|
||||
private readonly _disposables = new DisposableStore();
|
||||
private _ussTimer: ReturnType<typeof setInterval> | null = null;
|
||||
private _trajTimer: ReturnType<typeof setInterval> | null = null;
|
||||
private _ussSnapshots = new Map<string, number>();
|
||||
private _trajSnapshots = new Map<string, ITrajectorySnapshot>();
|
||||
private _activeSessionId = '';
|
||||
private _running = false;
|
||||
|
||||
// ─── USS Events ─────────────────────────────────────────────────────
|
||||
|
||||
private readonly _onStateChanged = this._disposables.add(new EventEmitter<IStateChange>());
|
||||
/** Fires when any monitored USS key changes size */
|
||||
public readonly onStateChanged: Event<IStateChange> = this._onStateChanged.event;
|
||||
|
||||
private readonly _onNewConversation = this._disposables.add(new EventEmitter<void>());
|
||||
/** Fires when trajectory summaries grow (new conversation likely) */
|
||||
public readonly onNewConversation: Event<void> = this._onNewConversation.event;
|
||||
|
||||
// ─── Trajectory Events ──────────────────────────────────────────────
|
||||
|
||||
private readonly _onStepCountChanged = this._disposables.add(new EventEmitter<IStepCountChange>());
|
||||
/** Fires when a session's step count changes (agent made progress) */
|
||||
public readonly onStepCountChanged: Event<IStepCountChange> = this._onStepCountChanged.event;
|
||||
|
||||
private readonly _onActiveSessionChanged = this._disposables.add(new EventEmitter<IActiveSessionChange>());
|
||||
/** Fires when the active (most recent) session changes */
|
||||
public readonly onActiveSessionChanged: Event<IActiveSessionChange> = this._onActiveSessionChanged.event;
|
||||
|
||||
/** Keys we monitor for USS changes */
|
||||
private readonly _watchedKeys = [
|
||||
USSKeys.TRAJECTORY_SUMMARIES,
|
||||
USSKeys.AGENT_PREFERENCES,
|
||||
USSKeys.USER_STATUS,
|
||||
];
|
||||
|
||||
constructor(private readonly _state: StateBridge) { }
|
||||
|
||||
/**
|
||||
* Start polling for state changes.
|
||||
*
|
||||
* @param intervalMs - USS polling interval (default: 3000ms)
|
||||
* @param trajectoryIntervalMs - Trajectory polling interval (default: 5000ms).
|
||||
* Set to 0 to disable trajectory polling (saves CPU).
|
||||
*/
|
||||
start(intervalMs: number = 3000, trajectoryIntervalMs: number = 5000): void {
|
||||
if (this._running) return;
|
||||
|
||||
this._running = true;
|
||||
log.info(`Starting event monitor (USS: ${intervalMs}ms, Traj: ${trajectoryIntervalMs}ms)`);
|
||||
|
||||
// Initial USS snapshot
|
||||
this._takeUSSSnapshot().catch(() => { });
|
||||
|
||||
// USS polling
|
||||
this._ussTimer = setInterval(async () => {
|
||||
try {
|
||||
await this._pollUSS();
|
||||
} catch (error) {
|
||||
log.error('USS poll error', error);
|
||||
}
|
||||
}, intervalMs);
|
||||
|
||||
// Trajectory polling (optional, heavier)
|
||||
if (trajectoryIntervalMs > 0) {
|
||||
this._pollTrajectories().catch(() => { });
|
||||
|
||||
this._trajTimer = setInterval(async () => {
|
||||
try {
|
||||
await this._pollTrajectories();
|
||||
} catch (error) {
|
||||
log.error('Trajectory poll error', error);
|
||||
}
|
||||
}, trajectoryIntervalMs);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop polling.
|
||||
*/
|
||||
stop(): void {
|
||||
if (this._ussTimer) {
|
||||
clearInterval(this._ussTimer);
|
||||
this._ussTimer = null;
|
||||
}
|
||||
if (this._trajTimer) {
|
||||
clearInterval(this._trajTimer);
|
||||
this._trajTimer = null;
|
||||
}
|
||||
this._running = false;
|
||||
log.info('Event monitor stopped');
|
||||
}
|
||||
|
||||
/** Check if the monitor is currently running. */
|
||||
get isRunning(): boolean {
|
||||
return this._running;
|
||||
}
|
||||
|
||||
/** Get the currently active session ID. */
|
||||
get activeSessionId(): string {
|
||||
return this._activeSessionId;
|
||||
}
|
||||
|
||||
// ─── USS Polling ────────────────────────────────────────────────────
|
||||
|
||||
private async _takeUSSSnapshot(): Promise<void> {
|
||||
for (const key of this._watchedKeys) {
|
||||
try {
|
||||
const value = await this._state.getRawValue(key);
|
||||
this._ussSnapshots.set(key, value ? value.length : 0);
|
||||
} catch {
|
||||
this._ussSnapshots.set(key, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async _pollUSS(): Promise<void> {
|
||||
for (const key of this._watchedKeys) {
|
||||
try {
|
||||
const value = await this._state.getRawValue(key);
|
||||
const newSize = value ? value.length : 0;
|
||||
const previousSize = this._ussSnapshots.get(key) ?? 0;
|
||||
|
||||
if (newSize !== previousSize) {
|
||||
log.debug(`USS change: ${key} (${previousSize} -> ${newSize})`);
|
||||
this._ussSnapshots.set(key, newSize);
|
||||
this._onStateChanged.fire({ key, newSize, previousSize });
|
||||
|
||||
if (key === USSKeys.TRAJECTORY_SUMMARIES && newSize > previousSize) {
|
||||
this._onNewConversation.fire();
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Skip errors during polling
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Trajectory Polling ─────────────────────────────────────────────
|
||||
|
||||
private async _pollTrajectories(): Promise<void> {
|
||||
let trajectories: Array<{
|
||||
googleAgentId: string;
|
||||
trajectoryId: string;
|
||||
summary: string;
|
||||
lastStepIndex: number;
|
||||
lastModifiedTime: string;
|
||||
}>;
|
||||
|
||||
try {
|
||||
const raw = await vscode.commands.executeCommand<string>('antigravity.getDiagnostics');
|
||||
if (!raw || typeof raw !== 'string') return;
|
||||
const diag = JSON.parse(raw);
|
||||
if (!Array.isArray(diag.recentTrajectories)) return;
|
||||
trajectories = diag.recentTrajectories;
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for step count changes in each trajectory
|
||||
for (const traj of trajectories) {
|
||||
const id = traj.googleAgentId;
|
||||
if (!id) continue;
|
||||
|
||||
const prev = this._trajSnapshots.get(id);
|
||||
const newCount = traj.lastStepIndex ?? 0;
|
||||
|
||||
if (prev && prev.stepCount !== newCount) {
|
||||
const delta = newCount - prev.stepCount;
|
||||
log.debug(`Step change: "${traj.summary}" ${prev.stepCount} -> ${newCount} (+${delta})`);
|
||||
|
||||
this._onStepCountChanged.fire({
|
||||
sessionId: id,
|
||||
title: traj.summary ?? 'Untitled',
|
||||
previousCount: prev.stepCount,
|
||||
newCount,
|
||||
delta,
|
||||
});
|
||||
}
|
||||
|
||||
this._trajSnapshots.set(id, {
|
||||
id,
|
||||
title: traj.summary ?? 'Untitled',
|
||||
stepCount: newCount,
|
||||
lastModified: traj.lastModifiedTime ?? '',
|
||||
});
|
||||
}
|
||||
|
||||
// Check for active session change (first entry = most recent)
|
||||
if (trajectories.length > 0) {
|
||||
const newActiveId = trajectories[0].googleAgentId;
|
||||
if (newActiveId && newActiveId !== this._activeSessionId) {
|
||||
const previousId = this._activeSessionId;
|
||||
this._activeSessionId = newActiveId;
|
||||
|
||||
// Only fire event after initial snapshot (not on first detection)
|
||||
if (previousId !== '') {
|
||||
log.debug(`Active session changed: "${trajectories[0].summary}"`);
|
||||
this._onActiveSessionChanged.fire({
|
||||
sessionId: newActiveId,
|
||||
title: trajectories[0].summary ?? 'Untitled',
|
||||
previousSessionId: previousId,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.stop();
|
||||
this._disposables.dispose();
|
||||
}
|
||||
}
|
||||
9
antigravity-sdk-main/src/transport/index.ts
Normal file
9
antigravity-sdk-main/src/transport/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Transport module re-exports.
|
||||
* @module transport
|
||||
*/
|
||||
|
||||
export { CommandBridge, AntigravityCommands } from './command-bridge';
|
||||
export { StateBridge, USSKeys } from './state-bridge';
|
||||
export { EventMonitor, type IStateChange } from './event-monitor';
|
||||
export { LSBridge, Models, type ModelId, type IHeadlessCascadeOptions, type ISendMessageOptions } from './ls-bridge';
|
||||
725
antigravity-sdk-main/src/transport/ls-bridge.ts
Normal file
725
antigravity-sdk-main/src/transport/ls-bridge.ts
Normal file
@@ -0,0 +1,725 @@
|
||||
/**
|
||||
* Language Server Bridge — Direct ConnectRPC calls to the local LS.
|
||||
*
|
||||
* UPDATED 2026-03-01 (v1.3.0):
|
||||
* Fixed CSRF token authentication (Issue #1).
|
||||
* The LS binary is launched with --csrf_token as a CLI argument.
|
||||
* Previous versions did not send this token, causing 401 "missing CSRF token".
|
||||
*
|
||||
* Discovery strategy (multi-layer):
|
||||
* 1. Process CLI args — extract --port and --csrf_token from LS process
|
||||
* 2. getDiagnostics console logs — fallback for port discovery
|
||||
* 3. Manual override — setConnection(port, csrfToken)
|
||||
*
|
||||
* Service: exa.language_server_pb.LanguageServerService
|
||||
* Protocol: HTTPS POST with JSON body + x-csrf-token header
|
||||
*
|
||||
* @module transport/ls-bridge
|
||||
*/
|
||||
|
||||
import { Logger } from '../core/logger';
|
||||
|
||||
const log = new Logger('LSBridge');
|
||||
|
||||
/** Known model IDs (verified 2026-02-28) */
|
||||
export const Models = {
|
||||
GEMINI_FLASH: 1018,
|
||||
GEMINI_PRO_LOW: 1164,
|
||||
GEMINI_PRO_HIGH: 1165,
|
||||
CLAUDE_SONNET: 1163,
|
||||
CLAUDE_OPUS: 1154,
|
||||
GPT_OSS: 342,
|
||||
} as const;
|
||||
|
||||
export type ModelId = typeof Models[keyof typeof Models] | number;
|
||||
|
||||
/** Options for creating a headless cascade */
|
||||
export interface IHeadlessCascadeOptions {
|
||||
/** Text prompt to send */
|
||||
text: string;
|
||||
/** Model ID (default: Gemini 3 Flash = 1018) */
|
||||
model?: ModelId;
|
||||
/** Planner type: 'conversational' (default) or 'normal' */
|
||||
plannerType?: 'conversational' | 'normal';
|
||||
}
|
||||
|
||||
/** Options for sending a message to existing cascade */
|
||||
export interface ISendMessageOptions {
|
||||
/** Target cascade ID */
|
||||
cascadeId: string;
|
||||
/** Text to send */
|
||||
text: string;
|
||||
/** Model ID (default: Gemini 3 Flash = 1018) */
|
||||
model?: ModelId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Conversation annotation fields (from jetski_cortex.proto ConversationAnnotations).
|
||||
*
|
||||
* These are metadata annotations on a conversation that the user can set.
|
||||
* The LS stores these natively and they persist across sessions.
|
||||
*/
|
||||
export interface IConversationAnnotations {
|
||||
/** Custom user title -- overrides the auto-generated summary */
|
||||
title?: string;
|
||||
/** Tags/labels for organization */
|
||||
tags?: string[];
|
||||
/** Whether this conversation is archived */
|
||||
archived?: boolean;
|
||||
/** Whether this conversation is starred (pinned) */
|
||||
starred?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Direct bridge to the Language Server via ConnectRPC.
|
||||
*
|
||||
* Discovers the LS port and CSRF token from the LS process CLI args,
|
||||
* then makes authenticated HTTPS POST calls to the LS endpoints.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const ls = new LSBridge(commandBridge);
|
||||
* await ls.initialize();
|
||||
*
|
||||
* // Create a headless cascade
|
||||
* const cascadeId = await ls.createCascade({
|
||||
* text: 'Analyze test coverage',
|
||||
* model: Models.GEMINI_FLASH,
|
||||
* });
|
||||
*
|
||||
* // Send follow-up
|
||||
* await ls.sendMessage({ cascadeId, text: 'Focus on edge cases' });
|
||||
*
|
||||
* // Switch UI to it
|
||||
* await ls.focusCascade(cascadeId);
|
||||
* ```
|
||||
*/
|
||||
export class LSBridge {
|
||||
private _port: number | null = null;
|
||||
private _csrfToken: string | null = null;
|
||||
private _useTls: boolean = false;
|
||||
private _executeCommand: <T = any>(command: string, ...args: any[]) => Promise<T>;
|
||||
|
||||
constructor(executeCommand: <T = any>(command: string, ...args: any[]) => Promise<T>) {
|
||||
this._executeCommand = executeCommand;
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover the Language Server port and CSRF token.
|
||||
* Must be called before other methods.
|
||||
*
|
||||
* Discovery chain:
|
||||
* 1. Parse LS process CLI arguments (--port, --csrf_token)
|
||||
* 2. Fallback: getDiagnostics console logs (port only)
|
||||
* 3. Manual: call setConnection() after initialize() returns false
|
||||
*/
|
||||
async initialize(): Promise<boolean> {
|
||||
// Strategy 1: discover from LS process CLI args (port + CSRF)
|
||||
const fromProcess = await this._discoverFromProcess();
|
||||
if (fromProcess) {
|
||||
this._port = fromProcess.port;
|
||||
this._csrfToken = fromProcess.csrfToken;
|
||||
this._useTls = fromProcess.useTls;
|
||||
log.info(`LS discovered from process: port=${this._port}, tls=${this._useTls}, csrf=${this._csrfToken ? 'found' : 'missing'}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Strategy 2: fallback to getDiagnostics logs (port only, no CSRF)
|
||||
this._port = await this._discoverPortFromDiagnostics();
|
||||
if (this._port) {
|
||||
log.warn(`LS port from diagnostics: ${this._port}, but CSRF token not found — RPC calls may fail with 401`);
|
||||
return true;
|
||||
}
|
||||
|
||||
log.warn('Could not discover LS connection. Use setConnection(port, csrfToken) manually.');
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Whether the bridge is ready (port discovered) */
|
||||
get isReady(): boolean {
|
||||
return this._port !== null;
|
||||
}
|
||||
|
||||
/** The discovered LS port */
|
||||
get port(): number | null {
|
||||
return this._port;
|
||||
}
|
||||
|
||||
/** Whether CSRF token is available */
|
||||
get hasCsrfToken(): boolean {
|
||||
return this._csrfToken !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually set the LS connection parameters.
|
||||
*
|
||||
* Use this when auto-discovery fails (e.g., non-standard install,
|
||||
* or you've discovered the port/token through other means like `lsof`).
|
||||
*
|
||||
* @param port - LS port number
|
||||
* @param csrfToken - CSRF token from LS process CLI args
|
||||
* @param useTls - Whether to use HTTPS (default: false, extension_server uses HTTP)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const ls = new LSBridge(commandBridge);
|
||||
* const ok = await ls.initialize();
|
||||
* if (!ok) {
|
||||
* // Manual fallback: get port and csrf from your own discovery
|
||||
* ls.setConnection(54321, 'abc123-csrf-token');
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
setConnection(port: number, csrfToken: string, useTls: boolean = false): void {
|
||||
this._port = port;
|
||||
this._csrfToken = csrfToken;
|
||||
this._useTls = useTls;
|
||||
log.info(`LS connection set manually: port=${port}, tls=${useTls}, csrf=${csrfToken ? 'provided' : 'empty'}`);
|
||||
}
|
||||
|
||||
// ─── Headless Cascade API ────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Create a new cascade and optionally send a message.
|
||||
* Fully headless — no UI panel opened, no conversation switched.
|
||||
*
|
||||
* @returns cascadeId or null on failure
|
||||
*/
|
||||
async createCascade(options: IHeadlessCascadeOptions): Promise<string | null> {
|
||||
this._ensureReady();
|
||||
|
||||
// Step 1: StartCascade
|
||||
const startResp = await this._rpc('StartCascade', { source: 0 });
|
||||
const cascadeId = startResp?.cascadeId;
|
||||
if (!cascadeId) {
|
||||
log.error('StartCascade returned no cascadeId');
|
||||
return null;
|
||||
}
|
||||
log.info(`Cascade created: ${cascadeId}`);
|
||||
|
||||
// Step 2: SendUserCascadeMessage
|
||||
if (options.text) {
|
||||
await this._sendMessage(cascadeId, options.text, options.model, options.plannerType);
|
||||
log.info(`Message sent to: ${cascadeId}`);
|
||||
}
|
||||
|
||||
return cascadeId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message to an existing cascade.
|
||||
*
|
||||
* @returns true if sent successfully
|
||||
*/
|
||||
async sendMessage(options: ISendMessageOptions): Promise<boolean> {
|
||||
this._ensureReady();
|
||||
await this._sendMessage(options.cascadeId, options.text, options.model);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch the UI to show a specific cascade conversation.
|
||||
*/
|
||||
async focusCascade(cascadeId: string): Promise<void> {
|
||||
this._ensureReady();
|
||||
await this._rpc('SmartFocusConversation', { cascadeId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel a running cascade invocation.
|
||||
*/
|
||||
async cancelCascade(cascadeId: string): Promise<void> {
|
||||
this._ensureReady();
|
||||
await this._rpc('CancelCascadeInvocation', { cascadeId });
|
||||
}
|
||||
|
||||
// ─── Conversation Annotations API ───────────────────────────────
|
||||
|
||||
/**
|
||||
* Native conversation annotations (verified from jetski_cortex.proto).
|
||||
*
|
||||
* ConversationAnnotations protobuf fields:
|
||||
* - title (string) — custom user title, overrides auto-summary
|
||||
* - tags (string[]) — tags/labels
|
||||
* - archived (bool) — archive status
|
||||
* - starred (bool) — pinned/starred
|
||||
* - last_user_view_time (Timestamp)
|
||||
*
|
||||
* @param cascadeId - Conversation ID
|
||||
* @param annotations - Partial annotation fields to set
|
||||
* @param merge - If true, merge with existing annotations (default: true)
|
||||
*/
|
||||
async updateAnnotations(
|
||||
cascadeId: string,
|
||||
annotations: IConversationAnnotations,
|
||||
merge: boolean = true,
|
||||
): Promise<void> {
|
||||
this._ensureReady();
|
||||
|
||||
// Convert camelCase to snake_case for protobuf
|
||||
const proto: Record<string, any> = {};
|
||||
if (annotations.title !== undefined) proto.title = annotations.title;
|
||||
if (annotations.starred !== undefined) proto.starred = annotations.starred;
|
||||
if (annotations.archived !== undefined) proto.archived = annotations.archived;
|
||||
if (annotations.tags !== undefined) proto.tags = annotations.tags;
|
||||
|
||||
await this._rpc('UpdateConversationAnnotations', {
|
||||
cascadeId,
|
||||
annotations: proto,
|
||||
mergeAnnotations: merge,
|
||||
});
|
||||
log.info(`Annotations updated for ${cascadeId.substring(0, 8)}...`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a custom title for a conversation.
|
||||
*
|
||||
* This sets the `title` field in ConversationAnnotations.
|
||||
* When set, this title should be displayed instead of the
|
||||
* auto-generated `summary` from the LLM.
|
||||
*
|
||||
* @param cascadeId - Conversation ID
|
||||
* @param title - Custom title to set
|
||||
*/
|
||||
async setTitle(cascadeId: string, title: string): Promise<void> {
|
||||
await this.updateAnnotations(cascadeId, { title });
|
||||
}
|
||||
|
||||
/**
|
||||
* Star (pin) or unstar a conversation.
|
||||
*
|
||||
* This sets the `starred` field in ConversationAnnotations.
|
||||
*
|
||||
* @param cascadeId - Conversation ID
|
||||
* @param starred - true to star, false to unstar
|
||||
*/
|
||||
async setStar(cascadeId: string, starred: boolean): Promise<void> {
|
||||
await this.updateAnnotations(cascadeId, { starred });
|
||||
}
|
||||
|
||||
// ─── Conversation Read API ──────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Get details of a specific conversation.
|
||||
*/
|
||||
async getConversation(cascadeId: string): Promise<any> {
|
||||
this._ensureReady();
|
||||
return this._rpc('GetConversation', { cascadeId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all cascade trajectories (conversation list).
|
||||
*/
|
||||
async listCascades(): Promise<any> {
|
||||
this._ensureReady();
|
||||
const resp = await this._rpc('GetAllCascadeTrajectories', {});
|
||||
return resp?.trajectorySummaries ?? {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get trajectory descriptions (lighter than full trajectories).
|
||||
* Returns { trajectories: [...] }.
|
||||
*/
|
||||
async getTrajectoryDescriptions(): Promise<any> {
|
||||
this._ensureReady();
|
||||
return this._rpc('GetUserTrajectoryDescriptions', {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user status (tier, models, etc.)
|
||||
*/
|
||||
async getUserStatus(): Promise<any> {
|
||||
this._ensureReady();
|
||||
return this._rpc('GetUserStatus', {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a raw RPC call to any LS method.
|
||||
* @param method - RPC method name (e.g. 'StartCascade')
|
||||
* @param payload - JSON payload
|
||||
*/
|
||||
async rawRPC(method: string, payload: any): Promise<any> {
|
||||
this._ensureReady();
|
||||
return this._rpc(method, payload);
|
||||
}
|
||||
|
||||
// ─── Internal ────────────────────────────────────────────────────
|
||||
|
||||
private _ensureReady(): void {
|
||||
if (!this._port) {
|
||||
throw new Error('LSBridge not initialized. Call initialize() first.');
|
||||
}
|
||||
}
|
||||
|
||||
private async _sendMessage(
|
||||
cascadeId: string,
|
||||
text: string,
|
||||
model?: ModelId,
|
||||
plannerType?: string,
|
||||
): Promise<void> {
|
||||
const payload: any = {
|
||||
cascadeId,
|
||||
items: [{ chunk: { case: 'text', value: text } }],
|
||||
cascadeConfig: {
|
||||
plannerConfig: {
|
||||
plannerTypeConfig: {
|
||||
case: plannerType || 'conversational',
|
||||
value: {},
|
||||
},
|
||||
requestedModel: {
|
||||
choice: { case: 'model', value: model || Models.GEMINI_FLASH },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await this._rpc('SendUserCascadeMessage', payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover LS port and CSRF token from the Language Server process.
|
||||
*
|
||||
* VERIFIED 2026-03-01 from Antigravity extension.js source:
|
||||
*
|
||||
* 1. CSRF header is "x-codeium-csrf-token" (NOT x-csrf-token)
|
||||
* 2. CSRF value is --csrf_token from CLI (NOT --extension_server_csrf_token)
|
||||
* 3. ConnectRPC endpoint is on httpsPort (HTTPS) or httpPort (HTTP)
|
||||
* These ports are NOT in CLI args (--random_port flag means random).
|
||||
* We discover them via netstat/PID, excluding extension_server_port.
|
||||
*
|
||||
* Source code proof:
|
||||
* n.header.set("x-codeium-csrf-token", e) // header name
|
||||
* address = `127.0.0.1:${te.httpsPort}` // ConnectRPC address
|
||||
* csrfToken = a = d.randomUUID() → --csrf_token // token source
|
||||
* t.headers["x-codeium-csrf-token"] === this.csrfToken ? ... : 403
|
||||
*
|
||||
* Discovery: 2 phases
|
||||
* Phase 1: Get-CimInstance/ps → PID, --csrf_token, --extension_server_port
|
||||
* Phase 2: netstat → find LISTENING ports for PID, exclude ext_server_port
|
||||
*/
|
||||
private async _discoverFromProcess(): Promise<{ port: number; csrfToken: string; useTls: boolean } | null> {
|
||||
try {
|
||||
const platform = process.platform;
|
||||
|
||||
// Phase 1: find LS process, extract PID, csrf_token, extension_server_port
|
||||
let processInfo = await this._findLSProcess(platform);
|
||||
if (!processInfo) {
|
||||
log.debug('No LS processes found');
|
||||
return null;
|
||||
}
|
||||
|
||||
log.debug(`LS process found: PID=${processInfo.pid}, csrf=present, ext_port=${processInfo.extPort}`);
|
||||
|
||||
// Phase 2: find actual ConnectRPC port via netstat
|
||||
const connectPort = await this._findConnectPort(platform, processInfo.pid, processInfo.extPort);
|
||||
if (!connectPort) {
|
||||
log.debug('Could not find ConnectRPC port via netstat, trying extension_server_port as fallback');
|
||||
// Fallback: try extension_server_port with HTTP
|
||||
if (processInfo.extPort) {
|
||||
return { port: processInfo.extPort, csrfToken: processInfo.csrfToken, useTls: false };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
port: connectPort.port,
|
||||
csrfToken: processInfo.csrfToken,
|
||||
useTls: connectPort.tls,
|
||||
};
|
||||
|
||||
} catch (err) {
|
||||
log.debug('Process discovery failed', err);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Phase 1: Find the LS process for this workspace.
|
||||
*/
|
||||
private async _findLSProcess(
|
||||
platform: string,
|
||||
): Promise<{ pid: number; csrfToken: string; extPort: number } | null> {
|
||||
const { exec } = require('child_process');
|
||||
const { promisify } = require('util');
|
||||
const execAsync = promisify(exec);
|
||||
let output: string;
|
||||
|
||||
if (platform === 'win32') {
|
||||
// Use -EncodedCommand to avoid all PowerShell escaping issues with $_ and quotes
|
||||
const psScript = "Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -match 'language_server' -and $_.CommandLine -match 'csrf_token' } | ForEach-Object { $_.ProcessId.ToString() + '|' + $_.CommandLine }";
|
||||
const encoded = Buffer.from(psScript, 'utf16le').toString('base64');
|
||||
const result = await execAsync(
|
||||
`powershell.exe -NoProfile -EncodedCommand ${encoded}`,
|
||||
{ encoding: 'utf8', timeout: 10000, windowsHide: true },
|
||||
);
|
||||
output = result.stdout;
|
||||
} else {
|
||||
const result = await execAsync(
|
||||
'ps -eo pid,args 2>/dev/null | grep language_server | grep csrf_token | grep -v grep',
|
||||
{ encoding: 'utf8', timeout: 5000 },
|
||||
);
|
||||
output = result.stdout;
|
||||
}
|
||||
|
||||
const lines = output.split('\n').filter((l: string) => l.trim().length > 0);
|
||||
if (lines.length === 0) return null;
|
||||
|
||||
const workspaceHint = this._getWorkspaceHint();
|
||||
let bestLine: string | null = null;
|
||||
|
||||
if (workspaceHint) {
|
||||
for (const line of lines) {
|
||||
if (line.includes(workspaceHint)) {
|
||||
bestLine = line;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!bestLine) bestLine = lines[0];
|
||||
|
||||
// Extract PID (first field before | on Windows, first token on Unix)
|
||||
let pid: number;
|
||||
if (platform === 'win32') {
|
||||
pid = parseInt(bestLine.split('|')[0].trim(), 10);
|
||||
} else {
|
||||
pid = parseInt(bestLine.trim().split(/\s+/)[0], 10);
|
||||
}
|
||||
|
||||
const csrfToken = this._extractArg(bestLine, 'csrf_token');
|
||||
const extPortStr = this._extractArg(bestLine, 'extension_server_port');
|
||||
const extPort = extPortStr ? parseInt(extPortStr, 10) : 0;
|
||||
|
||||
if (!csrfToken || isNaN(pid)) return null;
|
||||
|
||||
return { pid, csrfToken, extPort };
|
||||
}
|
||||
|
||||
/**
|
||||
* Phase 2: Find ConnectRPC port via netstat.
|
||||
*
|
||||
* The LS process listens on multiple ports:
|
||||
* - httpsPort (HTTPS, ConnectRPC) ← this is what we want
|
||||
* - httpPort (HTTP, ConnectRPC) ← also works
|
||||
* - lspPort (LSP JSON-RPC)
|
||||
* - extension_server_port is separate (for Extension Host IPC)
|
||||
*
|
||||
* We find all LISTENING ports for the LS PID, exclude ext_server_port,
|
||||
* then try HTTPS first (preferred), fall back to HTTP.
|
||||
*/
|
||||
private async _findConnectPort(
|
||||
platform: string,
|
||||
pid: number,
|
||||
extPort: number,
|
||||
): Promise<{ port: number; tls: boolean } | null> {
|
||||
try {
|
||||
const { exec } = require('child_process');
|
||||
const { promisify } = require('util');
|
||||
const execAsync = promisify(exec);
|
||||
let output: string;
|
||||
|
||||
if (platform === 'win32') {
|
||||
const result = await execAsync(
|
||||
`netstat -aon | findstr "LISTENING" | findstr "${pid}"`,
|
||||
{ encoding: 'utf8', timeout: 5000, windowsHide: true },
|
||||
);
|
||||
output = result.stdout;
|
||||
} else {
|
||||
const result = await execAsync(
|
||||
`ss -tlnp 2>/dev/null | grep "pid=${pid}" || netstat -tlnp 2>/dev/null | grep "${pid}"`,
|
||||
{ encoding: 'utf8', timeout: 5000 },
|
||||
);
|
||||
output = result.stdout;
|
||||
}
|
||||
|
||||
// Extract all listening ports for this PID
|
||||
const portMatches = output.matchAll(/127\.0\.0\.1:(\d+)/g);
|
||||
const ports: number[] = [];
|
||||
for (const m of portMatches) {
|
||||
const p = parseInt(m[1], 10);
|
||||
// Exclude extension_server_port
|
||||
if (p !== extPort && !ports.includes(p)) {
|
||||
ports.push(p);
|
||||
}
|
||||
}
|
||||
|
||||
if (ports.length === 0) return null;
|
||||
|
||||
log.debug(`LS ports (excl ext ${extPort}): ${ports.join(', ')}`);
|
||||
|
||||
// Try to identify httpsPort vs httpPort by probing
|
||||
// Strategy: try HTTPS first on each port (httpsPort is preferred)
|
||||
for (const port of ports) {
|
||||
const tls = await this._probePort(port, true);
|
||||
if (tls) return { port, tls: true };
|
||||
}
|
||||
|
||||
// Fallback: try HTTP
|
||||
for (const port of ports) {
|
||||
const http = await this._probePort(port, false);
|
||||
if (http) return { port, tls: false };
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
log.debug('netstat port discovery failed', err);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Quick probe: check if a port accepts ConnectRPC requests.
|
||||
* Returns true if the port responds (even with error) on the given protocol.
|
||||
*/
|
||||
private _probePort(port: number, useTls: boolean): Promise<boolean> {
|
||||
const mod = useTls ? require('https') : require('http');
|
||||
const proto = useTls ? 'https' : 'http';
|
||||
return new Promise((resolve) => {
|
||||
const req = mod.request(`${proto}://127.0.0.1:${port}/exa.language_server_pb.LanguageServerService/GetUserStatus`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'Content-Length': 2 },
|
||||
rejectUnauthorized: false,
|
||||
timeout: 2000,
|
||||
}, (res: any) => {
|
||||
// 401 = correct endpoint, just missing CSRF (expected)
|
||||
// 200 = also correct (unlikely without CSRF but possible)
|
||||
resolve(res.statusCode === 401 || res.statusCode === 200);
|
||||
});
|
||||
req.on('error', () => resolve(false));
|
||||
req.on('timeout', () => { req.destroy(); resolve(false); });
|
||||
req.write('{}');
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a workspace hint string used to match the correct LS process.
|
||||
*
|
||||
* The LS process has --workspace_id like:
|
||||
* file_d_3A_programming_better_antigravity
|
||||
* which is an encoded version of the workspace URI.
|
||||
*/
|
||||
private _getWorkspaceHint(): string {
|
||||
try {
|
||||
const vscode = require('vscode');
|
||||
const folders = vscode.workspace?.workspaceFolders;
|
||||
if (folders && folders.length > 0) {
|
||||
// Convert workspace path to LS workspace_id format
|
||||
// e.g., "d:\programming\better-antigravity" -> "better_antigravity"
|
||||
// (LS uses underscored path segments)
|
||||
const folder = folders[0].uri.fsPath;
|
||||
const parts = folder.replace(/\\/g, '/').split('/');
|
||||
// Use last 2-3 segments for matching
|
||||
return parts.slice(-2).join('_').replace(/[-.\s]/g, '_').toLowerCase();
|
||||
}
|
||||
} catch {
|
||||
// vscode not available (e.g., testing)
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a CLI argument value from a command-line string.
|
||||
* Supports both --key=value and --key value formats.
|
||||
*/
|
||||
private _extractArg(cmdLine: string, argName: string): string | null {
|
||||
// --argName=value
|
||||
const eqMatch = cmdLine.match(new RegExp(`--${argName}=([^\\s"]+)`));
|
||||
if (eqMatch) return eqMatch[1];
|
||||
|
||||
// --argName value
|
||||
const spaceMatch = cmdLine.match(new RegExp(`--${argName}\\s+([^\\s"]+)`));
|
||||
if (spaceMatch) return spaceMatch[1];
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback: discover port from getDiagnostics console logs.
|
||||
* NOTE: This does NOT discover the CSRF token.
|
||||
* In recent Antigravity versions, the port URL may no longer appear in logs.
|
||||
*/
|
||||
private async _discoverPortFromDiagnostics(): Promise<number | null> {
|
||||
try {
|
||||
const raw = await this._executeCommand<string>('antigravity.getDiagnostics');
|
||||
if (!raw || typeof raw !== 'string') return null;
|
||||
const diag = JSON.parse(raw);
|
||||
|
||||
const logs: string = diag.agentWindowConsoleLogs || '';
|
||||
|
||||
// Pattern: 127.0.0.1:{port}/exa.language_server_pb
|
||||
const m1 = logs.match(/127\.0\.0\.1:(\d+)\/exa\.language_server_pb/);
|
||||
if (m1) return parseInt(m1[1], 10);
|
||||
|
||||
// Fallback: any 127.0.0.1:{port} in HTTPS context
|
||||
const m2 = logs.match(/https?:\/\/127\.0\.0\.1:(\d+)/);
|
||||
if (m2) return parseInt(m2[1], 10);
|
||||
|
||||
// Check mainThreadLogs for port info
|
||||
if (diag.mainThreadLogs) {
|
||||
const mainLogs = typeof diag.mainThreadLogs === 'string'
|
||||
? diag.mainThreadLogs
|
||||
: JSON.stringify(diag.mainThreadLogs);
|
||||
const m3 = mainLogs.match(/127\.0\.0\.1:(\d+)/);
|
||||
if (m3) return parseInt(m3[1], 10);
|
||||
}
|
||||
} catch (err) {
|
||||
log.error('Failed to discover LS port from diagnostics', err);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an authenticated RPC call to the Language Server.
|
||||
* Sends x-csrf-token header when available.
|
||||
*
|
||||
* VERIFIED 2026-03-01:
|
||||
* - extension_server_port uses plain HTTP (no TLS)
|
||||
* - Main LS port (--random_port) uses HTTPS with self-signed cert
|
||||
*/
|
||||
private async _rpc(method: string, payload: any): Promise<any> {
|
||||
const httpModule = this._useTls ? require('https') : require('http');
|
||||
const protocol = this._useTls ? 'https' : 'http';
|
||||
const url = `${protocol}://127.0.0.1:${this._port}/exa.language_server_pb.LanguageServerService/${method}`;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const body = JSON.stringify(payload);
|
||||
const headers: Record<string, string | number> = {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': Buffer.byteLength(body),
|
||||
};
|
||||
|
||||
// CSRF header: "x-codeium-csrf-token" (verified from extension.js source)
|
||||
if (this._csrfToken) {
|
||||
headers['x-codeium-csrf-token'] = this._csrfToken;
|
||||
}
|
||||
|
||||
const reqOptions: any = {
|
||||
method: 'POST',
|
||||
headers,
|
||||
};
|
||||
|
||||
// Self-signed TLS when using HTTPS
|
||||
if (this._useTls) {
|
||||
reqOptions.rejectUnauthorized = false;
|
||||
}
|
||||
|
||||
const req = httpModule.request(url, reqOptions, (res: any) => {
|
||||
let data = '';
|
||||
res.on('data', (chunk: string) => { data += chunk; });
|
||||
res.on('end', () => {
|
||||
if (res.statusCode === 200) {
|
||||
try { resolve(JSON.parse(data)); }
|
||||
catch { resolve(data); }
|
||||
} else {
|
||||
const hint = res.statusCode === 401
|
||||
? ' (CSRF token may be invalid or missing -- try setConnection() with the correct token)'
|
||||
: '';
|
||||
reject(new Error(`LS ${method}: ${res.statusCode} -- ${data.substring(0, 200)}${hint}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
req.on('error', (err: Error) => reject(err));
|
||||
req.write(body);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
}
|
||||
518
antigravity-sdk-main/src/transport/state-bridge.ts
Normal file
518
antigravity-sdk-main/src/transport/state-bridge.ts
Normal file
@@ -0,0 +1,518 @@
|
||||
/**
|
||||
* State Bridge — reads Antigravity's USS state from the SQLite database.
|
||||
*
|
||||
* Antigravity stores settings, conversation metadata, and agent preferences
|
||||
* in `state.vscdb` (SQLite). This bridge provides read-only access to that data.
|
||||
*
|
||||
* VERIFIED against live state.vscdb on 2026-02-28.
|
||||
*
|
||||
* @module transport/state-bridge
|
||||
*/
|
||||
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import { IDisposable } from '../core/disposable';
|
||||
import { StateReadError } from '../core/errors';
|
||||
import { Logger } from '../core/logger';
|
||||
import type {
|
||||
IAgentPreferences,
|
||||
TerminalExecutionPolicy,
|
||||
ArtifactReviewPolicy,
|
||||
} from '../core/types';
|
||||
|
||||
const log = new Logger('StateBridge');
|
||||
|
||||
/**
|
||||
* USS (Unified State Sync) keys in state.vscdb.
|
||||
*
|
||||
* VERIFIED: All keys listed below were confirmed to exist
|
||||
* in a live Antigravity v1.107.0 installation on 2026-02-28.
|
||||
* Values are Base64-encoded protobuf unless noted otherwise.
|
||||
*/
|
||||
export const USSKeys = {
|
||||
/** Agent preferences — terminal policy, review policy, secure mode, etc. (1020 bytes) */
|
||||
AGENT_PREFERENCES: 'antigravityUnifiedStateSync.agentPreferences',
|
||||
|
||||
/** Conversation/trajectory summaries — titles, timestamps, workspace URIs (74KB+) */
|
||||
TRAJECTORY_SUMMARIES: 'antigravityUnifiedStateSync.trajectorySummaries',
|
||||
|
||||
/** Agent manager window state (192 bytes) */
|
||||
AGENT_MANAGER_WINDOW: 'antigravityUnifiedStateSync.agentManagerWindow',
|
||||
|
||||
/** Enterprise override store (56 bytes) */
|
||||
OVERRIDE_STORE: 'antigravityUnifiedStateSync.overrideStore',
|
||||
|
||||
/** Model preferences — selected model, sentinel key */
|
||||
MODEL_PREFERENCES: 'antigravityUnifiedStateSync.modelPreferences',
|
||||
|
||||
/** Artifact review state (1204 bytes) */
|
||||
ARTIFACT_REVIEW: 'antigravityUnifiedStateSync.artifactReview',
|
||||
|
||||
/** Browser preferences (380 bytes) */
|
||||
BROWSER_PREFERENCES: 'antigravityUnifiedStateSync.browserPreferences',
|
||||
|
||||
/** Editor preferences (108 bytes) */
|
||||
EDITOR_PREFERENCES: 'antigravityUnifiedStateSync.editorPreferences',
|
||||
|
||||
/** Tab preferences (404 bytes) */
|
||||
TAB_PREFERENCES: 'antigravityUnifiedStateSync.tabPreferences',
|
||||
|
||||
/** Window preferences (44 bytes) */
|
||||
WINDOW_PREFERENCES: 'antigravityUnifiedStateSync.windowPreferences',
|
||||
|
||||
/** Scratch/playground workspaces (268 bytes) */
|
||||
SCRATCH_WORKSPACES: 'antigravityUnifiedStateSync.scratchWorkspaces',
|
||||
|
||||
/** Sidebar workspaces — recent workspace list (5604 bytes) */
|
||||
SIDEBAR_WORKSPACES: 'antigravityUnifiedStateSync.sidebarWorkspaces',
|
||||
|
||||
/** User status info (5196 bytes) */
|
||||
USER_STATUS: 'antigravityUnifiedStateSync.userStatus',
|
||||
|
||||
/** Model credits/usage info */
|
||||
MODEL_CREDITS: 'antigravityUnifiedStateSync.modelCredits',
|
||||
|
||||
/** Onboarding state (140 bytes) */
|
||||
ONBOARDING: 'antigravityUnifiedStateSync.onboarding',
|
||||
|
||||
/** Seen NUX (new user experience) IDs (76 bytes) */
|
||||
SEEN_NUX_IDS: 'antigravityUnifiedStateSync.seenNuxIds',
|
||||
|
||||
// ⚠️ Jetski-specific state (separate sync namespace)
|
||||
/** Agent manager initialization state — contains auth tokens, workspace map (5144 bytes) */
|
||||
AGENT_MANAGER_INIT: 'jetskiStateSync.agentManagerInitState',
|
||||
|
||||
// ⚠️ Non-USS but relevant keys
|
||||
/** All user settings — JSON format */
|
||||
ALL_USER_SETTINGS: 'antigravityUserSettings.allUserSettings',
|
||||
|
||||
/** Allowed model configs for commands */
|
||||
ALLOWED_COMMAND_MODEL_CONFIGS: 'antigravity_allowed_command_model_configs',
|
||||
|
||||
/** Chat session store index (JSON: {"version":1,"entries":{}}) */
|
||||
CHAT_SESSION_INDEX: 'chat.ChatSessionStore.index',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Keys that contain sensitive data and MUST NOT be exposed through the SDK.
|
||||
*
|
||||
* VERIFIED 2026-02-28:
|
||||
* - oauthToken: OAuth access token (732 bytes)
|
||||
* - agentManagerInitState: Contains LIVE ya29.* access token + g1//* refresh token!
|
||||
* - antigravityAuthStatus: Auth status
|
||||
*/
|
||||
const SENSITIVE_KEYS = new Set([
|
||||
'antigravityUnifiedStateSync.oauthToken',
|
||||
'jetskiStateSync.agentManagerInitState',
|
||||
'antigravityAuthStatus',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Protobuf sentinel keys found in agentPreferences.
|
||||
*
|
||||
* ALL 16 sentinel keys verified from live state.vscdb on 2026-02-28.
|
||||
* Each sentinel key string is followed by a small Base64 value encoding
|
||||
* a protobuf varint (the actual preference value).
|
||||
*/
|
||||
const SENTINEL_KEYS = {
|
||||
PLANNING_MODE: 'planningModeSentinelKey',
|
||||
ARTIFACT_REVIEW_POLICY: 'artifactReviewPolicySentinelKey',
|
||||
TERMINAL_AUTO_EXECUTION_POLICY: 'terminalAutoExecutionPolicySentinelKey',
|
||||
TERMINAL_ALLOWED_COMMANDS: 'terminalAllowedCommandsSentinelKey',
|
||||
TERMINAL_DENIED_COMMANDS: 'terminalDeniedCommandsSentinelKey',
|
||||
ALLOW_NON_WORKSPACE_FILES: 'allowAgentAccessNonWorkspaceFilesSentinelKey',
|
||||
ALLOW_GITIGNORE_ACCESS: 'allowCascadeAccessGitignoreFilesSentinelKey',
|
||||
SECURE_MODE: 'secureModeSentinelKey',
|
||||
EXPLAIN_FIX_IN_CONVO: 'explainAndFixInCurrentConversationSentinelKey',
|
||||
AUTO_CONTINUE_ON_MAX: 'autoContinueOnMaxGeneratorInvocationsSentinelKey',
|
||||
DISABLE_AUTO_OPEN_EDITED: 'disableAutoOpenEditedFilesSentinelKey',
|
||||
ENABLE_SOUNDS: 'enableSoundsForSpecialEventsSentinelKey',
|
||||
DISABLE_AUTO_FIX_LINTS: 'disableCascadeAutoFixLintsSentinelKey',
|
||||
ENABLE_SHELL_INTEGRATION: 'enableShellIntegrationSentinelKey',
|
||||
SANDBOX_ALLOW_NETWORK: 'sandboxAllowNetworkSentinelKey',
|
||||
ENABLE_TERMINAL_SANDBOX: 'enableTerminalSandboxSentinelKey',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Reads Antigravity's internal state from the SQLite database.
|
||||
*
|
||||
* Uses **sql.js** (pure JavaScript SQLite, compiled to WASM) which is
|
||||
* verified to work in Antigravity's Extension Host (unlike better-sqlite3
|
||||
* which fails due to ABI mismatch with Electron v22.21.1 / ABI v140).
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const bridge = new StateBridge();
|
||||
* await bridge.initialize();
|
||||
*
|
||||
* const prefs = await bridge.getAgentPreferences();
|
||||
* console.log(prefs.terminalExecutionPolicy);
|
||||
* ```
|
||||
*/
|
||||
export class StateBridge implements IDisposable {
|
||||
private _dbPath: string | null = null;
|
||||
private _db: any = null; // sql.js Database instance
|
||||
private _disposed = false;
|
||||
|
||||
/**
|
||||
* Initialize the state bridge by locating and opening state database.
|
||||
*
|
||||
* @throws {StateReadError} If the database cannot be found
|
||||
*/
|
||||
async initialize(): Promise<void> {
|
||||
const dbPath = this._findStateDb();
|
||||
|
||||
if (!dbPath) {
|
||||
throw new StateReadError('state.vscdb', 'Could not locate Antigravity state database');
|
||||
}
|
||||
|
||||
this._dbPath = dbPath;
|
||||
|
||||
// Open with sql.js (pure JS — verified working in Extension Host)
|
||||
try {
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
// Try to load sql.js from multiple locations:
|
||||
// 1. Adjacent sql-wasm.js (for VSIX bundles where consumer copies it to dist/)
|
||||
// 2. Standard require('sql.js') (for npm install / dev setups)
|
||||
let initSqlJs: any;
|
||||
const localSqlJs = path.join(__dirname, 'sql-wasm.js');
|
||||
if (fs.existsSync(localSqlJs)) {
|
||||
initSqlJs = require(localSqlJs);
|
||||
} else {
|
||||
initSqlJs = require('sql.js');
|
||||
}
|
||||
|
||||
// Auto-locate sql-wasm.wasm — try multiple paths so devs
|
||||
// don't need to manually copy anything after `npm install`
|
||||
const candidates = [
|
||||
// 1. Adjacent to this file (if wasm was bundled/copied to dist/)
|
||||
path.join(__dirname, 'sql-wasm.wasm'),
|
||||
// 2. sql.js package dist/ (standard npm install)
|
||||
path.resolve(__dirname, '..', 'node_modules', 'sql.js', 'dist', 'sql-wasm.wasm'),
|
||||
// 3. Hoisted node_modules (monorepo / npm workspaces)
|
||||
path.resolve(__dirname, '..', '..', 'node_modules', 'sql.js', 'dist', 'sql-wasm.wasm'),
|
||||
// 4. Walk up to find it (deep hoisting)
|
||||
path.resolve(__dirname, '..', '..', '..', 'node_modules', 'sql.js', 'dist', 'sql-wasm.wasm'),
|
||||
];
|
||||
|
||||
// Try require.resolve — works in all layouts
|
||||
try {
|
||||
const sqlJsMain = require.resolve('sql.js');
|
||||
candidates.unshift(path.join(path.dirname(sqlJsMain), 'sql-wasm.wasm'));
|
||||
} catch {
|
||||
// sql.js might not have a resolvable main in all setups
|
||||
}
|
||||
|
||||
let wasmPath: string | null = null;
|
||||
for (const p of candidates) {
|
||||
if (fs.existsSync(p)) {
|
||||
wasmPath = p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!wasmPath) {
|
||||
throw new Error('sql-wasm.wasm not found in any expected location');
|
||||
}
|
||||
|
||||
const SQL = await initSqlJs({
|
||||
locateFile: () => wasmPath!,
|
||||
});
|
||||
const fileBuffer = fs.readFileSync(dbPath);
|
||||
this._db = new SQL.Database(fileBuffer);
|
||||
log.info(`State database opened via sql.js: ${dbPath}`);
|
||||
} catch (error) {
|
||||
log.warn('sql.js not available, will use child_process fallback', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a raw value from the state database.
|
||||
*
|
||||
* @param key - The SQLite key to read
|
||||
* @returns The raw string value, or null if not found
|
||||
* @throws {StateReadError} If the key is sensitive or read fails
|
||||
*/
|
||||
async getRawValue(key: string): Promise<string | null> {
|
||||
if (this._disposed) {
|
||||
throw new StateReadError(key, 'StateBridge has been disposed');
|
||||
}
|
||||
|
||||
if (!this._dbPath) {
|
||||
throw new StateReadError(key, 'StateBridge not initialized');
|
||||
}
|
||||
|
||||
// Block access to sensitive keys
|
||||
if (SENSITIVE_KEYS.has(key)) {
|
||||
throw new StateReadError(key, 'Access to sensitive keys is blocked by the SDK for security');
|
||||
}
|
||||
|
||||
try {
|
||||
if (this._db) {
|
||||
return this._querySqlJs(key);
|
||||
}
|
||||
return await this._queryChildProcess(key);
|
||||
} catch (error) {
|
||||
if (error instanceof StateReadError) throw error;
|
||||
const msg = error instanceof Error ? error.message : String(error);
|
||||
throw new StateReadError(key, msg);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get agent preferences from USS.
|
||||
*
|
||||
* @returns Parsed agent preferences
|
||||
*/
|
||||
async getAgentPreferences(): Promise<IAgentPreferences> {
|
||||
const raw = await this.getRawValue(USSKeys.AGENT_PREFERENCES);
|
||||
|
||||
if (!raw) {
|
||||
log.warn('No agent preferences found, returning defaults');
|
||||
return this._defaultPreferences();
|
||||
}
|
||||
|
||||
try {
|
||||
return this._parseAgentPreferences(raw);
|
||||
} catch (error) {
|
||||
log.error('Failed to parse preferences, returning defaults', error);
|
||||
return this._defaultPreferences();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all stored USS keys from the state database.
|
||||
*
|
||||
* @returns List of key names related to Antigravity (excludes sensitive keys)
|
||||
*/
|
||||
async getAntigravityKeys(): Promise<string[]> {
|
||||
if (!this._dbPath) {
|
||||
throw new StateReadError('*', 'StateBridge not initialized');
|
||||
}
|
||||
|
||||
let keys: string[];
|
||||
|
||||
if (this._db) {
|
||||
const result = this._db.exec(
|
||||
"SELECT key FROM ItemTable WHERE key LIKE '%antigravity%' OR key LIKE '%jetskiStateSync%' OR key LIKE 'chat.%'",
|
||||
);
|
||||
keys = result.length > 0 ? result[0].values.map((r: any[]) => r[0] as string) : [];
|
||||
} else {
|
||||
const result = await this._queryChildProcess('*');
|
||||
keys = result ? result.split('\n').map((l: string) => l.trim()).filter(Boolean) : [];
|
||||
}
|
||||
|
||||
// Filter out sensitive keys
|
||||
return keys.filter((k) => !SENSITIVE_KEYS.has(k));
|
||||
}
|
||||
|
||||
/**
|
||||
* Query using sql.js (in-process, pure JS).
|
||||
*/
|
||||
private _querySqlJs(key: string): string | null {
|
||||
const stmt = this._db.prepare('SELECT value FROM ItemTable WHERE key = $key');
|
||||
stmt.bind({ $key: key });
|
||||
if (stmt.step()) {
|
||||
const row = stmt.getAsObject();
|
||||
stmt.free();
|
||||
return (row.value as string) ?? null;
|
||||
}
|
||||
stmt.free();
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Query using child_process sqlite3 CLI (fallback).
|
||||
*/
|
||||
private async _queryChildProcess(key: string): Promise<string | null> {
|
||||
const { exec } = require('child_process');
|
||||
const { promisify } = require('util');
|
||||
const execAsync = promisify(exec);
|
||||
const sql =
|
||||
key === '*'
|
||||
? "SELECT key FROM ItemTable WHERE key LIKE '%antigravity%' OR key LIKE '%jetskiStateSync%'"
|
||||
: `SELECT value FROM ItemTable WHERE key = '${key.replace(/'/g, "''")}'`;
|
||||
|
||||
try {
|
||||
const { stdout } = await execAsync(`sqlite3 "${this._dbPath}" "${sql}"`, {
|
||||
encoding: 'utf8',
|
||||
timeout: 5000,
|
||||
});
|
||||
return stdout.trim() || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Locate the state.vscdb file across platforms.
|
||||
*/
|
||||
private _findStateDb(): string | null {
|
||||
const candidates: string[] = [];
|
||||
|
||||
// Windows (VERIFIED: this is the correct path)
|
||||
const appData = process.env.APPDATA;
|
||||
if (appData) {
|
||||
candidates.push(path.join(appData, 'Antigravity', 'User', 'globalStorage', 'state.vscdb'));
|
||||
}
|
||||
|
||||
// macOS
|
||||
const home = process.env.HOME;
|
||||
if (home) {
|
||||
candidates.push(
|
||||
path.join(
|
||||
home,
|
||||
'Library',
|
||||
'Application Support',
|
||||
'Antigravity',
|
||||
'User',
|
||||
'globalStorage',
|
||||
'state.vscdb',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Linux
|
||||
if (home) {
|
||||
candidates.push(
|
||||
path.join(home, '.config', 'Antigravity', 'User', 'globalStorage', 'state.vscdb'),
|
||||
);
|
||||
}
|
||||
|
||||
for (const candidate of candidates) {
|
||||
if (fs.existsSync(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse agent preferences from Base64(Protobuf).
|
||||
*
|
||||
* The protobuf structure uses "sentinel keys" as string fields:
|
||||
* - `planningModeSentinelKey` → nested message with Base64(varint)
|
||||
* - `terminalAutoExecutionPolicySentinelKey` → nested message with Base64(varint)
|
||||
* - `artifactReviewPolicySentinelKey` → nested message with Base64(varint)
|
||||
*
|
||||
* Each sentinel value is itself a small Base64 string (e.g., "EAM=" = varint 3 = EAGER).
|
||||
*/
|
||||
private _parseAgentPreferences(raw: string): IAgentPreferences {
|
||||
const buffer = Buffer.from(raw, 'base64');
|
||||
const text = buffer.toString('utf8');
|
||||
|
||||
// Extract all sentinel values
|
||||
const terminalPolicy = this._extractSentinelValue(text, SENTINEL_KEYS.TERMINAL_AUTO_EXECUTION_POLICY);
|
||||
const artifactPolicy = this._extractSentinelValue(text, SENTINEL_KEYS.ARTIFACT_REVIEW_POLICY);
|
||||
const planningMode = this._extractSentinelValue(text, SENTINEL_KEYS.PLANNING_MODE);
|
||||
const secureMode = this._extractSentinelValue(text, SENTINEL_KEYS.SECURE_MODE);
|
||||
const terminalSandbox = this._extractSentinelValue(text, SENTINEL_KEYS.ENABLE_TERMINAL_SANDBOX);
|
||||
const sandboxNetwork = this._extractSentinelValue(text, SENTINEL_KEYS.SANDBOX_ALLOW_NETWORK);
|
||||
const shellIntegration = this._extractSentinelValue(text, SENTINEL_KEYS.ENABLE_SHELL_INTEGRATION);
|
||||
const nonWorkspaceFiles = this._extractSentinelValue(text, SENTINEL_KEYS.ALLOW_NON_WORKSPACE_FILES);
|
||||
const gitignoreAccess = this._extractSentinelValue(text, SENTINEL_KEYS.ALLOW_GITIGNORE_ACCESS);
|
||||
const explainFix = this._extractSentinelValue(text, SENTINEL_KEYS.EXPLAIN_FIX_IN_CONVO);
|
||||
const autoContinue = this._extractSentinelValue(text, SENTINEL_KEYS.AUTO_CONTINUE_ON_MAX);
|
||||
const disableAutoOpen = this._extractSentinelValue(text, SENTINEL_KEYS.DISABLE_AUTO_OPEN_EDITED);
|
||||
const enableSounds = this._extractSentinelValue(text, SENTINEL_KEYS.ENABLE_SOUNDS);
|
||||
const disableAutoFix = this._extractSentinelValue(text, SENTINEL_KEYS.DISABLE_AUTO_FIX_LINTS);
|
||||
|
||||
return {
|
||||
terminalExecutionPolicy: (terminalPolicy ?? 1) as TerminalExecutionPolicy,
|
||||
artifactReviewPolicy: (artifactPolicy ?? 1) as ArtifactReviewPolicy,
|
||||
planningMode: planningMode ?? 0,
|
||||
secureModeEnabled: (secureMode ?? 0) === 1,
|
||||
terminalSandboxEnabled: (terminalSandbox ?? 0) === 1,
|
||||
sandboxAllowNetwork: (sandboxNetwork ?? 0) === 1,
|
||||
shellIntegrationEnabled: (shellIntegration ?? 1) === 1,
|
||||
allowNonWorkspaceFiles: (nonWorkspaceFiles ?? 0) === 1,
|
||||
allowGitignoreAccess: (gitignoreAccess ?? 0) === 1,
|
||||
explainFixInCurrentConvo: (explainFix ?? 0) === 1,
|
||||
autoContinueOnMax: autoContinue ?? 0,
|
||||
disableAutoOpenEdited: (disableAutoOpen ?? 0) === 1,
|
||||
enableSounds: (enableSounds ?? 0) === 1,
|
||||
disableAutoFixLints: (disableAutoFix ?? 0) === 1,
|
||||
allowedCommands: [],
|
||||
deniedCommands: [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a varint value from a protobuf sentinel key.
|
||||
*
|
||||
* The structure is: sentinel_key_string followed by a small
|
||||
* Base64 value like "EAM=" (which decodes to a protobuf varint).
|
||||
*
|
||||
* Known mappings:
|
||||
* - "CAE=" → field 1, value 1 (OFF / ALWAYS)
|
||||
* - "EAI=" → field 2, value 2 (AUTO / TURBO)
|
||||
* - "EAM=" → field 2, value 3 (EAGER / AUTO)
|
||||
*/
|
||||
private _extractSentinelValue(text: string, sentinelKey: string): number | null {
|
||||
const idx = text.indexOf(sentinelKey);
|
||||
if (idx === -1) return null;
|
||||
|
||||
// After the sentinel key, look for a small Base64 fragment
|
||||
const after = text.substring(idx + sentinelKey.length, idx + sentinelKey.length + 30);
|
||||
|
||||
// Match a Base64 chunk (typically 4-8 chars ending with =)
|
||||
const b64Match = after.match(/([A-Za-z0-9+/]{2,8}={0,2})/);
|
||||
if (!b64Match) return null;
|
||||
|
||||
try {
|
||||
const decoded = Buffer.from(b64Match[1], 'base64');
|
||||
// Protobuf varint: last byte of the value
|
||||
// For simple single-byte varints, the value is in the lower 7 bits
|
||||
if (decoded.length >= 2) {
|
||||
// The first byte is (field_number << 3 | wire_type)
|
||||
// The second byte is the actual value
|
||||
return decoded[1];
|
||||
} else if (decoded.length === 1) {
|
||||
return decoded[0];
|
||||
}
|
||||
} catch {
|
||||
// Not valid base64
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private _defaultPreferences(): IAgentPreferences {
|
||||
return {
|
||||
terminalExecutionPolicy: 1 as TerminalExecutionPolicy, // OFF
|
||||
artifactReviewPolicy: 1 as ArtifactReviewPolicy, // ALWAYS
|
||||
planningMode: 0,
|
||||
secureModeEnabled: false,
|
||||
terminalSandboxEnabled: false,
|
||||
sandboxAllowNetwork: false,
|
||||
shellIntegrationEnabled: true,
|
||||
allowNonWorkspaceFiles: false,
|
||||
allowGitignoreAccess: false,
|
||||
explainFixInCurrentConvo: false,
|
||||
autoContinueOnMax: 0,
|
||||
disableAutoOpenEdited: false,
|
||||
enableSounds: false,
|
||||
disableAutoFixLints: false,
|
||||
allowedCommands: [],
|
||||
deniedCommands: [],
|
||||
};
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._disposed = true;
|
||||
|
||||
if (this._db) {
|
||||
try {
|
||||
this._db.close();
|
||||
} catch {
|
||||
// Ignore close errors
|
||||
}
|
||||
this._db = null;
|
||||
}
|
||||
|
||||
this._dbPath = null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user