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<ServerSettings, "providers" | "providerInstances">; readonly instanceId: ProviderInstanceId; readonly instance: ProviderInstanceConfig; readonly driver: ProviderDriverKind; readonly isDefault: boolean; readonly textGenerationModelSelection?: | ServerSettings["textGenerationModelSelection"] | undefined;}): Partial<UnifiedSettings> { 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<typeof resolveServerBackgroundActivitySettings>, 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"], }, };}
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<ServerSettings, "providers" | "providerInstances">;
readonly instanceId: ProviderInstanceId;
readonly instance: ProviderInstanceConfig;
readonly driver: ProviderDriverKind;
readonly isDefault: boolean;
readonly textGenerationModelSelection?:
| ServerSettings["textGenerationModelSelection"]
| undefined;
}): Partial<UnifiedSettings> {
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<typeof resolveServerBackgroundActivitySettings>,
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"],
},
};
}