feat: Add Antigravity provider integration + fix native mode startup
Some checks failed
Release Binaries / release (push) Has been cancelled

- Added Antigravity AI provider with Google OAuth authentication
- New integration client (antigravity.ts) with automatic endpoint fallback
- API routes for /api/antigravity/* (models, auth-status, test, chat)
- AntigravitySettings.tsx for Advanced Settings panel
- Updated session-api.ts and session-actions.ts for provider routing
- Updated opencode.jsonc with Antigravity plugin and 11 models:
  - Gemini 3 Pro Low/High, Gemini 3 Flash
  - Claude Sonnet 4.5 (+ thinking variants)
  - Claude Opus 4.5 (+ thinking variants)
  - GPT-OSS 120B Medium

- Fixed native mode startup error (was trying to launch __nomadarch_native__ as binary)
- Native mode workspaces now skip binary launch and are immediately ready
This commit is contained in:
Gemini AI
2025-12-27 04:01:38 +04:00
Unverified
parent 4aa4795d4b
commit bb1c0d81f2
10 changed files with 1747 additions and 9 deletions

View File

@@ -1116,6 +1116,144 @@ async function streamZAIChat(
})
}
async function streamAntigravityChat(
instanceId: string,
sessionId: string,
providerId: string,
modelId: string,
messages: ExternalChatMessage[],
messageId: string,
assistantMessageId: string,
assistantPartId: string,
): Promise<void> {
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), STREAM_TIMEOUT_MS)
// Get workspace path for tool execution
const instance = instances().get(instanceId)
const workspacePath = instance?.folder || ""
const response = await fetch("/api/antigravity/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
signal: controller.signal,
body: JSON.stringify({
model: modelId,
messages,
stream: true,
workspacePath,
enableTools: true,
}),
})
if (!response.ok) {
const errorText = await response.text().catch(() => "")
throw new Error(errorText || `Antigravity chat failed (${response.status})`)
}
const store = messageStoreBus.getOrCreate(instanceId)
store.beginStreamingUpdate()
let fullText = ""
let lastUpdateAt = 0
try {
await readSseStream(response, (data) => {
try {
const chunk = JSON.parse(data)
if (chunk?.error) throw new Error(chunk.error)
// Handle tool execution results (special events from backend)
if (chunk?.type === "tool_result") {
const toolResult = `\n\n✅ **Tool Executed:** ${chunk.content}\n\n`
fullText += toolResult
store.applyPartUpdate({
messageId: assistantMessageId,
part: { id: assistantPartId, type: "text", text: fullText } as any,
})
// Dispatch file change event to refresh sidebar
if (typeof window !== "undefined") {
console.log(`[EVENT] Dispatching FILE_CHANGE_EVENT for ${instanceId}`);
window.dispatchEvent(new CustomEvent(FILE_CHANGE_EVENT, { detail: { instanceId } }))
}
// Auto-trigger preview for HTML file writes
const content = chunk.content || ""
if (content.includes("Successfully wrote") &&
(content.includes(".html") || content.includes("index.") || content.includes(".htm"))) {
if (typeof window !== "undefined") {
const htmlMatch = content.match(/to\s+([^\s]+\.html?)/)
if (htmlMatch) {
const relativePath = htmlMatch[1]
const origin = typeof window !== "undefined" ? window.location.origin : "http://localhost:3000"
const apiOrigin = origin.replace(":3000", ":9898")
const previewUrl = `${apiOrigin}/api/workspaces/${instanceId}/serve/${relativePath}`
console.log(`[EVENT] Auto-preview triggered for ${previewUrl}`);
window.dispatchEvent(new CustomEvent(BUILD_PREVIEW_EVENT, {
detail: { url: previewUrl, instanceId }
}))
}
}
}
return
}
const delta =
chunk?.choices?.[0]?.delta?.content ??
chunk?.choices?.[0]?.message?.content
if (typeof delta !== "string" || delta.length === 0) return
fullText += delta
const now = Date.now()
if (now - lastUpdateAt > 40) { // Limit to ~25 updates per second
lastUpdateAt = now
store.applyPartUpdate({
messageId: assistantMessageId,
part: { id: assistantPartId, type: "text", text: fullText } as any,
})
}
} catch (e) {
if (e instanceof Error) throw e
}
})
// Always apply final text update
store.applyPartUpdate({
messageId: assistantMessageId,
part: { id: assistantPartId, type: "text", text: fullText } as any,
})
} finally {
clearTimeout(timeoutId)
store.endStreamingUpdate()
}
store.upsertMessage({
id: assistantMessageId,
sessionId,
role: "assistant",
status: "complete",
updatedAt: Date.now(),
isEphemeral: false,
})
store.setMessageInfo(assistantMessageId, {
id: assistantMessageId,
role: "assistant",
providerID: providerId,
modelID: modelId,
time: { created: store.getMessageInfo(assistantMessageId)?.time?.created ?? Date.now(), completed: Date.now() },
} as any)
store.upsertMessage({
id: messageId,
sessionId,
role: "user",
status: "sent",
updatedAt: Date.now(),
isEphemeral: false,
})
}
async function sendMessage(
instanceId: string,
sessionId: string,
@@ -1264,7 +1402,7 @@ async function sendMessage(
addDebugLog(`Merge System Instructions: ${Math.round(tPre2 - tPre1)}ms`, "warn")
}
if (providerId === "ollama-cloud" || providerId === "qwen-oauth" || providerId === "opencode-zen" || providerId === "zai") {
if (providerId === "ollama-cloud" || providerId === "qwen-oauth" || providerId === "opencode-zen" || providerId === "zai" || providerId === "antigravity") {
const store = messageStoreBus.getOrCreate(instanceId)
const now = Date.now()
const assistantMessageId = createId("msg")
@@ -1347,6 +1485,17 @@ async function sendMessage(
assistantMessageId,
assistantPartId,
)
} else if (providerId === "antigravity") {
await streamAntigravityChat(
instanceId,
sessionId,
providerId,
effectiveModel.modelId,
externalMessages,
messageId,
assistantMessageId,
assistantPartId,
)
} else {
const qwenManager = new QwenOAuthManager()
const token = await qwenManager.getValidToken()
@@ -1428,7 +1577,9 @@ async function sendMessage(
? "Z.AI request failed"
: providerId === "opencode-zen"
? "OpenCode Zen request failed"
: "Qwen request failed",
: providerId === "antigravity"
? "Antigravity request failed"
: "Qwen request failed",
message: error?.message || "Request failed",
variant: "error",
duration: 8000,