Merge PR114 dictation foundation for approved provider-row finishing

Preserve original contributor commit 3bb28e5f29. Resolve only the settings-dialog conflict by retaining current General/update behavior: the donor General-only language placement is explicitly superseded by the human-approved row BELOW the full-width Deepgram key. The assigned implementation worker will implement that placement and approved shared dropdown finishing. Foundation only, not a checked candidate.
This commit is contained in:
amosblomqvist
2026-10-27 21:33:40 +02:00
17 changed files with 1153 additions and 71 deletions
+90
View File
@@ -0,0 +1,90 @@
/** Nova-3 monolingual language codes, including documented locale aliases.
* Source: https://developers.deepgram.com/docs/models-languages-overview
* Multilingual is a separate mode; its supported language set differs.
*/
const NOVA_3_LANGUAGES = [
["Afrikaans", ["af", "af-ZA"]],
["Arabic", ["ar", "ar-AE", "ar-SA", "ar-QA", "ar-KW", "ar-SY", "ar-LB", "ar-PS", "ar-JO", "ar-EG", "ar-SD", "ar-TD", "ar-MA", "ar-DZ", "ar-TN", "ar-IQ", "ar-IR"]],
["Armenian", ["hy"]],
["Assamese", ["as", "as-IN"]],
["Belarusian", ["be"]],
["Bengali", ["bn"]],
["Bosnian", ["bs"]],
["Bulgarian", ["bg"]],
["Catalan", ["ca"]],
["Chinese (Cantonese, Traditional)", ["zh-HK"]],
["Chinese (Mandarin, Simplified)", ["zh", "zh-CN", "zh-Hans"]],
["Chinese (Mandarin, Traditional)", ["zh-TW", "zh-Hant"]],
["Croatian", ["hr"]],
["Czech", ["cs", "cs-CZ"]],
["Danish", ["da", "da-DK"]],
["Dutch", ["nl"]],
["English", ["en", "en-US", "en-AU", "en-GB", "en-IN", "en-NZ"]],
["Estonian", ["et"]],
["Finnish", ["fi"]],
["Flemish", ["nl-BE"]],
["French", ["fr", "fr-CA"]],
["Georgian", ["ka", "ka-GE"]],
["German", ["de"]],
["German (Switzerland)", ["de-CH"]],
["Greek", ["el"]],
["Gujarati", ["gu", "gu-IN"]],
["Hebrew", ["he"]],
["Hindi", ["hi"]],
["Hungarian", ["hu"]],
["Indonesian", ["id"]],
["Italian", ["it"]],
["Japanese", ["ja"]],
["Kannada", ["kn"]],
["Kazakh", ["kk", "kk-KZ"]],
["Korean", ["ko", "ko-KR"]],
["Latvian", ["lv"]],
["Lithuanian", ["lt"]],
["Macedonian", ["mk"]],
["Malay", ["ms"]],
["Marathi", ["mr"]],
["Mongolian", ["mn"]],
["Nepali", ["ne"]],
["Norwegian", ["no"]],
["Pashto", ["ps", "ps-AF"]],
["Persian", ["fa"]],
["Polish", ["pl"]],
["Portuguese", ["pt", "pt-BR", "pt-PT"]],
["Punjabi", ["pa", "pa-IN"]],
["Romanian", ["ro"]],
["Russian", ["ru"]],
["Serbian", ["sr"]],
["Slovak", ["sk"]],
["Slovenian", ["sl"]],
["Spanish", ["es", "es-419"]],
["Swedish", ["sv", "sv-SE"]],
["Tagalog", ["tl"]],
["Tamil", ["ta"]],
["Telugu", ["te"]],
["Thai", ["th", "th-TH"]],
["Turkish", ["tr", "tr-TR"]],
["Ukrainian", ["uk"]],
["Urdu", ["ur"]],
["Vietnamese", ["vi"]],
] as const;
export type DictationLanguage = "multi" | (typeof NOVA_3_LANGUAGES)[number][1][number];
export const DICTATION_LANGUAGES: readonly { code: DictationLanguage; label: string }[] = [
{ code: "multi", label: "Multilingual" },
...NOVA_3_LANGUAGES.flatMap(([name, codes]) =>
codes.map((code, i) => ({ code, label: i === 0 ? name : `${name} (${code})` })),
).sort((a, b) => a.label.localeCompare(b.label, "en")),
];
export function isDictationLanguage(value: unknown): value is DictationLanguage {
return DICTATION_LANGUAGES.some(({ code }) => code === value);
}
/** Only language varies. Keep the owner's tuned streaming parameters. */
export function deepgramListenUrl(language: DictationLanguage): string {
return "wss://api.deepgram.com/v1/listen" +
"?model=nova-3&encoding=linear16&sample_rate=16000&channels=1" +
"&smart_format=true&punctuate=true&endpointing=300" +
`&language=${encodeURIComponent(language)}`;
}
+1
View File
@@ -59,6 +59,7 @@ export * from "./fuzzy.ts";
// the user-settings record's shape/sanitizer (pure) and its fully-resolved
// view. Browser-safe — the Wave-4 panel imports these directly.
export * from "./settings.ts";
export * from "./dictation.ts";
// graph-deletion.ts (wave 6): the whole module is PURE but storage-port
// driven — the deletion RUNS server-side only. The shell needs exactly one
// two type-only things from it: the ChatDeleteMode union (the delete
+5
View File
@@ -35,6 +35,7 @@ import {
type BackgroundModelRole,
type FactorySettings,
type GeneralSettings,
type DictationSettings,
type CompactionSettings,
type GraphSettings,
type ModelSlot,
@@ -69,6 +70,7 @@ export type EffectiveAppearance = FactorySettings["appearance"];
/** The concrete view: every knob resolved, no absent fields. */
export interface EffectiveSettings {
general: Required<GeneralSettings>;
dictation: Required<DictationSettings>;
agent: {
models: Record<BackgroundModelRole, EffectiveModelSpec | undefined>;
/** Delta map as stored — consumers resolve absent key → enabled. */
@@ -176,6 +178,9 @@ export function resolveSettings(record: UserSettings | null): EffectiveSettings
general: {
autoUpdate: record?.general?.autoUpdate ?? FACTORY_SETTINGS.general.autoUpdate,
},
dictation: {
language: record?.dictation?.language ?? FACTORY_SETTINGS.dictation.language,
},
agent: {
models,
tools: agent.tools ?? {},
+16
View File
@@ -32,6 +32,7 @@ import { AUTH_PROVIDER_KEYS } from "./auth-keys.ts";
import type { ModelRole } from "../../config.ts";
import type { ChatSettings } from "./contracts/chat.ts";
import type { SearchEngineId } from "./search.ts";
import { isDictationLanguage, type DictationLanguage } from "./dictation.ts";
/** The schema version this code understands. Bump ONLY for breaking shape
* changes (a field renamed/meaning changed); pure additions never bump. */
@@ -235,6 +236,11 @@ export interface GeneralSettings {
autoUpdate?: boolean;
}
/** One app-wide dictation preference, shared by every composer. */
export interface DictationSettings {
language?: DictationLanguage;
}
/** Agent settings — everything about the non-conversation machinery. */
export interface AgentSettings {
/** Background-role model/thinking overrides (absent role = config pin). */
@@ -361,6 +367,7 @@ export interface CompactionSettings {
export interface UserSettings {
version: number;
general?: GeneralSettings;
dictation?: DictationSettings;
agent?: AgentSettings;
/** The strong/cheap/vision model slots (model slots wave, 2026-09-20). */
models?: ModelsSettings;
@@ -378,6 +385,7 @@ export interface UserSettings {
export interface FactorySettings {
version: number;
general: Required<GeneralSettings>;
dictation: Required<DictationSettings>;
appearance: {
mode: "dark" | "light";
background: string | null;
@@ -397,6 +405,7 @@ export interface FactorySettings {
export const FACTORY_SETTINGS: FactorySettings = {
version: CURRENT_SETTINGS_VERSION,
general: { autoUpdate: true },
dictation: { language: "en" },
appearance: {
mode: "dark",
background: null, // the dark default (#141414) lives in the mode's derivation curve
@@ -615,6 +624,13 @@ export function sanitizeUserSettings(raw: unknown): UserSettings | null {
if (r.general !== null && typeof r.general === "object" && !Array.isArray(r.general)) {
out.general = sanitizeGeneral(r.general as Record<string, unknown>);
}
if (r.dictation !== undefined) {
const raw = r.dictation;
out.dictation = raw !== null && typeof raw === "object" && !Array.isArray(raw) &&
isDictationLanguage((raw as Record<string, unknown>).language)
? { language: (raw as DictationSettings).language }
: {};
}
if (r.agent !== null && typeof r.agent === "object" && !Array.isArray(r.agent)) {
out.agent = sanitizeAgent(r.agent as Record<string, unknown>);
}
+12
View File
@@ -19,12 +19,14 @@ import type {
BackgroundModelRole,
CompactionSettings,
GeneralSettings,
DictationSettings,
GraphSettings,
ModelsSettings,
RoleModelOverride,
} from "../core/settings.ts";
import { BACKGROUND_MODEL_ROLES, COMPACTION_THRESHOLD_FLOOR, EXTRACTION_CHUNK_TOKENS_FLOOR, MODEL_SLOTS } from "../core/settings.ts";
import { WRITABLE_TOOL_KEYS } from "../core/pending.ts";
import { isDictationLanguage } from "../core/dictation.ts";
/** Parsed-or-rejected — `ok: false` carries the 400's error message. */
export type SettingsBodyResult<T> =
@@ -375,6 +377,15 @@ export function parseCompactionSettingsBody(body: unknown): SettingsBodyResult<C
return { ok: true, settings };
}
/** dictation: { language?: one of Nova-3's supported language codes } */
export function parseDictationSettingsBody(body: unknown): SettingsBodyResult<DictationSettings> {
const raw = asRecord(body);
if (!raw) return { ok: false, error: "Invalid JSON body" };
if (raw.language === undefined) return { ok: true, settings: {} };
if (!isDictationLanguage(raw.language)) return { ok: false, error: "Unsupported dictation language" };
return { ok: true, settings: { language: raw.language } };
}
/** The section→parser map for PUT /api/settings/[section] (the route also
* carries the special prompts sub-resource). */
export const SETTINGS_SECTIONS = {
@@ -385,6 +396,7 @@ export const SETTINGS_SECTIONS = {
graph: parseGraphSettingsBody,
compaction: parseCompactionSettingsBody,
models: parseModelsSettingsBody,
dictation: parseDictationSettingsBody,
} as const;
export type SettingsSectionName = keyof typeof SETTINGS_SECTIONS;