|
No activity in this window.
@@ -567,7 +685,7 @@ export function UsagePage() {
? formatHourShort(period.hourStart, window.timeZone)
: formatDayShort(period.day)}
|
- {activeProviders.map((provider) => (
+ {visibleProviders.map((provider) => (
environment.error !== null);
const stale = environments.filter((environment) =>
staleEnvironments.includes(environment.environmentId),
);
- if (failed.length === 0 && stale.length === 0 && duplicateSources.length === 0) {
+ const hasDevinSource = environments.some((environment) =>
+ environment.summary?.sources.some(
+ (source) => source.fingerprint.provider === "devin" && source.status !== "missing",
+ ),
+ );
+ if (
+ failed.length === 0 &&
+ stale.length === 0 &&
+ duplicateSources.length === 0 &&
+ !hasDevinSource
+ ) {
return null;
}
@@ -655,10 +785,80 @@ function UsageCoverageNotice({
{duplicateSources.join(", ")}
) : null}
+ {hasDevinSource ? (
+
+ Devin ACP usage is read from this T3 server's local event logs. Devin account billing
+ is kept separate from the local token/cost estimate
+ {accountUsage?.status === "available" ? " and is shown below." : "."}
+
+ ) : null}
);
}
+function formatAcus(value: number): string {
+ return new Intl.NumberFormat("en-US", {
+ maximumFractionDigits: 2,
+ }).format(value);
+}
+
+/** Official Devin account units, deliberately kept separate from token costs. */
+function DevinAccountUsageCard({ usage }: { readonly usage: UsageAccountConsumption }) {
+ const statusMessage =
+ usage.status === "notConfigured"
+ ? "Optional account usage is not configured. Add DEVIN_API_KEY and DEVIN_ORG_ID as sensitive Devin provider environment variables."
+ : usage.status === "forbidden"
+ ? "The configured Devin API key cannot read organization consumption. Use a cog_ service key with ViewOrgConsumption permission."
+ : usage.status === "failed"
+ ? (usage.message ?? "Devin account consumption could not be loaded.")
+ : null;
+
+ return (
+
+
+
+ Devin account usage
+ Official ACUs for this window
+
+ {usage.status === "available" ? (
+
+ {formatAcus(usage.totalAcus)} ACUs
+
+ ) : null}
+
+
+ {statusMessage !== null ? (
+ {statusMessage}
+ ) : (
+ <>
+
+ {usage.days.length} billing days returned
+ Source: {usage.source}
+ {usage.fetchedAt !== null ? (
+ Updated {formatDateTimeShort(usage.fetchedAt)}
+ ) : null}
+
+ {usage.days.length > 0 ? (
+
+ {usage.days
+ .toReversed()
+ .slice(0, 5)
+ .map((day) => (
+
+ {formatDayShort(day.day)}
+
+ {formatAcus(day.acus)} ACUs
+
+
+ ))}
+
+ ) : null}
+ >
+ )}
+
+ );
+}
+
/** Environment selection and scan progress share a permanent header control. */
function UsageEnvironmentFilter({
environments,
@@ -669,6 +869,7 @@ function UsageEnvironmentFilter({
isPartial,
duplicateSources,
staleEnvironments,
+ accountUsage,
}: {
readonly environments: readonly EnvironmentUsageStatus[];
readonly selectedEnvironments: readonly EnvironmentUsageStatus[];
@@ -678,6 +879,7 @@ function UsageEnvironmentFilter({
readonly isPartial: boolean;
readonly duplicateSources: readonly string[];
readonly staleEnvironments: readonly string[];
+ readonly accountUsage: UsageAccountConsumption | null;
}) {
const [modelPricesOpen, setModelPricesOpen] = useState(false);
const allSelected = selectedEnvironmentIds === null;
@@ -791,6 +993,7 @@ function UsageEnvironmentFilter({
environments={selectedEnvironments}
duplicateSources={duplicateSources}
staleEnvironments={staleEnvironments}
+ accountUsage={accountUsage}
/>
) : null}
diff --git a/apps/web/src/components/usage/UsageProviderChart.test.ts b/apps/web/src/components/usage/UsageProviderChart.test.ts
index 622d73d13844..d894e773312c 100644
--- a/apps/web/src/components/usage/UsageProviderChart.test.ts
+++ b/apps/web/src/components/usage/UsageProviderChart.test.ts
@@ -89,6 +89,7 @@ describe("buildPeriodColumns", () => {
{ provider: "codex", value: 10 },
{ provider: "claude", value: 20 },
{ provider: "grok", value: 0 },
+ { provider: "devin", value: 0 },
]);
});
diff --git a/apps/web/src/components/usage/usageExport.test.ts b/apps/web/src/components/usage/usageExport.test.ts
new file mode 100644
index 000000000000..02e7916b040a
--- /dev/null
+++ b/apps/web/src/components/usage/usageExport.test.ts
@@ -0,0 +1,92 @@
+import { describe, expect, it } from "vite-plus/test";
+
+import { mergeUsage } from "@t3tools/shared/usageMerge";
+import { UsageDay } from "@t3tools/contracts";
+import { usageToCsv, usageToJson } from "./usageExport";
+
+const input = {
+ sinceDay: UsageDay.make("2026-08-10"),
+ untilDay: UsageDay.make("2026-08-11"),
+ timeZone: "UTC",
+ resolution: "day" as const,
+};
+
+describe("usage exports", () => {
+ it("exports model rows as escaped CSV", () => {
+ const usage = mergeUsage(
+ [
+ {
+ environmentId: "env-a" as never,
+ label: "Local",
+ summary: {
+ contractVersion: 6,
+ readAt: "2026-08-11T00:00:00.000Z",
+ timeZone: "UTC",
+ sinceDay: input.sinceDay,
+ untilDay: input.untilDay,
+ buckets: [
+ {
+ day: input.sinceDay,
+ provider: "devin",
+ model: "gpt,5.6",
+ totals: {
+ uncachedInputTokens: 10,
+ cachedInputTokens: 2,
+ cacheCreationTokens: 0,
+ outputTokens: 5,
+ reasoningTokens: 1,
+ },
+ costUsd: 0.25,
+ cacheSavingsUsd: 0,
+ costSource: "providerReported",
+ records: 1,
+ unpricedRecords: 0,
+ sessions: 1,
+ },
+ ],
+ sources: [
+ {
+ fingerprint: {
+ hostId: "host",
+ provider: "devin",
+ resolvedHomePath: "/logs",
+ volumeId: "1:2",
+ },
+ status: "ok",
+ scannedFiles: 1,
+ skippedFiles: 0,
+ malformedRecords: 0,
+ distinctSessions: 1,
+ message: null,
+ },
+ ],
+ pricing: {
+ status: "unavailable",
+ source: "test",
+ fetchedAt: null,
+ knownModels: 0,
+ },
+ scanDurationMs: 1,
+ },
+ },
+ ],
+ 6,
+ );
+ const csv = usageToCsv(usage, input);
+
+ expect(csv).toContain("Provider,Model,Tokens,Cost (USD),Records,Cost share");
+ expect(csv).toContain('devin,"gpt,5.6"');
+ expect(csv).toContain("Total tokens,17");
+ });
+
+ it("serializes provider maps in JSON instead of dropping them", () => {
+ const usage = mergeUsage([], 6);
+ const parsed = JSON.parse(usageToJson(usage, input)) as {
+ readonly daily: readonly unknown[];
+ readonly window: { readonly timeZone: string };
+ };
+
+ expect(parsed.window.timeZone).toBe("UTC");
+ expect(parsed.daily).toEqual([]);
+ });
+});
diff --git a/apps/web/src/components/usage/usageExport.ts b/apps/web/src/components/usage/usageExport.ts
new file mode 100644
index 000000000000..6e8d38ea3fe9
--- /dev/null
+++ b/apps/web/src/components/usage/usageExport.ts
@@ -0,0 +1,88 @@
+import { USAGE_CONTRACT_VERSION, type UsageSummaryInput } from "@t3tools/contracts";
+import type { MergedUsage } from "@t3tools/shared/usageMerge";
+
+function csvCell(value: string | number): string {
+ const text = String(value);
+ return /[",\r\n]/u.test(text) ? `"${text.replaceAll('"', '""')}"` : text;
+}
+
+function csvRow(values: readonly (string | number)[]): string {
+ return values.map(csvCell).join(",");
+}
+
+/** Creates a spreadsheet-friendly export without exposing raw transcripts. */
+export function usageToCsv(usage: MergedUsage, input: UsageSummaryInput): string {
+ const rows: string[] = [
+ csvRow(["T3 Code usage export"]),
+ csvRow(["Since", input.sinceDay]),
+ csvRow(["Until", input.untilDay]),
+ csvRow(["Time zone", input.timeZone]),
+ csvRow(["Contract version", USAGE_CONTRACT_VERSION]),
+ "",
+ csvRow(["Provider", "Model", "Tokens", "Cost (USD)", "Records", "Cost share"]),
+ ];
+
+ for (const model of usage.models) {
+ rows.push(
+ csvRow([
+ model.provider,
+ model.model,
+ model.totalTokens,
+ model.costUsd,
+ model.records,
+ model.costShare,
+ ]),
+ );
+ }
+
+ rows.push(
+ "",
+ csvRow(["Total tokens", usage.totalTokens]),
+ csvRow(["Total cost (USD)", usage.costUsd]),
+ csvRow(["Sessions", usage.sessions]),
+ );
+ if (usage.accountUsage !== null) {
+ rows.push(
+ "",
+ csvRow(["Devin account usage status", usage.accountUsage.status]),
+ csvRow(["Devin account ACUs", usage.accountUsage.totalAcus]),
+ csvRow(["Devin account source", usage.accountUsage.source]),
+ );
+ }
+ return `${rows.join("\r\n")}\r\n`;
+}
+
+/** JSON export counterpart for scripts and issue reports. */
+export function usageToJson(usage: MergedUsage, input: UsageSummaryInput): string {
+ const serializePeriods = (periods: readonly (typeof usage.daily)[number][]) =>
+ periods.map((period) => ({
+ ...period,
+ byProvider: Object.fromEntries(period.byProvider),
+ }));
+
+ return JSON.stringify(
+ {
+ exportedAt: new Date().toISOString(),
+ window: input,
+ totals: {
+ costUsd: usage.costUsd,
+ totalTokens: usage.totalTokens,
+ uncachedInputTokens: usage.uncachedInputTokens,
+ cachedInputTokens: usage.cachedInputTokens,
+ cacheCreationTokens: usage.cacheCreationTokens,
+ outputTokens: usage.outputTokens,
+ reasoningTokens: usage.reasoningTokens,
+ records: usage.records,
+ sessions: usage.sessions,
+ },
+ providers: usage.providers,
+ models: usage.models,
+ daily: serializePeriods(usage.daily),
+ hourly: serializePeriods(usage.hourly),
+ costQuality: usage.costQuality,
+ accountUsage: usage.accountUsage,
+ },
+ null,
+ 2,
+ );
+}
diff --git a/apps/web/src/components/usage/usageProviders.ts b/apps/web/src/components/usage/usageProviders.ts
index efad95e531ad..39e4587c9db1 100644
--- a/apps/web/src/components/usage/usageProviders.ts
+++ b/apps/web/src/components/usage/usageProviders.ts
@@ -1,6 +1,6 @@
import type { UsageProviderKind } from "@t3tools/contracts";
-import { ClaudeAI, GrokIcon, type Icon, OpenAI } from "../Icons";
+import { ClaudeAI, DevinIcon, GrokIcon, type Icon, OpenAI } from "../Icons";
type UsageProviderPresentation = {
readonly label: string;
@@ -30,6 +30,11 @@ export const PROVIDER_PRESENTATION = {
color: "color-mix(in oklab, var(--contrast-foreground) 72%, var(--background))",
mark: GrokIcon,
},
+ devin: {
+ label: "Devin ACP",
+ color: "#6b7cff",
+ mark: DevinIcon,
+ },
} satisfies Record;
/** Stable provider reading order across charts, summaries, tables, and hover rows. */
diff --git a/apps/web/src/composerDraftStore.test.ts b/apps/web/src/composerDraftStore.test.ts
index 4a682b97910d..0938fc64ce10 100644
--- a/apps/web/src/composerDraftStore.test.ts
+++ b/apps/web/src/composerDraftStore.test.ts
@@ -30,9 +30,11 @@ const CODEX_INSTANCE = ProviderInstanceId.make("codex");
const CODEX_SECONDARY_INSTANCE = ProviderInstanceId.make("codex_secondary");
const CLAUDE_AGENT_INSTANCE = ProviderInstanceId.make("claudeAgent");
const CURSOR_INSTANCE = ProviderInstanceId.make("cursor");
+const DEVIN_INSTANCE = ProviderInstanceId.make("devin");
const CODEX_DRIVER = ProviderDriverKind.make("codex");
const CLAUDE_AGENT_DRIVER = ProviderDriverKind.make("claudeAgent");
const CURSOR_DRIVER = ProviderDriverKind.make("cursor");
+const DEVIN_DRIVER = ProviderDriverKind.make("devin");
type ProviderOptionSelectionBag = ReadonlyArray;
type ProviderOptionSelectionsByProvider = Partial>;
@@ -961,7 +963,9 @@ describe("composerDraftStore element contexts", () => {
expect(accepted).toBe(true);
const draft = draftFor(threadId, TEST_ENVIRONMENT_ID);
expect(draft?.elementContexts).toHaveLength(1);
- const entry = draft?.elementContexts[0]!;
+ const entry = draft?.elementContexts[0];
+ expect(entry).toBeDefined();
+ if (!entry) return;
expect(entry.id.startsWith("el_")).toBe(true);
expect(entry.threadId).toBe(threadId);
expect(entry.pickedAt.length).toBeGreaterThan(0);
@@ -1996,6 +2000,19 @@ describe("composerDraftStore modelSelection", () => {
);
});
+ it("stores Devin options through the provider options compatibility setter", () => {
+ const store = useComposerDraftStore.getState();
+
+ store.setModelOptions(
+ threadRef,
+ providerModelOptions({ devin: { reasoning: "high", fast: true } }),
+ );
+
+ expect(
+ draftFor(threadId, TEST_ENVIRONMENT_ID)?.modelSelectionByProvider[DEVIN_INSTANCE],
+ ).toEqual(modelSelection(DEVIN_DRIVER, "adaptive", { reasoning: "high", fast: true }));
+ });
+
it("preserves other provider options when switching the active model selection", () => {
const store = useComposerDraftStore.getState();
@@ -2299,6 +2316,39 @@ describe("composerDraftStore model seed migration", () => {
},
);
+ it("keeps off-provider Devin options when upgrading legacy storage", async () => {
+ vi.useFakeTimers();
+ try {
+ const storage = useComposerDraftStore.persist.getOptions().storage;
+ expect(storage).toBeDefined();
+ storage?.setItem(COMPOSER_DRAFT_STORAGE_KEY, {
+ version: 2,
+ state: {
+ draftsByThreadId: {},
+ draftThreadsByThreadId: {},
+ projectDraftThreadIdByProjectId: {},
+ stickyProvider: "codex",
+ stickyModel: "gpt-5.6-terra",
+ stickyModelOptions: providerModelOptions({
+ devin: { reasoning: "high", fast: true },
+ }),
+ },
+ } as never);
+ await vi.advanceTimersByTimeAsync(300);
+
+ await useComposerDraftStore.persist.rehydrate();
+
+ expect(useComposerDraftStore.getState().stickyModelSelectionByProvider).toMatchObject({
+ [DEVIN_INSTANCE]: modelSelection(DEVIN_DRIVER, "adaptive", {
+ reasoning: "high",
+ fast: true,
+ }),
+ });
+ } finally {
+ vi.useRealTimers();
+ }
+ });
+
it("strips seeded models only from empty draft sessions when upgrading storage", async () => {
vi.useFakeTimers();
try {
diff --git a/apps/web/src/composerDraftStore.ts b/apps/web/src/composerDraftStore.ts
index 5bdd823b4d88..388a0c377fc7 100644
--- a/apps/web/src/composerDraftStore.ts
+++ b/apps/web/src/composerDraftStore.ts
@@ -265,6 +265,14 @@ type ProviderOptionSelectionsByProvider = Partial<
Record>
>;
+const LEGACY_MODEL_OPTION_PROVIDER_KEYS = [
+ "codex",
+ "claudeAgent",
+ "cursor",
+ "devin",
+ "opencode",
+] as const;
+
type LegacyCodexFields = {
effort?: unknown;
codexFastMode?: unknown;
@@ -970,7 +978,7 @@ function normalizeProviderModelOptions(
): ProviderOptionSelectionsByProvider | null {
const candidate = value && typeof value === "object" ? (value as Record) : null;
const result: ProviderOptionSelectionsByProvider = {};
- for (const providerKey of ["codex", "claudeAgent", "cursor", "opencode"] as const) {
+ for (const providerKey of LEGACY_MODEL_OPTION_PROVIDER_KEYS) {
const selections = coerceProviderOptionSelections(candidate?.[providerKey]);
if (selections) {
result[providerKey] = selections;
@@ -1129,7 +1137,7 @@ function legacyToModelSelectionByProvider(
): Partial> {
const result: Partial> = {};
if (modelOptions) {
- for (const provider of ["codex", "claudeAgent", "cursor", "opencode"] as const) {
+ for (const provider of LEGACY_MODEL_OPTION_PROVIDER_KEYS) {
const options = modelOptions[provider];
if (options && options.length > 0) {
const driverKind = ProviderDriverKind.make(provider);
@@ -3076,7 +3084,7 @@ const composerDraftStore = create()(
}
const base = existing ?? createEmptyThreadDraft();
const nextMap = { ...base.modelSelectionByProvider };
- for (const provider of ["codex", "claudeAgent", "cursor", "opencode"] as const) {
+ for (const provider of LEGACY_MODEL_OPTION_PROVIDER_KEYS) {
if (!modelOptions || !(provider in modelOptions)) continue;
const opts = modelOptions[provider];
const driverKind = ProviderDriverKind.make(provider);
diff --git a/apps/web/src/lib/contextWindow.test.ts b/apps/web/src/lib/contextWindow.test.ts
index b87c0664403e..6e8a52d6c871 100644
--- a/apps/web/src/lib/contextWindow.test.ts
+++ b/apps/web/src/lib/contextWindow.test.ts
@@ -1,7 +1,17 @@
import { describe, expect, it } from "vite-plus/test";
-import { EventId, type OrchestrationThreadActivity, TurnId } from "@t3tools/contracts";
+import {
+ EventId,
+ ProviderDriverKind,
+ ProviderInstanceId,
+ type OrchestrationThreadActivity,
+ TurnId,
+} from "@t3tools/contracts";
-import { deriveLatestContextWindowSnapshot, formatContextWindowTokens } from "./contextWindow";
+import {
+ deriveKnownContextWindowSnapshot,
+ deriveLatestContextWindowSnapshot,
+ formatContextWindowTokens,
+} from "./contextWindow";
function makeActivity(id: string, kind: string, payload: unknown): OrchestrationThreadActivity {
return {
@@ -83,4 +93,104 @@ describe("contextWindow", () => {
expect(snapshot?.usedTokens).toBe(81_659);
expect(snapshot?.totalProcessedTokens).toBe(748_126);
});
+
+ it("uses the selected Devin model catalog limit before ACP usage arrives", () => {
+ const instanceId = ProviderInstanceId.make("devin");
+ const snapshot = deriveKnownContextWindowSnapshot({
+ selection: { instanceId, model: "glm-5-2" },
+ providers: [
+ {
+ instanceId,
+ driver: ProviderDriverKind.make("devin"),
+ enabled: true,
+ installed: true,
+ version: null,
+ status: "ready",
+ auth: { status: "authenticated" },
+ checkedAt: "2026-03-23T00:00:00.000Z",
+ models: [
+ {
+ slug: "glm-5-2",
+ name: "GLM-5.2",
+ isCustom: false,
+ capabilities: { optionDescriptors: [] },
+ contextWindowTokens: 200_000,
+ },
+ ],
+ slashCommands: [],
+ skills: [],
+ },
+ ],
+ updatedAt: "2026-03-23T00:00:00.000Z",
+ });
+
+ expect(snapshot).toMatchObject({
+ usedTokens: 0,
+ maxTokens: 200_000,
+ remainingTokens: 200_000,
+ usedPercentage: 0,
+ model: "glm-5-2",
+ });
+ });
+
+ it("uses the selected context-window option instead of the catalog maximum", () => {
+ const instanceId = ProviderInstanceId.make("devin");
+ const snapshot = deriveKnownContextWindowSnapshot({
+ selection: {
+ instanceId,
+ model: "glm-5-2",
+ options: [{ id: "contextWindow", value: "200k" }],
+ },
+ providers: [
+ {
+ instanceId,
+ driver: ProviderDriverKind.make("devin"),
+ enabled: true,
+ installed: true,
+ version: null,
+ status: "ready",
+ auth: { status: "authenticated" },
+ checkedAt: "2026-03-23T00:00:00.000Z",
+ models: [
+ {
+ slug: "glm-5-2",
+ name: "GLM-5.2",
+ isCustom: false,
+ capabilities: {
+ optionDescriptors: [
+ {
+ id: "contextWindow",
+ label: "Context window",
+ type: "select",
+ options: [
+ { id: "200k", label: "200K" },
+ { id: "1m", label: "1M" },
+ ],
+ },
+ ],
+ },
+ contextWindowTokens: 1_000_000,
+ },
+ ],
+ slashCommands: [],
+ skills: [],
+ },
+ ],
+ updatedAt: "2026-03-23T00:00:00.000Z",
+ });
+
+ expect(snapshot?.maxTokens).toBe(200_000);
+ expect(snapshot?.remainingTokens).toBe(200_000);
+ });
+
+ it("does not invent a context meter when the catalog has no limit", () => {
+ const instanceId = ProviderInstanceId.make("devin");
+ expect(
+ deriveKnownContextWindowSnapshot({
+ selection: { instanceId, model: "custom" },
+ providers: [],
+ updatedAt: "2026-03-23T00:00:00.000Z",
+ }),
+ ).toBeNull();
+ });
});
diff --git a/apps/web/src/lib/contextWindow.ts b/apps/web/src/lib/contextWindow.ts
index 4e2ee139cd85..13098622b67c 100644
--- a/apps/web/src/lib/contextWindow.ts
+++ b/apps/web/src/lib/contextWindow.ts
@@ -1,4 +1,9 @@
-import type { OrchestrationThreadActivity, ThreadTokenUsageSnapshot } from "@t3tools/contracts";
+import type {
+ ModelSelection,
+ OrchestrationThreadActivity,
+ ServerProvider,
+ ThreadTokenUsageSnapshot,
+} from "@t3tools/contracts";
function asRecord(value: unknown): Record | null {
return value && typeof value === "object" ? (value as Record) : null;
@@ -12,6 +17,22 @@ function asBoolean(value: unknown): boolean | null {
return typeof value === "boolean" ? value : null;
}
+/**
+ * Context-window options are provider-defined strings (Devin uses values such
+ * as `200k` and `1m`). Parse the common forms so the meter reflects the active
+ * option rather than always using the model's largest advertised limit.
+ */
+function parseContextWindowOption(value: unknown): number | null {
+ if (typeof value !== "string") return null;
+ const match = /^(\d+(?:\.\d+)?)\s*(k|m)?$/iu.exec(value.trim());
+ if (!match) return null;
+ const amount = Number(match[1]);
+ if (!Number.isFinite(amount) || amount <= 0) return null;
+ const multiplier = match[2]?.toLowerCase() === "m" ? 1_000_000 : match[2] ? 1_000 : 1;
+ const tokens = Math.round(amount * multiplier);
+ return tokens > 0 ? tokens : null;
+}
+
type NullableContextWindowUsage = {
readonly [Key in keyof ThreadTokenUsageSnapshot]: undefined extends ThreadTokenUsageSnapshot[Key]
? Exclude | null
@@ -25,6 +46,30 @@ export type ContextWindowSnapshot = NullableContextWindowUsage & {
readonly updatedAt: string;
};
+/** Map a provider driver kind to a user-facing display name. */
+export function formatProviderDisplayName(provider: string | null | undefined): string {
+ if (!provider) return "This agent";
+ switch (provider) {
+ case "claudeAgent":
+ case "claude":
+ return "Claude";
+ case "codex":
+ return "Codex";
+ case "devin":
+ return "Devin";
+ case "cursor":
+ return "Cursor";
+ case "opencode":
+ return "OpenCode";
+ default: {
+ // Title-case unknown driver kinds so they read reasonably.
+ const trimmed = provider.replace(/Agent$/i, "").trim();
+ if (trimmed.length === 0) return provider;
+ return trimmed.charAt(0).toUpperCase() + trimmed.slice(1);
+ }
+ }
+}
+
export function deriveLatestContextWindowSnapshot(
activities: ReadonlyArray,
): ContextWindowSnapshot | null {
@@ -51,22 +96,30 @@ export function deriveLatestContextWindowSnapshot(
usedTokens,
totalProcessedTokens: asFiniteNumber(payload?.totalProcessedTokens),
maxTokens,
+ model: typeof payload?.model === "string" ? payload.model : null,
+ providerSessionId:
+ typeof payload?.providerSessionId === "string" ? payload.providerSessionId : null,
remainingTokens,
usedPercentage,
remainingPercentage,
inputTokens: asFiniteNumber(payload?.inputTokens),
cachedInputTokens: asFiniteNumber(payload?.cachedInputTokens),
+ cacheCreationTokens: asFiniteNumber(payload?.cacheCreationTokens),
outputTokens: asFiniteNumber(payload?.outputTokens),
reasoningOutputTokens: asFiniteNumber(payload?.reasoningOutputTokens),
lastUsedTokens: asFiniteNumber(payload?.lastUsedTokens),
lastInputTokens: asFiniteNumber(payload?.lastInputTokens),
lastCachedInputTokens: asFiniteNumber(payload?.lastCachedInputTokens),
+ lastCacheCreationTokens: asFiniteNumber(payload?.lastCacheCreationTokens),
lastOutputTokens: asFiniteNumber(payload?.lastOutputTokens),
lastReasoningOutputTokens: asFiniteNumber(payload?.lastReasoningOutputTokens),
toolUses: asFiniteNumber(payload?.toolUses),
durationMs: asFiniteNumber(payload?.durationMs),
compactsAutomatically: asBoolean(payload?.compactsAutomatically) ?? false,
autoCompactThreshold: asFiniteNumber(payload?.autoCompactThreshold),
+ lastCostUsd: asFiniteNumber(payload?.lastCostUsd),
+ sessionCostUsd: asFiniteNumber(payload?.sessionCostUsd),
+ costCurrency: typeof payload?.costCurrency === "string" ? payload.costCurrency : null,
updatedAt: activity.createdAt,
};
}
@@ -74,6 +127,66 @@ export function deriveLatestContextWindowSnapshot(
return null;
}
+/**
+ * Build a zero-usage context snapshot from provider catalog metadata.
+ *
+ * ACP providers are allowed to omit usage notifications until the first turn
+ * (and some older Devin CLI builds never send a usage update at all). Keeping
+ * the known model limit visible avoids hiding the context control merely
+ * because no token event has arrived yet. A real `context-window.updated`
+ * activity always takes precedence in the caller.
+ */
+export function deriveKnownContextWindowSnapshot(input: {
+ readonly selection: ModelSelection | null | undefined;
+ readonly providers: ReadonlyArray;
+ readonly updatedAt: string;
+}): ContextWindowSnapshot | null {
+ const selection = input.selection;
+ if (!selection) return null;
+
+ const provider = input.providers.find(
+ (candidate) => candidate.instanceId === selection.instanceId,
+ );
+ const model = provider?.models.find((candidate) => candidate.slug === selection.model);
+ const selectedContextWindow = selection.options?.find(
+ (option) => option.id === "contextWindow",
+ )?.value;
+ const maxTokens = parseContextWindowOption(selectedContextWindow) ?? model?.contextWindowTokens;
+ if (typeof maxTokens !== "number" || !Number.isFinite(maxTokens) || maxTokens <= 0) {
+ return null;
+ }
+
+ return {
+ usedTokens: 0,
+ totalProcessedTokens: 0,
+ maxTokens,
+ model: selection.model,
+ providerSessionId: null,
+ remainingTokens: maxTokens,
+ usedPercentage: 0,
+ remainingPercentage: 100,
+ inputTokens: null,
+ cachedInputTokens: null,
+ cacheCreationTokens: null,
+ outputTokens: null,
+ reasoningOutputTokens: null,
+ lastUsedTokens: null,
+ lastInputTokens: null,
+ lastCachedInputTokens: null,
+ lastCacheCreationTokens: null,
+ lastOutputTokens: null,
+ lastReasoningOutputTokens: null,
+ toolUses: null,
+ durationMs: null,
+ compactsAutomatically: false,
+ autoCompactThreshold: null,
+ lastCostUsd: null,
+ sessionCostUsd: null,
+ costCurrency: null,
+ updatedAt: input.updatedAt,
+ };
+}
+
export function formatContextWindowTokens(value: number | null): string {
if (value === null || !Number.isFinite(value)) {
return "0";
diff --git a/apps/web/src/session-logic.test.ts b/apps/web/src/session-logic.test.ts
index 401934bce5ba..ce9ab3028012 100644
--- a/apps/web/src/session-logic.test.ts
+++ b/apps/web/src/session-logic.test.ts
@@ -7,7 +7,12 @@ import {
type OrchestrationThreadActivity,
} from "@t3tools/contracts";
import { describe, expect, it } from "vite-plus/test";
+import { derivePendingRequests } from "@t3tools/client-runtime/pending-requests";
import { resolveWorkEntryToolPresentation } from "@t3tools/client-runtime/work-log/presentation";
+import {
+ hasToolActivityData,
+ toolActivityDataBody,
+} from "@t3tools/client-runtime/work-log/tool-presentation";
import {
createMessageAttachmentPreviewProjector,
@@ -61,6 +66,33 @@ function makeActivity(overrides: {
};
}
+describe("pending approvals", () => {
+ it("preserves Devin's advertised options without adding unavailable session actions", () => {
+ const options = [
+ { decision: "accept", label: "Allow once" },
+ { decision: "decline", label: "Reject" },
+ { decision: "cancel", label: "Cancel" },
+ ];
+ const requested = makeActivity({
+ kind: "approval.requested",
+ payload: {
+ requestId: "devin-permission",
+ requestType: "command_execution_approval",
+ options,
+ },
+ });
+
+ expect(derivePendingRequests([requested]).approvals).toEqual([
+ {
+ requestId: "devin-permission",
+ requestKind: "command",
+ createdAt: requested.createdAt,
+ options,
+ },
+ ]);
+ });
+});
+
describe("deriveActivePlanState", () => {
it("returns the latest plan update for the active turn", () => {
const activities: OrchestrationThreadActivity[] = [
@@ -825,6 +857,46 @@ describe("deriveWorkLogEntries", () => {
expect(entry?.toolLifecycleStatus).toBe("completed");
});
+ it("presents canonical ACP resource URI and text for generic tool activities", () => {
+ const data = {
+ toolCallId: "devin-resource-tool",
+ kind: "other",
+ resource: {
+ uri: "urn:acp:fixture:resource-link",
+ name: "schema fixture",
+ description: "typed protocol fixture",
+ mimeType: "text/markdown",
+ text: "Embedded resource notes",
+ },
+ content: [
+ {
+ type: "content",
+ content: { type: "text", text: "ordinary tool output" },
+ },
+ ],
+ };
+ const [entry] = deriveWorkLogEntries([
+ makeActivity({
+ id: "devin-resource-tool",
+ kind: "tool.completed",
+ summary: "Resource fixture",
+ payload: {
+ itemType: "dynamic_tool_call",
+ status: "completed",
+ title: "Resource fixture",
+ data,
+ },
+ }),
+ ]);
+
+ expect(entry?.toolData).toBe(data);
+ expect(entry).toBeDefined();
+ if (!entry) return;
+ expect(hasToolActivityData(entry)).toBe(true);
+ expect(toolActivityDataBody(entry)).toContain("urn:acp:fixture:resource-link");
+ expect(toolActivityDataBody(entry)).toContain("Embedded resource notes");
+ });
+
it("preserves MCP server, tool, arguments, and results for expanded display", () => {
const item = {
type: "mcpToolCall",
diff --git a/apps/web/src/session-logic.ts b/apps/web/src/session-logic.ts
index bbef3a4945a6..6f360c1440e6 100644
--- a/apps/web/src/session-logic.ts
+++ b/apps/web/src/session-logic.ts
@@ -16,7 +16,10 @@ import {
workLogEntryIsToolLike,
type WorkLogToolLifecycleStatus,
} from "@t3tools/client-runtime/work-log/presentation";
-import { extractToolActivityPresentation } from "@t3tools/client-runtime/work-log/tool-presentation";
+import {
+ extractToolActivityData,
+ extractToolActivityPresentation,
+} from "@t3tools/client-runtime/work-log/tool-presentation";
import {
isToolLifecycleItemType,
type AssetResource,
@@ -24,6 +27,7 @@ import {
type OrchestrationThreadActivity,
type OrchestrationProposedPlanId,
type ToolLifecycleItemType,
+ ProviderDriverKind,
type ThreadId,
type TurnId,
} from "@t3tools/contracts";
@@ -43,6 +47,49 @@ export type { PendingApproval, PendingUserInput } from "@t3tools/client-runtime/
export { formatDuration } from "@t3tools/shared/orchestrationTiming";
+export type ProviderPickerKind = ProviderDriverKind;
+
+export const PROVIDER_OPTIONS: Array<{
+ value: ProviderPickerKind;
+ label: string;
+ available: boolean;
+ /** Shown on the model picker sidebar when relevant */
+ pickerSidebarBadge?: "new" | "soon";
+}> = [
+ { value: ProviderDriverKind.make("codex"), label: "Codex", available: true },
+ { value: ProviderDriverKind.make("claudeAgent"), label: "Claude", available: true },
+ {
+ value: ProviderDriverKind.make("opencode"),
+ label: "OpenCode",
+ available: true,
+ pickerSidebarBadge: "new",
+ },
+ {
+ value: ProviderDriverKind.make("cursor"),
+ label: "Cursor",
+ available: true,
+ pickerSidebarBadge: "new",
+ },
+ {
+ value: ProviderDriverKind.make("devin"),
+ label: "Devin",
+ available: true,
+ pickerSidebarBadge: "new",
+ },
+ {
+ value: ProviderDriverKind.make("grok"),
+ label: "Grok",
+ available: true,
+ pickerSidebarBadge: "new",
+ },
+ {
+ value: ProviderDriverKind.make("antigravity"),
+ label: "Antigravity",
+ available: true,
+ pickerSidebarBadge: "new",
+ },
+];
+
export {
workEntryDisplayIndicatesToolFailure,
workEntryIndicatesToolSuccess,
@@ -581,12 +628,9 @@ function toDerivedWorkLogEntry(activity: OrchestrationThreadActivity): DerivedWo
if (toolPresentation.toolSource) {
entry.toolSource = toolPresentation.toolSource;
}
- if (itemType === "mcp_tool_call") {
- const data = asRecord(payload?.data);
- const toolData = typeof data?.toolName === "string" ? (data.item ?? data) : data?.item;
- if (toolData !== undefined) {
- entry.toolData = toolData;
- }
+ const toolData = extractToolActivityData(payload);
+ if (toolData !== undefined) {
+ entry.toolData = toolData;
}
if (itemType) {
entry.itemType = itemType;
diff --git a/docs/README.md b/docs/README.md
index 4e6f82bfb826..83ca551de557 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -17,7 +17,7 @@
- [Remote access](./user/remote-access.md)
- [Running in the background](./user/background-service.md)
- [Updating T3 Code](./user/updating.md)
-- Provider guides: [Codex](./user/providers-codex.md) · [Claude](./user/providers-claude.md) · [OpenCode](./user/providers-opencode.md) · [Antigravity](./user/providers-antigravity.md)
+- Provider guides: [Codex](./user/providers-codex.md) · [Claude](./user/providers-claude.md) · [Devin](./user/providers-devin.md) · [OpenCode](./user/providers-opencode.md) · [Antigravity](./user/providers-antigravity.md)
---
diff --git a/docs/internals/providers.md b/docs/internals/providers.md
index ec40c49810dc..c1311c70d91b 100644
--- a/docs/internals/providers.md
+++ b/docs/internals/providers.md
@@ -93,6 +93,40 @@ checkpoints but cannot roll back its conversation. The [checkpoint boundary](./o
therefore rejects revert before touching files. Native permission and question option IDs must
also survive normalization; a display label is not necessarily a valid reply.
+## Devin ACP
+
+Devin runs its CLI as an ACP subprocess (see
+[DevinDriver](../../apps/server/src/provider/Drivers/DevinDriver.ts)). Model discovery follows the
+same startup and settings-change refresh path as the other managed providers. Its CLI family
+variants are normalized into parent model rows with reasoning, speed, and context-window option
+descriptors before the snapshot is published. Devin snapshots carry a provider-owned model catalog
+version; when that version changes, the registry replaces (rather than merges) the previous
+snapshot and rewrites the per-instance status cache.
+
+Devin ACP telemetry is normalized in `DevinAdapter` from both `usage_update` notifications (context
+window and cumulative session cost) and prompt response usage (per-turn token deltas). The adapter
+emits the shared `thread.token-usage.updated` event, which feeds the composer context meter and the
+Usage service. Usage scans canonical `events..log` files only; native ACP protocol lines
+are retained for diagnostics but are not treated as billing records. When explicitly configured,
+the Usage service also reads Devin's organization consumption endpoint with a server-only service
+key and organization ID. That ACU result is an optional `UsageSummary.accountUsage` field and
+remains separate from token/cost buckets.
+
+The Devin capability boundary is:
+
+| Capability | Status | Boundary |
+| ------------------ | -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| MCP baseline | Local transport verified; live Devin handoff failing | `DevinAdapter` sends the existing T3 MCP HTTP server declaration with provider-scoped bearer auth. Direct local MCP `initialize` and `tools/list` work. The opt-in real Devin turn is not passing: it makes zero T3 MCP broker requests and does not return the marker. |
+| Resources | Transport and projection ready; live emission unverified | A pure typed ACP normalizer, adapter-boundary sanitization, canonical event retention, and generic web/mobile projection coverage are present. The installed Devin CLI emitted no resource blocks, so live Devin resource emission and rendering are unverified. |
+| Elicitation | Unsupported | Public ACP form requests do not provide a per-elicitation correlation ID, and the installed Devin CLI emitted none; no handler is wired. |
+| Child-agent events | Unsupported | No documented or typed stable child-agent stream, identity, lifecycle, or parent linkage was observed. Ordinary tools remain generic. Standard ACP does not guarantee a child-agent event stream, so the Agents panel cannot promise Devin child-agent telemetry. |
+
+These capabilities reuse the provider-neutral web, desktop, mobile, and remote paths. No Devin-specific
+UI or contract was added.
+
+The focused Devin MCP probe is run with `vp run test:devin-smoke`. It is opt-in and skips during
+normal tests. If `devin` is not on `PATH`, set `T3_DEVIN_BINARY_PATH` to the CLI binary.
+
## Attachments and stored history
Attachments live outside the project workspace. [ProviderService](../../apps/server/src/provider/Layers/ProviderService.ts)
diff --git a/docs/superpowers/specs/2026-09-07-devin-acp-capability-parity-design.md b/docs/superpowers/specs/2026-09-07-devin-acp-capability-parity-design.md
new file mode 100644
index 000000000000..59c7d36ec752
--- /dev/null
+++ b/docs/superpowers/specs/2026-09-07-devin-acp-capability-parity-design.md
@@ -0,0 +1,185 @@
+# Devin ACP Capability Parity Design
+
+**Status:** Draft for written-spec review\
+**Date:** 2026-09-07
+
+## Goal
+
+Bring Devin's MCP and richer ACP behavior to parity with the existing T3 provider experience across the server, shared contracts, client runtime, web, mobile, desktop, and remote connection modes.
+
+The implementation must reuse T3's existing provider-neutral activity, pending-request, and subagent models. Devin-specific code belongs at the ACP adapter boundary.
+
+## Current baseline
+
+The branch already contains the basic Devin MCP path:
+
+- `DevinAdapter` reads the thread-scoped `McpProviderSession` and passes the T3 MCP HTTP server to Devin ACP through `session/new` and `session/load`.
+- `McpSessionRegistry` issues provider-scoped bearer credentials and revokes them with the provider session.
+- `McpHttpServer` exposes the authenticated `/mcp` endpoint and the existing preview toolkit.
+- `scripts/devin-mcp-smoke.ts` runs an opt-in live test against the real Devin CLI and verifies `tools/list`, `preview_status`, and one MCP-enabled turn.
+- The shared runtime already represents MCP tool activity, pending requests, and subagent tasks for other providers.
+
+The Task 2 skill work did not alter this MCP path. The next work is a parity and capability expansion, not a second MCP transport.
+
+## Scope
+
+The work is divided into four independently testable phases.
+
+### Phase 1: MCP acceptance baseline
+
+Harden and run the existing real Devin MCP smoke path. The acceptance path must prove:
+
+1. Devin ACP starts with the T3 MCP server configuration.
+2. The MCP endpoint accepts the provider-scoped bearer credential.
+3. MCP initialization and `tools/list` succeed.
+4. Devin calls `preview_status` exactly once through T3's MCP broker.
+5. The assistant completes the turn with the expected response.
+6. The MCP session and provider credential are revoked during cleanup.
+
+This phase is opt-in and must not become part of the normal test suite because it requires a real authenticated Devin CLI and consumes a real provider turn.
+
+### Phase 2: Resource parity
+
+Support Devin resource content using the same representation and display behavior already used by other providers.
+
+The adapter will first capture real Devin ACP/MCP payloads. It will then normalize supported resource content into the existing provider runtime item/tool data shape. Existing text, tool-call, and activity presentation remains intact when a resource block is absent or unsupported.
+
+The default behavior is not a new Devin-specific resource card or resource browser. If the shared runtime cannot retain a required resource field, add an optional backward-compatible contract field and teach the existing web/mobile presentation layer to consume it for all providers that supply it.
+
+Malformed or unsupported resource blocks are nonfatal. The surrounding turn and ordinary text/tool activity must remain usable, and bounded raw data may be retained for diagnostics.
+
+### Phase 3: Elicitation parity
+
+Wire Devin ACP elicitation requests into T3's existing pending-request flow.
+
+- MCP-originated elicitation maps to the existing `mcp-elicitation` request kind and its current approval presentation when the payload has approval-style semantics.
+- General ACP questions map to the existing user-input request model when the payload contains structured questions and answers.
+- Web and mobile reuse the current pending request components and shared response commands.
+- Responses are translated back to the native Devin ACP response shape by the adapter.
+- Cancellation, interruption, session stop, model restart, connection failure, stale responses, and malformed requests settle safely without leaving a blocked turn.
+
+No Devin-specific elicitation UI is introduced.
+
+### Phase 4: Subagent parity
+
+Capture and inspect Devin's native subagent or child-agent events. Only events that expose stable identity, lifecycle, and parent linkage are mapped into the existing `task.*` runtime events and `subagentRuntime` fold.
+
+- Stable start, progress, and terminal signals become existing task activity.
+- Repeated lifecycle signals are idempotent.
+- Parent/child linkage is preserved when Devin supplies it.
+- Incomplete or unknown events remain diagnostic data and do not create misleading Agents-panel entries.
+- If the Devin CLI does not expose a stable child-agent stream, the provider reports the capability as unavailable rather than synthesizing subagents from ordinary tool calls.
+
+No Devin-specific Agents panel is introduced.
+
+## Non-goals
+
+- Replacing the existing T3 MCP server or credential model.
+- Creating a separate Devin-only contract family for events already represented by T3 contracts.
+- Adding a new resource browser, resource card, elicitation dialog, or Agents panel.
+- Changing MCP, ACP, or subagent behavior for other providers unless a narrowly scoped provider-neutral fix is required.
+- Treating ordinary tool calls as subagent events without stable Devin identity data.
+- Claiming support for a Devin capability that is not demonstrated by a real CLI payload or a protocol fixture derived from one.
+
+## Architecture and data flow
+
+```text
+Devin ACP / MCP
+ |
+ v
+DevinAdapter
+ - ACP request handlers
+ - Devin payload normalization
+ - MCP credential/session wiring
+ |
+ v
+ProviderRuntimeEvent
+ |
+ v
+Server ingestion and projections
+ |
+ +--> client-runtime activity/pending-request/subagent folds
+ |
+ +--> Web existing work-log and request UI
+ |
+ +--> Mobile existing activity and request UI
+```
+
+`AcpSessionRuntime` remains a transport and protocol seam. It may gain provider-neutral hooks only when the same behavior is useful to more than Devin and can be expressed without provider conditionals.
+
+The Devin adapter owns native method names, payload validation, lifecycle cleanup, and response conversion. The server's canonical event and request shapes remain the boundary shared by local, remote, relay, tunnel, desktop, web, and mobile clients.
+
+## Compatibility, failure, and security rules
+
+### Provider and client compatibility
+
+- Existing optional contract fields remain optional so older clients can read newer activity safely.
+- Existing web and mobile components remain the presentation path.
+- Desktop inherits web behavior.
+- Remote connections receive the same typed events as local connections.
+- Other provider adapters remain unchanged unless a shared ACP/runtime improvement is proven necessary.
+
+### Failure behavior
+
+- The MCP smoke test reports clear setup, authentication, protocol, tool-call, and cleanup failures.
+- Resource decode failures do not fail the surrounding turn.
+- Elicitation failures become visible provider request failures and never leave a permanently pending request.
+- Subagent events with insufficient identity are ignored for user-facing projections and retained only within bounded diagnostics.
+- Session stop, interruption, model restart, and connection loss settle pending approvals and user inputs using the existing cleanup behavior.
+
+### Security and performance
+
+- Provider-scoped MCP bearer credentials remain hashed in the registry and are revoked when their provider session or thread ends.
+- Logs never include bearer tokens, complete environment values, or complete user prompts.
+- Native payloads and tool output are bounded before persistence or runtime emission.
+- No discovery or probe runs once per ordinary turn.
+- Live tests are opt-in and never run against the user's shared T3 home.
+
+## Testing and acceptance
+
+### Server and provider tests
+
+- Devin ACP fixtures cover resource content, elicitation requests/responses, and every supported subagent event shape.
+- Devin adapter tests prove normalization, response round trips, cleanup, stale-request handling, duplicate lifecycle handling, and unsupported-event behavior.
+- MCP session registry and HTTP server tests continue to cover credential scope, authorization, tool listing, and revocation.
+- Existing Devin skill, ACP, provider, registry, usage, and text-generation tests remain green.
+
+### Contract and client-runtime tests
+
+- Contract schemas decode new optional fields and reject unsafe malformed data.
+- Provider runtime ingestion persists the canonical events correctly.
+- Pending-request folds preserve the existing `mcp-elicitation` and user-input behavior.
+- Subagent folds derive the same Agent-panel model used by other providers.
+
+### Web and mobile tests
+
+- Existing work-log components display Devin MCP/resource activity through the shared presentation path.
+- Existing pending-request components render and resolve Devin elicitation without a Devin-specific branch.
+- Existing Agents-panel components render mapped Devin subagents and ignore unsupported events.
+- Web and mobile typechecks pass independently.
+
+### Live acceptance
+
+When the real Devin CLI supports the relevant capability, add an opt-in live probe for it. The existing smoke test remains the required baseline for Phase 1. Resource, elicitation, and subagent live tests must use real captured protocol behavior; fixtures alone cannot claim provider support.
+
+### Final verification gates
+
+Before a phase is considered complete:
+
+- Focused tests for the changed server, contracts, client-runtime, web, and mobile paths pass.
+- Relevant server, web, and mobile typechecks pass.
+- `git diff --check` is clean and no conflict markers exist in source.
+- The live Devin MCP smoke test passes when the local CLI is available.
+- No unrelated provider behavior changes.
+- Any unsupported Devin capability is reported explicitly.
+
+## Rollout order
+
+Land and verify each phase independently:
+
+1. MCP smoke baseline.
+2. Resource parity.
+3. Elicitation parity.
+4. Subagent parity or an explicit unsupported-capability result based on protocol evidence.
+
+Each phase should produce a reviewable commit and a focused verification report before the next phase begins.
diff --git a/docs/user/install.md b/docs/user/install.md
index 17e9291bf1a0..0c820bdf55ee 100644
--- a/docs/user/install.md
+++ b/docs/user/install.md
@@ -67,14 +67,15 @@ and enable the provider you want. Installation, login, and configuration belong
to that environment's machine, even when you connect from a phone or another
computer.
-| Provider | Install and authenticate |
-| ----------- | -------------------------------------------------------------------------------------------- |
-| Codex | Install [Codex CLI](https://developers.openai.com/codex/cli), then run `codex login`. |
-| Claude | Install [Claude Code](https://claude.com/product/claude-code), then run `claude auth login`. |
-| Cursor | Install [Cursor CLI](https://cursor.com/cli), then run `agent login`. |
-| Grok Build | Install [Grok Build CLI](https://x.ai/cli), then run `grok login`. |
-| OpenCode | Install [OpenCode](https://opencode.ai), then run `opencode auth login`. |
-| Antigravity | Install and sign in with Google from T3 Code's provider settings. |
+| Provider | Install and authenticate |
+| ----------- | ------------------------------------------------------------------------------------------------------ |
+| Codex | Install [Codex CLI](https://developers.openai.com/codex/cli), then run `codex login`. |
+| Claude | Install [Claude Code](https://claude.com/product/claude-code), then run `claude auth login`. |
+| Cursor | Install [Cursor CLI](https://cursor.com/cli), then run `agent login`. |
+| Devin | Install the [Devin CLI](https://docs.devin.ai/work-with-devin/devin-cli), then run `devin auth login`. |
+| Grok Build | Install [Grok Build CLI](https://x.ai/cli), then run `grok login`. |
+| OpenCode | Install [OpenCode](https://opencode.ai), then run `opencode auth login`. |
+| Antigravity | Install and sign in with Google from T3 Code's provider settings. |
Provider CLIs must be on the server's `PATH`. If T3 Code cannot find one, set its
**Binary path** in provider settings, especially when using a version manager.
diff --git a/docs/user/providers-devin.md b/docs/user/providers-devin.md
new file mode 100644
index 000000000000..fb0fcade9eac
--- /dev/null
+++ b/docs/user/providers-devin.md
@@ -0,0 +1,77 @@
+# Devin
+
+T3 Code can run Devin through the Agent Client Protocol (ACP) exposed by the Devin CLI.
+
+## Setup
+
+1. Install the [Devin CLI](https://docs.devin.ai/work-with-devin/devin-cli).
+2. Authenticate in a terminal:
+
+ ```bash
+ devin auth login
+ ```
+
+3. Restart T3 Code so it inherits the updated `PATH`, then enable **Devin** in Settings.
+
+T3 Code starts `devin acp` for each session. The model picker is populated from
+`devin models list --format json`, including the `adaptive` model.
+
+Model families are refreshed when T3 Code starts and when Devin settings change. Reasoning level,
+speed, and context-window variants appear as options on a single family row; restarting T3 Code
+after upgrading this integration also refreshes any older cached model list.
+
+If T3 Code cannot find the executable, set **Binary path** in the Devin provider settings to the
+absolute path of the `devin` executable. On Windows, an existing T3 Code process may need to be
+restarted after the CLI installer updates your user `PATH`.
+
+## Usage and context window
+
+The **Usage** page includes token and cost records returned by Devin ACP prompt responses. These
+records are read from the T3 server's canonical provider event logs, so they are available for
+Devin sessions run through that server and remain local to the environment. The page labels this
+source as **Devin ACP** and supports model/provider filtering and CSV export.
+
+Devin's ACP `usage_update` notification also powers the context-window meter beside the composer.
+Open the meter to see the current context percentage, used/max tokens, total processed tokens, and
+provider-reported turn/session estimates when the CLI supplies them. A model switch keeps the T3
+thread identity and asks Devin to restore the prior session through ACP `session/load`.
+
+The Usage page's local token/cost estimate is not Devin plan billing. If you have an Enterprise
+organization, you can optionally add a separate `cog_...` Devin service-user key with
+`ViewOrgConsumption` permission and the organization ID as sensitive provider environment
+variables named `DEVIN_API_KEY` and `DEVIN_ORG_ID`. T3 calls Devin's organization consumption API
+on the server and shows returned ACUs in a separate card; ACUs are never mixed into token costs.
+The ordinary Devin CLI login token is not reused for this request. Without those optional
+variables, the page continues to show the local estimate and explains that account ACUs are not
+configured. Sessions outside T3 remain visible in Devin's Billing/Session Insights views.
+
+Provider event logs are retained for a limited period by the server's observability policy. Export
+or review a Usage window before rotating logs if you need a longer local record.
+
+## Capability limits
+
+Devin sessions use T3's shared ACP integration paths. The current Devin CLI does not provide
+a supported child-agent event stream or elicitation flow, and T3 preserves supported resource
+metadata through the generic activity path; live Devin resource emission/rendering has not been
+verified because the installed CLI emitted no resource blocks. These capabilities are therefore not
+available as Devin-specific controls in T3 Code.
+
+## Remote servers
+
+When you pair a phone or hosted web app with a remote T3 server, Devin runs on the remote machine.
+Install and authenticate the Devin CLI there, and configure its binary path in that server's
+provider settings; installing Devin only on the client device is not sufficient. A production build
+can serve the modified web UI, but it still needs a running compatible T3 server (`t3 serve`) for
+ACP sessions, Usage scanning, and event-log telemetry.
+
+## Permissions
+
+T3 Code maps its permission modes to Devin ACP modes:
+
+- **Ask** uses Devin's `ask` mode.
+- **Auto-accept edits** uses `accept-edits`.
+- **Auto** uses `smart`.
+- **Full access** uses `bypass`.
+- Plan interactions use Devin's `plan` mode.
+
+Devin support is currently marked Early Access.
diff --git a/docs/user/usage.md b/docs/user/usage.md
index 4be084ea299e..cda799269332 100644
--- a/docs/user/usage.md
+++ b/docs/user/usage.md
@@ -2,10 +2,17 @@
## Understand your usage
-**Usage** combines Codex, Claude Code, and Grok Build session history from your connected
+**Usage** combines Codex, Claude Code, Grok Build, and Devin ACP session history from your connected
environments. It shows token use, cache savings, model breakdowns, and estimated API-equivalent
cost. These estimates are not your subscription bill.
+Devin ACP records come from canonical T3 provider event logs and cover sessions driven through the
+selected T3 server. Use the provider filter above the breakdown to focus the chart and tables, and
+use the download button to export the current window as CSV. The page can optionally show official
+Devin organization ACUs in a separate section when a `cog_...` service key with
+`ViewOrgConsumption` permission and `DEVIN_ORG_ID` are configured on the server; ACUs are never
+combined with token-cost estimates. The normal CLI login is not an account-usage credential.
+
Totals depend on the history available on each server. Grok turns without a saved completed-turn
record are missing from the totals.
diff --git a/package.json b/package.json
index 4e5aca36d135..d8a9de42c269 100644
--- a/package.json
+++ b/package.json
@@ -32,6 +32,7 @@
"test": "vp run -r test",
"test:resource-monitor": "cargo test --locked --manifest-path native/resource-monitor/Cargo.toml",
"test:desktop-smoke": "vp run --filter @t3tools/desktop smoke-test",
+ "test:devin-smoke": "node scripts/devin-mcp-smoke.ts",
"fmt": "vp fmt",
"fmt:check": "vp fmt --check",
"dist:desktop:artifact": "node scripts/build-desktop-artifact.ts",
@@ -50,6 +51,7 @@
"@effect/tsgo": "catalog:",
"@types/node": "catalog:",
"@typescript/native-preview": "catalog:",
+ "@vitest/coverage-v8": "catalog:",
"knip": "6.34.0",
"vite-plus": "catalog:"
},
diff --git a/packages/client-runtime/src/state/subagentRuntime.ts b/packages/client-runtime/src/state/subagentRuntime.ts
index e441de32db48..c2558559022f 100644
--- a/packages/client-runtime/src/state/subagentRuntime.ts
+++ b/packages/client-runtime/src/state/subagentRuntime.ts
@@ -499,6 +499,7 @@ export function foldSubagentActivities(
}
const detail = asString(payload.detail);
if (detail && agent.title === agent.id) agent.title = detail;
+ agent.recentActivity = appendActivity(agent.recentActivity, at, "Started");
agent.updatedAt = at;
break;
}
@@ -537,6 +538,10 @@ export function foldSubagentActivities(
}
const error = asString(payload.error);
if (error) agent.error = bounded(error);
+ const status = asString(payload.status);
+ if (status) {
+ agent.recentActivity = appendActivity(agent.recentActivity, at, `Status: ${status}`);
+ }
agent.usage = mergeUsageMax(agent.usage, asUsage(payload.typedUsage));
agent.updatedAt = at;
break;
@@ -568,6 +573,9 @@ export function foldSubagentActivities(
if (endedAt && !wasTerminal && isTerminalSubagentStatus(agent.status)) {
agent.completedAt = endedAt;
}
+ if (status) {
+ agent.recentActivity = appendActivity(agent.recentActivity, at, `Status: ${status}`);
+ }
agent.updatedAt = at;
break;
}
@@ -597,6 +605,11 @@ export function foldSubagentActivities(
agent.result = agent.result ?? bounded(summary);
}
}
+ agent.recentActivity = appendActivity(
+ agent.recentActivity,
+ at,
+ summary ?? `Status: ${agent.status}`,
+ );
agent.usage = mergeUsageMax(agent.usage, asUsage(payload.typedUsage));
break;
}
@@ -609,6 +622,11 @@ export function foldSubagentActivities(
agent.result = bounded(summary);
}
}
+ agent.recentActivity = appendActivity(
+ agent.recentActivity,
+ at,
+ summary ?? `Status: ${status}`,
+ );
agent.usage = mergeUsageMax(agent.usage, asUsage(payload.typedUsage));
agent.updatedAt = at;
break;
diff --git a/packages/client-runtime/src/work-log/toolPresentation.test.ts b/packages/client-runtime/src/work-log/toolPresentation.test.ts
index fe88f15769c5..d25befe8fd46 100644
--- a/packages/client-runtime/src/work-log/toolPresentation.test.ts
+++ b/packages/client-runtime/src/work-log/toolPresentation.test.ts
@@ -1,8 +1,72 @@
import { describe, expect, it } from "@effect/vitest";
-import { extractToolActivityPresentation } from "./toolPresentation.ts";
+import {
+ extractToolActivityData,
+ extractToolActivityPresentation,
+ hasToolActivityData,
+ toolActivityDataBody,
+} from "./toolPresentation.ts";
+
+const devinResourceToolEvent = {
+ type: "item.completed",
+ payload: {
+ itemType: "dynamic_tool_call",
+ status: "completed",
+ title: "Resource fixture",
+ data: {
+ toolCallId: "devin-resource-tool",
+ kind: "other",
+ resource: {
+ uri: "urn:acp:fixture:resource-link",
+ name: "schema fixture",
+ description: "typed protocol fixture",
+ mimeType: "text/markdown",
+ },
+ content: [
+ {
+ type: "content",
+ content: { type: "text", text: "ordinary tool output" },
+ },
+ ],
+ },
+ },
+} as const;
describe("extractToolActivityPresentation", () => {
+ it("expands generic resource URI and embedded text without serializing collapsed rows", () => {
+ const resource = { uri: "urn:notes", text: "Resource notes" };
+ const entry = { itemType: "dynamic_tool_call", toolData: { resource } };
+ expect(hasToolActivityData(entry)).toBe(true);
+ expect(toolActivityDataBody(entry)).toBe(`Resource\n${JSON.stringify(resource, null, 2)}`);
+ expect(hasToolActivityData({ itemType: "dynamic_tool_call", toolData: {} })).toBe(false);
+ expect(toolActivityDataBody({ itemType: "dynamic_tool_call" })).toBeUndefined();
+ });
+
+ it("retains canonical ACP resource metadata for generic tool activity", () => {
+ expect(extractToolActivityData(devinResourceToolEvent.payload)).toBe(
+ devinResourceToolEvent.payload.data,
+ );
+ });
+
+ it("keeps MCP item extraction precedence when MCP data also has a resource", () => {
+ const item = {
+ type: "mcpToolCall",
+ server: "t3-code",
+ tool: "preview_status",
+ result: { content: [{ type: "text", text: "attached" }] },
+ };
+ const data = {
+ toolName: "mcp__t3_code__preview_status",
+ item,
+ resource: devinResourceToolEvent.payload.data.resource,
+ };
+
+ expect(extractToolActivityData({ itemType: "mcp_tool_call", data })).toBe(item);
+ expect(toolActivityDataBody({ itemType: "mcp_tool_call", toolData: item })).toBe(
+ `MCP call\n${JSON.stringify(item, null, 2)}`,
+ );
+ });
+
it("reads provider-neutral presentation fields", () => {
expect(
extractToolActivityPresentation({
diff --git a/packages/client-runtime/src/work-log/toolPresentation.ts b/packages/client-runtime/src/work-log/toolPresentation.ts
index 9b007bf81c33..d85534e59e41 100644
--- a/packages/client-runtime/src/work-log/toolPresentation.ts
+++ b/packages/client-runtime/src/work-log/toolPresentation.ts
@@ -11,6 +11,38 @@ export interface ExtractedToolActivityPresentation {
readonly toolSource?: ToolActivitySource;
}
+/**
+ * Keeps the bounded resource metadata that providers attach to an otherwise
+ * generic tool row. MCP rows retain their existing focused payload shape.
+ */
+export function extractToolActivityData(payloadValue: unknown): unknown {
+ const payload = asRecord(payloadValue);
+ const data = asRecord(payload?.data);
+ if (!data) return undefined;
+ if (payload?.itemType === "mcp_tool_call") {
+ return typeof data.toolName === "string" ? (data.item ?? data) : data.item;
+ }
+ return "resource" in data ? data : undefined;
+}
+
+type ToolActivityDataEntry = { readonly itemType?: string; readonly toolData?: unknown };
+
+/** Checks expandability without serializing payloads on collapsed rows. */
+export function hasToolActivityData(entry: ToolActivityDataEntry): boolean {
+ return entry.itemType === "mcp_tool_call"
+ ? entry.toolData !== undefined
+ : asRecord(asRecord(entry.toolData)?.resource) !== undefined;
+}
+
+/** Shared expanded detail for MCP calls and bounded generic resource metadata. */
+export function toolActivityDataBody(entry: ToolActivityDataEntry): string | undefined {
+ if (!hasToolActivityData(entry)) return undefined;
+ if (entry.itemType === "mcp_tool_call") {
+ return `MCP call\n${JSON.stringify(entry.toolData, null, 2)}`;
+ }
+ return `Resource\n${JSON.stringify(asRecord(entry.toolData)?.resource, null, 2)}`;
+}
+
function asRecord(value: unknown): Record | undefined {
return value !== null && typeof value === "object" && !Array.isArray(value)
? (value as Record)
diff --git a/packages/contracts/src/model.ts b/packages/contracts/src/model.ts
index bce1a766bc9b..dc3acc4fdb39 100644
--- a/packages/contracts/src/model.ts
+++ b/packages/contracts/src/model.ts
@@ -124,6 +124,16 @@ function canonicalSelectionsToLegacyObject(
export const ModelCapabilities = Schema.Struct({
optionDescriptors: Schema.optional(Schema.Array(ProviderOptionDescriptor)),
+ /**
+ * Per-model input modalities. All default to `true` when absent so providers
+ * that do not populate them keep their existing attachment behavior. Text is
+ * always supported and has no field. A provider sets a field to `false` only
+ * to signal that the model rejects that modality, so the composer can disable
+ * or warn about the corresponding attachment.
+ */
+ inputImages: Schema.optional(Schema.Boolean),
+ inputAudio: Schema.optional(Schema.Boolean),
+ inputFiles: Schema.optional(Schema.Boolean),
});
export type ModelCapabilities = typeof ModelCapabilities.Type;
@@ -143,9 +153,29 @@ export type CustomModelEntry = typeof CustomModelEntry.Type;
export const CustomModelSetting = Schema.Union([Schema.String, CustomModelEntry]);
export type CustomModelSetting = typeof CustomModelSetting.Type;
+/**
+ * Provider-reported model pricing, expressed in USD per one million tokens.
+ *
+ * This is deliberately metadata rather than a billing record: it describes
+ * the rate advertised by a provider at probe time and is used to calculate a
+ * transparent local estimate when a transcript does not include a cost.
+ */
+export const ModelPricing = Schema.Struct({
+ inputPerMillion: Schema.Number.check(Schema.isGreaterThanOrEqualTo(0)),
+ cachedInputPerMillion: Schema.optional(Schema.Number.check(Schema.isGreaterThanOrEqualTo(0))),
+ cacheCreationPerMillion: Schema.optional(Schema.Number.check(Schema.isGreaterThanOrEqualTo(0))),
+ outputPerMillion: Schema.Number.check(Schema.isGreaterThanOrEqualTo(0)),
+ /** The context advertised alongside the rate, when the provider supplies it. */
+ contextWindowTokens: Schema.optional(Schema.Number.check(Schema.isGreaterThanOrEqualTo(1))),
+ currency: Schema.optional(TrimmedNonEmptyString),
+ source: Schema.optional(TrimmedNonEmptyString),
+});
+export type ModelPricing = typeof ModelPricing.Type;
+
const CODEX_DRIVER_KIND = ProviderDriverKind.make("codex");
const CLAUDE_DRIVER_KIND = ProviderDriverKind.make("claudeAgent");
const CURSOR_DRIVER_KIND = ProviderDriverKind.make("cursor");
+const DEVIN_DRIVER_KIND = ProviderDriverKind.make("devin");
const GROK_DRIVER_KIND = ProviderDriverKind.make("grok");
const OPENCODE_DRIVER_KIND = ProviderDriverKind.make("opencode");
@@ -170,6 +200,7 @@ export const DEFAULT_MODEL_BY_PROVIDER: Partial>
[CODEX_DRIVER_KIND]: "Codex",
[CLAUDE_DRIVER_KIND]: "Claude",
[CURSOR_DRIVER_KIND]: "Cursor",
+ [DEVIN_DRIVER_KIND]: "Devin",
[GROK_DRIVER_KIND]: "Grok",
[OPENCODE_DRIVER_KIND]: "OpenCode",
};
diff --git a/packages/contracts/src/providerRuntime.ts b/packages/contracts/src/providerRuntime.ts
index af1baac74f9d..1dde17f037cb 100644
--- a/packages/contracts/src/providerRuntime.ts
+++ b/packages/contracts/src/providerRuntime.ts
@@ -316,19 +316,29 @@ export const ThreadTokenUsageSnapshot = Schema.Struct({
usedTokens: NonNegativeInt,
totalProcessedTokens: Schema.optional(NonNegativeInt),
maxTokens: Schema.optional(PositiveInt),
+ /** Canonical model/session attribution for providers that report it. */
+ model: Schema.optional(TrimmedNonEmptyStringSchema),
+ providerSessionId: Schema.optional(TrimmedNonEmptyStringSchema),
inputTokens: Schema.optional(NonNegativeInt),
cachedInputTokens: Schema.optional(NonNegativeInt),
+ cacheCreationTokens: Schema.optional(NonNegativeInt),
outputTokens: Schema.optional(NonNegativeInt),
reasoningOutputTokens: Schema.optional(NonNegativeInt),
lastUsedTokens: Schema.optional(NonNegativeInt),
lastInputTokens: Schema.optional(NonNegativeInt),
lastCachedInputTokens: Schema.optional(NonNegativeInt),
+ lastCacheCreationTokens: Schema.optional(NonNegativeInt),
lastOutputTokens: Schema.optional(NonNegativeInt),
lastReasoningOutputTokens: Schema.optional(NonNegativeInt),
toolUses: Schema.optional(NonNegativeInt),
durationMs: Schema.optional(NonNegativeInt),
compactsAutomatically: Schema.optional(Schema.Boolean),
autoCompactThreshold: Schema.optional(PositiveInt),
+ /** Provider-reported cost for the latest usage interval, when available. */
+ lastCostUsd: Schema.optional(Schema.Number.check(Schema.isGreaterThanOrEqualTo(0))),
+ /** Provider-reported cumulative cost for the session, when available. */
+ sessionCostUsd: Schema.optional(Schema.Number.check(Schema.isGreaterThanOrEqualTo(0))),
+ costCurrency: Schema.optional(TrimmedNonEmptyStringSchema),
});
export type ThreadTokenUsageSnapshot = typeof ThreadTokenUsageSnapshot.Type;
diff --git a/packages/contracts/src/server.ts b/packages/contracts/src/server.ts
index 3ea7bed8f1c4..f7f5fbaea80d 100644
--- a/packages/contracts/src/server.ts
+++ b/packages/contracts/src/server.ts
@@ -22,7 +22,7 @@ import {
ResolvedKeybindingsConfig,
} from "./keybindings.ts";
import { EditorId, FileManagerRevealKind, RemoteOpenTarget } from "./editor.ts";
-import { ModelCapabilities } from "./model.ts";
+import { ModelCapabilities, ModelPricing } from "./model.ts";
import { ProviderDriverKind, ProviderInstanceId } from "./providerInstance.ts";
import { ServerProviderUsageLimits, UsageLimitSourceSnapshots } from "./providerUsageLimits.ts";
import { ServerSettings } from "./settings.ts";
@@ -77,6 +77,12 @@ export const ServerProviderModel = Schema.Struct({
isDefault: Schema.optional(Schema.Boolean),
isLegacy: Schema.optional(Schema.Boolean),
capabilities: Schema.NullOr(ModelCapabilities),
+ /** Provider-advertised rate for this model/variant, when known. */
+ pricing: Schema.optional(ModelPricing),
+ /** Rates for the hidden concrete variants represented by a grouped model. */
+ pricingByVariant: Schema.optional(Schema.Record(Schema.String, ModelPricing)),
+ /** Provider-advertised context size for a standalone model. */
+ contextWindowTokens: Schema.optional(Schema.Number.check(Schema.isGreaterThanOrEqualTo(1))),
});
export type ServerProviderModel = typeof ServerProviderModel.Type;
@@ -222,6 +228,11 @@ export const ServerProvider = Schema.Struct({
// Human-readable reason populated when `availability === "unavailable"`.
// Surfaces in the UI alongside the missing-driver affordance.
unavailableReason: Schema.optional(TrimmedNonEmptyString),
+ // Optional provider-owned model catalog version. Providers that change the
+ // shape or identity of their model inventory can bump this value so stale
+ // persisted snapshots are ignored instead of being merged back into the
+ // newly discovered catalog.
+ modelCatalogVersion: Schema.optional(TrimmedNonEmptyString),
models: Schema.Array(ServerProviderModel),
slashCommands: Schema.Array(ServerProviderSlashCommand).pipe(
Schema.withDecodingDefault(Effect.succeed([])),
diff --git a/packages/contracts/src/settings.test.ts b/packages/contracts/src/settings.test.ts
index f01d24c3ac8c..2016171a31ad 100644
--- a/packages/contracts/src/settings.test.ts
+++ b/packages/contracts/src/settings.test.ts
@@ -7,6 +7,7 @@ import {
ClientSettingsPatch,
ClaudeSettings,
DEFAULT_SERVER_SETTINGS,
+ defaultEnabledForDriver,
resolveProviderInstanceEnabled,
ServerSettings,
ServerSettingsPatch,
@@ -441,10 +442,20 @@ describe("provider enabled defaults", () => {
expect(decoded.providers.codex.enabled).toBe(true);
expect(decoded.providers.claudeAgent.enabled).toBe(true);
expect(decoded.providers.cursor.enabled).toBe(false);
+ expect(decoded.providers.devin.enabled).toBe(false);
expect(decoded.providers.grok.enabled).toBe(false);
expect(decoded.providers.opencode.enabled).toBe(false);
});
+ it("derives per-driver defaults from the settings schemas", () => {
+ expect(defaultEnabledForDriver(ProviderDriverKind.make("codex"))).toBe(true);
+ expect(defaultEnabledForDriver(ProviderDriverKind.make("cursor"))).toBe(false);
+ expect(defaultEnabledForDriver(ProviderDriverKind.make("devin"))).toBe(false);
+ expect(defaultEnabledForDriver(ProviderDriverKind.make("grok"))).toBe(false);
+ // Unknown fork drivers stay enabled; their own build decides otherwise.
+ expect(defaultEnabledForDriver(ProviderDriverKind.make("ollama"))).toBe(true);
+ });
+
it("keeps Cursor enabled when an existing user explicitly opted in", () => {
const cursor = ProviderDriverKind.make("cursor");
const cursorId = ProviderInstanceId.make("cursor");
diff --git a/packages/contracts/src/settings.ts b/packages/contracts/src/settings.ts
index 7e25c8444dbb..5a0a827fa627 100644
--- a/packages/contracts/src/settings.ts
+++ b/packages/contracts/src/settings.ts
@@ -601,6 +601,31 @@ export const CursorSettings = makeProviderSettingsSchema(
);
export type CursorSettings = typeof CursorSettings.Type;
+export const DevinSettings = makeProviderSettingsSchema(
+ {
+ // Devin is opt-in because it requires a separately installed and authenticated CLI.
+ enabled: Schema.Boolean.pipe(
+ Schema.withDecodingDefault(Effect.succeed(false)),
+ Schema.annotateKey({ providerSettingsForm: { hidden: true } }),
+ ),
+ binaryPath: makeBinaryPathSetting("devin").pipe(
+ Schema.annotateKey({
+ title: "Binary path",
+ description: "Path to the Devin CLI binary.",
+ providerSettingsForm: { placeholder: "devin", clearWhenEmpty: "omit" },
+ }),
+ ),
+ customModels: Schema.Array(Schema.String).pipe(
+ Schema.withDecodingDefault(Effect.succeed([])),
+ Schema.annotateKey({ providerSettingsForm: { hidden: true } }),
+ ),
+ },
+ {
+ order: ["binaryPath"],
+ },
+);
+export type DevinSettings = typeof DevinSettings.Type;
+
export const GrokSettings = makeProviderSettingsSchema(
{
// Off by default (like Cursor and OpenCode): the binding is not yet
@@ -962,6 +987,7 @@ export const ServerSettings = Schema.Struct({
codex: CodexSettings.pipe(Schema.withDecodingDefault(Effect.succeed({}))),
claudeAgent: ClaudeSettings.pipe(Schema.withDecodingDefault(Effect.succeed({}))),
cursor: CursorSettings.pipe(Schema.withDecodingDefault(Effect.succeed({}))),
+ devin: DevinSettings.pipe(Schema.withDecodingDefault(Effect.succeed({}))),
grok: GrokSettings.pipe(Schema.withDecodingDefault(Effect.succeed({}))),
opencode: OpenCodeSettings.pipe(Schema.withDecodingDefault(Effect.succeed({}))),
antigravity: AntigravitySettings.pipe(Schema.withDecodingDefault(Effect.succeed({}))),
@@ -1009,7 +1035,7 @@ export const providerInstanceConfigEnabledFlag = (config: unknown): boolean | un
* through `DEFAULT_SERVER_SETTINGS`, so the schema's decoding default stays
* the single source of truth. Unknown (fork) drivers default to enabled.
*/
-const defaultEnabledForDriver = (driver: ProviderDriverKind): boolean => {
+export const defaultEnabledForDriver = (driver: ProviderDriverKind): boolean => {
const legacyDefaults = DEFAULT_SERVER_SETTINGS.providers as Record<
string,
{ readonly enabled?: boolean } | undefined
@@ -1113,6 +1139,12 @@ const CursorSettingsPatch = Schema.Struct({
customModels: Schema.optionalKey(Schema.Array(CustomModelSetting)),
});
+const DevinSettingsPatch = Schema.Struct({
+ enabled: Schema.optionalKey(Schema.Boolean),
+ binaryPath: Schema.optionalKey(TrimmedString),
+ customModels: Schema.optionalKey(Schema.Array(Schema.String)),
+});
+
const GrokSettingsPatch = Schema.Struct({
enabled: Schema.optionalKey(Schema.Boolean),
binaryPath: Schema.optionalKey(TrimmedString),
@@ -1192,6 +1224,7 @@ export const ServerSettingsPatch = Schema.Struct({
codex: Schema.optionalKey(CodexSettingsPatch),
claudeAgent: Schema.optionalKey(ClaudeSettingsPatch),
cursor: Schema.optionalKey(CursorSettingsPatch),
+ devin: Schema.optionalKey(DevinSettingsPatch),
grok: Schema.optionalKey(GrokSettingsPatch),
opencode: Schema.optionalKey(OpenCodeSettingsPatch),
antigravity: Schema.optionalKey(AntigravitySettingsPatch),
diff --git a/packages/contracts/src/usage.ts b/packages/contracts/src/usage.ts
index c36a4557c294..882a27eda423 100644
--- a/packages/contracts/src/usage.ts
+++ b/packages/contracts/src/usage.ts
@@ -3,7 +3,8 @@
*
* Each environment scans the provider CLIs' own on-disk session transcripts
* (`~/.claude/projects/**\/*.jsonl`, `~/.codex/sessions/**\/*.jsonl`,
- * `~/.grok/sessions/**\/updates.jsonl`) rather than relying on T3 Code's own
+ * `~/.grok/sessions/**\/updates.jsonl`, and T3's Devin ACP event logs) rather
+ * than relying on T3 Code's own
* orchestration projections, so usage stays complete even for turns that were
* never driven through T3 Code. This mirrors the approach `ccusage` takes.
*
@@ -12,6 +13,7 @@
*
* @module usage
*/
+import * as Effect from "effect/Effect";
import * as Schema from "effect/Schema";
import { NonNegativeInt, TrimmedNonEmptyString } from "./baseSchemas.ts";
@@ -21,18 +23,18 @@ import { NonNegativeInt, TrimmedNonEmptyString } from "./baseSchemas.ts";
* client renders partial coverage when an environment reports an older version
* rather than failing the whole page.
*/
-export const USAGE_CONTRACT_VERSION = 5 as const;
+export const USAGE_CONTRACT_VERSION = 6 as const;
/**
* Oldest {@link UsageSummary} version a current client will still merge.
*
- * v5 only adds `grok` to {@link UsageProviderKind}; v4 Claude/Codex buckets
- * remain valid, so mixed-version environments keep those totals instead of
- * treating every older server as stale.
+ * v5 adds Grok buckets; v6 adds Devin buckets and optional account usage.
+ * v4 Claude/Codex buckets remain valid, so mixed-version environments keep
+ * those totals instead of treating every older server as stale.
*/
export const USAGE_MERGE_COMPATIBLE_SINCE = 4 as const;
-export const UsageProviderKind = Schema.Literals(["claude", "codex", "grok"]);
+export const UsageProviderKind = Schema.Literals(["claude", "codex", "grok", "devin"]);
export type UsageProviderKind = typeof UsageProviderKind.Type;
/**
@@ -169,6 +171,39 @@ export const UsagePricing = Schema.Struct({
});
export type UsagePricing = typeof UsagePricing.Type;
+/**
+ * Optional account-level Devin consumption returned by the official API.
+ * This is intentionally separate from UsageBucket: ACUs are account billing
+ * units, not token counts, and must never be mixed into the local estimate.
+ */
+export const UsageAccountConsumptionStatus = Schema.Literals([
+ "available",
+ "notConfigured",
+ "forbidden",
+ "failed",
+]);
+export type UsageAccountConsumptionStatus = typeof UsageAccountConsumptionStatus.Type;
+
+export const UsageAccountConsumptionDay = Schema.Struct({
+ day: UsageDay,
+ acus: Schema.Number.check(Schema.isGreaterThanOrEqualTo(0)),
+ byProduct: Schema.Record(Schema.String, Schema.Number).pipe(
+ Schema.withDecodingDefault(Effect.succeed({})),
+ ),
+});
+export type UsageAccountConsumptionDay = typeof UsageAccountConsumptionDay.Type;
+
+export const UsageAccountConsumption = Schema.Struct({
+ provider: Schema.Literal("devin"),
+ status: UsageAccountConsumptionStatus,
+ source: TrimmedNonEmptyString,
+ fetchedAt: Schema.NullOr(Schema.String),
+ totalAcus: Schema.Number.check(Schema.isGreaterThanOrEqualTo(0)),
+ days: Schema.Array(UsageAccountConsumptionDay),
+ message: Schema.NullOr(TrimmedNonEmptyString),
+});
+export type UsageAccountConsumption = typeof UsageAccountConsumption.Type;
+
export const UsageSummaryInput = Schema.Struct({
/** Inclusive first day of the window, in `timeZone`. */
sinceDay: UsageDay,
@@ -197,6 +232,8 @@ export const UsageSummary = Schema.Struct({
buckets: Schema.Array(UsageBucket),
sources: Schema.Array(UsageSource),
pricing: UsagePricing,
+ /** Optional official Devin account consumption; absent when not configured. */
+ accountUsage: Schema.optional(UsageAccountConsumption),
/** Wall-clock cost of the scan, surfaced in diagnostics. */
scanDurationMs: NonNegativeInt,
});
diff --git a/packages/shared/src/model.test.ts b/packages/shared/src/model.test.ts
index d2c64e3d9458..038e18f4dc47 100644
--- a/packages/shared/src/model.test.ts
+++ b/packages/shared/src/model.test.ts
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vite-plus/test";
-import { ProviderInstanceId, type ModelCapabilities } from "@t3tools/contracts";
+import { ProviderDriverKind, ProviderInstanceId, type ModelCapabilities } from "@t3tools/contracts";
import {
applyClaudePromptEffortPrefix,
@@ -7,6 +7,7 @@ import {
buildProviderOptionSelectionsFromDescriptors,
createModelCapabilities,
createModelSelection,
+ getModelInputCapabilities,
getModelSelectionBooleanOptionValue,
getModelSelectionStringOptionValue,
getProviderOptionDescriptors,
@@ -14,6 +15,8 @@ import {
toCustomModelSetting,
getProviderOptionBooleanSelectionValue,
getProviderOptionStringSelectionValue,
+ normalizeCustomModelSlug,
+ normalizeModelSlug,
} from "./model.ts";
const codexCaps: ModelCapabilities = createModelCapabilities({
@@ -165,6 +168,68 @@ describe("descriptor helpers", () => {
});
});
+describe("model slug normalization", () => {
+ it("preserves exact custom slugs instead of expanding provider aliases", () => {
+ const cursor = ProviderDriverKind.make("cursor");
+
+ expect(normalizeModelSlug("opus-4.6", cursor)).toBe("claude-opus-4-6");
+ expect(normalizeCustomModelSlug(" opus-4.6 ")).toBe("opus-4.6");
+ });
+});
+
+describe("input capabilities", () => {
+ it("omits capability fields when no modality is disabled", () => {
+ const caps = createModelCapabilities({ optionDescriptors: [] });
+ expect(caps.inputImages).toBeUndefined();
+ expect(caps.inputAudio).toBeUndefined();
+ expect(caps.inputFiles).toBeUndefined();
+ });
+
+ it("only records modalities that are explicitly disabled", () => {
+ const caps = createModelCapabilities({
+ optionDescriptors: [],
+ inputImages: false,
+ inputAudio: false,
+ });
+ expect(caps.inputImages).toBe(false);
+ expect(caps.inputAudio).toBe(false);
+ expect(caps.inputFiles).toBeUndefined();
+ });
+
+ it("ignores explicit true so the wire shape stays minimal", () => {
+ const caps = createModelCapabilities({
+ optionDescriptors: [],
+ inputImages: true,
+ inputFiles: true,
+ });
+ expect(caps.inputImages).toBeUndefined();
+ expect(caps.inputFiles).toBeUndefined();
+ });
+
+ it("resolves absent fields to supported", () => {
+ expect(getModelInputCapabilities(undefined)).toEqual({
+ images: true,
+ audio: true,
+ files: true,
+ });
+ expect(getModelInputCapabilities({ optionDescriptors: [] })).toEqual({
+ images: true,
+ audio: true,
+ files: true,
+ });
+ });
+
+ it("resolves explicitly disabled fields to false", () => {
+ expect(
+ getModelInputCapabilities({
+ optionDescriptors: [],
+ inputImages: false,
+ inputAudio: false,
+ }),
+ ).toEqual({ images: false, audio: false, files: true });
+ });
+});
+
describe("applyClaudePromptEffortPrefix", () => {
it("keeps slash commands intact when ultrathink is selected", () => {
expect(applyClaudePromptEffortPrefix("/compact", "ultrathink")).toBe("/compact");
diff --git a/packages/shared/src/model.ts b/packages/shared/src/model.ts
index d6cb26f25be3..2c94682c6188 100644
--- a/packages/shared/src/model.ts
+++ b/packages/shared/src/model.ts
@@ -21,9 +21,36 @@ export interface SelectableModelOption {
export function createModelCapabilities(input: {
optionDescriptors: ReadonlyArray;
+ inputImages?: boolean;
+ inputAudio?: boolean;
+ inputFiles?: boolean;
}): ModelCapabilities {
return {
optionDescriptors: input.optionDescriptors.map(cloneDescriptor),
+ ...(input.inputImages === false ? { inputImages: false } : {}),
+ ...(input.inputAudio === false ? { inputAudio: false } : {}),
+ ...(input.inputFiles === false ? { inputFiles: false } : {}),
+ };
+}
+
+/**
+ * Resolved input modalities for a model. Absent fields default to `true`
+ * (supported) so providers that never populate them keep working. Text is
+ * always supported and is not represented here.
+ */
+export interface ModelInputCapabilities {
+ images: boolean;
+ audio: boolean;
+ files: boolean;
+}
+
+export function getModelInputCapabilities(
+ caps: ModelCapabilities | null | undefined,
+): ModelInputCapabilities {
+ return {
+ images: caps?.inputImages !== false,
+ audio: caps?.inputAudio !== false,
+ files: caps?.inputFiles !== false,
};
}
diff --git a/packages/shared/src/usageMerge.test.ts b/packages/shared/src/usageMerge.test.ts
index 6c706395c6ff..26b8fa507b42 100644
--- a/packages/shared/src/usageMerge.test.ts
+++ b/packages/shared/src/usageMerge.test.ts
@@ -2,13 +2,16 @@ import {
USAGE_CONTRACT_VERSION,
type EnvironmentId,
type UsageBucket,
+ type UsageAccountConsumption,
type UsageDay,
type UsageProviderKind,
- type UsageSummary,
+ UsageSummary,
} from "@t3tools/contracts";
import { describe, expect, it } from "vite-plus/test";
+import * as Schema from "effect/Schema";
import { mergeUsage, type EnvironmentUsage } from "./usageMerge.ts";
+const decodeUsageSummary = Schema.decodeUnknownSync(UsageSummary);
function bucket(overrides: Partial = {}): UsageBucket {
return {
@@ -42,6 +45,7 @@ function summary(
distinctSessions?: number;
}[],
contractVersion: number = USAGE_CONTRACT_VERSION,
+ accountUsage?: UsageAccountConsumption,
): UsageSummary {
return {
contractVersion,
@@ -65,6 +69,7 @@ function summary(
message: null,
})),
pricing: { status: "fresh", source: "litellm", fetchedAt: null, knownModels: 10 },
+ ...(accountUsage === undefined ? {} : { accountUsage }),
scanDurationMs: 1,
};
}
@@ -74,6 +79,54 @@ function environment(id: string, usageSummary: UsageSummary): EnvironmentUsage {
}
describe("mergeUsage", () => {
+ it("prefers available official Devin account usage over an unconfigured environment", () => {
+ const notConfigured: UsageAccountConsumption = {
+ provider: "devin",
+ status: "notConfigured",
+ source: "Devin organization consumption API (ACUs)",
+ fetchedAt: null,
+ totalAcus: 0,
+ days: [],
+ message: "missing credentials",
+ };
+ const available: UsageAccountConsumption = {
+ provider: "devin",
+ status: "available",
+ source: "Devin organization consumption API (ACUs)",
+ fetchedAt: "2026-08-31T00:00:00.000Z",
+ totalAcus: 3.5,
+ days: [],
+ message: null,
+ };
+
+ const merged = mergeUsage(
+ [
+ environment(
+ "env-a",
+ summary(
+ [],
+ [{ provider: "devin", hostId: "a", homePath: "/logs" }],
+ USAGE_CONTRACT_VERSION,
+ notConfigured,
+ ),
+ ),
+ environment(
+ "env-b",
+ summary(
+ [],
+ [{ provider: "devin", hostId: "b", homePath: "/logs" }],
+ USAGE_CONTRACT_VERSION,
+ available,
+ ),
+ ),
+ ],
+ USAGE_CONTRACT_VERSION,
+ );
+
+ expect(merged.accountUsage?.status).toBe("available");
+ expect(merged.accountUsage?.totalAcus).toBe(3.5);
+ });
+
it("sums environments that read different transcript directories", () => {
const merged = mergeUsage(
[
@@ -155,11 +208,7 @@ describe("mergeUsage", () => {
),
environment(
"env-b",
- summary(
- [bucket()],
- [{ provider: "claude", hostId: "linux", homePath: "/b" }],
- USAGE_CONTRACT_VERSION - 2,
- ),
+ summary([bucket()], [{ provider: "claude", hostId: "linux", homePath: "/b" }], 3),
),
],
USAGE_CONTRACT_VERSION,
@@ -169,7 +218,7 @@ describe("mergeUsage", () => {
expect(merged.staleEnvironments).toEqual(["env-b"]);
});
- it("keeps the previous compatible contract version so additive provider expansions still merge", () => {
+ it.each([4, 5])("keeps version %i after additive provider expansions", (contractVersion) => {
const merged = mergeUsage(
[
environment(
@@ -181,10 +230,12 @@ describe("mergeUsage", () => {
),
environment(
"env-b",
- summary(
- [bucket({ costUsd: 4, provider: "codex", model: "gpt-5.6-sol" })],
- [{ provider: "codex", hostId: "linux", homePath: "/b" }],
- USAGE_CONTRACT_VERSION - 1,
+ decodeUsageSummary(
+ summary(
+ [bucket({ costUsd: 4, provider: "codex", model: "gpt-5.6-sol" })],
+ [{ provider: "codex", hostId: "linux", homePath: "/b" }],
+ contractVersion,
+ ),
),
),
],
diff --git a/packages/shared/src/usageMerge.ts b/packages/shared/src/usageMerge.ts
index 95982bf507da..3caa9785bbb1 100644
--- a/packages/shared/src/usageMerge.ts
+++ b/packages/shared/src/usageMerge.ts
@@ -10,6 +10,7 @@ import {
USAGE_MERGE_COMPATIBLE_SINCE,
type EnvironmentId,
type UsageBucket,
+ type UsageAccountConsumption,
type UsageProviderKind,
type UsageSourceFingerprint,
type UsageSummary,
@@ -81,6 +82,8 @@ export interface MergedUsage {
readonly duplicateSources: readonly string[];
readonly contributingEnvironments: readonly EnvironmentId[];
readonly staleEnvironments: readonly EnvironmentId[];
+ /** Optional official Devin ACU data; separate from token/cost totals. */
+ readonly accountUsage: UsageAccountConsumption | null;
}
/**
@@ -200,6 +203,7 @@ const EMPTY_MERGED: MergedUsage = {
duplicateSources: [],
contributingEnvironments: [],
staleEnvironments: [],
+ accountUsage: null,
};
/**
@@ -270,8 +274,22 @@ export function mergeUsage(
}
>();
const contributingEnvironments: EnvironmentId[] = [];
+ let accountUsage: UsageAccountConsumption | null = null;
for (const environment of current) {
+ const candidateAccountUsage = environment.summary.accountUsage;
+ if (candidateAccountUsage !== undefined) {
+ // A connected environment may be configured without account credentials
+ // while another one can report official ACUs. Prefer the usable result,
+ // but retain a single diagnostic state when every environment is missing
+ // credentials or has failed.
+ if (
+ accountUsage === null ||
+ (candidateAccountUsage.status === "available" && accountUsage.status !== "available")
+ ) {
+ accountUsage = candidateAccountUsage;
+ }
+ }
const { buckets, sessionsByProvider } = ownedContribution(environment, ownerByFingerprint);
if (buckets.length > 0) contributingEnvironments.push(environment.environmentId);
@@ -422,5 +440,6 @@ export function mergeUsage(
duplicateSources: duplicates,
contributingEnvironments,
staleEnvironments,
+ accountUsage,
};
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 95403e871a9b..af19ed2e71b3 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -27,6 +27,9 @@ catalogs:
'@typescript/native-preview':
specifier: 7.0.0-dev.20260604.1
version: 7.0.0-dev.20260604.1
+ '@vitest/coverage-v8':
+ specifier: 4.1.11
+ version: 4.1.11
jose:
specifier: 6.2.2
version: 6.2.2
@@ -118,12 +121,15 @@ importers:
'@typescript/native-preview':
specifier: 'catalog:'
version: 7.0.0-dev.20260604.1
+ '@vitest/coverage-v8':
+ specifier: 'catalog:'
+ version: 4.1.11(@vitest/browser@4.1.11)(vitest@4.1.11)
knip:
specifier: 6.34.0
version: 6.34.0
vite-plus:
specifier: 'catalog:'
- version: 0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)
+ version: 0.3.0(@types/node@24.12.4)(@vitest/coverage-v8@4.1.11)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)
apps/desktop:
dependencies:
@@ -193,7 +199,7 @@ importers:
version: 4.3.3
vite-plus:
specifier: 'catalog:'
- version: 0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)
+ version: 0.3.0(@types/node@24.12.4)(@vitest/coverage-v8@4.1.11)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)
apps/marketing:
dependencies:
@@ -547,7 +553,7 @@ importers:
version: link:../../packages/effect-codex-app-server
vite-plus:
specifier: 'catalog:'
- version: 0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)
+ version: 0.3.0(@types/node@24.12.4)(@vitest/coverage-v8@4.1.11)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)
apps/web:
dependencies:
@@ -719,7 +725,7 @@ importers:
version: '@voidzero-dev/vite-plus-core@0.3.0(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0)'
vite-plus:
specifier: 'catalog:'
- version: 0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)
+ version: 0.3.0(@types/node@24.12.4)(@vitest/coverage-v8@4.1.11)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)
infra/relay:
dependencies:
@@ -774,7 +780,7 @@ importers:
version: '@voidzero-dev/vite-plus-core@0.3.0(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0)'
vite-plus:
specifier: 'catalog:'
- version: 0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)
+ version: 0.3.0(@types/node@24.12.4)(@vitest/coverage-v8@4.1.11)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)
oxlint-plugin-t3code:
dependencies:
@@ -793,7 +799,7 @@ importers:
version: 4.0.0-beta.103(patch_hash=a16b1e870d8c29e4a98b17cc4c638a4ff471753d8ca3487f78a22b759caf951b)(effect@4.0.0-beta.103(patch_hash=af36b7948b6f9c56623074662b51dade5699880c1a7c71245de73e13c3185fb6))
vite-plus:
specifier: 'catalog:'
- version: 0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)
+ version: 0.3.0(@types/node@24.12.4)(@vitest/coverage-v8@4.1.11)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)
packages/client-runtime:
dependencies:
@@ -830,7 +836,7 @@ importers:
version: 2.0.2
vite-plus:
specifier: 'catalog:'
- version: 0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)
+ version: 0.3.0(@types/node@24.12.4)(@vitest/coverage-v8@4.1.11)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)
packages/contracts:
dependencies:
@@ -843,7 +849,7 @@ importers:
version: 4.0.0-beta.103(patch_hash=a16b1e870d8c29e4a98b17cc4c638a4ff471753d8ca3487f78a22b759caf951b)(effect@4.0.0-beta.103(patch_hash=af36b7948b6f9c56623074662b51dade5699880c1a7c71245de73e13c3185fb6))
vite-plus:
specifier: 'catalog:'
- version: 0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)
+ version: 0.3.0(@types/node@24.12.4)(@vitest/coverage-v8@4.1.11)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)
packages/effect-acp:
dependencies:
@@ -865,7 +871,7 @@ importers:
version: 24.12.4
vite-plus:
specifier: 'catalog:'
- version: 0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)
+ version: 0.3.0(@types/node@24.12.4)(@vitest/coverage-v8@4.1.11)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)
packages/effect-codex-app-server:
dependencies:
@@ -887,7 +893,7 @@ importers:
version: 24.12.4
vite-plus:
specifier: 'catalog:'
- version: 0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)
+ version: 0.3.0(@types/node@24.12.4)(@vitest/coverage-v8@4.1.11)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)
packages/shared:
dependencies:
@@ -921,7 +927,7 @@ importers:
version: 24.12.4
vite-plus:
specifier: 'catalog:'
- version: 0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)
+ version: 0.3.0(@types/node@24.12.4)(@vitest/coverage-v8@4.1.11)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)
packages/ssh:
dependencies:
@@ -946,7 +952,7 @@ importers:
version: 24.12.4
vite-plus:
specifier: 'catalog:'
- version: 0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)
+ version: 0.3.0(@types/node@24.12.4)(@vitest/coverage-v8@4.1.11)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)
packages/tailscale:
dependencies:
@@ -965,7 +971,7 @@ importers:
version: 24.12.4
vite-plus:
specifier: 'catalog:'
- version: 0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)
+ version: 0.3.0(@types/node@24.12.4)(@vitest/coverage-v8@4.1.11)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)
scripts:
dependencies:
@@ -1002,7 +1008,7 @@ importers:
version: 6.0.3
vite-plus:
specifier: 'catalog:'
- version: 0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)
+ version: 0.3.0(@types/node@24.12.4)(@vitest/coverage-v8@4.1.11)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)
packages:
@@ -1656,6 +1662,10 @@ packages:
'@types/react':
optional: true
+ '@bcoe/v8-coverage@1.0.2':
+ resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==}
+ engines: {node: '>=18'}
+
'@blazediff/core@1.9.1':
resolution: {integrity: sha512-ehg3jIkYKulZh+8om/O25vkvSsXXwC+skXmyA87FFx6A/45eqOkZsBltMw/TVteb0mloiGT8oGRTcjRAz66zaA==}
@@ -5213,6 +5223,15 @@ packages:
peerDependencies:
vitest: 4.1.11
+ '@vitest/coverage-v8@4.1.11':
+ resolution: {integrity: sha512-8MVGEFnJIcdGjcbfKmeq8z0pZHH0JlVtoVZH9Q/qwUp6wyFnEJUBMrw9DCaj+ra3vShGmhavjalMIhPNxZAUcw==}
+ peerDependencies:
+ '@vitest/browser': 4.1.11
+ vitest: 4.1.11
+ peerDependenciesMeta:
+ '@vitest/browser':
+ optional: true
+
'@vitest/expect@4.1.11':
resolution: {integrity: sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==}
@@ -5783,6 +5802,9 @@ packages:
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
engines: {node: '>=12'}
+ ast-v8-to-istanbul@1.0.5:
+ resolution: {integrity: sha512-UPAgKJFSEGMWSDr3LX4tqnAb4f7KGT8O40Tyx8wbYmmZ/yn58lNCm8h3svs3eXgiGd5AXxz8NDOvXWvicq+rJA==}
+
astro@7.0.3:
resolution: {integrity: sha512-CK+G+Tl2DMV1EXCwVG45vyurxf2IfRTklMxDhRKn+tst9Yl8rWXpudL62Fa6zin5Bt968FBvuyASj1aJShROZg==}
engines: {node: '>=22.12.0', npm: '>=9.6.5', pnpm: '>=7.1.0'}
@@ -7528,6 +7550,9 @@ packages:
resolution: {integrity: sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==}
engines: {node: ^16.14.0 || >=18.0.0}
+ html-escaper@2.0.2:
+ resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
+
html-escaper@3.0.3:
resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==}
@@ -7757,6 +7782,18 @@ packages:
isomorphic.js@0.2.5:
resolution: {integrity: sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==}
+ istanbul-lib-coverage@3.2.2:
+ resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==}
+ engines: {node: '>=8'}
+
+ istanbul-lib-report@3.0.1:
+ resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==}
+ engines: {node: '>=10'}
+
+ istanbul-reports@3.2.0:
+ resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==}
+ engines: {node: '>=8'}
+
jake@10.9.4:
resolution: {integrity: sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==}
engines: {node: '>=10'}
@@ -7798,6 +7835,9 @@ packages:
resolution: {integrity: sha512-z/wZZgDrkNV1eA0ULjM/F9/50Ya8fbzgKneSpoPsXSGd0KnpdtHfOZWK+GcwLk+EZbS4F9RBhU+K2RgzuDaItw==}
engines: {node: '>=20'}
+ js-tokens@10.0.0:
+ resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==}
+
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
@@ -8071,6 +8111,10 @@ packages:
magicast@0.5.3:
resolution: {integrity: sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==}
+ make-dir@4.0.0:
+ resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==}
+ engines: {node: '>=10'}
+
makeerror@1.0.12:
resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==}
@@ -11724,6 +11768,8 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.16
+ '@bcoe/v8-coverage@1.0.2': {}
+
'@blazediff/core@1.9.1': {}
'@bruits/satteri-darwin-arm64@0.9.3':
@@ -15276,7 +15322,7 @@ snapshots:
'@testing-library/dom': 10.4.1
'@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1)
'@vitest/browser': 4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0))(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(utf-8-validate@6.0.6)(vitest@4.1.11)
- vitest: 4.1.11(@types/node@24.12.4)(@vitest/browser-preview@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0))(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))
+ vitest: 4.1.11(@types/node@24.12.4)(@vitest/browser-preview@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0))(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))
transitivePeerDependencies:
- bufferutil
- msw
@@ -15292,7 +15338,7 @@ snapshots:
pngjs: 7.0.0
sirv: 3.0.2
tinyrainbow: 3.1.0
- vitest: 4.1.11(@types/node@24.12.4)(@vitest/browser-preview@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0))(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))
+ vitest: 4.1.11(@types/node@24.12.4)(@vitest/browser-preview@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0))(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))
ws: 8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)
transitivePeerDependencies:
- bufferutil
@@ -15300,6 +15346,22 @@ snapshots:
- utf-8-validate
- vite
+ '@vitest/coverage-v8@4.1.11(@vitest/browser@4.1.11)(vitest@4.1.11)':
+ dependencies:
+ '@bcoe/v8-coverage': 1.0.2
+ '@vitest/utils': 4.1.11
+ ast-v8-to-istanbul: 1.0.5
+ istanbul-lib-coverage: 3.2.2
+ istanbul-lib-report: 3.0.1
+ istanbul-reports: 3.2.0
+ magicast: 0.5.3
+ obug: 2.1.3
+ std-env: 4.1.0
+ tinyrainbow: 3.1.0
+ vitest: 4.1.11(@types/node@24.12.4)(@vitest/browser-preview@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0))(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))
+ optionalDependencies:
+ '@vitest/browser': 4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0))(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(utf-8-validate@6.0.6)(vitest@4.1.11)
+
'@vitest/expect@4.1.11':
dependencies:
'@standard-schema/spec': 1.1.0
@@ -15797,6 +15859,12 @@ snapshots:
assertion-error@2.0.1: {}
+ ast-v8-to-istanbul@1.0.5:
+ dependencies:
+ '@jridgewell/trace-mapping': 0.3.31
+ estree-walker: 3.0.3
+ js-tokens: 10.0.0
+
astro@7.0.3(@astrojs/markdown-remark@7.2.0)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.12.4)(aws4fetch@1.0.20)(idb-keyval@6.2.1)(ioredis@5.11.0)(jiti@2.7.0)(rollup@4.61.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0):
dependencies:
'@astrojs/compiler-rs': 0.2.3(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)
@@ -17932,6 +18000,8 @@ snapshots:
dependencies:
lru-cache: 10.4.3
+ html-escaper@2.0.2: {}
+
html-escaper@3.0.3: {}
html-url-attributes@3.0.1: {}
@@ -18138,6 +18208,19 @@ snapshots:
isomorphic.js@0.2.5: {}
+ istanbul-lib-coverage@3.2.2: {}
+
+ istanbul-lib-report@3.0.1:
+ dependencies:
+ istanbul-lib-coverage: 3.2.2
+ make-dir: 4.0.0
+ supports-color: 7.2.0
+
+ istanbul-reports@3.2.0:
+ dependencies:
+ html-escaper: 2.0.2
+ istanbul-lib-report: 3.0.1
+
jake@10.9.4:
dependencies:
async: 3.2.6
@@ -18183,6 +18266,8 @@ snapshots:
js-cookie@3.0.7: {}
+ js-tokens@10.0.0: {}
+
js-tokens@4.0.0: {}
js-yaml@4.2.0:
@@ -18426,6 +18511,10 @@ snapshots:
'@babel/types': 7.29.7
source-map-js: 1.2.1
+ make-dir@4.0.0:
+ dependencies:
+ semver: 7.8.5
+
makeerror@1.0.12:
dependencies:
tmpl: 1.0.5
@@ -19593,7 +19682,7 @@ snapshots:
'@oxc-resolver/binding-win32-arm64-msvc': 11.24.2
'@oxc-resolver/binding-win32-x64-msvc': 11.24.2
- oxfmt@0.64.0(vite-plus@0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)):
+ oxfmt@0.64.0(vite-plus@0.3.0(@types/node@24.12.4)(@vitest/coverage-v8@4.1.11)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)):
dependencies:
tinypool: 2.1.0
optionalDependencies:
@@ -19616,7 +19705,7 @@ snapshots:
'@oxfmt/binding-win32-arm64-msvc': 0.64.0
'@oxfmt/binding-win32-ia32-msvc': 0.64.0
'@oxfmt/binding-win32-x64-msvc': 0.64.0
- vite-plus: 0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)
+ vite-plus: 0.3.0(@types/node@24.12.4)(@vitest/coverage-v8@4.1.11)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)
oxlint-tsgolint@7.0.2001:
optionalDependencies:
@@ -19627,7 +19716,7 @@ snapshots:
'@oxlint-tsgolint/win32-arm64': 7.0.2001
'@oxlint-tsgolint/win32-x64': 7.0.2001
- oxlint@1.79.0(oxlint-tsgolint@7.0.2001)(vite-plus@0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)):
+ oxlint@1.79.0(oxlint-tsgolint@7.0.2001)(vite-plus@0.3.0(@types/node@24.12.4)(@vitest/coverage-v8@4.1.11)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)):
optionalDependencies:
'@oxlint/binding-android-arm-eabi': 1.79.0
'@oxlint/binding-android-arm64': 1.79.0
@@ -19649,7 +19738,7 @@ snapshots:
'@oxlint/binding-win32-ia32-msvc': 1.79.0
'@oxlint/binding-win32-x64-msvc': 1.79.0
oxlint-tsgolint: 7.0.2001
- vite-plus: 0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)
+ vite-plus: 0.3.0(@types/node@24.12.4)(@vitest/coverage-v8@4.1.11)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)
p-cancelable@2.1.1: {}
@@ -21475,7 +21564,7 @@ snapshots:
'@types/unist': 3.0.3
vfile-message: 4.0.3
- vite-plus@0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0):
+ vite-plus@0.3.0(@types/node@24.12.4)(@vitest/coverage-v8@4.1.11)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0):
dependencies:
'@oxc-project/types': 0.146.0
'@oxlint/plugins': 1.79.0
@@ -21489,11 +21578,11 @@ snapshots:
'@vitest/spy': 4.1.11
'@vitest/utils': 4.1.11
'@voidzero-dev/vite-plus-core': 0.3.0(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0)
- oxfmt: 0.64.0(vite-plus@0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0))
- oxlint: 1.79.0(oxlint-tsgolint@7.0.2001)(vite-plus@0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0))
+ oxfmt: 0.64.0(vite-plus@0.3.0(@types/node@24.12.4)(@vitest/coverage-v8@4.1.11)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0))
+ oxlint: 1.79.0(oxlint-tsgolint@7.0.2001)(vite-plus@0.3.0(@types/node@24.12.4)(@vitest/coverage-v8@4.1.11)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0))
oxlint-tsgolint: 7.0.2001
vite: '@voidzero-dev/vite-plus-core@0.3.0(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0)'
- vitest: 4.1.11(@types/node@24.12.4)(@vitest/browser-preview@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0))(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))
+ vitest: 4.1.11(@types/node@24.12.4)(@vitest/browser-preview@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0))(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))
optionalDependencies:
'@voidzero-dev/vite-plus-darwin-arm64': 0.3.0
'@voidzero-dev/vite-plus-darwin-x64': 0.3.0
@@ -21537,7 +21626,7 @@ snapshots:
optionalDependencies:
vite: '@voidzero-dev/vite-plus-core@0.3.0(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0)'
- vitest@4.1.11(@types/node@24.12.4)(@vitest/browser-preview@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0))(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3)):
+ vitest@4.1.11(@types/node@24.12.4)(@vitest/browser-preview@4.1.11)(@vitest/coverage-v8@4.1.11)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0))(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3)):
dependencies:
'@vitest/expect': 4.1.11
'@vitest/mocker': 4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0))(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))
@@ -21551,7 +21640,7 @@ snapshots:
magic-string: 0.30.21
obug: 2.1.3
pathe: 2.0.3
- picomatch: 4.0.4
+ picomatch: 4.0.7
std-env: 4.1.0
tinybench: 2.9.0
tinyexec: 1.2.4
@@ -21562,6 +21651,7 @@ snapshots:
optionalDependencies:
'@types/node': 24.12.4
'@vitest/browser-preview': 4.1.11(@voidzero-dev/vite-plus-core@0.3.0(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0))(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(utf-8-validate@6.0.6)(vitest@4.1.11)
+ '@vitest/coverage-v8': 4.1.11(@vitest/browser@4.1.11)(vitest@4.1.11)
transitivePeerDependencies:
- msw
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index ee1bd25547f6..c69e820513a9 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -48,6 +48,7 @@ catalog:
"@tailwindcss/vite": 4.3.3
"@types/node": 24.12.4
"@typescript/native-preview": 7.0.0-dev.20260604.1
+ "@vitest/coverage-v8": 4.1.11
effect: 4.0.0-beta.103
jose: 6.2.2
lightningcss: 1.33.0
diff --git a/scripts/devin-mcp-smoke.ts b/scripts/devin-mcp-smoke.ts
new file mode 100644
index 000000000000..3c7f0daf241f
--- /dev/null
+++ b/scripts/devin-mcp-smoke.ts
@@ -0,0 +1,29 @@
+// @effect-diagnostics nodeBuiltinImport:off
+// @effect-diagnostics globalConsole:off
+import * as NodeChildProcess from "node:child_process";
+import * as NodePath from "node:path";
+import * as NodeURL from "node:url";
+
+const repoRoot = NodePath.resolve(NodePath.dirname(NodeURL.fileURLToPath(import.meta.url)), "..");
+const vpExecutable = process.platform === "win32" ? "vp.cmd" : "vp";
+const args = ["test", "run", "apps/server/src/provider/acp/DevinAcpCliProbe.test.ts"];
+const command = [vpExecutable, ...args].join(" ");
+
+const result = NodeChildProcess.spawnSync(vpExecutable, args, {
+ cwd: repoRoot,
+ env: {
+ ...process.env,
+ T3_DEVIN_ACP_PROBE: "0",
+ T3_DEVIN_MCP_SMOKE: "1",
+ },
+ shell: process.platform === "win32",
+ stdio: "inherit",
+ windowsHide: true,
+});
+
+if (result.error) {
+ console.error(`${command}: ${result.error.message}`);
+ process.exitCode = 1;
+} else {
+ process.exitCode = result.status ?? 1;
+}
|