Files
karl f5b03c6fb0
Core CI / core (ubuntu-latest) (push) Failing after 3m6s
Core CI / core (macos-latest) (push) Canceled after 0s
Core CI / core (windows-latest) (push) Canceled after 0s
Core CI / desktop (ubuntu-latest) (push) Successful in 5m41s
Core CI / desktop (macos-latest) (push) Canceled after 0s
Core CI / desktop (windows-latest) (push) Canceled after 0s
feat: add local agent MCP tools and optional source-grounded AI answers
2026-10-12 20:28:38 +02:00

87 lines
2.2 KiB
TypeScript

/** Optional generation is separate from local search and never starts itself. */
export interface AiConfiguration {
enabled: boolean;
base_url: string;
model: string;
api_key: string | null;
}
const bytes = (text: string) => new TextEncoder().encode(text).length;
export function aiConfigurationProblem(config: AiConfiguration): string | null {
if (!config.enabled) return "Optional AI is disabled.";
let endpoint: URL;
try {
endpoint = new URL(config.base_url);
} catch {
return "Enter your OpenAI-compatible endpoint URL.";
}
if (
!["http:", "https:"].includes(endpoint.protocol) ||
endpoint.username ||
endpoint.password ||
endpoint.search ||
endpoint.hash ||
bytes(config.base_url) > 2048
)
return "Use an HTTP(S) endpoint without URL credentials, query or fragment.";
if (
!config.model.trim() ||
bytes(config.model) > 200 ||
/[\u0000-\u001f\u007f]/u.test(config.model)
)
return "Enter a model name (up to 200 bytes).";
if (
config.api_key &&
(bytes(config.api_key) > 2048 ||
/[\u0000-\u001f\u007f]/u.test(config.api_key))
)
return "The session API key is invalid or too long.";
return null;
}
export function aiQuestionProblem(question: string): string | null {
return !question.trim()
? "Enter a question about this passage."
: bytes(question) > 2048
? "The question exceeds 2048 bytes."
: null;
}
export function aiEndpointLabel(baseURL: string) {
try {
return new URL(baseURL).origin;
} catch {
return "your configured endpoint";
}
}
export function aiSourceEligible(
source?: { kind: string; number: number },
media = false,
) {
return (
!media &&
Boolean(
source &&
["page", "paragraph", "line"].includes(source.kind) &&
Number.isInteger(source.number) &&
source.number > 0,
)
);
}
export class AiRevision {
private revision = 0;
private selection = "";
invalidate() {
this.revision++;
}
select(selection: string) {
if (selection !== this.selection) {
this.selection = selection;
this.invalidate();
}
}
current() {
return this.revision;
}
accepts(revision: number) {
return revision === this.revision;
}
}