Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
0fa559a
feat(contracts): port Devin settings, model pricing, and usage contracts
mzakyali Sep 7, 2026
afdb0e2
feat(devin): port provider driver, adapter, and text generation
mzakyali Sep 7, 2026
cfc408e
feat(devin): port provider registry with Devin-specific retention
mzakyali Sep 7, 2026
74e7943
feat(devin): port usage service, pricing, and account consumption
mzakyali Sep 7, 2026
16598ce
test(devin): port ACP, adapter, provider, and text generation tests
mzakyali Sep 7, 2026
292721c
feat(devin): port web, mobile, and shared UI for Devin provider
mzakyali Sep 7, 2026
4189d7d
docs(devin): update provider documentation and add smoke harness
mzakyali Sep 7, 2026
36dce6f
feat(devin): discover skills via `devin skills list --json`
mzakyali Sep 7, 2026
8e57a98
feat(devin): dispatch known $skill mentions as @skills:name
mzakyali Sep 7, 2026
4251a5b
feat(devin): expose skills through snapshotForCwd
mzakyali Sep 7, 2026
11886c5
Merge remote-tracking branch 'upstream/main' into feat/devin-upstream…
mzakyali Sep 7, 2026
123105a
docs(devin): define ACP capability parity
mzakyali Sep 7, 2026
450ac0a
test(devin): harden MCP smoke cleanup
mzakyali Sep 7, 2026
e4e0a4c
test(devin): diagnose MCP handoff boundary
mzakyali Sep 7, 2026
b44b38b
test(devin): capture sanitized ACP capability fixtures
mzakyali Sep 7, 2026
08cf763
fix(devin): tighten sanitized ACP capture scope
mzakyali Sep 7, 2026
4732380
fix(devin): exclude startup metadata from ACP fixtures
mzakyali Sep 7, 2026
62cfa0c
feat(devin): normalize ACP resource content
mzakyali Sep 7, 2026
2cafd36
feat(devin): preserve normalized ACP resources
mzakyali Sep 7, 2026
5bbbf25
feat(devin): preserve normalized ACP resources
mzakyali Sep 7, 2026
8aa07bc
fix(client): retain ACP resource activity
mzakyali Sep 7, 2026
c4b3687
fix(client): preserve MCP resource extraction precedence
mzakyali Sep 7, 2026
b3cbec2
test(devin): verify elicitation across clients
mzakyali Sep 7, 2026
23f03be
test(devin): classify ACP subagent events
mzakyali Sep 7, 2026
39ef1bf
docs(devin): document ACP capability parity
mzakyali Sep 7, 2026
20a34b8
docs(devin): clarify resource capability status
mzakyali Sep 7, 2026
dcecb6d
test(devin): narrow subagent update fixture
mzakyali Sep 7, 2026
af85032
fix(devin): honor ACP lifecycle and approval boundaries
mzakyali Sep 7, 2026
1ac0dcd
fix(usage): isolate provider rates and restore compatible controls
mzakyali Sep 7, 2026
54f5e34
fix(client): display resources in expanded tool activity
mzakyali Sep 7, 2026
c15299b
fix(devin): preserve advertised approval options
mzakyali Sep 7, 2026
c6bb22a
test(mobile): complete Devin approval activity fixture
mzakyali Sep 7, 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
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, Devin, Grok Build, OpenCode, and Google Antigravity. If they're set up on your computer, T3 Code can control them.

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

Expand All @@ -13,11 +13,12 @@ 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, Devin, Grok Build, OpenCode, and Antigravity. 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`
> - Devin: install [Devin CLI](https://docs.devin.ai/work-with-devin/devin-cli) and run `devin auth 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.
Expand Down
Binary file added apps/mobile/assets/devin-logo.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
80 changes: 79 additions & 1 deletion apps/mobile/src/components/ProviderIcon.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { Image } from "expo-image";
import { Path, Svg } from "react-native-svg";
import { Image as NativeImage } from "react-native";
import { Circle, Path, Rect, Svg } from "react-native-svg";
import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider";

