Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# T3 Code

T3 Code is a minimal GUI for coding agents. A Node WebSocket server wraps provider CLIs and agents (Codex, Claude Code, Cursor, Grok, OpenCode, Antigravity) and serves web, desktop, and mobile clients.
T3 Code is a minimal GUI for coding agents. A Node WebSocket server wraps provider CLIs and agents (Codex, Claude Code, Cursor, Grok, OpenCode, Antigravity, Devin) and serves web, desktop, and mobile clients.

You can think of T3 Code as an open source "bring-your-own-subscription" alternative to apps like Claude Desktop, Codex App, Cursor Glass and Conductor.

Expand Down Expand Up @@ -68,7 +68,7 @@ The most common defect in this repo is a change that works on the path you teste

- **Entry points.** A behavior reachable from the chat view is usually also reachable from Settings, the command palette, and a keybinding. Fixing one is not fixing the feature.
- **Clients.** Web, desktop (wraps web, adds Electron shell/IPC), and mobile (React Native, separate navigation). Shared logic lives in `packages/client-runtime`
- **Providers.** Codex, Claude, Cursor, Grok, OpenCode, and Antigravity each have an adapter. Provider-shaped features need a decision per adapter, even if the decision is "not supported here".
- **Providers.** Codex, Claude, Cursor, Grok, OpenCode, Antigravity, and Devin each have an adapter. Provider-shaped features need a decision per adapter, even if the decision is "not supported here".
- **Contracts.** Anything crossing the wire is typed in `packages/contracts`. Change the schema and the server, web, mobile, and desktop all follow.
- **Reverse states.** If you added a way in, add the way out and the way to see it. Snooze needs unsnooze. Close needs reopen. A one-way door is a bug.
- **Connection modes.** Local, remote/relay, and tunnel behave differently. Multi-device and multi-environment cases are real.
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

