Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
3bbf5c7
feat(antigravity): runtime 1.4.5 with thread-scoped uploads, typed tu…
t3dotgg Nov 1, 2026
ff85302
fix(antigravity): keep Windows on 1.4.1 and support 1.4.5 in the mani…
t3dotgg Nov 1, 2026
9190358
fix(antigravity): keep cold-resume uploads, refuse foreign upload IDs…
t3dotgg Nov 1, 2026
ce2c9c6
fix(antigravity): check upload ownership per thread, omit cut patches…
t3dotgg Nov 1, 2026
9eeefda
fix(acp): find a snippet edit beside status blocks
t3dotgg Nov 1, 2026
386202d
docs(antigravity): name the mobile Work button for subagent work
t3dotgg Nov 1, 2026
7698786
fix(antigravity): keep main's shared attachment folder
t3dotgg Nov 2, 2026
2d2dacd
fix(acp): give no diff preview when any diff is an unplaced snippet
t3dotgg Nov 2, 2026
bbdfc9d
fix(antigravity): count cached input, hide partly placed edits, and m…
t3dotgg Nov 2, 2026
e77ea0c
fix(antigravity): place edits within the search range and skip auto b…
t3dotgg Nov 2, 2026
a265bfe
fix(antigravity): place only whole-line matches and refuse unprovable…
t3dotgg Nov 2, 2026
7cde362
fix(antigravity): count added line breaks, place CRLF lines, and scop…
t3dotgg Nov 2, 2026
20a3d4e
fix(antigravity): omit line-break changes, search CRLF as LF, and ref…
t3dotgg Nov 2, 2026
fb5582c
fix(antigravity): refuse only cut siblings that could be the same launch
t3dotgg Nov 2, 2026
03d5ecf
fix(antigravity): omit ending-only edits and keep byte order marks in…
t3dotgg Nov 2, 2026
8623766
fix(antigravity): omit previews for cleared lines without a line break
t3dotgg Nov 2, 2026
f7f0059
feat(antigravity): run 1.4.5 on Windows too
t3dotgg Nov 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 116 additions & 0 deletions apps/mobile/src/features/threads/NativeChildWorkSheet.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import type {
EnvironmentId,
OrchestrationV2GetNativeChildWorkResult,
OrchestrationV2NativeChildWork,
ThreadId,
} from "@t3tools/contracts";
import { ScrollView, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";

import { AppText as Text } from "../../components/ui/AppText";
import { Button } from "../../components/ui/Button";
import { Sheet } from "../../components/ui/Sheet";
import { orchestrationEnvironment } from "../../state/orchestration";
import { useEnvironmentQuery } from "../../state/query";

const stepLabel = (type: string) => {
const words = type.toLowerCase().replaceAll("_", " ");
return words.charAt(0).toUpperCase() + words.slice(1);
};

/** Read-only view of what a provider-native subagent's children did. Mount only while open. */
export function NativeChildWorkSheet(props: {
readonly environmentId: EnvironmentId;
readonly threadId: ThreadId;
readonly onClose: () => void;
}) {
const insets = useSafeAreaInsets();
const query = useEnvironmentQuery<OrchestrationV2GetNativeChildWorkResult, unknown>(
orchestrationEnvironment.nativeChildWork({
environmentId: props.environmentId,
input: { threadId: props.threadId },
}),
);
const children = query.data?.children;
return (
<Sheet title="Subagent work" onClose={props.onClose}>
<ScrollView
contentInsetAdjustmentBehavior="automatic"
contentContainerStyle={{
gap: 16,
paddingHorizontal: 20,
paddingTop: 12,
paddingBottom: Math.max(20, insets.bottom),
}}
>
<Text className="font-sans text-xs text-foreground-secondary">
Read only. These subagents cannot take messages.
</Text>
{query.error !== null ? (
<Text selectable className="font-sans text-sm text-danger-foreground">
{query.error}
</Text>
) : children === undefined ? (
<Text className="font-sans text-sm text-foreground-secondary">Loading…</Text>
) : children.length === 0 ? (
<Text className="font-sans text-sm text-foreground-secondary">
The provider has no record of which subagents this call started.
</Text>
) : (
children.map((child) => <NativeChild key={child.conversationId} child={child} />)
)}
<View className="flex-row justify-end">
<Button
label="Refresh"
tone="secondary"
disabled={query.isPending}
onPress={query.refresh}
/>
</View>
</ScrollView>
</Sheet>
);
}

function NativeChild(props: { readonly child: OrchestrationV2NativeChildWork }) {
const { child } = props;
return (
<View className="gap-2">
<Text className="font-t3-bold text-sm text-foreground">{child.role || "Subagent"}</Text>
<Text selectable className="font-sans text-sm text-foreground-secondary">
{child.prompt}
</Text>
{child.truncated ? (
<Text className="font-sans text-xs text-foreground-secondary">
Earlier steps are not shown.
</Text>
) : null}
{child.steps === null ? (
<Text className="font-sans text-xs text-foreground-secondary">No transcript yet.</Text>
) : (
<View className="gap-2 border-l border-border ps-3">
{child.steps.map((step) => (
<View key={step.index} className="gap-1">
<Text className="font-t3-bold text-xs text-foreground-secondary">
{stepLabel(step.type)}
{step.status !== null && step.status !== "DONE"
? ` · ${stepLabel(step.status)}`
: ""}
</Text>
{step.toolCalls.length === 0 ? null : (
<Text selectable className="font-mono text-xs text-foreground">
{step.toolCalls.map((call) => `${call.name} ${call.input}`).join("\n")}
</Text>
)}
{step.text === null ? null : (
<Text selectable numberOfLines={12} className="font-mono text-xs text-foreground">
{step.text}
</Text>
)}
</View>
))}
</View>
)}
</View>
);
}
13 changes: 12 additions & 1 deletion apps/mobile/src/features/threads/ProviderSubagentBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ import {
formatProviderSubagentStatus,
type ProviderSubagentStatus,
} from "@t3tools/client-runtime/state/thread-execution";
import { isOrchestrationV2WorkActive } from "@t3tools/contracts";
import { isOrchestrationV2WorkActive, type EnvironmentId, type ThreadId } from "@t3tools/contracts";
import { useState } from "react";
import { View } from "react-native";

import { AppText as Text } from "../../components/ui/AppText";
import { ProviderIcon } from "../../components/ProviderIcon";
import { Button } from "../../components/ui/Button";
import { NativeChildWorkSheet } from "./NativeChildWorkSheet";
import { useVisibleSecondClock } from "./use-visible-second-clock";

export function ProviderSubagentBar(props: {
Expand All @@ -16,7 +18,10 @@ export function ProviderSubagentBar(props: {
readonly effortLabel: string | null;
readonly status: ProviderSubagentStatus | null;
readonly onOpenParent: (() => void) | null;
/** Set for providers whose children T3 reads on demand instead of streaming. */
readonly inspect: { readonly environmentId: EnvironmentId; readonly threadId: ThreadId } | null;
}) {
const [inspecting, setInspecting] = useState(false);
const live = props.status !== null && isOrchestrationV2WorkActive(props.status.status);
const nowMs = useVisibleSecondClock(live);
const statusLabel = formatProviderSubagentStatus(props.status, nowMs);
Expand Down Expand Up @@ -59,9 +64,15 @@ export function ProviderSubagentBar(props: {
{statusLabel} · Runs on its own
</Text>
</View>
{props.inspect ? (
<Button label="Work" tone="secondary" onPress={() => setInspecting(true)} />
) : null}
{props.onOpenParent ? (
<Button label="Open parent" tone="secondary" onPress={props.onOpenParent} />
) : null}
{props.inspect && inspecting ? (
<NativeChildWorkSheet {...props.inspect} onClose={() => setInspecting(false)} />
) : null}
</View>
);
}
8 changes: 8 additions & 0 deletions apps/mobile/src/features/threads/ThreadDetailScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1722,6 +1722,14 @@ function ThreadDetailBody(
reportedModelSelection,
)}
status={props.providerSubagentStatus ?? null}
inspect={
providerSubagentProvider?.driver === "antigravity"
? {
environmentId: props.environmentId,
threadId: props.selectedThread.id,
}
: null
}
onOpenParent={
props.selectedThread.lineage.parentThreadId === null
? null
Expand Down
2 changes: 1 addition & 1 deletion apps/server/scripts/acp-mock-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -512,7 +512,7 @@ const program = Effect.gen(function* () {
parameterizedModelPicker = request.capabilities?._meta?.parameterizedModelPicker === true;
return {
protocolVersion: 2,
info: { name: "t3-acp-mock-agent", version: "0.0.0" },
info: { name: "t3-acp-mock-agent", version: process.env.T3_ACP_AGENT_VERSION ?? "0.0.0" },
...(omitInitializeModelState ? {} : { _meta: { modelState: modelState() } }),
capabilities: {
session: {
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/auth/RpcAuthorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export const RPC_REQUIRED_SCOPES = {
...CLIENT_GUARDED_RPC_SCOPES,
[ORCHESTRATION_V2_WS_METHODS.dispatchCommand]: AuthOrchestrationOperateScope,
[ORCHESTRATION_V2_WS_METHODS.getWorkflowScript]: AuthOrchestrationReadScope,
[ORCHESTRATION_V2_WS_METHODS.getNativeChildWork]: AuthOrchestrationReadScope,
[ORCHESTRATION_V2_WS_METHODS.stopWorkflow]: AuthOrchestrationOperateScope,
[ORCHESTRATION_V2_WS_METHODS.forceStopThread]: AuthOrchestrationOperateScope,
[ORCHESTRATION_V2_WS_METHODS.getTurnDiff]: AuthOrchestrationReadScope,
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/observability/RpcInstrumentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ type WsRpcMethod = RpcGroup.Rpcs<typeof WsRpcGroup>["_tag"];
const RPC_AGGREGATES = {
[ORCHESTRATION_V2_WS_METHODS.dispatchCommand]: "orchestrationV2",
[ORCHESTRATION_V2_WS_METHODS.getWorkflowScript]: "orchestration",
[ORCHESTRATION_V2_WS_METHODS.getNativeChildWork]: "orchestration",
[ORCHESTRATION_V2_WS_METHODS.stopWorkflow]: "orchestration",
[ORCHESTRATION_V2_WS_METHODS.forceStopThread]: "orchestration",
[ORCHESTRATION_V2_WS_METHODS.getTurnItem]: "orchestration",
Expand Down
157 changes: 157 additions & 0 deletions apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ import {
acpPostSettleContinuationOfferEvidence,
acpPostSettleWakeEvidence,
acpToolCallDiffPatch,
acpToolCallSnippetEdit,
makeAcpAdapterV2,
type AcpAdapterV2ExtensionContext,
type AcpAdapterV2Flavor,
Expand Down Expand Up @@ -138,6 +139,32 @@ describe("acpPerTurnPromptUsage", () => {
reasoningTokens: 3,
},
);
// Antigravity 1.4.5 for usageMetadata {prompt 42, cached 24, candidates 3, thoughts 9, total 54}.
assert.deepEqual(
acpPerTurnPromptUsage(
{
inputTokens: 42,
outputTokens: 3,
totalTokens: 54,
cachedReadTokens: 24,
cachedWriteTokens: 0,
thoughtTokens: 9,
},
false,
"completed",
true,
),
{
usageScope: "main_agent",
usageStatus: "complete",
hasSubagents: false,
inputTokens: 42,
outputTokens: 12,
cachedInputTokens: 24,
cacheCreationTokens: 0,
reasoningTokens: 9,
},
);
});

it("clamps subsets that exceed their parents and drops unset counters", () => {
Expand Down Expand Up @@ -231,6 +258,45 @@ describe("acpPerTurnPromptUsage", () => {
});

describe("acpToolCallDiffPatch", () => {
it("keeps an unplaced snippet as recorded text instead of numbering it from line 1", () => {
const content = [
{
type: "diff",
path: "/repo/a.ts",
oldText: "const target = 10;",
newText: "const target = 100;",
patch: { format: "snippet", text: "@@ -1,1 +1,1 @@ snippet, line unknown\n" },
},
];
assert.isUndefined(acpToolCallDiffPatch(content));
// An omitted preview is not re-diffed from the bare snippets either.
assert.isUndefined(acpToolCallDiffPatch([{ ...content[0], patch: { format: "omitted" } }]));
// A status block beside the snippet must not hide it.
for (const blocks of [
content,
[{ type: "content", content: { type: "text", text: "Edited." } }, ...content],
]) {
assert.isUndefined(acpToolCallDiffPatch(blocks));
assert.deepEqual(acpToolCallSnippetEdit(blocks), {
oldStr: "const target = 10;",
newStr: "const target = 100;",
});
}
// Beside another diff, the snippet is neither a patch nor a placed edit.
const twoDiffs = [
...content,
{
type: "diff",
path: "/repo/b.ts",
oldText: "b\n",
newText: "B\n",
patch: { text: "@@ -1 +1 @@\n-b\n+B\n" },
},
];
assert.isUndefined(acpToolCallDiffPatch(twoDiffs));
assert.isUndefined(acpToolCallSnippetEdit(twoDiffs));
});

it("builds per-file patches from ACP hunks and recorded old/new text", () => {
for (const operation of ["add", "delete"]) {
const text = operation === "add" ? "@@ -0,0 +1 @@\n+content\n" : "@@ -1 +0,0 @@\n-content\n";
Expand Down Expand Up @@ -661,6 +727,97 @@ describe("AcpAdapterV2", () => {
assert.equal(promptOrdinal, 2, "continuation turn must not dispatch session/prompt");
}).pipe(Effect.provide(layerTest), Effect.scoped),
);
it.live("fails an end_turn the outcome check rejects and restarts before the next turn", () =>
Effect.gen(function* () {
const path = yield* Path.Path;
const instanceId = ProviderInstanceId.make("acp-test-prompt-outcome");
const threadId = ThreadId.make("thread-acp-prompt-outcome");
const quota = {
class: "usage_limit",
message: "Quota reached.",
code: "RESOURCE_EXHAUSTED",
retryable: true,
resetAt: "2030-01-01T00:00:00.000Z",
} as const;
// The runtime ordinal of each prompt, and the session each check watched.
const promptRuntimes: Array<number> = [];
const watchedSessions: Array<string> = [];
const adapter = yield* makeAcpAdapterV2({
instanceId,
selfInvocation: yield* resolveSelfInvocation(),
flavor: {
driver: ACP_TEST_DRIVER,
capabilities: AcpProviderCapabilitiesV2,
watchPromptOutcome: (sessionId) =>
Effect.sync(() => {
watchedSessions.push(sessionId);
const failed = watchedSessions.length === 1;
return () => Effect.succeed(failed ? quota : undefined);
}),
makeRuntime: makeMockRuntime({
childProcessSpawner: yield* ChildProcessSpawner.ChildProcessSpawner,
mockAgentPath: yield* path.fromFileUrl(
new URL("../../../scripts/acp-mock-agent.ts", import.meta.url),
),
wrapRuntime: (runtime, runtimeOrdinal) => ({
...runtime,
prompt: () =>
Effect.sync(() => {
promptRuntimes.push(runtimeOrdinal);
return { stopReason: "end_turn" as const };
}),
}),
}),
},
});
const runtimePolicy = ProviderAdapter.ProviderAdapterV2RuntimePolicy.make({
runtimeMode: "full-access",
interactionMode: "default",
cwd: process.cwd(),
});
const modelSelection = { instanceId, model: "default" } as const;
const runtime = yield* adapter.openSession({
threadId,
providerSessionId: ProviderSessionId.make("provider-session-acp-prompt-outcome"),
modelSelection,
runtimePolicy,
});
const providerThread = yield* runtime.ensureThread({
threadId,
modelSelection,
runtimePolicy,
});
const runTurn = (ordinal: number) =>
Effect.gen(function* () {
yield* runtime.startTurn(
makeTurnInput({
threadId,
providerThread,
instanceId,
runtimePolicy,
now: yield* DateTime.now,
ordinal,
}),
);
const events = yield* runtime.events.pipe(
Stream.takeUntil((event) => event.type === "turn.terminal"),
Stream.runCollect,
);
return events.at(-1);
});

const failed = yield* runTurn(1);
if (failed?.type !== "turn.terminal" || failed.status !== "failed") {
return assert.fail("expected a failed terminal");
}
assert.deepEqual(failed.failure, quota);
const completed = yield* runTurn(2);
assert.equal(completed?.type === "turn.terminal" ? completed.status : null, "completed");
assert.deepEqual(promptRuntimes, [1, 2], "the next turn runs on a fresh runtime");
assert.equal(watchedSessions.length, 2);
assert.equal(watchedSessions[0], watchedSessions[1]);
}).pipe(Effect.provide(layerTest), Effect.scoped),
);
it.live("retains returned usage when Stop finalizes before the prompt callback", () =>
Effect.gen(function* () {
const path = yield* Path.Path;
Expand Down
Loading
Loading