Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
d6c6ebe
feat(web): add workspace thread tabs system
kbrianps Aug 15, 2026
4976664
fix(web): address review comments on workspace tabs
kbrianps Aug 15, 2026
9d0cf27
fix(settings): preserve existing comments in useSettingsRestore
kbrianps Aug 15, 2026
6f077c2
fix(web): support thread action menu from active tab and guard middle…
kbrianps Aug 15, 2026
f571f77
fix(web): preserve surviving active tab on close-all and fix focus-vi…
kbrianps Aug 15, 2026
4e2aaef
fix(web): avoid navigating away from surviving pinned tab in close-all
kbrianps Aug 15, 2026
0f102e1
fix(web): keep settled shelf collapsed when closed
kbrianps Aug 14, 2026
bf9a14c
feat(web): enable dragging threads across sidebar shelves
kbrianps Aug 14, 2026
1c69b1d
fix(web): address review findings on cross-shelf drag and drop
kbrianps Aug 15, 2026
6f3798a
fix(web): handle queued turns and await unsnooze/unsettle before pinning
kbrianps Aug 15, 2026
2ec9b74
feat(web): add unread notification badge to sidebar trigger
kbrianps Aug 15, 2026
c1ecf6a
fix(web): use semantic badge tokens and improve accessible labels
kbrianps Aug 15, 2026
f962bec
fix(web): use white text color for badge contrast on destructive back…
kbrianps Aug 15, 2026
721859f
fix(web): align unread notification badge directly with hasUnseenComp…
kbrianps Aug 15, 2026
7706434
test(web): tighten assertions on sidebar unread badge and aria-label
kbrianps Aug 15, 2026
e9422c3
feat(web): add turn completion sound with settings toggle
kbrianps Aug 15, 2026
92a4084
fix(web): avoid false turn completion chimes during checkpoints and h…
kbrianps Aug 15, 2026
628b991
fix(web): clear tracking state on thread archive
kbrianps Aug 15, 2026
b6a0d73
feat(provider): add Antigravity provider driver
kbrianps Aug 14, 2026
e6d6c2e
fix(provider): address bot feedback on aliases, error hygiene, and pr…
kbrianps Aug 14, 2026
bb89553
fix(provider): handle spawn failure cleanup and persist settings patch
kbrianps Aug 14, 2026
8431253
fix(provider): wrap structured output schema in fromJsonString and cl…
kbrianps Aug 14, 2026
3711e17
fix(provider): pass prompt via stdin, handle instanceId and thread lo…
kbrianps Aug 14, 2026
ca17416
fix(provider): harden Antigravity adapter and text generation
kbrianps Aug 15, 2026
d5fa4a6
fix(provider): atomic monitor turn cleanup, complete turn on interrup…
kbrianps Aug 15, 2026
8827d57
fix(provider): emit turn.completed with interrupted state on interrupt
kbrianps Aug 15, 2026
653ca62
feat(web): add Codex status view
kbrianps Aug 12, 2026
8ac44cb
feat(web): mirror Codex CLI status
kbrianps Aug 12, 2026
6555daa
feat(status): add Claude rate limits
kbrianps Aug 13, 2026
42a37e9
fix(status): address review feedback
kbrianps Aug 13, 2026
7e6fe12
fix(provider): serialize refresh hooks
kbrianps Aug 13, 2026
c88dcac
fix(status): address follow-up review feedback
kbrianps Aug 13, 2026
664515a
fix(server): decouple claude capability initialization and usage time…
kbrianps Aug 15, 2026
e865ee3
fix(web): unify rate-limit meter rows, timestamp format, and card hig…
kbrianps Aug 15, 2026
7161540
fix(web): render % used for Claude and % left for Codex in RateLimitRow
kbrianps Aug 15, 2026
af14f74
fix(web): add motion-reduce to rate-limit progress bar
kbrianps Aug 15, 2026
2d01e02
fix(desktop): restore Linux dock icon association
kbrianps Aug 12, 2026
4e56082
test(desktop): cover Linux window class pre-ready
kbrianps Aug 12, 2026
5a69626
chore: clean up unused imports and variables in nightly build
kbrianps Aug 15, 2026
e55a6cc
feat(web): add cross-provider model handoff with structured context
kbrianps Aug 15, 2026
58e65d6
Merge branch 'feat/cross-provider-handoff' into brian-nightly
kbrianps Aug 15, 2026
3f326ff
fix(web): refine thread handoff dialog accessibility and attachment p…
kbrianps Aug 15, 2026
725cf20
feat(web): enable full-text message content search in sidebar
kbrianps Aug 15, 2026
00287b0
style(web): pair light and dark text colors for sidebar search role b…
kbrianps Aug 16, 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
23 changes: 22 additions & 1 deletion apps/desktop/src/app/DesktopPreReadyPlatform.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
import * as Context from "effect/Context";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import { beforeEach, vi } from "vite-plus/test";
import { afterEach, beforeEach, vi } from "vite-plus/test";

