137 lines · 4.9 KB · TypeScript
RawDownload
import type { ModelSelection, ProviderDriverKind, ProviderInstanceId } from "@t3tools/contracts";import {  CLAUDE_RESUME_COMPACTION_NEVER_ANSWER,  isClaudeResumeCompactionQuestion,} from "@t3tools/shared/claudeCompaction";import {  resolveSelectableProviderInstanceEntry,  type ProviderInstanceEntry,} from "../../providerInstances";import { getTriggerDisplayModelName, type ModelEsque } from "./providerIconUtils"; const CLAUDE_RESUME_COMPACTION_MINUTES = 70;const CLAUDE_RESUME_COMPACTION_TOKENS = 100_000; export function providerSupportsManualCompaction(  provider: ProviderInstanceEntry | null | undefined,): boolean {  return provider?.snapshot.slashCommands.some((command) => command.name === "compact") ?? false;} export function hasAvailableCompactionProvider(input: {  readonly providers: ReadonlyArray<ProviderInstanceEntry>;  readonly driverKind: ProviderDriverKind;  readonly instanceId: ProviderInstanceId | null;  readonly lockedInstanceId: ProviderInstanceId | null;}): boolean {  const driverProviders = input.providers.filter(    (provider) => provider.driverKind === input.driverKind,  );  const lockedContinuationGroupKey = input.lockedInstanceId    ? driverProviders.find((provider) => provider.instanceId === input.lockedInstanceId)        ?.continuationGroupKey    : undefined;  const compatibleProviders = lockedContinuationGroupKey    ? driverProviders.filter(        (provider) => provider.continuationGroupKey === lockedContinuationGroupKey,      )    : driverProviders;   return providerSupportsManualCompaction(    resolveSelectableProviderInstanceEntry(compatibleProviders, input.instanceId ?? undefined),  );} export function hasDismissedResumeCompaction(  activities: ReadonlyArray<{ readonly kind: string; readonly payload: unknown }>,): boolean {  return activities.some((activity) => {    if (activity.kind !== "user-input.resolved") return false;    const payload = activity.payload;    if (!payload || typeof payload !== "object") return false;    const answers = (payload as { readonly answers?: unknown }).answers;    if (!answers || typeof answers !== "object" || Array.isArray(answers)) return false;     return Object.entries(answers).some(      ([question, answer]) =>        isClaudeResumeCompactionQuestion(question) &&        answer === CLAUDE_RESUME_COMPACTION_NEVER_ANSWER,    );  });} export function shouldOfferResumeCompaction(input: {  readonly provider: string | null | undefined;  readonly usedTokens: number | null | undefined;  readonly updatedAt: string | null | undefined;  readonly now: string;}): boolean {  if (    input.provider !== "claudeAgent" ||    (input.usedTokens ?? 0) < CLAUDE_RESUME_COMPACTION_TOKENS  ) {    return false;  }   const updatedAt = Date.parse(input.updatedAt ?? "");  const now = Date.parse(input.now);  return (    Number.isFinite(updatedAt) &&    Number.isFinite(now) &&    now - updatedAt >= CLAUDE_RESUME_COMPACTION_MINUTES * 60_000  );} export function resolveContextWindowModelDisplayName(  selection: ModelSelection | null | undefined,  modelOptionsByInstance: ReadonlyMap<ProviderInstanceId, ReadonlyArray<ModelEsque>>,): string | null {  if (!selection) {    return null;  }   const selectedModel = modelOptionsByInstance    .get(selection.instanceId)    ?.find((model) => model.slug === selection.model);   return selectedModel ? getTriggerDisplayModelName(selectedModel) : selection.model;} export function formatContextWindowCompactionMessage(  modelDisplayName: string | null | undefined,  autoCompactThreshold?: number | null,): string {  if (typeof autoCompactThreshold === "number" && autoCompactThreshold > 0) {    return `Compacts automatically at ${autoCompactThreshold.toLocaleString("en-US")} tokens.`;  }  return modelDisplayName    ? `Context for ${modelDisplayName} compacts automatically when needed.`    : "Context compacts automatically when needed.";} /** * Whether the footer should hold the meter's slot before a snapshot exists. * * The snapshot comes from thread activities, which load after the shell. * Reserving the slot while the detail loads, for a started thread, keeps the * attach button still until the meter mounts. Once the detail is in, a * missing snapshot means there is no usage to show and nothing is reserved. * * The meter renders from stored activities whatever the provider's state, so * only a provider known not to stream usage skips the reservation. An unknown * provider (catalog still loading, or the thread's provider disabled) reserves. */export function shouldReserveContextWindowMeter(input: {  readonly meterEnabled: boolean;  readonly detailLoading: boolean;  readonly threadStarted: boolean;  /** `null` while the thread's provider is not in the catalog. */  readonly providerReportsContextWindow: boolean | null;}): boolean {  return (    input.meterEnabled &&    input.detailLoading &&    input.threadStarted &&    input.providerReportsContextWindow !== false  );}