From 06a4a00b67856e3f007cdf90e5ad977dc02fc712 Mon Sep 17 00:00:00 2001 From: Christoph Acham <32296133+christophacham@users.noreply.github.com> Date: Wed, 4 Nov 2026 14:50:32 +0100 Subject: [PATCH 1/6] feat(server): Mistral Vibe is a built-in provider Mistral Vibe was only reachable through the ACP Registry or a hand-entered local command. Add provider-vibe, which drives the user's vibe-acp over ACP with its own tile and icon, disabled by default like the other non-default providers. T3 runtime modes select Vibe's agents (Supervised: ask, Auto-accept edits: accept-edits, Full access: auto-approve). The status check reads vibe-acp --version and _auth/status, and lists models from a prompt-less session/new, so it costs nothing. Vibe's retry notices and -31001 rate limit map to provider retries and usage limits. The ACP Registry hides its mistral-vibe entry. Co-Authored-By: Claude Opus 5.5 --- apps/server/package.json | 1 + apps/server/src/policy/managedProviders.ts | 2 + apps/server/src/provider/builtInDrivers.ts | 3 + apps/server/src/provider/model-manifest.json | 6 + .../src/provider/providerStatusCache.ts | 1 + apps/server/src/settingsSecrets.ts | 2 + package.json | 2 +- packages/client-runtime/package.json | 1 + .../client-runtime/src/providerClients.ts | 2 + packages/contracts/src/model.ts | 4 + packages/contracts/src/settings.ts | 2 +- packages/provider-vibe/package.json | 36 +++ packages/provider-vibe/src/client.ts | 43 +++ packages/provider-vibe/src/server.ts | 6 + .../src/server/acpSupport.test.ts | 69 +++++ .../provider-vibe/src/server/acpSupport.ts | 130 ++++++++++ packages/provider-vibe/src/server/adapter.ts | 122 +++++++++ packages/provider-vibe/src/server/driver.ts | 147 +++++++++++ .../provider-vibe/src/server/status.test.ts | 89 +++++++ packages/provider-vibe/src/server/status.ts | 245 ++++++++++++++++++ packages/provider-vibe/src/settings.ts | 37 +++ packages/provider-vibe/tsconfig.json | 7 + pnpm-lock.yaml | 37 +++ scripts/release-smoke.ts | 1 + 24 files changed, 993 insertions(+), 2 deletions(-) create mode 100644 packages/provider-vibe/package.json create mode 100644 packages/provider-vibe/src/client.ts create mode 100644 packages/provider-vibe/src/server.ts create mode 100644 packages/provider-vibe/src/server/acpSupport.test.ts create mode 100644 packages/provider-vibe/src/server/acpSupport.ts create mode 100644 packages/provider-vibe/src/server/adapter.ts create mode 100644 packages/provider-vibe/src/server/driver.ts create mode 100644 packages/provider-vibe/src/server/status.test.ts create mode 100644 packages/provider-vibe/src/server/status.ts create mode 100644 packages/provider-vibe/src/settings.ts create mode 100644 packages/provider-vibe/tsconfig.json diff --git a/apps/server/package.json b/apps/server/package.json index 53484dfc12c4a..0a9799ba670a3 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -51,6 +51,7 @@ "@t3tools/provider-muse": "workspace:*", "@t3tools/provider-opencode": "workspace:*", "@t3tools/provider-pi": "workspace:*", + "@t3tools/provider-vibe": "workspace:*", "@t3tools/source-control-azure-devops": "workspace:*", "@t3tools/source-control-bitbucket": "workspace:*", "@t3tools/source-control-core": "workspace:*", diff --git a/apps/server/src/policy/managedProviders.ts b/apps/server/src/policy/managedProviders.ts index ac390097e150d..5136f68dab1ce 100644 --- a/apps/server/src/policy/managedProviders.ts +++ b/apps/server/src/policy/managedProviders.ts @@ -20,6 +20,7 @@ import { cursorClient } from "@t3tools/provider-cursor/client"; import { fxClient } from "@t3tools/provider-fx/client"; import { grokClient } from "@t3tools/provider-grok/client"; import { kiroClient } from "@t3tools/provider-kiro/client"; +import { vibeClient } from "@t3tools/provider-vibe/client"; import { museClient } from "@t3tools/provider-muse/client"; import { openCodeClient } from "@t3tools/provider-opencode/client"; import { piClient } from "@t3tools/provider-pi/client"; @@ -47,6 +48,7 @@ const PACKAGED_CLIENTS: ReadonlyArray = [ grokClient, fxClient, kiroClient, + vibeClient, museClient, openCodeClient, piClient, diff --git a/apps/server/src/provider/builtInDrivers.ts b/apps/server/src/provider/builtInDrivers.ts index 342a6780e9ad0..1450d54adcbdb 100644 --- a/apps/server/src/provider/builtInDrivers.ts +++ b/apps/server/src/provider/builtInDrivers.ts @@ -16,6 +16,7 @@ import type { ProviderUsageReaderEnv, } from "@t3tools/provider-core/server/driver"; import { KiroDriver, type KiroDriverEnv } from "@t3tools/provider-kiro/server"; +import { VibeDriver, type VibeDriverEnv } from "@t3tools/provider-vibe/server"; export type BuiltInDriversEnv = | AcpRegistryDriverEnv @@ -26,6 +27,7 @@ export type BuiltInDriversEnv = | FxDriverEnv | GrokDriverEnv | KiroDriverEnv + | VibeDriverEnv | OpenCodeDriverEnv | PiDriverEnv | MuseDriverEnv; @@ -39,6 +41,7 @@ export const BUILT_IN_DRIVERS: ReadonlyArray=2.27.0", "ranges": [{ "range": ">=2.27.0", "status": "supported" }] }, + { + "driver": "vibe", + "t3CodeRange": ">=0.1.7", + "recommendedRange": ">=3.0.3", + "ranges": [{ "range": ">=3.0.3", "status": "supported" }] + }, { "driver": "pi", "t3CodeRange": ">=0.0.46", diff --git a/apps/server/src/provider/providerStatusCache.ts b/apps/server/src/provider/providerStatusCache.ts index 66e7d64d70a64..94558a28128cc 100644 --- a/apps/server/src/provider/providerStatusCache.ts +++ b/apps/server/src/provider/providerStatusCache.ts @@ -36,6 +36,7 @@ const BUILT_IN_DRIVER_ORDER: ReadonlyArray = [ "antigravity", "fx", "kiro", + "vibe", ]; const driverRank = (driver: string): number => { diff --git a/apps/server/src/settingsSecrets.ts b/apps/server/src/settingsSecrets.ts index 17640237ff5ca..7b64a1c6788ed 100644 --- a/apps/server/src/settingsSecrets.ts +++ b/apps/server/src/settingsSecrets.ts @@ -15,6 +15,7 @@ import { acpRegistryClient } from "@t3tools/provider-acp-registry/client"; import { cursorClient } from "@t3tools/provider-cursor/client"; import { grokClient } from "@t3tools/provider-grok/client"; import { kiroClient } from "@t3tools/provider-kiro/client"; +import { vibeClient } from "@t3tools/provider-vibe/client"; import { museClient } from "@t3tools/provider-muse/client"; import { openCodeClient } from "@t3tools/provider-opencode/client"; import { piClient } from "@t3tools/provider-pi/client"; @@ -35,6 +36,7 @@ const providerSecretFields = new Map([ cursorClient, grokClient, kiroClient, + vibeClient, museClient, openCodeClient, piClient, diff --git a/package.json b/package.json index d7cd927bc5613..55401a3f2bf14 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "report:renders": "node scripts/render-metrics.ts", "lint": "vp lint --report-unused-disable-directives", "knip": "knip --preprocessor ./scripts/knip-schemas.ts", - "knip:check": "knip --include files,dependencies --no-config-hints && knip --workspace apps/server --workspace apps/desktop --workspace apps/web --workspace packages/client-runtime --workspace packages/contracts --workspace packages/effect-acp --workspace packages/effect-codex-app-server --workspace packages/shared --workspace packages/ssh --workspace packages/tailscale --workspace packages/provider-core --workspace packages/provider-acp --workspace packages/provider-acp-registry --workspace packages/provider-testing --workspace packages/provider-pi --workspace packages/provider-muse --workspace packages/provider-opencode --workspace packages/provider-cursor --workspace packages/provider-fx --workspace packages/provider-grok --workspace packages/provider-kiro --workspace packages/secret-provider-core --workspace packages/secret-provider-definitions --workspace packages/secret-provider-onepassword --workspace packages/source-control-core --workspace packages/source-control-definitions --workspace packages/source-control-azure-devops --workspace packages/source-control-bitbucket --workspace packages/source-control-forgejo --workspace packages/source-control-gitcafe --workspace packages/source-control-github --workspace packages/source-control-gitlab --workspace packages/source-control-testing --exports --preprocessor ./scripts/knip-schemas.ts --no-config-hints", + "knip:check": "knip --include files,dependencies --no-config-hints && knip --workspace apps/server --workspace apps/desktop --workspace apps/web --workspace packages/client-runtime --workspace packages/contracts --workspace packages/effect-acp --workspace packages/effect-codex-app-server --workspace packages/shared --workspace packages/ssh --workspace packages/tailscale --workspace packages/provider-core --workspace packages/provider-acp --workspace packages/provider-acp-registry --workspace packages/provider-testing --workspace packages/provider-pi --workspace packages/provider-muse --workspace packages/provider-opencode --workspace packages/provider-cursor --workspace packages/provider-fx --workspace packages/provider-grok --workspace packages/provider-kiro --workspace packages/provider-vibe --workspace packages/secret-provider-core --workspace packages/secret-provider-definitions --workspace packages/secret-provider-onepassword --workspace packages/source-control-core --workspace packages/source-control-definitions --workspace packages/source-control-azure-devops --workspace packages/source-control-bitbucket --workspace packages/source-control-forgejo --workspace packages/source-control-gitcafe --workspace packages/source-control-github --workspace packages/source-control-gitlab --workspace packages/source-control-testing --exports --preprocessor ./scripts/knip-schemas.ts --no-config-hints", "knip:production": "knip --production --preprocessor ./scripts/knip-schemas.ts", "lint:mobile": "node scripts/mobile-native-static-check.ts", "mobile:install": "node scripts/mobile-native-client.ts ensure", diff --git a/packages/client-runtime/package.json b/packages/client-runtime/package.json index d145d31497407..ccbdd911ad972 100644 --- a/packages/client-runtime/package.json +++ b/packages/client-runtime/package.json @@ -536,6 +536,7 @@ "@t3tools/provider-muse": "workspace:*", "@t3tools/provider-opencode": "workspace:*", "@t3tools/provider-pi": "workspace:*", + "@t3tools/provider-vibe": "workspace:*", "@t3tools/shared": "workspace:*", "@t3tools/source-control-core": "workspace:*", "@t3tools/source-control-definitions": "workspace:*", diff --git a/packages/client-runtime/src/providerClients.ts b/packages/client-runtime/src/providerClients.ts index e23200653b963..e133dcf009909 100644 --- a/packages/client-runtime/src/providerClients.ts +++ b/packages/client-runtime/src/providerClients.ts @@ -15,6 +15,7 @@ import { kiroClient } from "@t3tools/provider-kiro/client"; import { museClient } from "@t3tools/provider-muse/client"; import { openCodeClient } from "@t3tools/provider-opencode/client"; import { piClient } from "@t3tools/provider-pi/client"; +import { vibeClient } from "@t3tools/provider-vibe/client"; export const providerClients = makeProviderClientRegistry([ { @@ -43,6 +44,7 @@ export const providerClients = makeProviderClientRegistry([ settingsSchema: AntigravitySettings, }, kiroClient, + vibeClient, museClient, piClient, acpRegistryClient, diff --git a/packages/contracts/src/model.ts b/packages/contracts/src/model.ts index 1a2a519e1abc7..ab1176b82c359 100644 --- a/packages/contracts/src/model.ts +++ b/packages/contracts/src/model.ts @@ -120,6 +120,7 @@ const CURSOR_DRIVER_KIND = ProviderDriverKind.make("cursor"); const GROK_DRIVER_KIND = ProviderDriverKind.make("grok"); const FX_DRIVER_KIND = ProviderDriverKind.make("fx"); const KIRO_DRIVER_KIND = ProviderDriverKind.make("kiro"); +const VIBE_DRIVER_KIND = ProviderDriverKind.make("vibe"); const MUSE_DRIVER_KIND = ProviderDriverKind.make("muse"); const PI_DRIVER_KIND = ProviderDriverKind.make("pi"); const ACP_REGISTRY_DRIVER_KIND = ProviderDriverKind.make("acpRegistry"); @@ -150,6 +151,8 @@ export const DEFAULT_MODEL_BY_PROVIDER: Partial> [GROK_DRIVER_KIND]: "Grok", [FX_DRIVER_KIND]: "fx", [KIRO_DRIVER_KIND]: "Kiro", + [VIBE_DRIVER_KIND]: "Mistral Vibe", [MUSE_DRIVER_KIND]: "Muse Code", [ACP_REGISTRY_DRIVER_KIND]: "ACP Registry", [PI_DRIVER_KIND]: "Pi", diff --git a/packages/contracts/src/settings.ts b/packages/contracts/src/settings.ts index 7f6e8a4aea8dd..2a935bb9e2adc 100644 --- a/packages/contracts/src/settings.ts +++ b/packages/contracts/src/settings.ts @@ -1396,7 +1396,7 @@ export const providerInstanceConfigEnabledFlag = (config: unknown): boolean | un // Must match each driver's enabled decoding default. const DEFAULT_DISABLED_PROVIDER_DRIVERS: ReadonlySet = new Set( - ["cursor", "grok", "fx", "kiro", "muse", "pi", "opencode", "antigravity"].map((driver) => + ["cursor", "grok", "fx", "kiro", "vibe", "muse", "pi", "opencode", "antigravity"].map((driver) => ProviderDriverKind.make(driver), ), ); diff --git a/packages/provider-vibe/package.json b/packages/provider-vibe/package.json new file mode 100644 index 0000000000000..b786dc2b61bfb --- /dev/null +++ b/packages/provider-vibe/package.json @@ -0,0 +1,36 @@ +{ + "name": "@t3tools/provider-vibe", + "private": true, + "type": "module", + "exports": { + "./client": { + "types": "./src/client.ts", + "import": "./src/client.ts" + }, + "./settings": { + "types": "./src/settings.ts", + "import": "./src/settings.ts" + }, + "./server": { + "types": "./src/server.ts", + "import": "./src/server.ts" + } + }, + "scripts": { + "typecheck": "tsc --noEmit", + "test": "vp test run --config ../../vite.config.ts --dir ." + }, + "dependencies": { + "@t3tools/contracts": "workspace:*", + "@t3tools/provider-acp": "workspace:*", + "@t3tools/provider-core": "workspace:*", + "@t3tools/shared": "workspace:*", + "effect": "catalog:" + }, + "devDependencies": { + "@effect/vitest": "catalog:", + "@types/node": "catalog:", + "effect-acp": "workspace:*", + "vite-plus": "catalog:" + } +} diff --git a/packages/provider-vibe/src/client.ts b/packages/provider-vibe/src/client.ts new file mode 100644 index 0000000000000..ba0e9e845f670 --- /dev/null +++ b/packages/provider-vibe/src/client.ts @@ -0,0 +1,43 @@ +/** + * Mistral Vibe's client definition: settings form, label, and glyph. + * Browser- and React Native-safe. + * + * @module provider-vibe/client + */ +import { ProviderDriverKind } from "@t3tools/contracts"; +import { defineProviderClient } from "@t3tools/provider-core/client"; + +import { VibeSettings } from "./settings.ts"; + +export const vibeClient = defineProviderClient({ + driverKind: ProviderDriverKind.make("vibe"), + label: "Mistral Vibe", + acpRegistryAgentIds: ["mistral-vibe"], + settingsSchema: VibeSettings, + environmentFields: [ + { + name: "MISTRAL_API_KEY", + label: "Mistral API key", + description: "Optional. Signs in without `vibe --setup`.", + placeholder: "Paste API key", + sensitive: true, + }, + ], + // mistralai/mistral-vibe distribution/zed/icons/mistral_vibe.svg (Apache-2.0). + icon: { + viewBox: "0 0 28 28", + fill: { light: "#000000", dark: "#FFFFFF" }, + paths: [ + { d: "M8.8147 5.35803H5.35791V8.46914H8.8147V5.35803Z" }, + { d: "M22.6419 5.35803H19.1851V8.46914H22.6419V5.35803Z" }, + { d: "M15.7283 15.7284H12.2715V18.8395H15.7283V15.7284Z" }, + { d: "M8.8147 15.7284H5.35791V18.8395H8.8147V15.7284Z" }, + { d: "M22.6419 15.7284H19.1851V18.8395H22.6419V15.7284Z" }, + { d: "M12.2715 8.81482H5.35791V11.9259H12.2715V8.81482Z" }, + { d: "M12.2718 19.1852H1.90137V22.2963H12.2718V19.1852Z" }, + { d: "M26.0989 19.1852H15.7285V22.2963H26.0989V19.1852Z" }, + { d: "M22.6419 12.2716H5.35791V15.3827H22.6419V12.2716Z" }, + { d: "M22.6421 8.81482H15.7285V11.9259H22.6421V8.81482Z" }, + ], + }, +}); diff --git a/packages/provider-vibe/src/server.ts b/packages/provider-vibe/src/server.ts new file mode 100644 index 0000000000000..b5f8a9e00a85a --- /dev/null +++ b/packages/provider-vibe/src/server.ts @@ -0,0 +1,6 @@ +/** + * Mistral Vibe's server entry: the driver the server registers. + * + * @module provider-vibe/server + */ +export { VibeDriver, type VibeDriverEnv } from "./server/driver.ts"; diff --git a/packages/provider-vibe/src/server/acpSupport.test.ts b/packages/provider-vibe/src/server/acpSupport.test.ts new file mode 100644 index 0000000000000..009a49dcde83d --- /dev/null +++ b/packages/provider-vibe/src/server/acpSupport.test.ts @@ -0,0 +1,69 @@ +import { assert, describe, it } from "@effect/vitest"; +import type { RuntimeMode } from "@t3tools/contracts"; +import * as EffectAcpErrors from "effect-acp/errors"; + +import { ProviderAdapterV2RuntimePolicy } from "@t3tools/provider-core/server/ProviderAdapter"; + +import { + vibePromptFailure, + vibeRuntimeModeForSessionMode, + vibeSessionModeForPolicy, +} from "./acpSupport.ts"; + +const policy = (runtimeMode: RuntimeMode, overrides: Record = {}) => + ProviderAdapterV2RuntimePolicy.make({ + runtimeMode, + interactionMode: "default", + cwd: "/work", + ...overrides, + }); + +describe("Vibe agents for T3 runtime modes", () => { + it("runs each runtime mode as its Vibe agent", () => { + const cases: ReadonlyArray = [ + ["approval-required", "ask", "approval-required"], + ["auto-accept-edits", "accept-edits", "auto-accept-edits"], + ["full-access", "auto-approve", "full-access"], + ["auto", "ask", "approval-required"], + ]; + for (const [runtimeMode, agent, granted] of cases) { + assert.equal(vibeSessionModeForPolicy(policy(runtimeMode)), agent, runtimeMode); + assert.equal(vibeRuntimeModeForSessionMode(agent), granted, agent); + } + assert.isUndefined(vibeRuntimeModeForSessionMode("smart-approve")); + }); + + // Vibe's own permissive agents would approve calls before T3's policy sees them. + it("keeps an explicit approval or sandbox policy on ask", () => { + assert.equal( + vibeSessionModeForPolicy(policy("full-access", { approvalPolicy: "on-request" })), + "ask", + ); + assert.equal( + vibeSessionModeForPolicy(policy("full-access", { sandboxPolicy: { type: "readOnly" } })), + "ask", + ); + }); +}); + +describe("vibePromptFailure", () => { + it("classifies Vibe's rate limit as a usage limit", () => { + const limit = new EffectAcpErrors.AcpRequestError({ + code: -31001, + errorMessage: "Rate limit exceeded for mistral (model: mistral-vibe-cli-latest).", + }); + assert.deepEqual(vibePromptFailure(limit), { + class: "usage_limit", + message: limit.errorMessage, + code: "-31001", + retryable: null, + }); + assert.equal( + vibePromptFailure( + new EffectAcpErrors.AcpRequestError({ code: -32603, errorMessage: "Internal error" }), + ).class, + "provider_error", + ); + assert.equal(vibePromptFailure(new Error("private defect")).message, "Provider turn failed."); + }); +}); diff --git a/packages/provider-vibe/src/server/acpSupport.ts b/packages/provider-vibe/src/server/acpSupport.ts new file mode 100644 index 0000000000000..7bb45857d0f8f --- /dev/null +++ b/packages/provider-vibe/src/server/acpSupport.ts @@ -0,0 +1,130 @@ +import { ProviderDriverKind, type RuntimeMode } from "@t3tools/contracts"; +import type { VibeSettings } from "../settings.ts"; +import type * as SecureRandom from "effect/crypto/SecureRandom"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Schema from "effect/Schema"; +import type * as Scope from "effect/Scope"; +import type * as ChildProcessSpawner from "effect/process/ChildProcessSpawner"; +import * as EffectAcpErrors from "effect-acp/errors"; + +import * as AcpSessionRuntime from "@t3tools/provider-acp/server/AcpSessionRuntime"; +import type { AcpAdapterV2ExtensionContext } from "@t3tools/provider-acp/server/adapter"; +import type * as ProviderAdapter from "@t3tools/provider-core/server/ProviderAdapter"; +import { makeProviderFailure } from "@t3tools/provider-core/server/failure"; + +export const VIBE_DRIVER_KIND = ProviderDriverKind.make("vibe"); +/** T3's slug for "Default": the session keeps Vibe's own active model. */ +export const VIBE_DEFAULT_MODEL_SLUG = "default"; +export const VIBE_MODE_CONFIG_ID = "mode"; + +/** + * Vibe's agents are permission profiles. Smart Approve, the match for Auto, + * ships hidden behind Vibe configuration, so Auto is not offered. + */ +export const VIBE_SUPPORTED_RUNTIME_MODES = [ + "approval-required", + "auto-accept-edits", + "full-access", +] as const satisfies ReadonlyArray; + +const VIBE_MODE_BY_RUNTIME_MODE = { + "approval-required": "ask", + "auto-accept-edits": "accept-edits", + "full-access": "auto-approve", +} as const satisfies Record<(typeof VIBE_SUPPORTED_RUNTIME_MODES)[number], string>; + +/** + * An explicit approval or sandbox policy keeps Vibe on `ask`, so every tool + * call reaches T3's policy check instead of Vibe approving it itself. + */ +export function vibeSessionModeForPolicy( + policy: ProviderAdapter.ProviderAdapterV2RuntimePolicy, +): string { + if (policy.approvalPolicy !== undefined || policy.sandboxPolicy !== undefined) return "ask"; + const mode = VIBE_SUPPORTED_RUNTIME_MODES.find((supported) => supported === policy.runtimeMode); + return mode === undefined ? "ask" : VIBE_MODE_BY_RUNTIME_MODE[mode]; +} + +export function vibeRuntimeModeForSessionMode(modeId: string): RuntimeMode | undefined { + return VIBE_SUPPORTED_RUNTIME_MODES.find((mode) => VIBE_MODE_BY_RUNTIME_MODE[mode] === modeId); +} + +/** Vibe answers a cancelled prompt with `stopReason: cancelled`. */ +const VIBE_ACP_CANCEL_BEHAVIOR = "wait-for-prompt" as const; + +// Mistral Vibe's application rate-limit code, outside ACP. +const VIBE_RATE_LIMITED = -31001; +const isAcpRequestError = Schema.is(EffectAcpErrors.AcpRequestError); + +const VibeSessionRetrying = Schema.Struct({ + sessionId: Schema.String, + category: Schema.Literals(["rate_limited", "server_error", "timed_out", "connection", "unknown"]), + detail: Schema.String, +}); + +/** Vibe reports its SDK backoff through this ACP extension. */ +export function registerVibeAcpExtensions(context: AcpAdapterV2ExtensionContext) { + return context.runtime.handleExtNotification("_session/retrying", VibeSessionRetrying, (notice) => + context.reportProviderRetry({ + sessionId: notice.sessionId, + failure: makeProviderFailure({ + message: notice.detail, + class: + notice.category === "rate_limited" + ? "usage_limit" + : notice.category === "unknown" + ? "provider_error" + : "transport_error", + retryable: true, + }), + }), + ); +} + +export function vibePromptFailure(cause: unknown) { + return makeProviderFailure({ + cause, + ...(isAcpRequestError(cause) + ? { + message: cause.errorMessage, + code: String(cause.code), + class: + cause.code === VIBE_RATE_LIMITED + ? ("usage_limit" as const) + : ("provider_error" as const), + } + : { class: "provider_error" as const }), + }); +} + +interface VibeAcpRuntimeInput extends Omit< + AcpSessionRuntime.AcpSessionRuntimeOptions, + "authMethodId" | "spawn" | "cancelBehavior" +> { + readonly settings: Pick; + readonly environment?: NodeJS.ProcessEnv; +} + +export const makeVibeAcpRuntime = Effect.fn("makeVibeAcpRuntime")(function* ( + input: VibeAcpRuntimeInput, +): Effect.fn.Return< + AcpSessionRuntime.AcpSessionRuntime["Service"], + EffectAcpErrors.AcpError, + ChildProcessSpawner.ChildProcessSpawner | SecureRandom.SecureRandom | Scope.Scope +> { + const { settings, environment, ...runtimeOptions } = input; + const context = yield* Layer.build( + AcpSessionRuntime.layer({ + ...runtimeOptions, + spawn: { + command: settings.binaryPath || "vibe-acp", + args: [], + cwd: input.cwd, + env: { ...environment }, + }, + cancelBehavior: VIBE_ACP_CANCEL_BEHAVIOR, + }), + ); + return yield* Effect.service(AcpSessionRuntime.AcpSessionRuntime).pipe(Effect.provide(context)); +}); diff --git a/packages/provider-vibe/src/server/adapter.ts b/packages/provider-vibe/src/server/adapter.ts new file mode 100644 index 0000000000000..9450d48cd8cc4 --- /dev/null +++ b/packages/provider-vibe/src/server/adapter.ts @@ -0,0 +1,122 @@ +import * as HostProcess from "@t3tools/shared/HostProcess"; +import { resolveSelfInvocation, type SelfInvocation } from "@t3tools/shared/nodeRuntime"; +import { readCustomModelEntries } from "@t3tools/shared/model"; +import { VibeSettings } from "../settings.ts"; +import type * as SecureRandom from "effect/crypto/SecureRandom"; +import * as Effect from "effect/Effect"; +import type * as FileSystem from "effect/FileSystem"; +import type * as Path from "effect/Path"; +import * as Schema from "effect/Schema"; +import type * as ChildProcessSpawner from "effect/process/ChildProcessSpawner"; + +import type * as McpProviderSessions from "@t3tools/provider-core/server/McpProviderSessions"; +import type * as ProviderHost from "@t3tools/provider-core/server/ProviderHost"; +import { makeAcpNativeLoggerFactory } from "@t3tools/provider-acp/server/nativeLogging"; +import { mergeProviderInstanceEnvironment } from "@t3tools/provider-core/server/instanceEnvironment"; +import * as ProviderEventLoggers from "@t3tools/provider-core/server/ProviderEventLoggers"; +import type * as IdAllocator from "@t3tools/provider-core/server/IdAllocator"; +import { + ProviderAdapterDriverCreateError, + type ProviderAdapterDriver, + type ProviderAdapterDriverCreateInput, +} from "@t3tools/provider-core/server/adapterDriver"; +import { + AcpProviderCapabilitiesV2, + makeAcpAdapterV2, + type AcpAdapterV2RuntimeInput, +} from "@t3tools/provider-acp/server/adapter"; +import { + makeVibeAcpRuntime, + registerVibeAcpExtensions, + VIBE_DRIVER_KIND, + vibePromptFailure, + vibeRuntimeModeForSessionMode, + vibeSessionModeForPolicy, +} from "./acpSupport.ts"; + +const DEFAULT_VIBE_SETTINGS = Schema.decodeSync(VibeSettings)({}); + +interface VibeAdapterV2Options { + readonly instanceId: Parameters[0]["instanceId"]; + readonly settings: VibeSettings; + readonly environment: NodeJS.ProcessEnv; + readonly selfInvocation: SelfInvocation; + readonly nativeLogging?: Parameters[0]["nativeLogging"]; +} + +const makeVibeAdapterV2 = Effect.fn("makeVibeAdapterV2")(function* (options: VibeAdapterV2Options) { + return yield* makeAcpAdapterV2({ + instanceId: options.instanceId, + selfInvocation: options.selfInvocation, + flavor: { + driver: VIBE_DRIVER_KIND, + runtimeHarness: "vibe", + capabilities: AcpProviderCapabilitiesV2, + customModels: readCustomModelEntries(options.settings.customModels).map( + (entry) => entry.slug, + ), + makeRuntime: ({ + runtimePolicy: _runtimePolicy, + processEnvironment, + ...input + }: AcpAdapterV2RuntimeInput) => + makeVibeAcpRuntime({ + ...input, + settings: options.settings, + environment: { ...options.environment, ...processEnvironment }, + }), + sessionModeForPolicy: vibeSessionModeForPolicy, + runtimeModeForSessionMode: vibeRuntimeModeForSessionMode, + registerExtensions: registerVibeAcpExtensions, + promptFailure: vibePromptFailure, + }, + ...(options.nativeLogging === undefined ? {} : { nativeLogging: options.nativeLogging }), + }); +}); + +export type VibeAdapterV2DriverEnv = + | ChildProcessSpawner.ChildProcessSpawner + | SecureRandom.SecureRandom + | FileSystem.FileSystem + | IdAllocator.IdAllocatorV2 + | Path.Path + | McpProviderSessions.McpProviderSessions + | ProviderEventLoggers.ProviderEventLoggers + | ProviderHost.ProviderHost; + +export const VibeAdapterV2Driver: ProviderAdapterDriver = { + driverKind: VIBE_DRIVER_KIND, + configSchema: VibeSettings, + defaultConfig: (): VibeSettings => DEFAULT_VIBE_SETTINGS, + create: Effect.fn("VibeAdapterV2Driver.create")( + function* (input: ProviderAdapterDriverCreateInput) { + const hostEnvironment = yield* HostProcess.Environment; + const providerEventLoggers = yield* ProviderEventLoggers.ProviderEventLoggers; + const makeNativeLogger = yield* makeAcpNativeLoggerFactory(); + return yield* makeVibeAdapterV2({ + instanceId: input.instanceId, + settings: { ...input.config, enabled: input.enabled }, + environment: yield* mergeProviderInstanceEnvironment(input.environment, hostEnvironment), + selfInvocation: yield* resolveSelfInvocation(), + nativeLogging: (threadId) => + makeNativeLogger({ + nativeEventLogger: providerEventLoggers.native, + provider: VIBE_DRIVER_KIND, + threadId, + }), + }); + }, + (effect, input) => + effect.pipe( + Effect.mapError( + (cause) => + new ProviderAdapterDriverCreateError({ + driver: VIBE_DRIVER_KIND, + instanceId: input.instanceId, + detail: "Failed to create the Mistral Vibe ACP adapter.", + cause, + }), + ), + ), + ), +}; diff --git a/packages/provider-vibe/src/server/driver.ts b/packages/provider-vibe/src/server/driver.ts new file mode 100644 index 0000000000000..809a2007cc498 --- /dev/null +++ b/packages/provider-vibe/src/server/driver.ts @@ -0,0 +1,147 @@ +import { TextGenerationError } from "@t3tools/contracts"; +import { VibeSettings } from "../settings.ts"; +import * as HostProcess from "@t3tools/shared/HostProcess"; +import * as SecureRandom from "effect/crypto/SecureRandom"; +import * as Effect from "effect/Effect"; +import * as Schema from "effect/Schema"; +import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner"; + +import { type VibeAdapterV2DriverEnv, VibeAdapterV2Driver } from "./adapter.ts"; +import { ProviderDriverError } from "@t3tools/provider-core/server/errors"; +import { buildInitialVibeProviderSnapshot, checkVibeProviderStatus } from "./status.ts"; +import { makeManagedServerProvider } from "@t3tools/provider-core/server/managedProvider"; +import { + defaultProviderContinuationIdentity, + type ProviderDriver, + type ProviderInstance, +} from "@t3tools/provider-core/server/driver"; +import { withInstanceIdentity } from "@t3tools/provider-core/server/instanceIdentity"; +import { mergeProviderInstanceEnvironment } from "@t3tools/provider-core/server/instanceEnvironment"; +import { makeManualOnlyProviderMaintenanceCapabilities } from "@t3tools/provider-core/server/maintenanceResolver"; +import { + haveProviderSnapshotSettingsChanged, + makeProviderSnapshotSettingsSource, + type ProviderSnapshotSettings, +} from "@t3tools/provider-core/server/snapshotSettings"; +import type { ProviderTextGeneration } from "@t3tools/provider-core/server/textGeneration"; +import { VIBE_DRIVER_KIND } from "./acpSupport.ts"; + +const decodeVibeSettings = Schema.decodeSync(VibeSettings); +// Vibe installs and updates through uv or its own installer; there is no npm +// package to read a latest version from. +const MAINTENANCE = makeManualOnlyProviderMaintenanceCapabilities({ + provider: VIBE_DRIVER_KIND, + packageName: null, +}); + +// See `supportsTextGeneration` in status.ts. +const unsupportedTextGeneration: ProviderTextGeneration = (() => { + const unsupported = (operation: string) => + Effect.fail( + new TextGenerationError({ + operation, + detail: "Mistral Vibe does not provide application text generation.", + }), + ); + return { + generateCommitMessage: () => unsupported("generateCommitMessage"), + generatePrContent: () => unsupported("generatePrContent"), + generateBranchName: () => unsupported("generateBranchName"), + generateThreadTitle: () => unsupported("generateThreadTitle"), + generateReviewGuide: () => unsupported("generateReviewGuide"), + }; +})(); + +export type VibeDriverEnv = VibeAdapterV2DriverEnv; + +export const VibeDriver: ProviderDriver = { + driverKind: VIBE_DRIVER_KIND, + metadata: { + displayName: "Mistral Vibe", + supportsMultipleInstances: true, + }, + configSchema: VibeSettings, + defaultConfig: (): VibeSettings => decodeVibeSettings({}), + create: Effect.fn("VibeDriver.create")(function* ({ + instanceId, + displayName, + accentColor, + environment, + enabled, + config, + }) { + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; + const crypto = yield* SecureRandom.SecureRandom; + const processEnv = yield* mergeProviderInstanceEnvironment( + environment, + yield* HostProcess.Environment, + ); + const continuationIdentity = defaultProviderContinuationIdentity({ + driverKind: VIBE_DRIVER_KIND, + instanceId, + }); + const stampIdentity = withInstanceIdentity({ + instanceId, + driverKind: VIBE_DRIVER_KIND, + displayName, + accentColor, + continuationGroupKey: continuationIdentity.continuationKey, + }); + const effectiveConfig = { ...config, enabled } satisfies VibeSettings; + const snapshotSettings = yield* makeProviderSnapshotSettingsSource(effectiveConfig); + const snapshot = yield* makeManagedServerProvider>({ + resolveMaintenance: () => Effect.succeed(MAINTENANCE), + getSettings: snapshotSettings.getSettings, + streamSettings: snapshotSettings.streamSettings, + haveSettingsChanged: haveProviderSnapshotSettingsChanged, + initialSnapshot: (settings) => + buildInitialVibeProviderSnapshot(settings.provider).pipe(Effect.map(stampIdentity)), + checkProvider: checkVibeProviderStatus(effectiveConfig, processEnv).pipe( + Effect.map(stampIdentity), + Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner), + Effect.provideService(SecureRandom.SecureRandom, crypto), + ), + }).pipe( + Effect.mapError( + (cause) => + new ProviderDriverError({ + driver: VIBE_DRIVER_KIND, + instanceId, + detail: "Failed to build the Mistral Vibe provider snapshot.", + cause, + }), + ), + ); + + const orchestrationAdapter = yield* VibeAdapterV2Driver.create({ + instanceId, + displayName, + accentColor, + environment, + enabled, + config, + }).pipe( + Effect.mapError( + (cause) => + new ProviderDriverError({ + driver: VIBE_DRIVER_KIND, + instanceId, + detail: "Failed to build the Mistral Vibe orchestration adapter.", + cause, + }), + ), + ); + + return { + instanceId, + driverKind: VIBE_DRIVER_KIND, + continuationIdentity, + displayName, + accentColor, + enabled, + snapshot, + orchestrationAdapter, + textGeneration: unsupportedTextGeneration, + } satisfies ProviderInstance; + }), +}; diff --git a/packages/provider-vibe/src/server/status.test.ts b/packages/provider-vibe/src/server/status.test.ts new file mode 100644 index 0000000000000..4c624de42b367 --- /dev/null +++ b/packages/provider-vibe/src/server/status.test.ts @@ -0,0 +1,89 @@ +import { assert, describe, it } from "@effect/vitest"; +import type * as EffectAcpSchema from "effect-acp/compat"; + +import { vibeAuthFromStatusReport, vibeModelsFromConfigOptions } from "./status.ts"; + +// Shaped like vibe-acp 3.0.3's `session/new` config options. +const VIBE_CONFIG_OPTIONS = [ + { + id: "mode", + name: "Session Mode", + category: "mode", + type: "select", + currentValue: "accept-edits", + options: [ + { value: "ask", name: "Ask", description: "Requires approval for tool executions" }, + { value: "accept-edits", name: "Accept Edits", description: "Auto-approves file edits only" }, + { + value: "auto-approve", + name: "Auto Approve", + description: "Auto-approves all tool executions", + }, + ], + }, + { + id: "model", + name: "Model", + category: "model", + type: "select", + currentValue: "mistral-medium-3.5", + options: [ + { + value: "mistral-medium-3.5", + name: "Mistral Medium 3.5", + description: "mistral-vibe-cli-latest", + }, + { value: "devstral-small", name: "Devstral Small" }, + ], + }, + { + id: "thinking", + name: "Thinking", + category: "thinking", + type: "select", + currentValue: "high", + options: [ + { value: "off", name: "Off" }, + { value: "high", name: "High" }, + ], + }, +] satisfies ReadonlyArray; + +describe("vibeAuthFromStatusReport", () => { + it("reads `_auth/status`", () => { + assert.deepEqual(vibeAuthFromStatusReport({ authenticated: true, authState: "os_keyring" }), { + status: "authenticated", + }); + assert.deepEqual(vibeAuthFromStatusReport({ authenticated: false, authState: "missing" }), { + status: "unauthenticated", + }); + assert.deepEqual(vibeAuthFromStatusReport({ authState: "os_keyring" }), { status: "unknown" }); + assert.deepEqual(vibeAuthFromStatusReport(null), { status: "unknown" }); + }); +}); + +describe("vibeModelsFromConfigOptions", () => { + it("lists Default and Vibe's models, with thinking but not the agent mode", () => { + const models = vibeModelsFromConfigOptions(VIBE_CONFIG_OPTIONS); + assert.deepEqual( + models.map((model) => [model.slug, model.name, model.isDefault ?? false]), + [ + ["default", "Default (Mistral Medium 3.5)", true], + ["mistral-medium-3.5", "Mistral Medium 3.5", false], + ["devstral-small", "Devstral Small", false], + ], + ); + assert.deepEqual(models[0]?.aliases, ["mistral-medium-3.5"]); + assert.deepEqual( + models[0]?.capabilities?.optionDescriptors?.map((descriptor) => descriptor.id), + ["thinking"], + ); + }); + + it("offers only Default before Vibe is signed in", () => { + assert.deepEqual( + vibeModelsFromConfigOptions([]).map((model) => model.name), + ["Default"], + ); + }); +}); diff --git a/packages/provider-vibe/src/server/status.ts b/packages/provider-vibe/src/server/status.ts new file mode 100644 index 0000000000000..fd252a35485e8 --- /dev/null +++ b/packages/provider-vibe/src/server/status.ts @@ -0,0 +1,245 @@ +import type * as EffectAcpSchema from "effect-acp/compat"; +import { type ServerProviderAuth, type ServerProviderModel } from "@t3tools/contracts"; +import type { VibeSettings } from "../settings.ts"; +import * as HostProcess from "@t3tools/shared/HostProcess"; +import { createModelCapabilities } from "@t3tools/shared/model"; +import { resolveSpawnCommand } from "@t3tools/shared/shell"; +import type * as SecureRandom from "effect/crypto/SecureRandom"; +import * as DateTime from "effect/DateTime"; +import * as Effect from "effect/Effect"; +import * as Option from "effect/Option"; +import * as Result from "effect/Result"; +import * as Schema from "effect/Schema"; +import * as ChildProcess from "effect/process/ChildProcess"; +import type * as ChildProcessSpawner from "effect/process/ChildProcessSpawner"; + +import { + acpProviderOptionDescriptors, + flattenSelectChoices, +} from "@t3tools/provider-acp/server/sessionConfig"; +import { findModelConfigOption } from "@t3tools/provider-acp/server/runtimeModel"; +import { + makeVibeAcpRuntime, + VIBE_DEFAULT_MODEL_SLUG, + VIBE_DRIVER_KIND, + VIBE_MODE_CONFIG_ID, + VIBE_SUPPORTED_RUNTIME_MODES, +} from "./acpSupport.ts"; +import { + AUTH_PROBE_TIMEOUT_MS, + buildServerProvider, + isCommandMissingCause, + parseGenericCliVersion, + type ProviderProbeResult, + providerModelsFromSettings, + type ServerProviderDraft, + spawnAndCollect, +} from "@t3tools/provider-core/server/snapshotProbe"; + +const VIBE_PRESENTATION = { + displayName: "Mistral Vibe", + showInteractionModeToggle: false, + supportedRuntimeModes: VIBE_SUPPORTED_RUNTIME_MODES, +} as const; +const VERSION_PROBE_TIMEOUT_MS = 10_000; + +/** `_auth/status` on vibe-acp 3.0: `authenticated` is whether the active provider can be used. */ +const decodeVibeAuthStatus = Schema.decodeUnknownOption( + Schema.Struct({ authenticated: Schema.Boolean }), +); + +export function vibeAuthFromStatusReport(report: unknown): ServerProviderAuth { + const decoded = decodeVibeAuthStatus(report); + if (Option.isNone(decoded)) return { status: "unknown" }; + return { status: decoded.value.authenticated ? "authenticated" : "unauthenticated" }; +} + +/** + * Vibe's model choices from a session's config options. "Default" keeps the + * session's own model. The mode option is left out: T3's runtime mode selects + * the Vibe agent. + */ +export function vibeModelsFromConfigOptions( + configOptions: ReadonlyArray, +): ReadonlyArray { + const capabilities = createModelCapabilities({ + optionDescriptors: acpProviderOptionDescriptors({ + configOptions: configOptions.filter((option) => option.id !== VIBE_MODE_CONFIG_ID), + modeState: undefined, + }), + }); + const modelOption = findModelConfigOption(configOptions); + const choices = modelOption?.type === "select" ? flattenSelectChoices(modelOption.options) : []; + const current = modelOption?.type === "select" ? modelOption.currentValue : undefined; + const currentName = choices.find((choice) => choice.value === current)?.name; + return [ + { + slug: VIBE_DEFAULT_MODEL_SLUG, + name: currentName === undefined ? "Default" : `Default (${currentName})`, + ...(current === undefined ? {} : { aliases: [current] }), + isCustom: false, + isDefault: true, + capabilities, + }, + ...choices + .filter((choice) => choice.value !== VIBE_DEFAULT_MODEL_SLUG) + .map((choice) => ({ + slug: choice.value, + name: choice.name || choice.value, + isCustom: false, + capabilities, + })), + ]; +} + +const snapshot = ( + settings: VibeSettings, + checkedAt: string, + probe: ProviderProbeResult, + configOptions: ReadonlyArray = [], +): ServerProviderDraft => { + const models = vibeModelsFromConfigOptions(configOptions); + return { + ...buildServerProvider({ + driver: VIBE_DRIVER_KIND, + presentation: VIBE_PRESENTATION, + enabled: settings.enabled, + checkedAt, + models: providerModelsFromSettings( + models, + settings.customModels, + models[0]?.capabilities ?? null, + ), + probe, + }), + supportsTextGeneration: false, + }; +}; + +export const buildInitialVibeProviderSnapshot = ( + settings: VibeSettings, +): Effect.Effect => + Effect.map(DateTime.now, (now) => + snapshot(settings, DateTime.formatIso(now), { + installed: settings.enabled, + version: null, + status: "warning", + auth: { status: "unknown" }, + message: settings.enabled + ? "Checking Mistral Vibe availability..." + : "Mistral Vibe is disabled in T3 Code settings.", + }), + ); + +const runVibeVersion = Effect.fn("runVibeVersion")(function* ( + settings: VibeSettings, + environment: NodeJS.ProcessEnv, +) { + const command = settings.binaryPath || "vibe-acp"; + const spawnCommand = yield* resolveSpawnCommand(command, ["--version"], { env: environment }); + return yield* spawnAndCollect( + command, + ChildProcess.make(spawnCommand.command, spawnCommand.args, { + env: environment, + shell: spawnCommand.shell, + }), + ); +}); + +/** + * `initialize` and `_auth/status` need no session. Only a signed-in Vibe gets + * a `session/new` for its model list: Vibe saves no session until a prompt, + * and signed out it refuses to open one. + */ +const probeVibeAcp = Effect.fn("VibeProvider.probeVibeAcp")(function* ( + settings: VibeSettings, + environment: NodeJS.ProcessEnv, +) { + const runtime = yield* makeVibeAcpRuntime({ + settings, + environment, + cwd: yield* HostProcess.WorkingDirectory, + clientInfo: { name: "t3-code-provider-probe", version: "0.0.0" }, + }); + yield* runtime.initialize(); + const auth = vibeAuthFromStatusReport(yield* runtime.request("_auth/status", {})); + if (auth.status !== "authenticated") return { auth, configOptions: undefined }; + const started = yield* runtime.start(); + return { auth, configOptions: started.sessionSetupResult.configOptions ?? [] }; +}, Effect.scoped); + +export const checkVibeProviderStatus = Effect.fn("checkVibeProviderStatus")(function* ( + settings: VibeSettings, + environment: NodeJS.ProcessEnv, +): Effect.fn.Return< + ServerProviderDraft, + never, + ChildProcessSpawner.ChildProcessSpawner | SecureRandom.SecureRandom +> { + const checkedAt = DateTime.formatIso(yield* DateTime.now); + if (!settings.enabled) { + return snapshot(settings, checkedAt, { + installed: false, + version: null, + status: "warning", + auth: { status: "unknown" }, + message: "Mistral Vibe is disabled in T3 Code settings.", + }); + } + + const versionResult = yield* runVibeVersion(settings, environment).pipe( + Effect.timeoutOption(VERSION_PROBE_TIMEOUT_MS), + Effect.result, + ); + if (Result.isFailure(versionResult) || Option.isNone(versionResult.success)) { + const missing = Result.isFailure(versionResult) && isCommandMissingCause(versionResult.failure); + return snapshot(settings, checkedAt, { + installed: !missing, + version: null, + status: "error", + auth: { status: "unknown" }, + message: missing + ? "Mistral Vibe (`vibe-acp`) is not installed or not on PATH." + : "Failed to run `vibe-acp --version`.", + }); + } + const versionOutput = versionResult.success.value; + const version = parseGenericCliVersion(`${versionOutput.stdout}\n${versionOutput.stderr}`); + if (versionOutput.code !== 0) { + return snapshot(settings, checkedAt, { + installed: true, + version, + status: "error", + auth: { status: "unknown" }, + message: "Mistral Vibe is installed but `vibe-acp` failed to run.", + }); + } + + const probed = yield* probeVibeAcp(settings, environment).pipe( + Effect.timeoutOption(AUTH_PROBE_TIMEOUT_MS), + Effect.map(Option.getOrUndefined), + Effect.orElseSucceed(() => undefined), + ); + const auth = probed?.auth ?? { status: "unknown" as const }; + return snapshot( + settings, + checkedAt, + { + installed: true, + version, + auth, + ...(auth.status === "authenticated" + ? { status: "ready" as const } + : auth.status === "unauthenticated" + ? { + status: "error" as const, + message: "Mistral Vibe is not signed in. Run `vibe --setup` or set MISTRAL_API_KEY.", + } + : { + status: "warning" as const, + message: "Could not confirm the Mistral Vibe sign-in.", + }), + }, + probed?.configOptions, + ); +}); diff --git a/packages/provider-vibe/src/settings.ts b/packages/provider-vibe/src/settings.ts new file mode 100644 index 0000000000000..452ef7ee74cdd --- /dev/null +++ b/packages/provider-vibe/src/settings.ts @@ -0,0 +1,37 @@ +/** + * Mistral Vibe instance settings. Shared by the server driver and the client + * settings form, so it holds only browser-safe schema code. + * + * @module provider-vibe/settings + */ +import { + CustomModelSetting, + makeBinaryPathSetting, + makeProviderSettingsSchema, +} from "@t3tools/contracts"; +import * as Effect from "effect/Effect"; +import * as Schema from "effect/Schema"; + +export const VibeSettings = makeProviderSettingsSchema( + { + enabled: Schema.Boolean.pipe( + Schema.withDecodingDefault(Effect.succeed(false)), + Schema.annotateKey({ providerSettingsForm: { hidden: true } }), + ), + binaryPath: makeBinaryPathSetting("vibe-acp").pipe( + Schema.annotateKey({ + title: "Binary path", + description: "Path to Mistral Vibe's ACP server, `vibe-acp`.", + providerSettingsForm: { placeholder: "vibe-acp", clearWhenEmpty: "omit" }, + }), + ), + customModels: Schema.Array(CustomModelSetting).pipe( + Schema.withDecodingDefault(Effect.succeed([])), + Schema.annotateKey({ providerSettingsForm: { hidden: true } }), + ), + }, + { + order: ["binaryPath"], + }, +); +export type VibeSettings = typeof VibeSettings.Type; diff --git a/packages/provider-vibe/tsconfig.json b/packages/provider-vibe/tsconfig.json new file mode 100644 index 0000000000000..374bac55202dd --- /dev/null +++ b/packages/provider-vibe/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "types": ["node"] + }, + "include": ["src"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4f5985a40bc6f..8677a77898836 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -873,6 +873,9 @@ importers: '@t3tools/provider-pi': specifier: workspace:* version: link:../../packages/provider-pi + '@t3tools/provider-vibe': + specifier: workspace:* + version: link:../../packages/provider-vibe '@t3tools/source-control-azure-devops': specifier: workspace:* version: link:../../packages/source-control-azure-devops @@ -1506,6 +1509,9 @@ importers: '@t3tools/provider-pi': specifier: workspace:* version: link:../provider-pi + '@t3tools/provider-vibe': + specifier: workspace:* + version: link:../provider-vibe '@t3tools/shared': specifier: workspace:* version: link:../shared @@ -1963,6 +1969,37 @@ importers: specifier: 24.12.4 version: 24.12.4 + packages/provider-vibe: + dependencies: + '@t3tools/contracts': + specifier: workspace:* + version: link:../contracts + '@t3tools/provider-acp': + specifier: workspace:* + version: link:../provider-acp + '@t3tools/provider-core': + specifier: workspace:* + version: link:../provider-core + '@t3tools/shared': + specifier: workspace:* + version: link:../shared + effect: + specifier: 4.1.2 + version: 4.1.2 + devDependencies: + '@effect/vitest': + specifier: 4.1.2 + version: 4.1.2(patch_hash=299510b71c36537a832a67e9d160a5f112a5f326a29e95c41cb45fc8234e47c7)(@astrojs/compiler-rs@0.5.1)(@tsc-rs/typescript@0.8.0)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5)(@volar/typescript@2.4.28(@tsc-rs/typescript@0.8.0))(bufferutil@4.1.0)(effect@4.1.2)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(playwright@1.60.0)(terser@5.48.0)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + '@types/node': + specifier: 24.12.4 + version: 24.12.4 + effect-acp: + specifier: workspace:* + version: link:../effect-acp + vite-plus: + specifier: 1.4.0 + version: 1.4.0(@astrojs/compiler-rs@0.5.1)(@tsc-rs/typescript@0.8.0)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5)(@volar/typescript@2.4.28(@tsc-rs/typescript@0.8.0))(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(playwright@1.60.0)(terser@5.48.0)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + packages/secret-provider-core: dependencies: '@t3tools/contracts': diff --git a/scripts/release-smoke.ts b/scripts/release-smoke.ts index a586d67ef6035..c62206b81c83f 100644 --- a/scripts/release-smoke.ts +++ b/scripts/release-smoke.ts @@ -40,6 +40,7 @@ const workspaceFiles = [ "packages/provider-kiro/package.json", "packages/provider-muse/package.json", "packages/provider-opencode/package.json", + "packages/provider-vibe/package.json", "packages/secret-provider-core/package.json", "packages/secret-provider-definitions/package.json", "packages/secret-provider-onepassword/package.json", From 4e13676ba405c2e003b7449bf9048803b95c8ca4 Mon Sep 17 00:00:00 2001 From: Christoph Acham <32296133+christophacham@users.noreply.github.com> Date: Wed, 4 Nov 2026 14:56:40 +0100 Subject: [PATCH 2/6] docs: Mistral Vibe provider guide Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 4 ++-- README.md | 5 +++-- docs/README.md | 2 +- docs/user/install.md | 28 +++++++++++++++------------- docs/user/permission-modes.md | 3 +++ docs/user/providers-vibe.md | 32 ++++++++++++++++++++++++++++++++ 6 files changed, 56 insertions(+), 18 deletions(-) create mode 100644 docs/user/providers-vibe.md diff --git a/AGENTS.md b/AGENTS.md index b919730ebe749..a6d1999397950 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # T3 Code -T3 Code is a minimal GUI for coding agents. A Node WebSocket server wraps provider CLIs and agents (Codex, Claude Code, Cursor, Grok, OpenCode, Antigravity, Kiro) and serves web, desktop, and mobile clients. +T3 Code is a minimal GUI for coding agents. A Node WebSocket server wraps provider CLIs and agents (Codex, Claude Code, Cursor, Grok, OpenCode, Antigravity, Kiro, Mistral Vibe) and serves web, desktop, and mobile clients. You can think of T3 Code as an open source "bring-your-own-subscription" alternative to apps like Claude Desktop, Codex App, Cursor Glass and Conductor. @@ -68,7 +68,7 @@ The most common defect in this repo is a change that works on the path you teste - **Entry points.** A behavior reachable from the chat view is usually also reachable from Settings, the command palette, and a keybinding. Fixing one is not fixing the feature. - **Clients.** Web, desktop (wraps web, adds Electron shell/IPC), and mobile (React Native, separate navigation). Shared logic lives in `packages/client-runtime` -- **Providers.** Codex, Claude, Cursor, Grok, OpenCode, Antigravity, and Kiro each have an adapter. Provider-shaped features need a decision per adapter, even if the decision is "not supported here". +- **Providers.** Codex, Claude, Cursor, Grok, OpenCode, Antigravity, Kiro, and Mistral Vibe each have an adapter. Provider-shaped features need a decision per adapter, even if the decision is "not supported here". - **Agents.** A capability a user can trigger is usually one an agent should reach through MCP tools, and scheduled tasks run the same paths. That only works when it is a service method, not handler code. - **Contracts.** Anything crossing the wire is typed in `packages/contracts`. Change the schema and the server, web, mobile, and desktop all follow. - **Reverse states.** If you added a way in, add the way out and the way to see it. Snooze needs unsnooze. Close needs reopen. A one-way door is a bug. diff --git a/README.md b/README.md index 1f63f3609ae0d..acbc8b32ca90e 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ T3 Code is an "agent harness control surface". It enables control of the agents on your machine with a best-in-class mobile app ([iOS](https://apps.apple.com/us/app/t3-code-remote-claude-more/id6787819824), [Android](https://play.google.com/store/apps/details?id=com.t3tools.t3code)), [web app](https://app.t3.codes) and [Electron-based desktop app](https://t3.codes). -Works with your subscriptions on Claude Code, Codex, Cursor, Grok Build, OpenCode, Google Antigravity, and fx. If they're set up on your computer, T3 Code can control them. +Works with your subscriptions on Claude Code, Codex, Cursor, Grok Build, OpenCode, Google Antigravity, fx, and Mistral Vibe. If they're set up on your computer, T3 Code can control them. ## "Wait, what are you selling me?" @@ -13,7 +13,7 @@ We wanted something performant, remote-ready, and truly open. If we ever go the ## Installation > [!WARNING] -> T3 Code currently supports Codex, Claude, Cursor, Grok Build, OpenCode, Antigravity, and fx. Install and authenticate at least one provider before use: +> T3 Code currently supports Codex, Claude, Cursor, Grok Build, OpenCode, Antigravity, fx, and Mistral Vibe. Install and authenticate at least one provider before use: > > - Codex: install [Codex CLI](https://developers.openai.com/codex/cli) and run `codex login` > - Claude: install [Claude Code](https://claude.com/product/claude-code) and run `claude auth login` @@ -22,6 +22,7 @@ We wanted something performant, remote-ready, and truly open. If we ever go the > - OpenCode: install [OpenCode](https://opencode.ai) and run `opencode auth login` > - Antigravity: enable it in Settings, then use **Install Antigravity** and **Sign in with Google**. No CLI is required. > - fx: install [fx](https://fx.sh) and run `fx login` on the machine running the T3 Code server ([setup guide](./docs/user/providers-fx.md)) +> - Mistral Vibe: install [Mistral Vibe](https://github.com/mistralai/mistral-vibe) and run `vibe --setup` on the machine running the T3 Code server ([setup guide](./docs/user/providers-vibe.md)) ### Command line diff --git a/docs/README.md b/docs/README.md index 48eb4deec9de1..b099a4a11d8f1 100644 --- a/docs/README.md +++ b/docs/README.md @@ -25,7 +25,7 @@ - [Running in the background](./user/background-service.md) - [Updating T3 Code](./user/updating.md) - [Triage an issue](./user/triage.md) -- Provider guides: [Codex](./user/providers-codex.md) · [Claude](./user/providers-claude.md) · [OpenCode](./user/providers-opencode.md) · [Antigravity](./user/providers-antigravity.md) · [Kiro](./user/providers-kiro.md) · [Pi](./user/providers-pi.md) · [Muse Code](./user/providers-muse.md) · [fx](./user/providers-fx.md) +- Provider guides: [Codex](./user/providers-codex.md) · [Claude](./user/providers-claude.md) · [OpenCode](./user/providers-opencode.md) · [Antigravity](./user/providers-antigravity.md) · [Kiro](./user/providers-kiro.md) · [Pi](./user/providers-pi.md) · [Muse Code](./user/providers-muse.md) · [fx](./user/providers-fx.md) · [Mistral Vibe](./user/providers-vibe.md) --- diff --git a/docs/user/install.md b/docs/user/install.md index fde265746004e..150c02ef26158 100644 --- a/docs/user/install.md +++ b/docs/user/install.md @@ -158,18 +158,19 @@ 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 | [Connect with ChatGPT](./providers-codex.md#connect-with-chatgpt), or install [Codex CLI](https://developers.openai.com/codex/cli) and 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`. | -| Kiro | Install [Kiro CLI](https://kiro.dev/docs/cli/), then run `kiro-cli 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. | -| Pi | Install [Pi](./providers-pi.md#set-up-pi) with Node.js 22.19 or newer, then run `pi` and `/login`. | -| Muse Code | Install [Muse Code](https://dev.meta.ai/docs/muse-code) on the server, run `muse login`, then enable it in Settings → Providers. | -| fx | Install [fx](https://fx.sh) with `curl -fsSL https://fx.sh/setup.sh \| bash`, then run `fx login`. | +| Provider | Install and authenticate | +| ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Codex | [Connect with ChatGPT](./providers-codex.md#connect-with-chatgpt), or install [Codex CLI](https://developers.openai.com/codex/cli) and 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`. | +| Kiro | Install [Kiro CLI](https://kiro.dev/docs/cli/), then run `kiro-cli login`. | +| Mistral Vibe | Install [Mistral Vibe](./providers-vibe.md#set-up-mistral-vibe), then run `vibe --setup`. | +| OpenCode | Install [OpenCode](https://opencode.ai), then run `opencode auth login`. | +| Antigravity | Install and sign in with Google from T3 Code's provider settings. | +| Pi | Install [Pi](./providers-pi.md#set-up-pi) with Node.js 22.19 or newer, then run `pi` and `/login`. | +| Muse Code | Install [Muse Code](https://dev.meta.ai/docs/muse-code) on the server, run `muse login`, then enable it in Settings → Providers. | +| fx | Install [fx](https://fx.sh) with `curl -fsSL https://fx.sh/setup.sh \| bash`, then run `fx login`. | 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. @@ -200,7 +201,8 @@ their original values. For provider-specific setup and accounts, see [Codex](./providers-codex.md), [Claude](./providers-claude.md), [OpenCode](./providers-opencode.md), [Antigravity](./providers-antigravity.md), [Kiro](./providers-kiro.md), -[Pi](./providers-pi.md), [Muse Code](./providers-muse.md), and [fx](./providers-fx.md). +[Pi](./providers-pi.md), [Muse Code](./providers-muse.md), [fx](./providers-fx.md), and +[Mistral Vibe](./providers-vibe.md). ## Next steps diff --git a/docs/user/permission-modes.md b/docs/user/permission-modes.md index 796e3f316122d..3e6867b5ef647 100644 --- a/docs/user/permission-modes.md +++ b/docs/user/permission-modes.md @@ -57,4 +57,7 @@ approvals for actions that support them. Kiro offers only **Supervised** and **Full access**. In **Supervised** Kiro also asks you to review a turn's file changes. See [Kiro permissions](./providers-kiro.md#permission-modes). +Mistral Vibe offers no **Auto**. Each other mode runs the matching Vibe agent. See +[Mistral Vibe permissions](./providers-vibe.md#permission-modes). + See the [provider guides](./install.md#providers) for setup and provider-specific limits. diff --git a/docs/user/providers-vibe.md b/docs/user/providers-vibe.md new file mode 100644 index 0000000000000..110151e480e9f --- /dev/null +++ b/docs/user/providers-vibe.md @@ -0,0 +1,32 @@ +# Mistral Vibe + +T3 Code runs [Mistral Vibe](https://github.com/mistralai/mistral-vibe) as an agent through its ACP +server, `vibe-acp`, using your own Mistral account and Vibe configuration. + +## Set Up Mistral Vibe + +1. Install Vibe on the machine running the T3 Code server: + `curl -LsSf https://mistral.ai/vibe/install.sh | bash`, or `uv tool install mistral-vibe`. +2. Run `vibe --setup` once in a terminal. You can instead set `MISTRAL_API_KEY` in the Mistral Vibe + provider's **Mistral API key** field. +3. Open **Settings → Providers**, add **Mistral Vibe**, and enable it. + +If `vibe-acp` is not on the server's `PATH`, set its **Binary path**. The installer usually puts it +in `~/.local/bin`. + +## Models + +The model picker lists the models Vibe offers once it is signed in. **Default** keeps the model +Vibe itself would start with. Models that support it show a **Thinking** setting with the levels +Vibe offers. + +## Permission Modes + +The permission mode picks the Vibe agent a thread runs as: + +- **Supervised** runs Vibe's `ask` agent, which asks you before every tool call. +- **Auto-accept edits** runs `accept-edits`: Vibe applies file edits itself and asks before other + tools. +- **Full access** runs `auto-approve`, which runs every tool without asking. + +Vibe offers no **Auto** mode in T3 Code. From 9b3b4949d7fc947e0df856bafd111ade6d80e804 Mon Sep 17 00:00:00 2001 From: Christoph Acham <32296133+christophacham@users.noreply.github.com> Date: Wed, 4 Nov 2026 15:03:55 +0100 Subject: [PATCH 3/6] fix(server): Vibe status keeps sign-in and offers only listed agents A failed or slow session/new after a confirmed sign-in reported the sign-in as unknown and dropped the models; it now stays signed in with a warning. Thinking levels attach only to the model Vibe listed them for. Runtime modes follow the agents the session lists, so a Vibe config that turns auto-approve off no longer offers Full access. Docs no longer promise a prompt before reads, and the managed policy lists vibe. Co-Authored-By: Claude Opus 5.5 --- docs/user/managed-policy.md | 2 +- docs/user/providers-vibe.md | 11 +- .../provider-vibe/src/server/status.test.ts | 44 ++++++- packages/provider-vibe/src/server/status.ts | 115 ++++++++++++------ 4 files changed, 127 insertions(+), 45 deletions(-) diff --git a/docs/user/managed-policy.md b/docs/user/managed-policy.md index 6017444e52a9f..52de8fa8c53dc 100644 --- a/docs/user/managed-policy.md +++ b/docs/user/managed-policy.md @@ -254,7 +254,7 @@ so restart T3 Code after that. `providers.allowedDrivers` lists the providers that may run, by ID: `codex` (Codex), `claudeAgent` (Claude), `cursor` (Cursor), `grok` (Grok), `opencode` (OpenCode), `antigravity` -(Antigravity), `fx` (fx), `kiro` (Kiro), `pi` (Pi), `muse` (Muse Code), and `acpRegistry` (ACP +(Antigravity), `fx` (fx), `kiro` (Kiro), `vibe` (Mistral Vibe), `pi` (Pi), `muse` (Muse Code), and `acpRegistry` (ACP Registry). `t3 policy check` rejects an ID this version does not know. Threads on other providers stay, marked unavailable, and users can still turn those providers off. diff --git a/docs/user/providers-vibe.md b/docs/user/providers-vibe.md index 110151e480e9f..abae760dedcc1 100644 --- a/docs/user/providers-vibe.md +++ b/docs/user/providers-vibe.md @@ -17,16 +17,17 @@ in `~/.local/bin`. ## Models The model picker lists the models Vibe offers once it is signed in. **Default** keeps the model -Vibe itself would start with. Models that support it show a **Thinking** setting with the levels -Vibe offers. +Vibe itself would start with. That model shows a **Thinking** setting with the levels Vibe offers +for it; other models run at Vibe's own thinking setting. ## Permission Modes The permission mode picks the Vibe agent a thread runs as: -- **Supervised** runs Vibe's `ask` agent, which asks you before every tool call. -- **Auto-accept edits** runs `accept-edits`: Vibe applies file edits itself and asks before other - tools. +- **Supervised** runs Vibe's `ask` agent. Commands and file changes wait for your approval; reads + and searches run without asking. +- **Auto-accept edits** runs `accept-edits`: Vibe applies file edits itself and asks before + commands. - **Full access** runs `auto-approve`, which runs every tool without asking. Vibe offers no **Auto** mode in T3 Code. diff --git a/packages/provider-vibe/src/server/status.test.ts b/packages/provider-vibe/src/server/status.test.ts index 4c624de42b367..f493ebeb02a5d 100644 --- a/packages/provider-vibe/src/server/status.test.ts +++ b/packages/provider-vibe/src/server/status.test.ts @@ -1,7 +1,12 @@ import { assert, describe, it } from "@effect/vitest"; import type * as EffectAcpSchema from "effect-acp/compat"; -import { vibeAuthFromStatusReport, vibeModelsFromConfigOptions } from "./status.ts"; +import { + vibeAuthFromStatusReport, + vibeModelsFromConfigOptions, + vibeProbeStatus, + vibeRuntimeModes, +} from "./status.ts"; // Shaped like vibe-acp 3.0.3's `session/new` config options. const VIBE_CONFIG_OPTIONS = [ @@ -63,7 +68,7 @@ describe("vibeAuthFromStatusReport", () => { }); describe("vibeModelsFromConfigOptions", () => { - it("lists Default and Vibe's models, with thinking but not the agent mode", () => { + it("lists Default and Vibe's models, with thinking only where Vibe listed it", () => { const models = vibeModelsFromConfigOptions(VIBE_CONFIG_OPTIONS); assert.deepEqual( models.map((model) => [model.slug, model.name, model.isDefault ?? false]), @@ -75,8 +80,10 @@ describe("vibeModelsFromConfigOptions", () => { ); assert.deepEqual(models[0]?.aliases, ["mistral-medium-3.5"]); assert.deepEqual( - models[0]?.capabilities?.optionDescriptors?.map((descriptor) => descriptor.id), - ["thinking"], + models.map((model) => + model.capabilities?.optionDescriptors?.map((descriptor) => descriptor.id), + ), + [["thinking"], ["thinking"], []], ); }); @@ -87,3 +94,32 @@ describe("vibeModelsFromConfigOptions", () => { ); }); }); + +describe("vibeProbeStatus", () => { + it("keeps a confirmed sign-in when the model list fails", () => { + assert.deepEqual(vibeProbeStatus({ status: "authenticated" }, true), { status: "ready" }); + assert.deepEqual(vibeProbeStatus({ status: "authenticated" }, false), { + status: "warning", + message: "Mistral Vibe is signed in but its models could not be listed.", + }); + assert.equal(vibeProbeStatus({ status: "unauthenticated" }, false).status, "error"); + assert.equal(vibeProbeStatus({ status: "unknown" }, false).status, "warning"); + }); +}); + +describe("vibeRuntimeModes", () => { + it("offers only the modes whose Vibe agent the session lists", () => { + const allModes = ["approval-required", "auto-accept-edits", "full-access"] as const; + assert.deepEqual(vibeRuntimeModes(VIBE_CONFIG_OPTIONS), allModes); + assert.deepEqual(vibeRuntimeModes(undefined), allModes); + const withoutAutoApprove = VIBE_CONFIG_OPTIONS.map((option) => + option.id === "mode" + ? { ...option, options: option.options.filter((choice) => choice.value !== "auto-approve") } + : option, + ); + assert.deepEqual(vibeRuntimeModes(withoutAutoApprove), [ + "approval-required", + "auto-accept-edits", + ]); + }); +}); diff --git a/packages/provider-vibe/src/server/status.ts b/packages/provider-vibe/src/server/status.ts index fd252a35485e8..91e1b7ed25c48 100644 --- a/packages/provider-vibe/src/server/status.ts +++ b/packages/provider-vibe/src/server/status.ts @@ -1,5 +1,9 @@ import type * as EffectAcpSchema from "effect-acp/compat"; -import { type ServerProviderAuth, type ServerProviderModel } from "@t3tools/contracts"; +import { + type RuntimeMode, + type ServerProviderAuth, + type ServerProviderModel, +} from "@t3tools/contracts"; import type { VibeSettings } from "../settings.ts"; import * as HostProcess from "@t3tools/shared/HostProcess"; import { createModelCapabilities } from "@t3tools/shared/model"; @@ -24,6 +28,7 @@ import { VIBE_DRIVER_KIND, VIBE_MODE_CONFIG_ID, VIBE_SUPPORTED_RUNTIME_MODES, + vibeRuntimeModeForSessionMode, } from "./acpSupport.ts"; import { AUTH_PROBE_TIMEOUT_MS, @@ -39,9 +44,9 @@ import { const VIBE_PRESENTATION = { displayName: "Mistral Vibe", showInteractionModeToggle: false, - supportedRuntimeModes: VIBE_SUPPORTED_RUNTIME_MODES, } as const; const VERSION_PROBE_TIMEOUT_MS = 10_000; +const NO_OPTIONS = createModelCapabilities({ optionDescriptors: [] }); /** `_auth/status` on vibe-acp 3.0: `authenticated` is whether the active provider can be used. */ const decodeVibeAuthStatus = Schema.decodeUnknownOption( @@ -56,13 +61,14 @@ export function vibeAuthFromStatusReport(report: unknown): ServerProviderAuth { /** * Vibe's model choices from a session's config options. "Default" keeps the - * session's own model. The mode option is left out: T3's runtime mode selects + * session's own model. Vibe lists thinking levels for that model only, so the + * others carry none. The mode option is left out: T3's runtime mode selects * the Vibe agent. */ export function vibeModelsFromConfigOptions( configOptions: ReadonlyArray, ): ReadonlyArray { - const capabilities = createModelCapabilities({ + const currentCapabilities = createModelCapabilities({ optionDescriptors: acpProviderOptionDescriptors({ configOptions: configOptions.filter((option) => option.id !== VIBE_MODE_CONFIG_ID), modeState: undefined, @@ -79,7 +85,7 @@ export function vibeModelsFromConfigOptions( ...(current === undefined ? {} : { aliases: [current] }), isCustom: false, isDefault: true, - capabilities, + capabilities: currentCapabilities, }, ...choices .filter((choice) => choice.value !== VIBE_DEFAULT_MODEL_SLUG) @@ -87,29 +93,46 @@ export function vibeModelsFromConfigOptions( slug: choice.value, name: choice.name || choice.value, isCustom: false, - capabilities, + capabilities: choice.value === current ? currentCapabilities : NO_OPTIONS, })), ]; } +/** + * The runtime modes whose Vibe agent the session lists. Vibe config can turn an + * agent off (`enable_auto_approve = false`), and selecting it then fails. + */ +export function vibeRuntimeModes( + configOptions: ReadonlyArray | undefined, +): ReadonlyArray { + const modeOption = configOptions?.find( + (option) => option.id === VIBE_MODE_CONFIG_ID && option.type === "select", + ); + if (modeOption?.type !== "select") return VIBE_SUPPORTED_RUNTIME_MODES; + const listed = flattenSelectChoices(modeOption.options).map((choice) => + vibeRuntimeModeForSessionMode(choice.value), + ); + const modes = VIBE_SUPPORTED_RUNTIME_MODES.filter((mode) => listed.includes(mode)); + return modes.length > 0 ? modes : VIBE_SUPPORTED_RUNTIME_MODES; +} + const snapshot = ( settings: VibeSettings, checkedAt: string, probe: ProviderProbeResult, - configOptions: ReadonlyArray = [], + configOptions?: ReadonlyArray, ): ServerProviderDraft => { - const models = vibeModelsFromConfigOptions(configOptions); + const models = vibeModelsFromConfigOptions(configOptions ?? []); return { ...buildServerProvider({ driver: VIBE_DRIVER_KIND, - presentation: VIBE_PRESENTATION, + presentation: { + ...VIBE_PRESENTATION, + supportedRuntimeModes: vibeRuntimeModes(configOptions), + }, enabled: settings.enabled, checkedAt, - models: providerModelsFromSettings( - models, - settings.customModels, - models[0]?.capabilities ?? null, - ), + models: providerModelsFromSettings(models, settings.customModels, NO_OPTIONS), probe, }), supportsTextGeneration: false, @@ -161,13 +184,45 @@ const probeVibeAcp = Effect.fn("VibeProvider.probeVibeAcp")(function* ( cwd: yield* HostProcess.WorkingDirectory, clientInfo: { name: "t3-code-provider-probe", version: "0.0.0" }, }); - yield* runtime.initialize(); - const auth = vibeAuthFromStatusReport(yield* runtime.request("_auth/status", {})); + const auth = yield* runtime.initialize().pipe( + Effect.andThen(runtime.request("_auth/status", {})), + Effect.map(vibeAuthFromStatusReport), + Effect.timeoutOption(AUTH_PROBE_TIMEOUT_MS), + Effect.map(Option.getOrElse((): ServerProviderAuth => ({ status: "unknown" }))), + Effect.orElseSucceed((): ServerProviderAuth => ({ status: "unknown" })), + ); if (auth.status !== "authenticated") return { auth, configOptions: undefined }; - const started = yield* runtime.start(); - return { auth, configOptions: started.sessionSetupResult.configOptions ?? [] }; + const configOptions = yield* runtime.start().pipe( + Effect.map((started) => started.sessionSetupResult.configOptions ?? []), + Effect.timeoutOption(AUTH_PROBE_TIMEOUT_MS), + Effect.map(Option.getOrUndefined), + Effect.orElseSucceed(() => undefined), + ); + return { auth, configOptions }; }, Effect.scoped); +/** The snapshot status for what the ACP probe learned. */ +export function vibeProbeStatus( + auth: ServerProviderAuth, + modelsListed: boolean, +): Pick { + if (auth.status === "unauthenticated") { + return { + status: "error", + message: "Mistral Vibe is not signed in. Run `vibe --setup` or set MISTRAL_API_KEY.", + }; + } + if (auth.status !== "authenticated") { + return { status: "warning", message: "Could not confirm the Mistral Vibe sign-in." }; + } + return modelsListed + ? { status: "ready" } + : { + status: "warning", + message: "Mistral Vibe is signed in but its models could not be listed.", + }; +} + export const checkVibeProviderStatus = Effect.fn("checkVibeProviderStatus")(function* ( settings: VibeSettings, environment: NodeJS.ProcessEnv, @@ -216,30 +271,20 @@ export const checkVibeProviderStatus = Effect.fn("checkVibeProviderStatus")(func } const probed = yield* probeVibeAcp(settings, environment).pipe( - Effect.timeoutOption(AUTH_PROBE_TIMEOUT_MS), - Effect.map(Option.getOrUndefined), - Effect.orElseSucceed(() => undefined), + Effect.orElseSucceed(() => ({ + auth: { status: "unknown" } satisfies ServerProviderAuth, + configOptions: undefined, + })), ); - const auth = probed?.auth ?? { status: "unknown" as const }; return snapshot( settings, checkedAt, { installed: true, version, - auth, - ...(auth.status === "authenticated" - ? { status: "ready" as const } - : auth.status === "unauthenticated" - ? { - status: "error" as const, - message: "Mistral Vibe is not signed in. Run `vibe --setup` or set MISTRAL_API_KEY.", - } - : { - status: "warning" as const, - message: "Could not confirm the Mistral Vibe sign-in.", - }), + auth: probed.auth, + ...vibeProbeStatus(probed.auth, probed.configOptions !== undefined), }, - probed?.configOptions, + probed.configOptions, ); }); From 6a304527dd301ca493cc333accb1e5a395fc0bfb Mon Sep 17 00:00:00 2001 From: Christoph Acham <32296133+christophacham@users.noreply.github.com> Date: Wed, 4 Nov 2026 15:11:13 +0100 Subject: [PATCH 4/6] fix(server): Vibe reports a config with no agent T3 can run When Vibe lists agents but none of ask, accept-edits or auto-approve, every runtime mode would fail at session setup; the provider now shows an error naming them instead of offering all three. A session with no config options counts as an unlisted model list, and the guide again says accept-edits asks before other tools, not only commands. Co-Authored-By: Claude Opus 5.5 --- docs/user/providers-vibe.md | 2 +- .../provider-vibe/src/server/status.test.ts | 31 +++++++++++++------ packages/provider-vibe/src/server/status.ts | 22 ++++++++----- 3 files changed, 36 insertions(+), 19 deletions(-) diff --git a/docs/user/providers-vibe.md b/docs/user/providers-vibe.md index abae760dedcc1..bd69439489ecf 100644 --- a/docs/user/providers-vibe.md +++ b/docs/user/providers-vibe.md @@ -27,7 +27,7 @@ The permission mode picks the Vibe agent a thread runs as: - **Supervised** runs Vibe's `ask` agent. Commands and file changes wait for your approval; reads and searches run without asking. - **Auto-accept edits** runs `accept-edits`: Vibe applies file edits itself and asks before - commands. + other tools. - **Full access** runs `auto-approve`, which runs every tool without asking. Vibe offers no **Auto** mode in T3 Code. diff --git a/packages/provider-vibe/src/server/status.test.ts b/packages/provider-vibe/src/server/status.test.ts index f493ebeb02a5d..f4424ded3c590 100644 --- a/packages/provider-vibe/src/server/status.test.ts +++ b/packages/provider-vibe/src/server/status.test.ts @@ -97,29 +97,40 @@ describe("vibeModelsFromConfigOptions", () => { describe("vibeProbeStatus", () => { it("keeps a confirmed sign-in when the model list fails", () => { - assert.deepEqual(vibeProbeStatus({ status: "authenticated" }, true), { status: "ready" }); - assert.deepEqual(vibeProbeStatus({ status: "authenticated" }, false), { + assert.deepEqual(vibeProbeStatus({ status: "authenticated" }, VIBE_CONFIG_OPTIONS), { + status: "ready", + }); + assert.deepEqual(vibeProbeStatus({ status: "authenticated" }, undefined), { status: "warning", message: "Mistral Vibe is signed in but its models could not be listed.", }); - assert.equal(vibeProbeStatus({ status: "unauthenticated" }, false).status, "error"); - assert.equal(vibeProbeStatus({ status: "unknown" }, false).status, "warning"); + assert.equal(vibeProbeStatus({ status: "unauthenticated" }, undefined).status, "error"); + assert.equal(vibeProbeStatus({ status: "unknown" }, undefined).status, "warning"); }); }); +const onlyAgents = (agents: ReadonlyArray) => + VIBE_CONFIG_OPTIONS.map((option) => + option.id === "mode" + ? { ...option, options: agents.map((agent) => ({ value: agent, name: agent })) } + : option, + ); + describe("vibeRuntimeModes", () => { it("offers only the modes whose Vibe agent the session lists", () => { const allModes = ["approval-required", "auto-accept-edits", "full-access"] as const; assert.deepEqual(vibeRuntimeModes(VIBE_CONFIG_OPTIONS), allModes); assert.deepEqual(vibeRuntimeModes(undefined), allModes); - const withoutAutoApprove = VIBE_CONFIG_OPTIONS.map((option) => - option.id === "mode" - ? { ...option, options: option.options.filter((choice) => choice.value !== "auto-approve") } - : option, - ); - assert.deepEqual(vibeRuntimeModes(withoutAutoApprove), [ + assert.deepEqual(vibeRuntimeModes(onlyAgents(["ask", "accept-edits"])), [ "approval-required", "auto-accept-edits", ]); }); + + // `enabled_agents = ["smart-approve"]` leaves no agent T3 Code can select. + it("reports a Vibe that enables none of T3's agents", () => { + const smartOnly = onlyAgents(["smart-approve"]); + assert.deepEqual(vibeRuntimeModes(smartOnly), []); + assert.equal(vibeProbeStatus({ status: "authenticated" }, smartOnly).status, "error"); + }); }); diff --git a/packages/provider-vibe/src/server/status.ts b/packages/provider-vibe/src/server/status.ts index 91e1b7ed25c48..472fe344efc38 100644 --- a/packages/provider-vibe/src/server/status.ts +++ b/packages/provider-vibe/src/server/status.ts @@ -112,8 +112,7 @@ export function vibeRuntimeModes( const listed = flattenSelectChoices(modeOption.options).map((choice) => vibeRuntimeModeForSessionMode(choice.value), ); - const modes = VIBE_SUPPORTED_RUNTIME_MODES.filter((mode) => listed.includes(mode)); - return modes.length > 0 ? modes : VIBE_SUPPORTED_RUNTIME_MODES; + return VIBE_SUPPORTED_RUNTIME_MODES.filter((mode) => listed.includes(mode)); } const snapshot = ( @@ -193,7 +192,7 @@ const probeVibeAcp = Effect.fn("VibeProvider.probeVibeAcp")(function* ( ); if (auth.status !== "authenticated") return { auth, configOptions: undefined }; const configOptions = yield* runtime.start().pipe( - Effect.map((started) => started.sessionSetupResult.configOptions ?? []), + Effect.map((started) => started.sessionSetupResult.configOptions ?? undefined), Effect.timeoutOption(AUTH_PROBE_TIMEOUT_MS), Effect.map(Option.getOrUndefined), Effect.orElseSucceed(() => undefined), @@ -204,7 +203,7 @@ const probeVibeAcp = Effect.fn("VibeProvider.probeVibeAcp")(function* ( /** The snapshot status for what the ACP probe learned. */ export function vibeProbeStatus( auth: ServerProviderAuth, - modelsListed: boolean, + configOptions: ReadonlyArray | undefined, ): Pick { if (auth.status === "unauthenticated") { return { @@ -215,11 +214,18 @@ export function vibeProbeStatus( if (auth.status !== "authenticated") { return { status: "warning", message: "Could not confirm the Mistral Vibe sign-in." }; } - return modelsListed + if (configOptions === undefined) { + return { + status: "warning", + message: "Mistral Vibe is signed in but its models could not be listed.", + }; + } + return vibeRuntimeModes(configOptions).length > 0 ? { status: "ready" } : { - status: "warning", - message: "Mistral Vibe is signed in but its models could not be listed.", + status: "error", + message: + "Mistral Vibe enables none of the agents T3 Code runs: ask, accept-edits, auto-approve.", }; } @@ -283,7 +289,7 @@ export const checkVibeProviderStatus = Effect.fn("checkVibeProviderStatus")(func installed: true, version, auth: probed.auth, - ...vibeProbeStatus(probed.auth, probed.configOptions !== undefined), + ...vibeProbeStatus(probed.auth, probed.configOptions), }, probed.configOptions, ); From a226f1a3c91c66b1856e53b13fcac88af4ea0953 Mon Sep 17 00:00:00 2001 From: Christoph Acham <32296133+christophacham@users.noreply.github.com> Date: Wed, 4 Nov 2026 19:31:47 +0100 Subject: [PATCH 5/6] chore(vibe): follow the current typecheck toolchain Refresh the Vibe workspace lockfile references after rebasing and document how to reload models. --- docs/user/providers-vibe.md | 3 +++ packages/provider-vibe/package.json | 2 +- pnpm-lock.yaml | 4 ++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/user/providers-vibe.md b/docs/user/providers-vibe.md index bd69439489ecf..bd5a32159910a 100644 --- a/docs/user/providers-vibe.md +++ b/docs/user/providers-vibe.md @@ -20,6 +20,9 @@ The model picker lists the models Vibe offers once it is signed in. **Default** Vibe itself would start with. That model shows a **Thinking** setting with the levels Vibe offers for it; other models run at Vibe's own thinking setting. +After changing your Vibe configuration or account, refresh provider status in **Settings → +Providers** to reload the model list. + ## Permission Modes The permission mode picks the Vibe agent a thread runs as: diff --git a/packages/provider-vibe/package.json b/packages/provider-vibe/package.json index b786dc2b61bfb..3d1fce71192aa 100644 --- a/packages/provider-vibe/package.json +++ b/packages/provider-vibe/package.json @@ -17,7 +17,7 @@ } }, "scripts": { - "typecheck": "tsc --noEmit", + "typecheck": "effect-tsgo tsrs --noEmit", "test": "vp test run --config ../../vite.config.ts --dir ." }, "dependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8677a77898836..2d4f1dd3a7bd6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1989,7 +1989,7 @@ importers: devDependencies: '@effect/vitest': specifier: 4.1.2 - version: 4.1.2(patch_hash=299510b71c36537a832a67e9d160a5f112a5f326a29e95c41cb45fc8234e47c7)(@astrojs/compiler-rs@0.5.1)(@tsc-rs/typescript@0.8.0)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5)(@volar/typescript@2.4.28(@tsc-rs/typescript@0.8.0))(bufferutil@4.1.0)(effect@4.1.2)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(playwright@1.60.0)(terser@5.48.0)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + version: 4.1.2(patch_hash=299510b71c36537a832a67e9d160a5f112a5f326a29e95c41cb45fc8234e47c7)(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5)(@volar/typescript@2.4.28(typescript@6.0.3))(bufferutil@4.1.0)(effect@4.1.2)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(playwright@1.60.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) '@types/node': specifier: 24.12.4 version: 24.12.4 @@ -1998,7 +1998,7 @@ importers: version: link:../effect-acp vite-plus: specifier: 1.4.0 - version: 1.4.0(@astrojs/compiler-rs@0.5.1)(@tsc-rs/typescript@0.8.0)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5)(@volar/typescript@2.4.28(@tsc-rs/typescript@0.8.0))(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(playwright@1.60.0)(terser@5.48.0)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + version: 1.4.0(@astrojs/compiler-rs@0.5.1)(@types/node@24.12.4)(@vitest/browser-playwright@5.0.5)(@vitest/ui@5.0.5)(@volar/typescript@2.4.28(typescript@6.0.3))(bufferutil@4.1.0)(esbuild@0.28.2)(jiti@2.7.0)(jsdom@30.1.0(@noble/hashes@1.8.0))(playwright@1.60.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) packages/secret-provider-core: dependencies: From 8e55655c08a61d9e259f1cdd03a09edbcbc81b65 Mon Sep 17 00:00:00 2001 From: Christoph Acham <32296133+christophacham@users.noreply.github.com> Date: Wed, 4 Nov 2026 20:25:52 +0100 Subject: [PATCH 6/6] fix(server): Vibe custom models reach vibe-acp The ACP runtime checks a model choice against the session's model list and only lets through custom slugs it is given. Vibe passed its custom models to the adapter but not the runtime, so every custom model was refused before Vibe saw it. Pass the same slugs to both, without `default`, which is T3's choice to keep the session's model. Co-Authored-By: Claude Opus 5.5 --- .../provider-vibe/src/server/acpSupport.test.ts | 13 +++++++++++++ packages/provider-vibe/src/server/acpSupport.ts | 8 ++++++++ packages/provider-vibe/src/server/adapter.ts | 8 ++++---- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/packages/provider-vibe/src/server/acpSupport.test.ts b/packages/provider-vibe/src/server/acpSupport.test.ts index 009a49dcde83d..f3410b85896fc 100644 --- a/packages/provider-vibe/src/server/acpSupport.test.ts +++ b/packages/provider-vibe/src/server/acpSupport.test.ts @@ -5,6 +5,7 @@ import * as EffectAcpErrors from "effect-acp/errors"; import { ProviderAdapterV2RuntimePolicy } from "@t3tools/provider-core/server/ProviderAdapter"; import { + vibeCustomModelSlugs, vibePromptFailure, vibeRuntimeModeForSessionMode, vibeSessionModeForPolicy, @@ -67,3 +68,15 @@ describe("vibePromptFailure", () => { assert.equal(vibePromptFailure(new Error("private defect")).message, "Provider turn failed."); }); }); + +describe("vibeCustomModelSlugs", () => { + // `default` is T3's keep-the-session-model choice, never a model id Vibe knows. + it("sends custom models to Vibe except the reserved default", () => { + assert.deepEqual( + vibeCustomModelSlugs({ + customModels: ["devstral-medium", "default", { slug: "codestral", name: "Codestral" }], + }), + ["devstral-medium", "codestral"], + ); + }); +}); diff --git a/packages/provider-vibe/src/server/acpSupport.ts b/packages/provider-vibe/src/server/acpSupport.ts index 7bb45857d0f8f..20dea35aafd16 100644 --- a/packages/provider-vibe/src/server/acpSupport.ts +++ b/packages/provider-vibe/src/server/acpSupport.ts @@ -1,5 +1,6 @@ import { ProviderDriverKind, type RuntimeMode } from "@t3tools/contracts"; import type { VibeSettings } from "../settings.ts"; +import { readCustomModelEntries } from "@t3tools/shared/model"; import type * as SecureRandom from "effect/crypto/SecureRandom"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; @@ -18,6 +19,13 @@ export const VIBE_DRIVER_KIND = ProviderDriverKind.make("vibe"); export const VIBE_DEFAULT_MODEL_SLUG = "default"; export const VIBE_MODE_CONFIG_ID = "mode"; +/** Custom model slugs to send to Vibe; `default` stays T3's keep-the-session-model choice. */ +export function vibeCustomModelSlugs(settings: Pick) { + return readCustomModelEntries(settings.customModels) + .map((entry) => entry.slug) + .filter((slug) => slug !== VIBE_DEFAULT_MODEL_SLUG); +} + /** * Vibe's agents are permission profiles. Smart Approve, the match for Auto, * ships hidden behind Vibe configuration, so Auto is not offered. diff --git a/packages/provider-vibe/src/server/adapter.ts b/packages/provider-vibe/src/server/adapter.ts index 9450d48cd8cc4..5b967fb6c191f 100644 --- a/packages/provider-vibe/src/server/adapter.ts +++ b/packages/provider-vibe/src/server/adapter.ts @@ -1,6 +1,5 @@ import * as HostProcess from "@t3tools/shared/HostProcess"; import { resolveSelfInvocation, type SelfInvocation } from "@t3tools/shared/nodeRuntime"; -import { readCustomModelEntries } from "@t3tools/shared/model"; import { VibeSettings } from "../settings.ts"; import type * as SecureRandom from "effect/crypto/SecureRandom"; import * as Effect from "effect/Effect"; @@ -29,6 +28,7 @@ import { makeVibeAcpRuntime, registerVibeAcpExtensions, VIBE_DRIVER_KIND, + vibeCustomModelSlugs, vibePromptFailure, vibeRuntimeModeForSessionMode, vibeSessionModeForPolicy, @@ -45,6 +45,7 @@ interface VibeAdapterV2Options { } const makeVibeAdapterV2 = Effect.fn("makeVibeAdapterV2")(function* (options: VibeAdapterV2Options) { + const customModels = vibeCustomModelSlugs(options.settings); return yield* makeAcpAdapterV2({ instanceId: options.instanceId, selfInvocation: options.selfInvocation, @@ -52,9 +53,7 @@ const makeVibeAdapterV2 = Effect.fn("makeVibeAdapterV2")(function* (options: Vib driver: VIBE_DRIVER_KIND, runtimeHarness: "vibe", capabilities: AcpProviderCapabilitiesV2, - customModels: readCustomModelEntries(options.settings.customModels).map( - (entry) => entry.slug, - ), + customModels, makeRuntime: ({ runtimePolicy: _runtimePolicy, processEnvironment, @@ -63,6 +62,7 @@ const makeVibeAdapterV2 = Effect.fn("makeVibeAdapterV2")(function* (options: Vib makeVibeAcpRuntime({ ...input, settings: options.settings, + customModels, environment: { ...options.environment, ...processEnvironment }, }), sessionModeForPolicy: vibeSessionModeForPolicy,