feat: render Antigravity terminal sessions in native chat

Co-authored-by: haoliangli <haoliangli@users.noreply.github.com>
This commit is contained in:
Neil
2026-09-19 04:53:05 -07:00
co-authored by haoliangli
parent 32a37f02b4
commit e5dcac8fe4
22 changed files with 582 additions and 25 deletions
+50
View File
@@ -0,0 +1,50 @@
# Antigravity terminal-backed Chat UI
Antigravity uses the existing experimental Chat UI over its terminal and saved
transcript. It is not a structured-session provider. The opt-in default-chat
setting applies, and users can return to the terminal.
## Transcript contract
The execution host reads
`.gemini/antigravity-cli/brain/<conversation-id>/.system_generated/logs/transcript.jsonl`.
A hook-reported transcript path takes precedence. Existing WSL exact-path and
host-isolation rules apply; a missing guest transcript must not fall back to a
native host's same-named conversation.
The sanitized fixture in `src/main/native-chat/__fixtures__/antigravity/` records
these observed shapes:
- User input is `USER_EXPLICIT/USER_INPUT` with a `USER_REQUEST` wrapper.
- Planner responses carry text, thinking, or tool calls. Tool arguments are in `args`.
- Tool output is a `MODEL` record with a tool-specific type such as `RUN_COMMAND`
or `VIEW_FILE`. It is not a `TOOL_RESULT` record.
- A planner step marked `DONE` can precede more tool work in the same user turn.
The decoder therefore emits no turn-completion verdict. The existing host-owned
hook/status system remains authoritative.
Full reads, incremental tails, and legacy journal imports reuse the same decoder.
Unknown record types are skipped. Tool failures retain a nonzero exit code or
`ERROR` status as an error result.
## Mixed versions
New hosts advertise `native-chat.antigravity.v1`. A new client's chat transport
checks the owning host before reading or subscribing to Antigravity. An older
host gets the existing update-runtime error; a failed capability probe gets the
existing read error. Neither becomes a perpetual first-transcript wait.
No stream opcode or message-block type was added.
## Verification limits
On macOS with installed agy 1.2.7, a resumed test conversation rendered from its
real saved transcript, and a message submitted in the chat composer appeared in
that transcript. Generation returned `401 ACCESS_TOKEN_TYPE_UNSUPPORTED`.
Hooks were disabled in the isolated app profile, so pane/session metadata was
supplied as a fixture; this did not validate actual hook delivery or live status
transitions. Tool rendering was checked separately with the sanitized fixture.
Five bounded real transcript samples supplied 309 tool/activity records; the
corrected decoder preserved all 309. This is offline format evidence, not proof
of a successful new model turn. Windows, Linux, WSL, SSH, paired-runtime execution,
permission questions, and successful tool-generating turns still need live QA.
@@ -120,7 +120,7 @@ function consumeAntigravityRecordLine(accumulator: SessionAccumulator, line: str
}
}
function extractAntigravityUserRequest(content: string): string | null {
export function extractAntigravityUserRequest(content: string): string | null {
const opener = '<USER_REQUEST>'
const startIndex = content.indexOf(opener)
if (startIndex === -1) {
@@ -0,0 +1,8 @@
This fixture preserves the record and nested tool-call shapes observed in local
Antigravity CLI transcripts on 2026-09-19. Conversation text, tool arguments,
timestamps, and step numbers are replaced with harmless synthetic values.
A planner step with `status: DONE` can precede a tool step and another planner
response without a new user prompt. It does not prove completion of the user turn.
Tool results use `source: MODEL` and their tool-specific step type; planner tool
calls carry arguments in `args`.
@@ -0,0 +1,5 @@
{"step_index":0,"source":"USER_EXPLICIT","type":"USER_INPUT","status":"DONE","created_at":"2026-09-19T10:00:00Z","content":"<USER_REQUEST>Inspect the sample file.</USER_REQUEST>"}
{"step_index":1,"source":"MODEL","type":"PLANNER_RESPONSE","status":"DONE","created_at":"2026-09-19T10:00:01Z","thinking":"I will inspect the sample.","tool_calls":[{"name":"run_command","args":{"CommandLine":"printf sample","Cwd":"/workspace","WaitMsBeforeAsync":1000,"toolAction":"Inspect","toolSummary":"Read sample"}}]}
{"step_index":2,"source":"MODEL","type":"RUN_COMMAND","status":"DONE","exit_code":0,"created_at":"2026-09-19T10:00:02Z","content":"sample"}
{"step_index":3,"source":"SYSTEM","type":"CHECKPOINT","status":"DONE","created_at":"2026-09-19T10:00:03Z"}
{"step_index":4,"source":"MODEL","type":"PLANNER_RESPONSE","status":"DONE","created_at":"2026-09-19T10:00:04Z","content":"The sample file contains sample."}
@@ -22,6 +22,7 @@ import type { NativeChatBlock, NativeChatMessage } from '../../../shared/native-
import { resolveNativeChatTranscriptAgent } from '../../../shared/native-chat-agent-support'
import { resolveSessionFilePath, type ResolveSessionFileOptions } from '../session-file-resolver'
import {
decodeAntigravityTranscriptLine,
decodeClaudeTranscriptLine,
decodeCodexTranscriptLine,
decodeGrokTranscriptLine,
@@ -172,6 +173,7 @@ export async function prepareLegacyTranscriptImport(input: {
const TRANSCRIPT_DECODERS = {
claude: decodeClaudeTranscriptLine,
codex: decodeCodexTranscriptLine,
antigravity: decodeAntigravityTranscriptLine,
grok: decodeGrokTranscriptLine,
omp: decodeOmpTranscriptLine
} as const
@@ -84,6 +84,8 @@ export type ResolveSessionFileOptions = {
grokSessionsDir?: string
/** Override the omp sessions root (`~/.omp/agent/sessions`). */
ompSessionsDir?: string
/** Antigravity CLI brain root on the execution host. */
antigravityBrainDir?: string
/** Authoritative transcript path reported by the agent hook
* (`providerSession.transcriptPath`). When set and the file exists, it is used
* directly — recent Claude Code names the transcript with a UUID that differs
@@ -210,6 +212,22 @@ async function resolveSessionFileById(
signal
)
}
if (transcriptAgent === 'antigravity') {
// A conversation id is one directory segment, never a caller-supplied path.
if (!/^[a-zA-Z0-9_-]+$/.test(trimmedId)) {
return null
}
return toHostReadableTranscriptPath(
join(
options.antigravityBrainDir ?? join(homedir(), '.gemini', 'antigravity-cli', 'brain'),
trimmedId,
'.system_generated',
'logs',
'transcript.jsonl'
),
{ signal }
)
}
if (transcriptAgent === 'grok') {
return resolveGrokSessionFile(trimmedId, options.grokSessionsDir ?? grokSessionsDir(), signal)
}
@@ -0,0 +1,123 @@
// Antigravity (AGY) JSONL line → NativeChatMessage decoder.
import type { NativeChatBlock, NativeChatMessage } from '../../shared/native-chat-types'
import {
asRecord,
extractString,
parseJsonObject,
timestampMs
} from '../ai-vault/session-scanner-values'
import { extractAntigravityUserRequest } from '../ai-vault/session-scanner-antigravity-parser'
const TOOL_STEP_TYPES = new Set([
'RUN_COMMAND',
'LIST_DIRECTORY',
'GENERIC',
'INVOKE_SUBAGENT',
'VIEW_FILE',
'CODE_ACTION',
'GREP_SEARCH',
'SEARCH_WEB'
])
export function decodeAntigravityTranscriptLine(
line: string,
fallbackId: string
): NativeChatMessage | null {
const record = parseJsonObject(line)
if (!record) {
return null
}
const timestamp = parseTimestamp(record.created_at ?? record.timestamp)
const stepIndex =
typeof record.step_index === 'number'
? String(record.step_index)
: (extractString(record.step_index) ?? extractString(record.id))
const id = stepIndex ?? fallbackId
const source = extractString(record.source)
const type = extractString(record.type)
if (
(source === 'USER_EXPLICIT' || source === 'USER') &&
(type === 'USER_INPUT' || type === 'REQUEST')
) {
const rawContent = extractString(record.content) ?? ''
const text = extractAntigravityUserRequest(rawContent) || rawContent
if (!text.trim()) {
return null
}
return {
id,
role: 'user',
blocks: [{ type: 'text', text }],
timestamp,
source: 'transcript'
}
}
if (source === 'MODEL' && type === 'PLANNER_RESPONSE') {
const blocks: NativeChatBlock[] = []
const thinking = extractString(record.thinking)
if (thinking && thinking.trim()) {
blocks.push({ type: 'text', text: `> *Thinking:*\n${thinking}` })
}
const content = extractString(record.content)
if (content && content.trim()) {
blocks.push({ type: 'text', text: content })
}
if (Array.isArray(record.tool_calls)) {
for (const call of record.tool_calls) {
const tool = asRecord(call)
if (tool) {
const name = extractString(tool.name) ?? 'tool'
const input = tool.args ?? tool.arguments ?? tool.input ?? null
blocks.push({
type: 'tool-call',
name,
input
})
}
}
}
if (blocks.length === 0) {
return null
}
return {
id,
role: 'assistant',
blocks,
timestamp,
source: 'transcript'
}
}
// Recorded tool steps are MODEL records, not TOOL_RESULT messages.
if (source === 'MODEL' && type && TOOL_STEP_TYPES.has(type)) {
const output = extractString(record.content)
if (!output) {
return null
}
const isError =
record.status === 'ERROR' || (typeof record.exit_code === 'number' && record.exit_code !== 0)
return {
id,
role: 'tool',
blocks: [{ type: 'tool-result', output, ...(isError ? { isError: true } : {}) }],
timestamp,
source: 'transcript'
}
}
return null
}
function parseTimestamp(value: unknown): number | null {
const parsed = timestampMs(value)
return Number.isFinite(parsed) ? parsed : null
}
@@ -0,0 +1,76 @@
import { readFileSync } from 'node:fs'
import { describe, expect, it } from 'vitest'
import { decodeAntigravityTranscriptLine } from './transcript-line-decoders-antigravity'
const lines = readFileSync(
new URL('./__fixtures__/antigravity/tool-turn.jsonl', import.meta.url),
'utf8'
)
.trim()
.split('\n')
function decode(record: unknown) {
return decodeAntigravityTranscriptLine(JSON.stringify(record), 'line-1')
}
describe('Antigravity transcript records', () => {
it('preserves the recorded prompt, tool arguments, output, and subsequent response', () => {
const messages = lines.map((line, index) =>
decodeAntigravityTranscriptLine(line, `line-${index}`)
)
expect(messages[0]).toMatchObject({
role: 'user',
blocks: [{ type: 'text', text: 'Inspect the sample file.' }]
})
expect(messages[1]?.blocks).toContainEqual({
type: 'tool-call',
name: 'run_command',
input: {
CommandLine: 'printf sample',
Cwd: '/workspace',
WaitMsBeforeAsync: 1000,
toolAction: 'Inspect',
toolSummary: 'Read sample'
}
})
expect(messages[2]).toMatchObject({
role: 'tool',
blocks: [{ type: 'tool-result', output: 'sample' }]
})
expect(messages[3]).toBeNull()
expect(messages[4]).toMatchObject({
role: 'assistant',
blocks: [{ type: 'text', text: 'The sample file contains sample.' }]
})
})
it.each([
'RUN_COMMAND',
'VIEW_FILE',
'LIST_DIRECTORY',
'GREP_SEARCH',
'CODE_ACTION',
'GENERIC',
'INVOKE_SUBAGENT'
])('keeps recorded %s output without treating step completion as turn completion', (type) => {
expect(decode({ source: 'MODEL', type, status: 'DONE', content: 'output' })).toMatchObject({
role: 'tool',
blocks: [{ type: 'tool-result', output: 'output' }]
})
})
it.each([{ status: 'ERROR' }, { status: 'DONE', exit_code: 1 }])(
'marks failed tool results',
(fields) => {
expect(
decode({ source: 'MODEL', type: 'RUN_COMMAND', content: 'failed', ...fields })?.blocks
).toEqual([{ type: 'tool-result', output: 'failed', isError: true }])
}
)
it('ignores malformed, empty, and system bookkeeping records', () => {
expect(decodeAntigravityTranscriptLine('{', 'line')).toBeNull()
expect(decode({ source: 'MODEL', type: 'PLANNER_RESPONSE' })).toBeNull()
expect(decode({ source: 'SYSTEM', type: 'CHECKPOINT', content: 'private context' })).toBeNull()
})
})
@@ -13,3 +13,5 @@ export { decodeClaudeTranscriptLine } from './transcript-line-decoders-claude'
export { decodeCodexTranscriptLine } from './transcript-line-decoders-codex'
export { decodeGrokTranscriptLine } from './transcript-line-decoders-grok'
export { decodeOmpTranscriptLine } from './transcript-line-decoders-omp'
export { decodeAntigravityTranscriptLine } from './transcript-line-decoders-antigravity'
@@ -0,0 +1,67 @@
import { copyFile, mkdir, mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { resolveSessionFilePath } from './session-file-resolver'
import { readNativeChatTranscript } from './transcript-reader'
import {
nativeChatLineDecoderForAgent,
readNativeChatTranscriptTailFile
} from './transcript-tail-reader'
import { nativeChatTurnLifecycleDecoderForAgent } from './transcript-turn-lifecycle'
let root: string | undefined
afterEach(async () => {
if (root) {
await rm(root, { recursive: true, force: true })
}
root = undefined
})
async function fixture() {
root = await mkdtemp(join(tmpdir(), 'orca-agy-chat-'))
const dir = join(root, 'conversation', '.system_generated', 'logs')
await mkdir(dir, { recursive: true })
const path = join(dir, 'transcript.jsonl')
await copyFile(new URL('./__fixtures__/antigravity/tool-turn.jsonl', import.meta.url), path)
return { path, options: { antigravityBrainDir: root } }
}
describe('Antigravity native transcript reading', () => {
it('resolves its CLI conversation and preserves full-reader / tail-reader parity', async () => {
const { path, options } = await fixture()
expect(await resolveSessionFilePath('antigravity', 'conversation', options)).toBe(path)
const full = await readNativeChatTranscript('antigravity', 'conversation', options)
const decode = nativeChatLineDecoderForAgent('antigravity')
expect(decode).not.toBeNull()
if (!decode || !('messages' in full)) {
throw new Error('transcript reader unavailable')
}
const tail = await readNativeChatTranscriptTailFile(path, 20, decode)
expect(tail.messages).toEqual(full.messages)
expect(full.messages.map((message) => message.role)).toEqual([
'user',
'assistant',
'tool',
'assistant'
])
expect(nativeChatTurnLifecycleDecoderForAgent('antigravity')).toBeNull()
expect(full.lifecycle).toBeUndefined()
expect(tail.lifecycle).toBeUndefined()
})
it('rejects path-shaped ids and does not substitute a native file for a WSL session', async () => {
const { options } = await fixture()
for (const id of ['../conversation', '..\\conversation', '/conversation', 'C:conversation']) {
expect(await resolveSessionFilePath('antigravity', id, options)).toBeNull()
}
expect(
await resolveSessionFilePath('antigravity', 'conversation', {
...options,
wslDistro: 'Ubuntu'
})
).toBeNull()
expect(await resolveSessionFilePath('antigravity', 'missing', options)).toBeNull()
})
})
@@ -9,6 +9,7 @@ import { resolveSessionFilePath, type ResolveSessionFileOptions } from './sessio
import { openTranscriptReadStream } from './wsl-transcript-fs-access'
import { wslTranscriptFsRefusal } from './wsl-transcript-fs-gate'
import {
decodeAntigravityTranscriptLine,
decodeClaudeTranscriptLine,
decodeCodexTranscriptLine,
decodeGrokTranscriptLine,
@@ -55,6 +56,9 @@ export async function readNativeChatTranscript(
}
try {
const transcriptAgent = resolveNativeChatTranscriptAgent(agent)
if (transcriptAgent === 'antigravity') {
return { messages: await readTranscript(filePath, decodeAntigravityTranscriptLine) }
}
if (transcriptAgent === 'claude') {
return { messages: await readTranscript(filePath, decodeClaudeTranscriptLine) }
}
@@ -6,6 +6,7 @@ import type {
import { resolveNativeChatTranscriptAgent } from '../../shared/native-chat-agent-support'
import { resolveSessionFilePath, type ResolveSessionFileOptions } from './session-file-resolver'
import {
decodeAntigravityTranscriptLine,
decodeClaudeTranscriptLine,
decodeCodexTranscriptLine,
decodeGrokTranscriptLine,
@@ -35,6 +36,9 @@ export type NativeChatLineDecoder = (line: string, fallbackId: string) => Native
export function nativeChatLineDecoderForAgent(agent: AgentType): NativeChatLineDecoder | null {
const transcriptAgent = resolveNativeChatTranscriptAgent(agent)
if (transcriptAgent === 'antigravity') {
return decodeAntigravityTranscriptLine
}
if (transcriptAgent === 'claude') {
return decodeClaudeTranscriptLine
}
@@ -0,0 +1,88 @@
import { describe, expect, it, vi } from 'vitest'
import { guardAntigravityChatTransport } from './native-chat-antigravity-capability'
import {
RUNTIME_NATIVE_CHAT_READ_ERROR,
RUNTIME_NATIVE_CHAT_TOO_OLD
} from './native-chat-runtime-contract'
const args = {
agent: 'antigravity' as const,
sessionId: 'conversation',
subscriptionId: 'subscription'
}
function setup(supports: () => Promise<boolean>) {
const stop = vi.fn()
const transport = {
readSession: vi.fn(async () => ({ messages: [] })),
subscribe: vi.fn(() => stop)
}
return { transport, stop, guarded: guardAntigravityChatTransport(transport, supports) }
}
describe('Antigravity chat host capability', () => {
it('rejects old hosts before read or subscribe without claiming the transcript is pending', async () => {
const { guarded, transport } = setup(async () => false)
expect(await guarded.readSession('antigravity', 'conversation')).toEqual({
error: RUNTIME_NATIVE_CHAT_TOO_OLD
})
const onFrame = vi.fn()
const close = await guarded.subscribe(args, onFrame)
await vi.waitFor(() =>
expect(onFrame).toHaveBeenCalledWith({
type: 'snapshot',
messages: [],
hasMore: false,
error: RUNTIME_NATIVE_CHAT_TOO_OLD
})
)
expect(transport.readSession).not.toHaveBeenCalled()
expect(transport.subscribe).not.toHaveBeenCalled()
close()
})
it('keeps host-contact failure separate from unsupported versions', async () => {
const { guarded } = setup(async () => {
throw new Error('offline')
})
expect(await guarded.readSession('antigravity', 'conversation')).toEqual({
error: RUNTIME_NATIVE_CHAT_READ_ERROR
})
})
it('does not open a subscription after teardown while the probe was pending', async () => {
let resolve: ((supported: boolean) => void) | undefined
const { guarded, transport } = setup(
() =>
new Promise((done) => {
resolve = done
})
)
const close = await guarded.subscribe(args, vi.fn())
close()
resolve?.(true)
await Promise.resolve()
await Promise.resolve()
expect(transport.subscribe).not.toHaveBeenCalled()
})
it('uses supported hosts and closes their subscription once', async () => {
const { guarded, transport, stop } = setup(async () => true)
await guarded.readSession('antigravity', 'conversation')
expect(transport.readSession).toHaveBeenCalledWith('antigravity', 'conversation')
const close = await guarded.subscribe(args, vi.fn())
await vi.waitFor(() => expect(transport.subscribe).toHaveBeenCalledOnce())
close()
expect(stop).toHaveBeenCalledOnce()
})
it('leaves existing agents on their existing transport', async () => {
const supports = vi.fn(async () => false)
const { guarded, transport } = setup(supports)
await guarded.readSession('claude', 'conversation')
await guarded.subscribe({ ...args, agent: 'claude' }, vi.fn())
expect(transport.readSession).toHaveBeenCalledOnce()
expect(transport.subscribe).toHaveBeenCalledOnce()
expect(supports).not.toHaveBeenCalled()
})
})
@@ -0,0 +1,70 @@
import type { NativeChatApi } from '../../../../preload/api-types'
import {
RUNTIME_NATIVE_CHAT_READ_ERROR,
RUNTIME_NATIVE_CHAT_TOO_OLD
} from './native-chat-runtime-contract'
type Transport = Pick<NativeChatApi, 'readSession' | 'subscribe'>
/** An older host cannot decode agy; distinguish that from a transcript awaiting its first write. */
export function guardAntigravityChatTransport(
transport: Transport,
supports: () => Promise<boolean>
): Transport {
const errorForHost = async (): Promise<string | null> => {
try {
return (await supports()) ? null : RUNTIME_NATIVE_CHAT_TOO_OLD
} catch {
return RUNTIME_NATIVE_CHAT_READ_ERROR
}
}
return {
readSession: async (agent, ...args) => {
const error = agent === 'antigravity' ? await errorForHost() : null
return error ? { error } : transport.readSession(agent, ...args)
},
subscribe: (args, onFrame) => {
if (args.agent !== 'antigravity') {
return transport.subscribe(args, onFrame)
}
let cancelled = false
let unsubscribe: (() => void) | undefined
void errorForHost()
.then(async (error) => {
if (cancelled) {
return
}
if (error) {
onFrame({ type: 'snapshot', messages: [], hasMore: false, error })
return
}
const stop = await Promise.resolve(
transport.subscribe(args, (frame) => {
if (!cancelled) {
onFrame(frame)
}
})
)
if (cancelled) {
stop()
} else {
unsubscribe = stop
}
})
.catch(() => {
if (!cancelled) {
onFrame({
type: 'snapshot',
messages: [],
hasMore: false,
error: RUNTIME_NATIVE_CHAT_READ_ERROR
})
}
})
return () => {
cancelled = true
unsubscribe?.()
}
}
}
}
@@ -4,6 +4,9 @@ import type {
} from '../../../../preload/api-types'
import type { NativeChatTurnLifecycle } from '../../../../shared/native-chat-types'
export const RUNTIME_NATIVE_CHAT_TOO_OLD =
'This remote runtime is too old to show agent chat history. Update the remote runtime to view it.'
export const RUNTIME_NATIVE_CHAT_READ_ERROR = "Couldn't read agent chat from the remote runtime."
export function parseRuntimeNativeChatTurnLifecycle(
@@ -1,7 +1,11 @@
import { ANTIGRAVITY_NATIVE_CHAT_RUNTIME_CAPABILITY } from '../../../../shared/protocol-version'
import { ensureLocalRuntimeCapabilities } from '@/runtime/local-runtime-capabilities'
import { guardAntigravityChatTransport } from './native-chat-antigravity-capability'
import type { NativeChatApi, NativeChatAppendedMessages } from '../../../../preload/api-types'
import { isWebClientLocation } from '@/lib/web-client-location'
import {
callRuntimeRpc,
runtimeEnvironmentSupportsCapability,
RuntimeRpcCallError,
type RuntimeClientTarget
} from '@/runtime/runtime-rpc-client'
@@ -9,7 +13,8 @@ import { isRuntimeCompatBlockError } from '@/runtime/runtime-protocol-compat'
import {
parseRuntimeNativeChatReadSessionResult,
parseRuntimeNativeChatTurnLifecycle,
RUNTIME_NATIVE_CHAT_READ_ERROR
RUNTIME_NATIVE_CHAT_READ_ERROR,
RUNTIME_NATIVE_CHAT_TOO_OLD
} from './native-chat-runtime-contract'
/** The read/subscribe surface the live-session hook needs, decoupled from where
@@ -17,9 +22,6 @@ import {
* hook and everything downstream (merge, assembler, pagination) are unchanged. */
export type NativeChatSessionTransport = Pick<NativeChatApi, 'readSession' | 'subscribe'>
const RUNTIME_TOO_OLD =
'This remote runtime is too old to show agent chat history. Update the remote runtime to view it.'
/** Backoff before re-opening a dropped runtime chat stream. Exported for tests. */
export const RUNTIME_NATIVE_CHAT_RECONNECT_MS = 2_000
@@ -30,10 +32,10 @@ export const RUNTIME_NATIVE_CHAT_RECONNECT_MS = 2_000
* never mislabeled as a version problem (KTD-4, not catch-all). */
export function toRuntimeNativeChatErrorMessage(err: unknown): string {
if (err instanceof RuntimeRpcCallError && err.code === 'method_not_found') {
return RUNTIME_TOO_OLD
return RUNTIME_NATIVE_CHAT_TOO_OLD
}
if (isRuntimeCompatBlockError(err)) {
return RUNTIME_TOO_OLD
return RUNTIME_NATIVE_CHAT_TOO_OLD
}
return RUNTIME_NATIVE_CHAT_READ_ERROR
}
@@ -268,8 +270,22 @@ function createRuntimeNativeChatTransport(environmentId: string): NativeChatSess
export function getNativeChatSessionTransport(
runtimeEnvironmentId: string | null
): NativeChatSessionTransport {
if (runtimeEnvironmentId && !isWebClientLocation()) {
return createRuntimeNativeChatTransport(runtimeEnvironmentId)
}
return localNativeChatTransport
const webClient = isWebClientLocation()
const remote = runtimeEnvironmentId && !webClient ? runtimeEnvironmentId : null
const transport = remote ? createRuntimeNativeChatTransport(remote) : localNativeChatTransport
return guardAntigravityChatTransport(transport, async () => {
if (remote) {
return runtimeEnvironmentSupportsCapability(
remote,
ANTIGRAVITY_NATIVE_CHAT_RUNTIME_CAPABILITY
)
}
const capabilities = webClient
? (await window.api.runtime.getStatus()).capabilities
: await ensureLocalRuntimeCapabilities()
if (!capabilities) {
throw new Error('Runtime capabilities unavailable')
}
return capabilities.includes(ANTIGRAVITY_NATIVE_CHAT_RUNTIME_CAPABILITY)
})
}
@@ -17,7 +17,8 @@ const EXPECTED_SUPPORTED_AGENTS = [
'openclaude',
'codex',
'grok',
'omp'
'omp',
'antigravity'
] as const satisfies readonly TuiAgent[]
const SUPPORTED_AGENTS_LABEL_KEY = 'auto.components.settings.NativeChatSupportedAgents.label'
@@ -7,15 +7,18 @@ import {
import { isNativeChatTranscriptLocalReadable } from './native-chat-transcript-readability'
describe('decideInitialAgentTabViewMode', () => {
it("returns 'chat' when native chat and the opt-in default setting are on", () => {
expect(
decideInitialAgentTabViewMode({
experimentalNativeChat: true,
openAgentTabsInChatByDefault: true,
agent: 'codex'
})
).toBe('chat')
})
it.each(['codex', 'antigravity'] as const)(
'opens %s in chat when native chat and the opt-in default setting are on',
(agent) => {
expect(
decideInitialAgentTabViewMode({
experimentalNativeChat: true,
openAgentTabsInChatByDefault: true,
agent
})
).toBe('chat')
}
)
it('returns undefined when native chat is disabled', () => {
expect(
+13
View File
@@ -8,6 +8,19 @@ import {
} from './agent-session-resume'
describe('agent session resume metadata', () => {
it('preserves the Antigravity hook transcript path for host-owned chat reads', () => {
expect(
extractAgentProviderSession('antigravity', {
conversationId: 'agy-conversation',
transcriptPath: '/workspace/brain/transcript.jsonl'
})
).toEqual({
key: 'conversation_id',
id: 'agy-conversation',
transcriptPath: '/workspace/brain/transcript.jsonl'
})
})
it('treats devin as a resumable TUI agent', () => {
expect(isResumableTuiAgent('devin')).toBe(true)
})
+1 -1
View File
@@ -201,7 +201,7 @@ export function extractAgentProviderSession(
}
case 'antigravity': {
const id = readSessionId(payload, ['conversationId'])
return id ? { key: 'conversation_id', id } : null
return id ? withTranscriptPath({ key: 'conversation_id', id }, payload) : null
}
case 'opencode':
case 'mimo-code': {
+4 -3
View File
@@ -1,6 +1,6 @@
import type { TuiAgent } from './tui-agent'
export type NativeChatTranscriptAgent = 'claude' | 'codex' | 'grok' | 'omp'
export type NativeChatTranscriptAgent = 'claude' | 'codex' | 'grok' | 'omp' | 'antigravity'
/** Agents whose transcripts the native chat view can parse and render, in the
* order the settings pane advertises them. */
@@ -9,7 +9,8 @@ export const NATIVE_CHAT_SUPPORTED_AGENT_LIST: readonly TuiAgent[] = [
'openclaude',
'codex',
'grok',
'omp'
'omp',
'antigravity'
]
export const NATIVE_CHAT_SUPPORTED_AGENTS: ReadonlySet<string> = new Set(
@@ -47,7 +48,7 @@ export function resolveNativeChatTranscriptAgent(
if (agent === 'claude' || agent === 'openclaude') {
return 'claude'
}
if (agent === 'codex' || agent === 'grok' || agent === 'omp') {
if (agent === 'codex' || agent === 'grok' || agent === 'omp' || agent === 'antigravity') {
return agent
}
return null
+3
View File
@@ -292,10 +292,13 @@ export const ELECTRON_REMOTE_RUNTIME_CLIENT_CAPABILITIES = [
export const ANTIGRAVITY_CONFIGURED_MODEL_RUNTIME_CAPABILITY =
'git.antigravity-configured-model.v1' as const
export const ANTIGRAVITY_NATIVE_CHAT_RUNTIME_CAPABILITY = 'native-chat.antigravity.v1' as const
export const ANTIGRAVITY_VISIBLE_READINESS_RUNTIME_CAPABILITY =
'terminal.antigravity-visible-readiness.v1' as const
export const RUNTIME_CAPABILITIES = [
ANTIGRAVITY_NATIVE_CHAT_RUNTIME_CAPABILITY,
ANTIGRAVITY_VISIBLE_READINESS_RUNTIME_CAPABILITY,
ANTIGRAVITY_CONFIGURED_MODEL_RUNTIME_CAPABILITY,
'files.pathsExist',