const { appendSwitchMock, getSwitchValueMock, hasSwitchMock, registerSchemesMock } = vi.hoisted(
() => ({
Expand Down Expand Up @@ -37,6 +37,10 @@ describe("DesktopPreReadyPlatform", () => {
registerSchemesMock.mockReset();
});

afterEach(() => {
vi.unstubAllEnvs();
});

it("reads an explicit Electron command-line switch value", () => {
const value = DesktopPreReadyPlatform.readCommandLineSwitchValue(
{
Expand Down Expand Up @@ -78,6 +82,23 @@ describe("DesktopPreReadyPlatform", () => {
assert.isNull(value);
});

it.effect("appends the packaged Linux window class before Electron is ready", () =>
Effect.gen(function* () {
vi.stubEnv("VITE_DEV_SERVER_URL", "");
hasSwitchMock.mockReturnValue(false);

const preReadyLayer = DesktopPreReadyPlatform.layer.pipe(
Layer.provide(Layer.succeed(HostProcessPlatform, "linux")),
);
const options = yield* Effect.gen(function* () {
return yield* DesktopPreReadyPlatform.DesktopPreReadyElectronOptions;
}).pipe(Effect.provide(preReadyLayer));

assert.equal(options.linux?.linuxWmClass, "t3code");
assert.deepEqual(appendSwitchMock.mock.calls[0], ["class", "t3code"]);
}),
);

it.effect(
"acquires a synchronous pre-ready layer before an asynchronous Clerk-shaped layer",
() =>
Expand Down
11 changes: 11 additions & 0 deletions apps/mobile/src/components/ProviderIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,17 @@ export function ProviderIcon(props: ProviderIconProps) {
);
}

if (props.provider === "antigravity") {
return (
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
<Path
fill="#1A73E8"
d="M12 24C12 17.3726 17.3726 12 24 12C17.3726 12 12 6.62742 12 0C12 6.62742 6.62742 12 0 12C6.62742 12 12 17.3726 12 24Z"
/>
</Svg>
);
}

if (props.provider === "grok") {
const fill = isDarkMode ? "#F5F5F5" : "#0F0F0F";
return (
Expand Down
1 change: 1 addition & 0 deletions apps/mobile/src/lib/modelOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ function providerDisplayLabel(provider: {
if (provider.displayName) return provider.displayName;
if (provider.driver === "codex") return "Codex";
if (provider.driver === "claudeAgent") return "Claude";
if (provider.driver === "antigravity") return "Antigravity";
return provider.instanceId;
}

Expand Down
168 changes: 168 additions & 0 deletions apps/server/src/provider/Drivers/AntigravityDriver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import { AntigravitySettings, ProviderDriverKind, type ServerProvider } from "@t3tools/contracts";
import * as Crypto from "effect/Crypto";
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as Path from "effect/Path";
import * as Schema from "effect/Schema";
import { HttpClient } from "effect/unstable/http";
import { ChildProcessSpawner } from "effect/unstable/process";

import * as BackgroundPolicy from "../../background/BackgroundPolicy.ts";
import { ServerConfig } from "../../config.ts";
import { ServerSettingsService } from "../../serverSettings.ts";
import { makeAntigravityTextGeneration } from "../../textGeneration/AntigravityTextGeneration.ts";
import { ProviderDriverError } from "../Errors.ts";
import { makeAntigravityAdapter } from "../Layers/AntigravityAdapter.ts";
import {
buildInitialAntigravityProviderSnapshot,
checkAntigravityProviderStatus,
enrichAntigravitySnapshot,
} from "../Layers/AntigravityProvider.ts";
import { ProviderEventLoggers } from "../Layers/ProviderEventLoggers.ts";
import { makeManagedServerProvider } from "../makeManagedServerProvider.ts";
import {
defaultProviderContinuationIdentity,
type ProviderDriver,
type ProviderInstance,
} from "../ProviderDriver.ts";
import type { ServerProviderDraft } from "../providerSnapshot.ts";
import { mergeProviderInstanceEnvironment } from "../ProviderInstanceEnvironment.ts";
import {
makeManualOnlyProviderMaintenanceCapabilities,
makeStaticProviderMaintenanceResolver,
resolveProviderMaintenanceCapabilitiesEffect,
} from "../providerMaintenance.ts";
import {
haveProviderSnapshotSettingsChanged,
makeProviderSnapshotSettingsSource,
type ProviderSnapshotSettings,
} from "../providerUpdateSettings.ts";

const decodeAntigravitySettings = Schema.decodeSync(AntigravitySettings);

const DRIVER_KIND = ProviderDriverKind.make("antigravity");
const UPDATE = makeStaticProviderMaintenanceResolver(
makeManualOnlyProviderMaintenanceCapabilities({
provider: DRIVER_KIND,
packageName: null,
}),
);

export type AntigravityDriverEnv =
| BackgroundPolicy.BackgroundPolicy
| ChildProcessSpawner.ChildProcessSpawner
| Crypto.Crypto
| FileSystem.FileSystem
| HttpClient.HttpClient
| Path.Path
| ProviderEventLoggers
| ServerConfig
| ServerSettingsService;

const withInstanceIdentity =
(input: {
readonly instanceId: ProviderInstance["instanceId"];
readonly displayName: string | undefined;
readonly accentColor: string | undefined;
readonly continuationGroupKey: string;
}) =>
(snapshot: ServerProviderDraft): ServerProvider => ({
...snapshot,
instanceId: input.instanceId,
driver: DRIVER_KIND,
...(input.displayName ? { displayName: input.displayName } : {}),
...(input.accentColor ? { accentColor: input.accentColor } : {}),
continuation: { groupKey: input.continuationGroupKey },
});

export const AntigravityDriver: ProviderDriver<AntigravitySettings, AntigravityDriverEnv> = {
driverKind: DRIVER_KIND,
metadata: {
displayName: "Antigravity",
supportsMultipleInstances: true,
},
configSchema: AntigravitySettings,
defaultConfig: (): AntigravitySettings => decodeAntigravitySettings({}),
create: ({ instanceId, displayName, accentColor, environment, enabled, config }) =>
Effect.gen(function* () {
const crypto = yield* Crypto.Crypto;
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
const httpClient = yield* HttpClient.HttpClient;
const serverSettings = yield* ServerSettingsService;
const eventLoggers = yield* ProviderEventLoggers;
const processEnv = mergeProviderInstanceEnvironment(environment);
const continuationIdentity = defaultProviderContinuationIdentity({
driverKind: DRIVER_KIND,
instanceId,
});
const stampIdentity = withInstanceIdentity({
instanceId,
displayName,
accentColor,
continuationGroupKey: continuationIdentity.continuationKey,
});
const effectiveConfig = { ...config, enabled } satisfies AntigravitySettings;
const maintenanceCapabilities = yield* resolveProviderMaintenanceCapabilitiesEffect(UPDATE, {
binaryPath: effectiveConfig.binaryPath,
env: processEnv,
});

const adapter = yield* makeAntigravityAdapter(effectiveConfig, {
environment: processEnv,
...(eventLoggers.native ? { nativeEventLogger: eventLoggers.native } : {}),
instanceId,
});
const textGeneration = yield* makeAntigravityTextGeneration(effectiveConfig, processEnv);

const checkProvider = checkAntigravityProviderStatus(effectiveConfig, processEnv).pipe(
Effect.map(stampIdentity),
Effect.provideService(Crypto.Crypto, crypto),
Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
);

const snapshotSettings = makeProviderSnapshotSettingsSource(effectiveConfig, serverSettings);
const snapshot = yield* makeManagedServerProvider<
ProviderSnapshotSettings<AntigravitySettings>
>({
maintenanceCapabilities,
getSettings: snapshotSettings.getSettings,
streamSettings: snapshotSettings.streamSettings,
haveSettingsChanged: haveProviderSnapshotSettingsChanged,
initialSnapshot: (settings) =>
buildInitialAntigravityProviderSnapshot(settings.provider).pipe(
Effect.map(stampIdentity),
),
checkProvider,
enrichSnapshot: ({ settings, snapshot: currentSnapshot, publishSnapshot }) =>
enrichAntigravitySnapshot({
snapshot: currentSnapshot,
maintenanceCapabilities,
enableProviderUpdateChecks: settings.enableProviderUpdateChecks,
publishSnapshot,
httpClient,
}),
}).pipe(
Effect.mapError(
(cause) =>
new ProviderDriverError({
driver: DRIVER_KIND,
instanceId,
detail: "Failed to build the Antigravity provider snapshot.",
cause,
}),
),
);

return {
instanceId,
driverKind: DRIVER_KIND,
continuationIdentity,
displayName,
accentColor,
enabled,
snapshot,
adapter,
textGeneration,
} satisfies ProviderInstance;
}),
};
1 change: 1 addition & 0 deletions apps/server/src/provider/Drivers/ClaudeDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv> = {
initialSnapshot: (settings) =>
makePendingClaudeProvider(settings.provider).pipe(Effect.map(stampIdentity)),
checkProvider,
beforeRefresh: Cache.invalidate(capabilitiesProbeCache, capabilitiesCacheKey),
enrichSnapshot: ({ settings, snapshot, publishSnapshot }) =>
enrichProviderSnapshotWithVersionAdvisory(snapshot, maintenanceCapabilities, {
enableProviderUpdateChecks: settings.enableProviderUpdateChecks,
Expand Down
141 changes: 141 additions & 0 deletions apps/server/src/provider/Layers/AntigravityAdapter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import * as NodeServices from "@effect/platform-node/NodeServices";
import { describe, expect, it } from "@effect/vitest";
import * as Effect from "effect/Effect";
import * as Fiber from "effect/Fiber";
import * as FileSystem from "effect/FileSystem";
import * as Path from "effect/Path";
import * as Schema from "effect/Schema";
import * as Stream from "effect/Stream";
import { AntigravitySettings, ProviderInstanceId, ThreadId } from "@t3tools/contracts";

import { makeAntigravityAdapter } from "./AntigravityAdapter.ts";

const decodeAntigravitySettings = Schema.decodeSync(AntigravitySettings);

const makeMockAgyScript = (lines: ReadonlyArray<string>) =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const dir = yield* fs.makeTempDirectory({ prefix: "agy-mock-" });
const scriptPath = path.join(dir, "fake-agy.sh");
const outputScript = [
"#!/bin/sh",
...lines.map((line) => `printf '%s\\n' '${line}'`),
"exit 0",
"",
].join("\n");
yield* fs.writeFileString(scriptPath, outputScript);
yield* fs.chmod(scriptPath, 0o755);
return scriptPath;
});

it.layer(NodeServices.layer)("makeAntigravityAdapter", (it) => {
it.effect("starts and stops a session", () =>
Effect.gen(function* () {
const adapter = yield* makeAntigravityAdapter(decodeAntigravitySettings({ enabled: true }));
const threadId = ThreadId.make("thread-1");

const session = yield* adapter.startSession({
threadId,
runtimeMode: "full-access",
});

expect(session.threadId).toBe(threadId);
expect(session.provider).toBe("antigravity");
expect(session.providerInstanceId).toBe("antigravity");
expect(session.status).toBe("ready");

yield* adapter.stopSession(threadId);
}),
);

it.effect("binds custom instanceId to session", () =>
Effect.gen(function* () {
const customInstanceId = ProviderInstanceId.make("antigravity-secondary");
const adapter = yield* makeAntigravityAdapter(decodeAntigravitySettings({ enabled: true }), {
instanceId: customInstanceId,
});
const threadId = ThreadId.make("thread-2");

const session = yield* adapter.startSession({
threadId,
runtimeMode: "full-access",
});

expect(session.providerInstanceId).toBe(customInstanceId);
yield* adapter.stopSession(threadId);
}),
);

it.effect("executes a turn and translates stream events", () =>
Effect.gen(function* () {
const mockScript = yield* makeMockAgyScript([
'{"event":"init","conversation_id":"conv-123"}',
'{"event":"step_update","step_update":{"step_index":0,"step_type":"agent_response","text_delta":"Hello from Antigravity!","state":"DONE","usage":{"total_tokens":100,"input_tokens":40,"output_tokens":60}}}',
'{"event":"result","result":{"conversation_id":"conv-123"}}',
]);

const adapter = yield* makeAntigravityAdapter(
decodeAntigravitySettings({
enabled: true,
binaryPath: mockScript,
}),
);

const threadId = ThreadId.make("thread-turn-test");
yield* adapter.startSession({ threadId, runtimeMode: "full-access" });

const runtimeEventsFiber = yield* Stream.take(adapter.streamEvents, 5).pipe(
Stream.runCollect,
Effect.forkChild,
);

yield* Effect.yieldNow;

const result = yield* adapter.sendTurn({
threadId,
input: "Hi",
});

expect(result.threadId).toBe(threadId);
expect(result.turnId).toBeDefined();

const eventsChunk = yield* Fiber.join(runtimeEventsFiber);
const types = Array.from(eventsChunk).map((e) => e.type);

expect(types).toContain("turn.started");
expect(types).toContain("session.state.changed");
expect(types).toContain("content.delta");
expect(types).toContain("thread.token-usage.updated");
expect(types).toContain("turn.completed");

yield* adapter.stopSession(threadId);
}),
);

it.effect("handles turn interruption safely", () =>
Effect.gen(function* () {
const mockScript = yield* makeMockAgyScript([
'{"event":"init","conversation_id":"conv-long"}',
]);

const adapter = yield* makeAntigravityAdapter(
decodeAntigravitySettings({
enabled: true,
binaryPath: mockScript,
}),
);

const threadId = ThreadId.make("thread-interrupt-test");
yield* adapter.startSession({ threadId, runtimeMode: "full-access" });

const turnResult = yield* adapter.sendTurn({
threadId,
input: "Test interrupt",
});

yield* adapter.interruptTurn(threadId, turnResult.turnId);
yield* adapter.stopSession(threadId);
}),
);
});
Loading
Loading