Files
pi-ask/src/config/defaults.ts
T

173 lines
4.8 KiB
TypeScript

import { DEFAULT_ASK_KEYMAPS } from "../constants/keymaps.ts";
import type {
AskConfig,
AskConfigFileV3,
AskNotificationChannel,
} from "./schema.ts";
const DEFAULT_EXTRACTION_RETRIES = 1;
const MAX_EXTRACTION_RETRIES = 3;
const DEFAULT_EXTRACTION_TIMEOUT_MS = 30_000;
export const DEFAULT_ASK_CONFIG: AskConfig = {
answer: {
extractionModels: [
{ provider: "openai-codex", id: "gpt-5.4-mini" },
{ provider: "github-copilot", id: "gpt-5.4-mini" },
{ provider: "anthropic", id: "claude-haiku-4-5" },
],
extractionRetries: DEFAULT_EXTRACTION_RETRIES,
extractionTimeoutMs: DEFAULT_EXTRACTION_TIMEOUT_MS,
},
behaviour: {
autoSubmitWhenAnsweredWithoutNotes: false,
confirmDismissWhenDirty: true,
doublePressReviewShortcuts: true,
showFooterHints: true,
},
keymaps: {
...DEFAULT_ASK_KEYMAPS,
},
notifications: {
channels: ["bell"],
enabled: true,
},
};
export function normalizeAskConfig(
config?: Partial<AskConfigFileV3> | AskConfig
): AskConfig {
return {
answer: {
extractionModels:
config?.answer?.extractionModels?.filter(isValidModelPreference) ??
DEFAULT_ASK_CONFIG.answer.extractionModels,
extractionRetries: clampInteger(
config?.answer?.extractionRetries,
0,
MAX_EXTRACTION_RETRIES,
DEFAULT_EXTRACTION_RETRIES
),
extractionTimeoutMs: positiveNumberOrDefault(
config?.answer?.extractionTimeoutMs,
DEFAULT_EXTRACTION_TIMEOUT_MS
),
},
behaviour: {
autoSubmitWhenAnsweredWithoutNotes:
config?.behaviour?.autoSubmitWhenAnsweredWithoutNotes ??
DEFAULT_ASK_CONFIG.behaviour.autoSubmitWhenAnsweredWithoutNotes,
confirmDismissWhenDirty:
config?.behaviour?.confirmDismissWhenDirty ??
DEFAULT_ASK_CONFIG.behaviour.confirmDismissWhenDirty,
doublePressReviewShortcuts:
config?.behaviour?.doublePressReviewShortcuts ??
DEFAULT_ASK_CONFIG.behaviour.doublePressReviewShortcuts,
showFooterHints:
config?.behaviour?.showFooterHints ??
DEFAULT_ASK_CONFIG.behaviour.showFooterHints,
},
keymaps: {
...DEFAULT_ASK_CONFIG.keymaps,
...(config?.keymaps ?? {}),
},
notifications: {
channels: normalizeNotificationChannels(config?.notifications?.channels),
enabled:
config?.notifications?.enabled ??
DEFAULT_ASK_CONFIG.notifications.enabled,
},
};
}
export function toAskConfigFileV3(config: AskConfig): AskConfigFileV3 {
const normalized = normalizeAskConfig(config);
return {
schemaVersion: 3,
answer: {
extractionModels: normalized.answer.extractionModels,
extractionRetries: normalized.answer.extractionRetries,
extractionTimeoutMs: normalized.answer.extractionTimeoutMs,
},
behaviour: {
autoSubmitWhenAnsweredWithoutNotes:
normalized.behaviour.autoSubmitWhenAnsweredWithoutNotes,
confirmDismissWhenDirty: normalized.behaviour.confirmDismissWhenDirty,
doublePressReviewShortcuts:
normalized.behaviour.doublePressReviewShortcuts,
showFooterHints: normalized.behaviour.showFooterHints,
},
keymaps: {
cancel: normalized.keymaps.cancel,
dismiss: normalized.keymaps.dismiss,
toggle: normalized.keymaps.toggle,
confirm: normalized.keymaps.confirm,
optionNote: normalized.keymaps.optionNote,
questionNote: normalized.keymaps.questionNote,
},
notifications: {
channels: normalized.notifications.channels,
enabled: normalized.notifications.enabled,
},
};
}
function normalizeNotificationChannels(
channels: unknown
): AskNotificationChannel[] {
if (!Array.isArray(channels)) {
return DEFAULT_ASK_CONFIG.notifications.channels;
}
const normalized = channels.filter(isValidNotificationChannel);
return normalized.length > 0
? normalized
: DEFAULT_ASK_CONFIG.notifications.channels;
}
function isValidNotificationChannel(
value: unknown
): value is AskNotificationChannel {
if (value === "bell" || value === "osc9" || value === "osc777") {
return true;
}
return (
!!value &&
typeof value === "object" &&
(value as { type?: unknown }).type === "command" &&
typeof (value as { command?: unknown }).command === "string" &&
(value as { command: string }).command.trim().length > 0
);
}
function isValidModelPreference(value: unknown): value is {
id: string;
provider: string;
} {
return (
!!value &&
typeof value === "object" &&
typeof (value as { id?: unknown }).id === "string" &&
(value as { id: string }).id.trim().length > 0 &&
typeof (value as { provider?: unknown }).provider === "string" &&
(value as { provider: string }).provider.trim().length > 0
);
}
function clampInteger(
value: unknown,
min: number,
max: number,
fallback: number
): number {
if (typeof value !== "number" || !Number.isFinite(value)) {
return fallback;
}
return Math.max(min, Math.min(max, Math.trunc(value)));
}
function positiveNumberOrDefault(value: unknown, fallback: number): number {
return typeof value === "number" && Number.isFinite(value) && value > 0
? value
: fallback;
}