546 lines · 16.9 KB · TypeScript
RawDownload
import {  type ModelCapabilities,  type OpenCodeSettings,  type ServerProviderModel,  type ServerProviderSkill,} from "@t3tools/contracts";import * as Cause from "effect/Cause";import * as Data from "effect/Data";import * as DateTime from "effect/DateTime";import * as Effect from "effect/Effect"; import { createModelCapabilities } from "@t3tools/shared/model";import { compareSemverVersions } from "@t3tools/shared/semver";import {  buildServerProvider,  COMPACT_SLASH_COMMAND,  nonEmptyTrimmed,  parseGenericCliVersion,  providerModelsFromSettings,  type ServerProviderDraft,} from "../providerSnapshot.ts";import {  MINIMUM_OPENCODE_VERSION,  OpenCodeRuntime,  openCodeRuntimeErrorDetail,  type OpenCodeInventory,} from "../opencodeRuntime.ts";import type { Agent, ProviderListResponse } from "@opencode-ai/sdk/v2";import * as OpenCodeServerOwner from "../OpenCodeServerOwner.ts"; const OPENCODE_PRESENTATION = {  displayName: "OpenCode",  showInteractionModeToggle: false,} as const;const OPENCODE_VERSION_PROBE_TIMEOUT = "4 seconds"; class OpenCodeProbeError extends Data.TaggedError("OpenCodeProbeError")<{  readonly cause?: unknown;  readonly detail: string;}> {} function normalizeProbeMessage(message: string): string | undefined {  const trimmed = message.trim();  if (trimmed.length === 0) {    return undefined;  }  if (    trimmed === "An error occurred in Effect.tryPromise" ||    trimmed === "An error occurred in Effect.try"  ) {    return undefined;  }  return trimmed;} function normalizedErrorMessage(cause: unknown): string | undefined {  if (cause instanceof OpenCodeProbeError) {    return normalizeProbeMessage(cause.detail);  }   if (!(cause instanceof Error)) {    return undefined;  }   return normalizeProbeMessage(cause.message);} function formatOpenCodeProbeError(input: {  readonly cause: unknown;  readonly isExternalServer: boolean;  readonly phase: "version" | "inventory";  readonly serverUrl: string;}): { readonly installed: boolean; readonly message: string } {  const detail = normalizedErrorMessage(input.cause);  const lower = detail?.toLowerCase() ?? "";   if (input.isExternalServer) {    if (      lower.includes("401") ||      lower.includes("403") ||      lower.includes("unauthorized") ||      lower.includes("forbidden")    ) {      return {        installed: true,        message: "OpenCode server rejected authentication. Check the server URL and password.",      };    }     if (      lower.includes("econnrefused") ||      lower.includes("enotfound") ||      lower.includes("fetch failed") ||      lower.includes("networkerror") ||      lower.includes("timed out") ||      lower.includes("timeout") ||      lower.includes("socket hang up")    ) {      return {        installed: true,        message: `Couldn't reach the configured OpenCode server at ${input.serverUrl}. Check that the server is running and the URL is correct.`,      };    }     return {      installed: true,      message: detail ?? "Failed to connect to the configured OpenCode server.",    };  }   if (lower.includes("enoent") || lower.includes("notfound")) {    return {      installed: false,      message: "OpenCode CLI (`opencode`) is not installed or not on PATH.",    };  }   if (lower.includes("quarantine")) {    return {      installed: true,      message:        "macOS is blocking the OpenCode binary (quarantine). Run `xattr -d com.apple.quarantine $(which opencode)` to fix this.",    };  }   if (lower.includes("invalid code signature") || lower.includes("corrupted")) {    return {      installed: true,      message:        "macOS killed the OpenCode process due to an invalid code signature. The binary may be corrupted — try reinstalling OpenCode.",    };  }   const failureLabel =    input.phase === "inventory"      ? "Failed to load OpenCode provider inventory"      : "Failed to execute OpenCode CLI health check";  return {    installed: true,    message: detail ? `${failureLabel}: ${detail}` : `${failureLabel}.`,  };} function titleCaseSlug(value: string): string {  const segments: Array<string> = [];  for (const segment of value.split(/[-_/]+/)) {    if (segment.length > 0) {      segments.push(segment.charAt(0).toUpperCase() + segment.slice(1));    }  }  return segments.join(" ");} function inferDefaultVariant(  providerID: string,  variants: ReadonlyArray<string>,): string | undefined {  if (variants.length === 1) {    return variants[0];  }  if (providerID === "anthropic" || providerID.startsWith("google")) {    return variants.includes("high") ? "high" : undefined;  }  if (providerID === "openai" || providerID === "opencode") {    return variants.includes("medium") ? "medium" : variants.includes("high") ? "high" : undefined;  }  return undefined;} function inferDefaultAgent(agents: ReadonlyArray<Agent>): string | undefined {  return agents.find((agent) => agent.name === "build")?.name ?? agents[0]?.name ?? undefined;} const DEFAULT_OPENCODE_MODEL_CAPABILITIES: ModelCapabilities = createModelCapabilities({  optionDescriptors: [    {      id: "variant",      label: "Reasoning",      type: "select",      options: [        { id: "low", label: "Low" },        { id: "medium", label: "Medium", isDefault: true },        { id: "high", label: "High" },        { id: "xhigh", label: "Extra High" },      ],      currentValue: "medium",    },    {      id: "agent",      label: "Agent",      type: "select",      options: [        { id: "build", label: "Build", isDefault: true },        { id: "plan", label: "Plan" },      ],      currentValue: "build",    },  ],}); function openCodeCapabilitiesForModel(input: {  readonly providerID: string;  readonly model: ProviderListResponse["all"][number]["models"][string];  readonly agents: ReadonlyArray<Agent>;}): ModelCapabilities {  const rawVariantValues = Object.keys(input.model.variants ?? {});  // When a model advertises no variants, synthesize the standard reasoning  // levels so the composer still offers a Reasoning selector (mirrors the  // Codex/Grok experience where reasoning is always configurable). The set  // covers the common OpenCode variant spectrum; `inferDefaultVariant`  // picks the provider-appropriate default (e.g. medium for openai/opencode).  const variantValues =    rawVariantValues.length > 0 ? rawVariantValues : ["low", "medium", "high", "xhigh"];  const defaultVariant = inferDefaultVariant(input.providerID, variantValues);  const variantOptions = variantValues.map((value) =>    defaultVariant === value      ? { id: value, label: titleCaseSlug(value), isDefault: true as const }      : { id: value, label: titleCaseSlug(value) },  );  const primaryAgents = input.agents.filter(    (agent) => !agent.hidden && (agent.mode === "primary" || agent.mode === "all"),  );  const defaultAgent = inferDefaultAgent(primaryAgents);  const agentOptions = primaryAgents.map((agent) =>    defaultAgent === agent.name      ? { id: agent.name, label: titleCaseSlug(agent.name), isDefault: true as const }      : { id: agent.name, label: titleCaseSlug(agent.name) },  );  return createModelCapabilities({    optionDescriptors: [      ...(variantOptions.length > 0        ? [            {              id: "variant",              label: "Reasoning",              type: "select" as const,              options: variantOptions,              ...(defaultVariant ? { currentValue: defaultVariant } : {}),            },          ]        : []),      ...(agentOptions.length > 0        ? [            {              id: "agent",              label: "Agent",              type: "select" as const,              options: agentOptions,              ...(defaultAgent ? { currentValue: defaultAgent } : {}),            },          ]        : []),    ],  });} function flattenOpenCodeModels(input: OpenCodeInventory): ReadonlyArray<ServerProviderModel> {  const connected = new Set(input.providerList.connected);  const models: Array<ServerProviderModel> = [];   for (const provider of input.providerList.all) {    if (!connected.has(provider.id)) {      continue;    }     for (const model of Object.values(provider.models)) {      const name = nonEmptyTrimmed(model.name);      if (!name) {        continue;      }       const subProvider = nonEmptyTrimmed(provider.name);      models.push({        slug: `${provider.id}/${model.id}`,        name,        ...(subProvider ? { subProvider } : {}),        isCustom: false,        capabilities: openCodeCapabilitiesForModel({          providerID: provider.id,          model,          agents: input.agents,        }),      });    }  }   return models.toSorted((left, right) => left.name.localeCompare(right.name));} function trimOptional(value: string | null | undefined): string | undefined {  const trimmed = value?.trim();  return trimmed && trimmed.length > 0 ? trimmed : undefined;} export function openCodeSkillsToServerProviderSkills(  input: OpenCodeInventory["skills"] | undefined,): ReadonlyArray<ServerProviderSkill> {  const skills: ServerProviderSkill[] = [];  for (const skill of input ?? []) {    const name = trimOptional(skill.name);    const path = trimOptional(skill.location);    if (!name || !path) {      continue;    }     const description = trimOptional(skill.description);    skills.push({      name,      path,      enabled: true,      ...(description ? { description, shortDescription: description } : {}),    });  }   return skills.toSorted((left, right) => left.name.localeCompare(right.name));} export const makePendingOpenCodeProvider = (  openCodeSettings: OpenCodeSettings,): Effect.Effect<ServerProviderDraft> =>  Effect.gen(function* () {    const checkedAt = yield* Effect.map(DateTime.now, DateTime.formatIso);    const models = providerModelsFromSettings(      [],      openCodeSettings.customModels,      DEFAULT_OPENCODE_MODEL_CAPABILITIES,    );     if (!openCodeSettings.enabled) {      return buildServerProvider({        presentation: OPENCODE_PRESENTATION,        enabled: false,        checkedAt,        models,        probe: {          installed: false,          version: null,          status: "warning",          auth: { status: "unknown" },          message:            openCodeSettings.serverUrl.trim().length > 0              ? "OpenCode is disabled in T3 Code settings. A server URL is configured."              : "OpenCode is disabled in T3 Code settings.",        },      });    }     return buildServerProvider({      presentation: OPENCODE_PRESENTATION,      enabled: true,      checkedAt,      models,      probe: {        installed: false,        version: null,        status: "warning",        auth: { status: "unknown" },        message: "OpenCode provider status has not been checked in this session yet.",      },    });  }); export const checkOpenCodeProviderStatus = Effect.fn("checkOpenCodeProviderStatus")(function* (  openCodeSettings: OpenCodeSettings,  cwd: string,  environment?: NodeJS.ProcessEnv,): Effect.fn.Return<  ServerProviderDraft,  never,  OpenCodeRuntime | OpenCodeServerOwner.OpenCodeServerOwner> {  const openCodeRuntime = yield* OpenCodeRuntime;  const serverOwner = yield* OpenCodeServerOwner.OpenCodeServerOwner;  const resolvedEnvironment = environment ?? process.env;  const checkedAt = DateTime.formatIso(yield* DateTime.now);  const customModels = openCodeSettings.customModels;  const isExternalServer = openCodeSettings.serverUrl.trim().length > 0;   const fallback = (    cause: unknown,    version: string | null = null,    phase: "version" | "inventory" = "version",  ) => {    const failure = formatOpenCodeProbeError({      cause,      isExternalServer,      phase,      serverUrl: openCodeSettings.serverUrl,    });    return buildServerProvider({      presentation: OPENCODE_PRESENTATION,      enabled: openCodeSettings.enabled,      checkedAt,      models: providerModelsFromSettings([], customModels, DEFAULT_OPENCODE_MODEL_CAPABILITIES),      probe: {        installed: failure.installed,        version,        status: "error",        auth: { status: "unknown" },        message: failure.message,      },    });  };   if (!openCodeSettings.enabled) {    return buildServerProvider({      presentation: OPENCODE_PRESENTATION,      enabled: false,      checkedAt,      models: providerModelsFromSettings([], customModels, DEFAULT_OPENCODE_MODEL_CAPABILITIES),      probe: {        installed: false,        version: null,        status: "warning",        auth: { status: "unknown" },        message: isExternalServer          ? "OpenCode is disabled in T3 Code settings. A server URL is configured."          : "OpenCode is disabled in T3 Code settings.",      },    });  }   let version: string | null = null;  if (!isExternalServer) {    const versionExit = yield* Effect.exit(      openCodeRuntime        .runOpenCodeCommand({          binaryPath: openCodeSettings.binaryPath,          args: ["--version"],          environment: resolvedEnvironment,        })        .pipe(          Effect.mapError(            (cause) => new OpenCodeProbeError({ cause, detail: openCodeRuntimeErrorDetail(cause) }),          ),          Effect.timeoutOrElse({            duration: OPENCODE_VERSION_PROBE_TIMEOUT,            orElse: () =>              Effect.fail(                new OpenCodeProbeError({                  detail: `OpenCode CLI version probe timed out after ${OPENCODE_VERSION_PROBE_TIMEOUT}.`,                }),              ),          }),        ),    );    if (versionExit._tag === "Failure") {      return fallback(Cause.squash(versionExit.cause));    }    version = parseGenericCliVersion(versionExit.value.stdout) ?? null;     if (!version) {      return fallback(        new Error(          `Unable to determine OpenCode version from \`opencode --version\` output. T3 Code requires OpenCode v${MINIMUM_OPENCODE_VERSION} or newer.`,        ),        null,      );    }    if (compareSemverVersions(version, MINIMUM_OPENCODE_VERSION) < 0) {      return buildServerProvider({        presentation: OPENCODE_PRESENTATION,        enabled: openCodeSettings.enabled,        checkedAt,        models: providerModelsFromSettings([], customModels, DEFAULT_OPENCODE_MODEL_CAPABILITIES),        probe: {          installed: true,          version,          status: "error",          auth: { status: "unknown" },          message: `OpenCode v${version} is too old. Upgrade to v${MINIMUM_OPENCODE_VERSION} or newer.`,        },      });    }  }   const loadInventory = (server: {    readonly url: string;    readonly serverPassword?: string;    readonly version: string;  }) =>    openCodeRuntime      .loadOpenCodeInventory(        openCodeRuntime.createOpenCodeSdkClient({          baseUrl: server.url,          directory: cwd,          ...(server.serverPassword !== undefined ? { serverPassword: server.serverPassword } : {}),        }),      )      .pipe(Effect.map((inventory) => ({ inventory, version: server.version })));  const inventoryEffect = isExternalServer    ? openCodeRuntime        .connectToOpenCodeServer({          binaryPath: openCodeSettings.binaryPath,          directory: cwd,          serverUrl: openCodeSettings.serverUrl,          ...(openCodeSettings.serverPassword            ? { serverPassword: openCodeSettings.serverPassword }            : {}),        })

Lines 1–500 of 546.