T3 Code is an "agent harness control surface". It enables control of the agents on your machine with a best-in-class mobile app ([iOS](https://apps.apple.com/us/app/t3-code-remote-claude-more/id6787819824), [Android](https://play.google.com/store/apps/details?id=com.t3tools.t3code)), [web app](https://app.t3.codes) and [Electron-based desktop app](https://t3.codes).

Works with your subscriptions on Claude Code, Codex, Cursor, Grok Build, OpenCode, and Google Antigravity. If they're set up on your computer, T3 Code can control them.
Works with your subscriptions on Claude Code, Codex, Cursor, Grok Build, OpenCode, Google Antigravity, and Devin. If they're set up on your computer, T3 Code can control them.

## "Wait, what are you selling me?"

Expand All @@ -13,14 +13,15 @@ We wanted something performant, remote-ready, and truly open. If we ever go the
## Installation

> [!WARNING]
> T3 Code currently supports Codex, Claude, Cursor, Grok Build, OpenCode, and Antigravity. Install and authenticate at least one provider before use:
> T3 Code currently supports Codex, Claude, Cursor, Grok Build, OpenCode, Antigravity, and Devin. Install and authenticate at least one provider before use:
>
> - Codex: install [Codex CLI](https://developers.openai.com/codex/cli) and run `codex login`
> - Claude: install [Claude Code](https://claude.com/product/claude-code) and run `claude auth login`
> - Cursor: install [Cursor CLI](https://cursor.com/cli) and run `agent login`
> - Grok Build: install [Grok Build CLI](https://x.ai/cli) and run `grok login`
> - OpenCode: install [OpenCode](https://opencode.ai) and run `opencode auth login`
> - Antigravity: enable it in Settings, then use **Install Antigravity** and **Sign in with Google**. No CLI is required.
> - Devin: install [Devin CLI](https://docs.devin.ai/cli) and run `devin auth login`

### Try it out (install-free)

Expand Down
13 changes: 13 additions & 0 deletions apps/mobile/src/components/ProviderIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,19 @@ export function ProviderIcon(props: ProviderIconProps) {
);
}

if (props.provider === "devin") {
// Official Devin chain-link mark (docs.devin.ai/logo/favicon.svg),
// nominative use. Mono fill adapts to the theme like the other marks.
return (
<Svg width={size} height={size} viewBox="0 0 425 425" fill="none">
<Path
fill={mono}
d="M70 159.333V91.3471C70 88.3592 71.594 85.5983 74.1816 84.1044L133.043 50.1205C135.631 48.6265 138.819 48.6265 141.407 50.1205L200.269 84.1044C202.856 85.5983 204.45 88.3592 204.45 91.3471V126.068C204.708 137.606 210.806 148.734 221.531 154.926C232.256 161.117 244.942 160.834 255.063 155.289L285.132 137.929C287.719 136.435 290.907 136.435 293.495 137.929L352.357 171.913C354.944 173.406 356.538 176.167 356.538 179.155V247.123C356.538 250.111 354.944 252.872 352.357 254.366L293.495 288.35C290.907 289.844 287.719 289.844 285.132 288.35L255.306 271.13C245.146 265.456 232.344 265.117 221.534 271.358C210.809 277.55 204.711 288.678 204.453 300.215V334.926C204.453 337.914 202.859 340.675 200.271 342.169L141.41 376.153C138.822 377.647 135.634 377.647 133.046 376.153L74.1845 342.169C71.5969 340.675 70.0028 337.914 70.0028 334.926V266.959C70.0029 263.971 71.5969 261.21 74.1845 259.716L133.046 225.732C135.634 224.238 138.822 224.238 141.41 225.732L171.547 243.132C181.656 248.638 194.306 248.906 205.005 242.729C215.815 236.488 221.922 225.231 222.088 213.595C221.83 202.057 215.732 189.737 205.008 183.545C194.283 177.353 181.597 177.636 171.476 183.181L141.269 200.72C138.67 202.229 135.461 202.228 132.864 200.716L74.1576 166.562C71.5835 165.065 70 162.311 70 159.333Z"
/>
</Svg>
);
}

// codex (and unknown drivers)
return (
<Svg width={size} height={size} viewBox="0 0 256 260" fill="none">
Expand Down
193 changes: 193 additions & 0 deletions apps/server/src/provider/Drivers/DevinDriver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
/**
* DevinDriver — `ProviderDriver` for the Devin CLI (`devin acp`) runtime.
*
* Devin exposes an ACP-based CLI. Model catalog and capability refreshes
* happen during the managed provider status check via Devin's session
* config options.
*
* Text generation is supported via the ACP runtime — `makeDevinTextGeneration`
* drives `runtime.prompt` with a structured-output schema and collects the
* agent's `agent_message_chunk` stream into a single JSON blob.
*
* @module provider/Drivers/DevinDriver
*/
import { DevinSettings, ProviderDriverKind } 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 { makeDevinTextGeneration } from "../../textGeneration/DevinTextGeneration.ts";
import { ProviderDriverError } from "../Errors.ts";
import { makeDevinAdapter } from "../Layers/DevinAdapter.ts";
import {
buildInitialDevinProviderSnapshot,
checkDevinProviderStatus,
enrichDevinSnapshot,
} from "../Layers/DevinProvider.ts";
import { ProviderEventLoggers } from "../Layers/ProviderEventLoggers.ts";
import { makeManagedServerProvider } from "../makeManagedServerProvider.ts";
import {
defaultProviderContinuationIdentity,
type ProviderDriver,
type ProviderInstance,
} from "../ProviderDriver.ts";
import { withInstanceIdentity } from "./instanceIdentity.ts";
import { mergeProviderInstanceEnvironment } from "../ProviderInstanceEnvironment.ts";
import {
makeProviderMaintenanceCapabilities,
type ProviderMaintenanceCapabilitiesResolver,
resolveProviderMaintenanceCapabilitiesEffect,
} from "../providerMaintenance.ts";
import {
haveProviderSnapshotSettingsChanged,
makeProviderSnapshotSettingsSource,
type ProviderSnapshotSettings,
} from "../providerUpdateSettings.ts";
import { probeDevinSkills } from "./DevinSkills.ts";
const decodeDevinSettings = Schema.decodeSync(DevinSettings);

const DRIVER_KIND = ProviderDriverKind.make("devin");
const UPDATE: ProviderMaintenanceCapabilitiesResolver = {
resolve: (options) =>
makeProviderMaintenanceCapabilities({
provider: DRIVER_KIND,
packageName: null,
updateExecutable: options?.binaryPath?.trim() || "devin",
updateArgs: ["update"],
updateLockKey: "devin",
}),
};

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

export const DevinDriver: ProviderDriver<DevinSettings, DevinDriverEnv> = {
driverKind: DRIVER_KIND,
metadata: {
displayName: "Devin",
supportsMultipleInstances: true,
},
configSchema: DevinSettings,
defaultConfig: (): DevinSettings => decodeDevinSettings({}),
create: ({ instanceId, displayName, accentColor, environment, enabled, config }) =>
Effect.gen(function* () {
const crypto = yield* Crypto.Crypto;
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
const fileSystem = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
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,
driverKind: DRIVER_KIND,
displayName,
accentColor,
continuationGroupKey: continuationIdentity.continuationKey,
});
const effectiveConfig = { ...config, enabled } satisfies DevinSettings;
const maintenanceCapabilities = yield* resolveProviderMaintenanceCapabilitiesEffect(UPDATE, {
binaryPath: effectiveConfig.binaryPath,
env: processEnv,
});

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

const checkProvider = checkDevinProviderStatus(effectiveConfig, processEnv).pipe(
Effect.map(stampIdentity),
Effect.provideService(Crypto.Crypto, crypto),
Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
Effect.provideService(FileSystem.FileSystem, fileSystem),
Effect.provideService(Path.Path, path),
);

const snapshotSettings = makeProviderSnapshotSettingsSource(effectiveConfig, serverSettings);
const snapshot = yield* makeManagedServerProvider<ProviderSnapshotSettings<DevinSettings>>({
maintenanceCapabilities,
getSettings: snapshotSettings.getSettings,
streamSettings: snapshotSettings.streamSettings,
haveSettingsChanged: haveProviderSnapshotSettingsChanged,
initialSnapshot: (settings) =>
buildInitialDevinProviderSnapshot(settings.provider).pipe(Effect.map(stampIdentity)),
checkProvider,
// Model catalog and capabilities come from Devin's session
// config options during provider checks.
enrichSnapshot: ({ settings, snapshot: currentSnapshot, publishSnapshot }) =>
enrichDevinSnapshot({
settings: settings.provider,
snapshot: currentSnapshot,
maintenanceCapabilities,
enableProviderUpdateChecks: settings.enableProviderUpdateChecks,
publishSnapshot,
stampIdentity,
httpClient,
}).pipe(Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner)),
}).pipe(
Effect.mapError(
(cause) =>
new ProviderDriverError({
driver: DRIVER_KIND,
instanceId,
detail: `Failed to build Devin snapshot: ${cause.message ?? String(cause)}`,
cause,
}),
),
);

return {
instanceId,
driverKind: DRIVER_KIND,
continuationIdentity,
displayName,
accentColor,
enabled,
snapshot,
snapshotForCwd: (cwd) =>
!effectiveConfig.enabled
? snapshot.getSnapshot
: Effect.all([
snapshot.getSnapshot,
probeDevinSkills(cwd, processEnv).pipe(
Effect.provideService(FileSystem.FileSystem, fileSystem),
Effect.provideService(Path.Path, path),
Effect.mapError(
(cause) =>
new ProviderDriverError({
driver: DRIVER_KIND,
instanceId,
detail: `Failed to discover Devin skills for '${cwd}'`,
cause,
}),
),
),
]).pipe(Effect.map(([machineSnapshot, skills]) => ({ ...machineSnapshot, skills }))),
adapter,
textGeneration,
} satisfies ProviderInstance;
}),
};
Loading
Loading