import type { BackgroundActivityProfile, BackgroundActivitySettings, ProviderDriverKind, ProviderInstanceConfig, PreviewViewportSetting, ProviderInstanceId, ServerSettings, SidebarProjectGroupingMode, UnifiedSettings, } from "@t3tools/contracts"; import { DEFAULT_UNIFIED_SETTINGS } from "@t3tools/contracts/settings"; import { getBackgroundActivityBaseProfile, normalizeBackgroundActivitySettings, normalizeServerBackgroundActivitySettings, resolveServerBackgroundActivitySettings, } from "@t3tools/shared/backgroundActivitySettings"; import * as Duration from "effect/Duration"; import * as Equal from "effect/Equal"; export function isProjectGroupingEnabled(mode: SidebarProjectGroupingMode): boolean { return mode !== "separate"; } export function projectGroupingModeFromToggle( enabled: boolean, lastEnabledMode: SidebarProjectGroupingMode = "repository", ): SidebarProjectGroupingMode { if (!enabled) return "separate"; return lastEnabledMode === "repository_path" ? "repository_path" : "repository"; } const LAST_ENABLED_PROJECT_GROUPING_MODE_KEY = "t3code:last-enabled-project-grouping-mode"; export function readLastEnabledProjectGroupingMode(): SidebarProjectGroupingMode { try { return localStorage.getItem(LAST_ENABLED_PROJECT_GROUPING_MODE_KEY) === "repository_path" ? "repository_path" : "repository"; } catch { return "repository"; } } export function rememberEnabledProjectGroupingMode(mode: SidebarProjectGroupingMode): void { if (mode === "separate") return; try { localStorage.setItem(LAST_ENABLED_PROJECT_GROUPING_MODE_KEY, mode); } catch { // Storage can be unavailable in restricted browser contexts. } } export function hasChangedBackgroundActivitySettings( settings: Pick< UnifiedSettings, | "backgroundActivity" | "backgroundActivityProfile" | "automaticGitFetchInterval" | "providerHealthRefreshInterval" >, ): boolean { return ( !Equal.equals(settings.backgroundActivity, DEFAULT_UNIFIED_SETTINGS.backgroundActivity) || settings.backgroundActivityProfile !== DEFAULT_UNIFIED_SETTINGS.backgroundActivityProfile || !Equal.equals( settings.automaticGitFetchInterval, DEFAULT_UNIFIED_SETTINGS.automaticGitFetchInterval, ) || !Equal.equals( settings.providerHealthRefreshInterval, DEFAULT_UNIFIED_SETTINGS.providerHealthRefreshInterval, ) ); } type TypographySettings = Pick< UnifiedSettings, | "fontFamilySans" | "fontFamilyComposer" | "fontFamilyCode" | "fontFamilyTerminal" | "fontSizeInterface" | "fontSizePrompt" | "fontSizeCode" | "fontSizeTerminal" >; /** Labels the font rows whose family or size differs from the defaults. */ export function getChangedTypographySettingLabels(settings: TypographySettings): string[] { return [ ...(settings.fontFamilySans !== DEFAULT_UNIFIED_SETTINGS.fontFamilySans || settings.fontSizeInterface !== DEFAULT_UNIFIED_SETTINGS.fontSizeInterface ? ["Interface font"] : []), ...(settings.fontFamilyComposer !== DEFAULT_UNIFIED_SETTINGS.fontFamilyComposer || settings.fontSizePrompt !== DEFAULT_UNIFIED_SETTINGS.fontSizePrompt ? ["Prompt font"] : []), ...(settings.fontFamilyCode !== DEFAULT_UNIFIED_SETTINGS.fontFamilyCode || settings.fontSizeCode !== DEFAULT_UNIFIED_SETTINGS.fontSizeCode ? ["Code font"] : []), ...(settings.fontFamilyTerminal !== DEFAULT_UNIFIED_SETTINGS.fontFamilyTerminal || settings.fontSizeTerminal !== DEFAULT_UNIFIED_SETTINGS.fontSizeTerminal ? ["Terminal font"] : []), ]; } export type BrowserDefaultSettings = Pick< UnifiedSettings, | "browserDefaultViewport" | "browserDefaultZoomFactor" | "browserDefaultAppearance" | "browserRecordingFrameRate" | "browserLinkTarget" | "browserAutoShowFloatingPreview" >; /** * True when two viewport settings describe the same viewport. * * The setting is a tagged union rather than a scalar, so identity comparison * reports every stored viewport as changed — including one that matches the * default. */ function isSamePreviewViewport( left: PreviewViewportSetting, right: PreviewViewportSetting, ): boolean { if (left._tag !== right._tag) return false; if (left._tag === "fill" || right._tag === "fill") return true; if (left.width !== right.width || left.height !== right.height) return false; return left._tag === "preset" && right._tag === "preset" ? left.presetId === right.presetId : true; } /** Labels the browser-default rows that differ from the defaults. */ export function getChangedBrowserSettingLabels(settings: BrowserDefaultSettings): string[] { return [ ...(isSamePreviewViewport( settings.browserDefaultViewport, DEFAULT_UNIFIED_SETTINGS.browserDefaultViewport, ) ? [] : ["Browser viewport"]), ...(settings.browserDefaultZoomFactor !== DEFAULT_UNIFIED_SETTINGS.browserDefaultZoomFactor ? ["Browser zoom"] : []), ...(settings.browserDefaultAppearance !== DEFAULT_UNIFIED_SETTINGS.browserDefaultAppearance ? ["Browser appearance"] : []), ...(settings.browserRecordingFrameRate !== DEFAULT_UNIFIED_SETTINGS.browserRecordingFrameRate ? ["Recording frame rate"] : []), ...(settings.browserLinkTarget !== DEFAULT_UNIFIED_SETTINGS.browserLinkTarget ? ["Open links in"] : []), ...(settings.browserAutoShowFloatingPreview !== DEFAULT_UNIFIED_SETTINGS.browserAutoShowFloatingPreview ? ["Floating preview"] : []), ]; } export function resolveBackgroundActivityProfileOption( settings: ServerSettings, ): BackgroundActivityProfile | "advanced" { const resolved = resolveServerBackgroundActivitySettings(settings); const normalized = normalizeBackgroundActivitySettings({ schemaVersion: 1, profile: "custom", baseProfile: resolved.profile, overrides: { automaticGitFetchInterval: resolved.automaticGitFetchInterval, providerHealthRefreshInterval: resolved.providerHealthRefreshInterval, hostPowerMonitorActiveInterval: resolved.hostPowerMonitorActiveInterval, hostPowerMonitorIdleInterval: resolved.hostPowerMonitorIdleInterval, idleClientTtl: resolved.idleClientTtl, pauseWhenHostLocked: resolved.pauseWhenHostLocked, pauseWhenHostLowPower: resolved.pauseWhenHostLowPower, pauseWhenClientLowPower: resolved.pauseWhenClientLowPower, pauseWhenOnBattery: resolved.pauseWhenOnBattery, }, }); return normalized.profile === "custom" ? "advanced" : normalized.profile; } export function backgroundActivitySharedPolicySettings( settings: ServerSettings, profile: BackgroundActivityProfile, ): BackgroundActivitySettings { const normalized = normalizeServerBackgroundActivitySettings(settings); return { schemaVersion: 1, profile: "custom", baseProfile: profile, overrides: normalized.profile === "custom" ? normalized.overrides : {}, }; } function collapseOtelSignalsUrl(input: { readonly tracesUrl: string; readonly metricsUrl: string; }): string | null { const tracesSuffix = "/traces"; const metricsSuffix = "/metrics"; if (!input.tracesUrl.endsWith(tracesSuffix) || !input.metricsUrl.endsWith(metricsSuffix)) { return null; } const tracesBase = input.tracesUrl.slice(0, -tracesSuffix.length); const metricsBase = input.metricsUrl.slice(0, -metricsSuffix.length); if (tracesBase !== metricsBase) { return null; } return `${tracesBase}/{traces,metrics}`; } export function formatDiagnosticsDescription(input: { readonly localTracingEnabled: boolean; readonly otlpTracesEnabled: boolean; readonly otlpTracesUrl?: string | undefined; readonly otlpMetricsEnabled: boolean; readonly otlpMetricsUrl?: string | undefined; }): string { const mode = input.localTracingEnabled ? "Local trace file" : "Terminal logs only"; const tracesUrl = input.otlpTracesEnabled ? input.otlpTracesUrl : undefined; const metricsUrl = input.otlpMetricsEnabled ? input.otlpMetricsUrl : undefined; if (tracesUrl && metricsUrl) { const collapsedUrl = collapseOtelSignalsUrl({ tracesUrl, metricsUrl }); return collapsedUrl ? `${mode}. Exporting OTEL to ${collapsedUrl}.` : `${mode}. Exporting OTEL traces to ${tracesUrl} and metrics to ${metricsUrl}.`; } if (tracesUrl) { return `${mode}. Exporting OTEL traces to ${tracesUrl}.`; } if (metricsUrl) { return `${mode}. Exporting OTEL metrics to ${metricsUrl}.`; } return `${mode}.`; } export function buildProviderInstanceUpdatePatch(input: { readonly settings: Pick; readonly instanceId: ProviderInstanceId; readonly instance: ProviderInstanceConfig; readonly driver: ProviderDriverKind; readonly isDefault: boolean; readonly textGenerationModelSelection?: | ServerSettings["textGenerationModelSelection"] | undefined; }): Partial { type LegacyProviderSettings = ServerSettings["providers"][keyof ServerSettings["providers"]]; const legacyProviderDefaults = DEFAULT_UNIFIED_SETTINGS.providers as Record< string, LegacyProviderSettings | undefined >; const legacyProviderDefault = input.isDefault ? legacyProviderDefaults[input.driver] : undefined; return { ...(legacyProviderDefault !== undefined ? { providers: { ...input.settings.providers, [input.driver]: legacyProviderDefault, } as ServerSettings["providers"], } : {}), providerInstances: { ...input.settings.providerInstances, [input.instanceId]: input.instance, }, ...(input.textGenerationModelSelection !== undefined ? { textGenerationModelSelection: input.textGenerationModelSelection } : {}), }; } // ── Background-activity interval helpers ───────────────────────────── // Shared by the General panel's interval rows and the Providers panel's // health-check row. export const PROVIDER_HEALTH_INTERVAL_STEP_SECONDS = 30; type BackgroundActivityOverridePatch = Partial<{ [K in keyof BackgroundActivitySettings["overrides"]]: | BackgroundActivitySettings["overrides"][K] | undefined; }>; export function durationToSeconds(duration: Duration.Duration): number { return Math.round(Duration.toMillis(duration) / 1_000); } export function normalizeIntervalSeconds(value: number | null, minimum = 0): number { if (value === null || !Number.isFinite(value)) { return minimum; } return Math.max(minimum, Math.round(value)); } export function backgroundActivityOverrideSettings( current: BackgroundActivitySettings, resolved: ReturnType, overrides: BackgroundActivityOverridePatch, ) { const nextOverrides: BackgroundActivityOverridePatch = { automaticGitFetchInterval: resolved.automaticGitFetchInterval, providerHealthRefreshInterval: resolved.providerHealthRefreshInterval, hostPowerMonitorActiveInterval: resolved.hostPowerMonitorActiveInterval, hostPowerMonitorIdleInterval: resolved.hostPowerMonitorIdleInterval, idleClientTtl: resolved.idleClientTtl, pauseWhenHostLocked: resolved.pauseWhenHostLocked, pauseWhenHostLowPower: resolved.pauseWhenHostLowPower, pauseWhenClientLowPower: resolved.pauseWhenClientLowPower, pauseWhenOnBattery: resolved.pauseWhenOnBattery, ...overrides, }; for (const [key, value] of Object.entries(nextOverrides)) { if (value === undefined) { delete nextOverrides[key as keyof typeof nextOverrides]; } } return { backgroundActivity: { schemaVersion: 1 as const, profile: "custom" as const, baseProfile: getBackgroundActivityBaseProfile(current), overrides: nextOverrides as BackgroundActivitySettings["overrides"], }, }; }