const DEVIN_LOGO = require("../../assets/devin-logo.webp") as number;

type ProviderIconProps = {
readonly provider: string | null | undefined;
readonly model?: string | null | undefined;
readonly size?: number;
};

Expand All @@ -12,6 +16,65 @@ export function ProviderIcon(props: ProviderIconProps) {
const isDarkMode = themeAppearance === "dark";
const size = props.size ?? 16;
const mono = isDarkMode ? "#e5e5e5" : "#171717";
const modelIdentity = props.model?.toLowerCase() ?? "";

if (props.provider === "devin" && modelIdentity) {
if (modelIdentity.includes("claude")) {
return <ProviderIcon provider="claudeAgent" size={size} />;
}
if (modelIdentity.includes("gpt") || modelIdentity.includes("codex")) {
return <ProviderIcon provider="codex" size={size} />;
}
if (modelIdentity.includes("grok")) {
return <ProviderIcon provider="grok" size={size} />;
}
if (modelIdentity.includes("gemini") || modelIdentity.includes("google")) {
return (
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
<Path
fill="#4E82EE"
d="M12 2c.7 5.3 4.7 9.3 10 10-5.3.7-9.3 4.7-10 10-.7-5.3-4.7-9.3-10-10 5.3-.7 9.3-4.7 10-10Z"
/>
</Svg>
);
}
if (
modelIdentity.includes("glm") ||
modelIdentity.includes("zhipu") ||
modelIdentity.includes("z.ai")
) {
return (
<Svg width={size} height={size} viewBox="0 0 30 30" fill="none">
<Rect x="1.5" y="1.5" width="27" height="27" rx="4" fill="#2D2D2D" />
<Path
fill="white"
d="M15.47 7.1 14.17 8.95c-.2.29-.54.47-.9.47h-7.1V7.09h9.3Zm8.83 0L13.14 22.91H5.7L16.86 7.1h7.44ZM14.53 22.91l1.31-1.86c.2-.29.54-.47.9-.47h7.09v2.33h-9.3Z"
/>
</Svg>
);
}
const brand = modelIdentity.includes("kimi")
? { color: "#6D5DFB", path: "M15.8 5.9a7 7 0 1 0 2.3 11.7 6.2 6.2 0 1 1-2.3-11.7Z" }
: modelIdentity.includes("deepseek")
? {
color: "#4D6BFE",
path: "M5.5 13.8c2.2.2 3.5-.4 4.5-1.8 1 1.8 2.8 2.7 5.2 2.4 1.3-.2 2.4-.8 3.3-1.8-.3 3.3-2.9 5.5-6.4 5.5-3.4 0-5.9-1.6-6.6-4.3Z",
}
: modelIdentity.includes("nemotron")
? {
color: "#76B900",
path: "M5.2 11.9c2.8-3.5 7.6-4.6 11.8-2.5-2.9-.4-5.6.3-7.3 2 1.4-1.1 3.8-1.4 5.5-.2 1.3.9 1.6 2.5.7 3.6-.8 1-2.4 1.2-3.5.5-1.7-1-1.6-3.5.2-4.3 2.2-1 4.8.7 4.8 3.2 0 1.6-.8 3-2 3.9-3.8 1.4-8.2-.5-9.5-4.3Z",
}
: null;
if (brand) {
return (
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
<Circle cx="12" cy="12" r="10" fill={brand.color} />
<Path d={brand.path} fill="white" />
</Svg>
);
}
}

if (props.provider?.trim().toLowerCase() === "antigravity") {
return (
Expand Down Expand Up @@ -61,6 +124,21 @@ export function ProviderIcon(props: ProviderIconProps) {
);
}

if (props.provider === "devin") {
return (
<NativeImage
source={DEVIN_LOGO}
resizeMode="contain"
style={{
width: size,
height: size,
tintColor: isDarkMode ? "#f5f5f5" : "#171717",
}}
accessible={false}
/>
);
}

if (props.provider === "opencode") {
return (
<Svg width={size} height={size} viewBox="0 0 32 40" fill="none">
Expand Down
5 changes: 5 additions & 0 deletions apps/mobile/src/features/threads/NewTaskDraftScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1353,6 +1353,11 @@ export function NewTaskDraftScreen(props: {
iconNode={
<ProviderIcon
provider={flow.selectedModelOption?.providerDriver}
model={
flow.selectedModelOption
? `${flow.selectedModelOption.selection.model} ${flow.selectedModelOption.label}`
: undefined
}
size={16}
/>
}
Expand Down
10 changes: 9 additions & 1 deletion apps/mobile/src/features/threads/ThreadComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -765,7 +765,15 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer
accessibilityLabel="Model and reasoning settings"
emphasized
iconNode={
<ProviderIcon provider={currentModelOption?.providerDriver} size={16} />
<ProviderIcon
provider={currentModelOption?.providerDriver}
model={
currentModelOption
? `${currentModelOption.selection.model} ${currentModelOption.label}`
: currentModelSelection.model
}
size={16}
/>
}
label={currentModelOption?.label ?? currentModelSelection.model}
maxWidth={152}
Expand Down
5 changes: 5 additions & 0 deletions apps/mobile/src/features/threads/ThreadSettingsSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,11 @@ function ModelRow(props: {
props.isLast ? "rounded-b-2xl" : "border-b border-border-subtle",
)}
>
<ProviderIcon
provider={props.option.providerDriver}
model={`${props.option.selection.model} ${props.option.label}`}
size={16}
/>
<View className="min-w-0 flex-1">
<View className="flex-row items-center gap-2">
<Text
Expand Down
104 changes: 96 additions & 8 deletions apps/mobile/src/features/usage/UsageRouteScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { EnvironmentId, USAGE_CONTRACT_VERSION } from "@t3tools/contracts";
import type { UsageAccountConsumption } from "@t3tools/contracts";
import { useNavigation } from "@react-navigation/native";
import {
isCompatibleUsageContractVersion,
Expand Down Expand Up @@ -76,7 +77,7 @@ export function UsageRouteScreen() {
const isPast24Hours = windowDays === 1;
const [selectedEnvironmentIds, setSelectedEnvironmentIds] =
useState<ReadonlySet<EnvironmentId> | null>(null);
const { merged, environments, selectedEnvironments, isPending, refresh } = useUsage(
const { merged, environments, selectedEnvironments, isPending, isPartial, refresh } = useUsage(
window,
selectedEnvironmentIds,
);
Expand Down Expand Up @@ -259,8 +260,6 @@ export function UsageRouteScreen() {
/>
) : (
<>
{/* Period and metric together: neither applies to Limits, and
both change every number below, so they share one bar. */}
<View className="flex-row items-center gap-3">
<SegmentedControl
options={WINDOW_OPTIONS}
Expand All @@ -277,11 +276,14 @@ export function UsageRouteScreen() {
className="w-36"
/>
</View>
{merged.duplicateSources.length > 0 ? (
<Text className="text-sm text-foreground-muted">
Counted once across environments sharing a transcript directory:{" "}
{merged.duplicateSources.join(", ")}
</Text>
<UsageCoverageNotice
environments={environments}
merged={merged}
isPartial={isPartial}
/>

{merged.accountUsage !== null ? (
<DevinAccountUsageSection usage={merged.accountUsage} />
) : null}
{isPending ? (
<Text className="py-16 text-center text-base text-foreground-muted">
Expand Down Expand Up @@ -318,6 +320,41 @@ export function UsageRouteScreen() {
);
}

function formatAcus(value: number): string {
return new Intl.NumberFormat("en-US", { maximumFractionDigits: 2 }).format(value);
}

function DevinAccountUsageSection({ usage }: { readonly usage: UsageAccountConsumption }) {
const message =
usage.status === "notConfigured"
? "Optional account ACU data is not configured on the server."
: usage.status === "forbidden"
? "The configured Devin key cannot read organization consumption."
: usage.status === "failed"
? (usage.message ?? "Devin account consumption could not be loaded.")
: null;

return (
<SettingsSection title="Devin account usage" card>
<View className="gap-1 p-4">
{usage.status === "available" ? (
<Text className="text-2xl font-t3-medium tabular-nums text-foreground">
{formatAcus(usage.totalAcus)} ACUs
</Text>
) : null}
<Text className="text-sm text-foreground-muted">
{message ?? "Official organization consumption for this window."}
</Text>
{usage.status === "available" ? (
<Text className="text-xs text-foreground-tertiary">
{usage.days.length} billing days returned · Source: {usage.source}
</Text>
) : null}
</View>
</SettingsSection>
);
}

function SegmentedControl<Value extends number | string>(props: {
readonly options: readonly {
readonly value: Value;
Expand Down Expand Up @@ -641,3 +678,54 @@ function usageEnvironmentStatus(environment: EnvironmentUsageStatus): string {
return environment.summary ? "Updating usage…" : "Loading usage…";
return "Usage up to date";
}

/**
* Says plainly when the totals are incomplete: an environment still answering,
* one that failed, or one whose transcripts another environment already
* reported.
*/
function UsageCoverageNotice(props: {
readonly environments: readonly EnvironmentUsageStatus[];
readonly merged: MergedUsage;
readonly isPartial: boolean;
}) {
const failed = props.environments.filter((environment) => environment.error !== null);
const stale = props.environments.filter((environment) =>
props.merged.staleEnvironments.includes(environment.environmentId),
);
const duplicateSources = props.merged.duplicateSources;
if (
failed.length === 0 &&
stale.length === 0 &&
duplicateSources.length === 0 &&
!props.isPartial
) {
return null;
}

return (
<View className="gap-1 rounded-[16px] border-continuous bg-card px-4 py-3">
{props.isPartial ? (
<Text className="text-sm text-foreground-muted">
Some environments are still reporting. Totals are partial.
</Text>
) : null}
{failed.map((environment) => (
<Text key={environment.environmentId} className="text-sm text-foreground-muted">
{environment.label} could not report usage.
</Text>
))}
{stale.map((environment) => (
<Text key={environment.environmentId} className="text-sm text-foreground-muted">
{environment.label} runs an older server version and is excluded from totals.
</Text>
))}
{duplicateSources.length > 0 ? (
<Text className="text-sm text-foreground-muted">
Counted once across environments sharing a transcript directory:{" "}
{duplicateSources.join(", ")}
</Text>
) : null}
</View>
);
}
6 changes: 5 additions & 1 deletion apps/mobile/src/features/usage/usageProviders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,27 @@ import { useAppearancePreferences } from "../settings/appearance/AppearancePrefe
* Series and table order. The chart stacks providers from the bottom in this
* order, so it also fixes which band sits on top of the bars.
*/
export const PROVIDER_ORDER: readonly UsageProviderKind[] = ["codex", "claude", "grok"];
export const PROVIDER_ORDER: readonly UsageProviderKind[] = ["codex", "claude", "grok", "devin"];

export const PROVIDER_LABEL: Record<UsageProviderKind, string> = {
claude: "Claude Code",
codex: "Codex",
grok: "Grok Build",
devin: "Devin ACP",
};

/**
* Claude's brand orange holds in both themes; Codex and Grok are neutrals and
* must flip with the theme or their bars vanish against the matching background.
* Devin uses its indigo accent in both themes so its provider band remains
* distinct from the neutral providers.
*/
export function useProviderColors(): Record<UsageProviderKind, string> {
const { themeAppearance: scheme } = useAppearancePreferences();
return {
claude: "#d97757",
codex: scheme === "dark" ? "#e6e6e6" : "#3c3c43",
grok: scheme === "dark" ? "#a1a1aa" : "#52525b",
devin: "#6b7cff",
};
}
Loading
Loading