fix(cli-tests,dashboard): align quiet-mode result spies and voiceInput allowlist
CLI JSON/create success lines write via result() (raw stdout) so quiet mode cannot drop machine-readable output; capture that seam in research/update/task tests instead of console.log. Allowlist nested voiceInput settings for the FN-7505 default-description guard and ship locale keys for Voice Input UI.
This commit is contained in:
@@ -1,4 +1,22 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
/*
|
||||
FNXC:CliQuietMode 2026-07-25-09:05:
|
||||
JSON and other result-bearing CLI output uses `result()` (raw stdout), not
|
||||
console.log, so quiet mode can suppress chatter without dropping payloads.
|
||||
Capture that seam instead of expecting log spies to see JSON.
|
||||
*/
|
||||
const resultSpy = vi.hoisted(() => vi.fn());
|
||||
vi.mock("../../output.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../../output.js")>();
|
||||
return {
|
||||
...actual,
|
||||
result: (text: string) => {
|
||||
resultSpy(text);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
import { runResearchCancel, runResearchCreate, runResearchExport, runResearchList, runResearchRetry, runResearchShow } from "../research.js";
|
||||
|
||||
function makeConstructibleMock<T extends (...args: any[]) => unknown>(impl?: T) {
|
||||
@@ -116,6 +134,7 @@ describe("research commands", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
resultSpy.mockClear();
|
||||
process.exit = vi.fn(((code?: number) => {
|
||||
throw new Error(`process.exit:${code ?? 0}`);
|
||||
}) as typeof process.exit);
|
||||
@@ -156,7 +175,7 @@ describe("research commands", () => {
|
||||
|
||||
it("lists runs as json", async () => {
|
||||
await runResearchList({ json: true, status: "completed", limit: 3 });
|
||||
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('"runs"'));
|
||||
expect(resultSpy).toHaveBeenCalledWith(expect.stringContaining('"runs"'));
|
||||
expect(researchStoreMock.listRuns).toHaveBeenCalledWith({ status: "completed", limit: 3 });
|
||||
});
|
||||
|
||||
@@ -175,7 +194,7 @@ describe("research commands", () => {
|
||||
await runResearchList({ json: true, status: "completed", limit: 3 });
|
||||
|
||||
expect(asyncStore.listRuns).toHaveBeenCalledWith({ status: "completed", limit: 3 });
|
||||
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('"runs"'));
|
||||
expect(resultSpy).toHaveBeenCalledWith(expect.stringContaining('"runs"'));
|
||||
});
|
||||
|
||||
it("rejects invalid list status", async () => {
|
||||
@@ -212,14 +231,14 @@ describe("research commands", () => {
|
||||
it("cancels a run", async () => {
|
||||
await runResearchCancel("RR-001", { json: true });
|
||||
expect(orchestratorMock.cancelRun).toHaveBeenCalledWith("RR-001");
|
||||
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('"cancelled"'));
|
||||
expect(resultSpy).toHaveBeenCalledWith(expect.stringContaining('"cancelled"'));
|
||||
});
|
||||
|
||||
it("retries a run", async () => {
|
||||
researchStoreMock.getRun.mockImplementation((id: string) => (id === "RR-003" ? { ...mockRun, id: "RR-003", status: "queued" } : { ...mockRun, status: "failed" }));
|
||||
await runResearchRetry("RR-001", { json: true });
|
||||
expect(orchestratorMock.retryRun).toHaveBeenCalledWith("RR-001");
|
||||
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('"retryOf"'));
|
||||
expect(resultSpy).toHaveBeenCalledWith(expect.stringContaining('"retryOf"'));
|
||||
});
|
||||
|
||||
it("errors when research is disabled", async () => {
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
|
||||
/*
|
||||
FNXC:CliQuietMode 2026-07-25-09:05:
|
||||
Task create/link success lines use `result()` so they survive quiet mode.
|
||||
Capture that seam for assertions that previously spied console.log.
|
||||
*/
|
||||
const resultSpy = vi.hoisted(() => vi.fn());
|
||||
vi.mock("../../output.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../../output.js")>();
|
||||
return {
|
||||
...actual,
|
||||
result: (text: string) => {
|
||||
resultSpy(text);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// Mock node:readline/promises before importing the module under test
|
||||
vi.mock("node:readline/promises", () => ({
|
||||
createInterface: vi.fn(),
|
||||
@@ -680,9 +696,9 @@ describe("project-aware task command behavior", () => {
|
||||
});
|
||||
|
||||
it("runTaskCreate links existing task on deterministic duplicate", async () => {
|
||||
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
const existing = makeTask({ id: "FN-777", description: "same task", column: "todo" });
|
||||
const mockCreateTask = vi.fn();
|
||||
resultSpy.mockClear();
|
||||
|
||||
vi.mocked(runDeterministicDuplicateGuard).mockResolvedValue({
|
||||
action: "duplicate",
|
||||
@@ -702,8 +718,7 @@ describe("project-aware task command behavior", () => {
|
||||
await runTaskCreate("same task");
|
||||
|
||||
expect(mockCreateTask).not.toHaveBeenCalled();
|
||||
expect(logSpy.mock.calls.some((call) => String(call[0]).includes("Linked existing FN-777"))).toBe(true);
|
||||
logSpy.mockRestore();
|
||||
expect(resultSpy.mock.calls.some((call) => String(call[0]).includes("Linked existing FN-777"))).toBe(true);
|
||||
});
|
||||
|
||||
it("runTaskCreate proceeds when no high-signal tokens are present", async () => {
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { beforeEach, afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const { execAsyncMock, existsSyncMock, readFileSyncMock, getCachedUpdateStatusMock, getConfiguredUpdateChannelMock, persistUpdateChannelMock } = vi.hoisted(() => ({
|
||||
/*
|
||||
FNXC:CliQuietMode 2026-07-25-09:05:
|
||||
`fn update --json` writes through `result()` (raw stdout) so quiet mode cannot
|
||||
drop machine-readable payloads. Capture that seam instead of console.log.
|
||||
*/
|
||||
const { execAsyncMock, existsSyncMock, readFileSyncMock, getCachedUpdateStatusMock, getConfiguredUpdateChannelMock, persistUpdateChannelMock, resultSpy } = vi.hoisted(() => ({
|
||||
execAsyncMock: vi.fn<(...args: unknown[]) => Promise<{ stdout: string; stderr: string }>>(),
|
||||
existsSyncMock: vi.fn<(path: string) => boolean>(),
|
||||
readFileSyncMock: vi.fn<(path: string, encoding: BufferEncoding) => string>(),
|
||||
@@ -12,8 +17,19 @@ const { execAsyncMock, existsSyncMock, readFileSyncMock, getCachedUpdateStatusMo
|
||||
} | null>(),
|
||||
getConfiguredUpdateChannelMock: vi.fn<() => Promise<"stable" | "beta">>(),
|
||||
persistUpdateChannelMock: vi.fn<(channel: "stable" | "beta") => Promise<void>>(),
|
||||
resultSpy: vi.fn<(text: string) => void>(),
|
||||
}));
|
||||
|
||||
vi.mock("../../output.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../../output.js")>();
|
||||
return {
|
||||
...actual,
|
||||
result: (text: string) => {
|
||||
resultSpy(text);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("node:child_process", async () => {
|
||||
const { promisify } = await import("node:util");
|
||||
const execFn: Record<PropertyKey, unknown> = vi.fn();
|
||||
@@ -47,6 +63,7 @@ describe("runUpdate", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
resultSpy.mockClear();
|
||||
process.exitCode = 0;
|
||||
|
||||
existsSyncMock.mockImplementation((path: string) => path.endsWith("package.json"));
|
||||
@@ -103,7 +120,7 @@ describe("runUpdate", () => {
|
||||
|
||||
await runUpdate({ json: true });
|
||||
|
||||
const output = logSpy.mock.calls[0]?.[0] as string;
|
||||
const output = resultSpy.mock.calls[0]?.[0] as string;
|
||||
const parsed = JSON.parse(output) as {
|
||||
currentVersion: string;
|
||||
latestVersion: string;
|
||||
@@ -272,11 +289,12 @@ describe("runUpdate", () => {
|
||||
vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ json: vi.fn().mockResolvedValue({ "dist-tags": { latest: testCase.latest } }) }));
|
||||
readFileSyncMock.mockReturnValueOnce(JSON.stringify({ name: "@runfusion/fusion", version: testCase.current }));
|
||||
logSpy.mockClear();
|
||||
resultSpy.mockClear();
|
||||
process.exitCode = 0;
|
||||
|
||||
await runUpdate({ check: true, json: true });
|
||||
|
||||
const output = logSpy.mock.calls[0]?.[0] as string;
|
||||
const output = resultSpy.mock.calls[0]?.[0] as string;
|
||||
expect(JSON.parse(output), `${testCase.latest} vs ${testCase.current}`).toMatchObject({
|
||||
currentVersion: testCase.current,
|
||||
latestVersion: testCase.latest,
|
||||
@@ -297,11 +315,12 @@ describe("runUpdate", () => {
|
||||
latestVersion,
|
||||
});
|
||||
logSpy.mockClear();
|
||||
resultSpy.mockClear();
|
||||
process.exitCode = 0;
|
||||
|
||||
await runUpdate({ check: true, json: true });
|
||||
|
||||
const output = logSpy.mock.calls.at(-1)?.[0] as string;
|
||||
const output = resultSpy.mock.calls.at(-1)?.[0] as string;
|
||||
expect(JSON.parse(output)).toMatchObject({
|
||||
currentVersion: "1.2.3",
|
||||
latestVersion,
|
||||
@@ -361,7 +380,7 @@ describe("runUpdate", () => {
|
||||
|
||||
await runUpdate({ check: true, json: true });
|
||||
|
||||
const parsed = JSON.parse(logSpy.mock.calls[0]?.[0] as string) as Record<string, unknown>;
|
||||
const parsed = JSON.parse(resultSpy.mock.calls[0]?.[0] as string) as Record<string, unknown>;
|
||||
expect(parsed).toMatchObject({ latestVersion: "1.3.0-beta.2", updateAvailable: true, channel: "beta" });
|
||||
expect(process.exitCode).toBe(1);
|
||||
});
|
||||
@@ -371,7 +390,7 @@ describe("runUpdate", () => {
|
||||
|
||||
await runUpdate({ check: true, json: true });
|
||||
|
||||
const parsed = JSON.parse(logSpy.mock.calls[0]?.[0] as string) as Record<string, unknown>;
|
||||
const parsed = JSON.parse(resultSpy.mock.calls[0]?.[0] as string) as Record<string, unknown>;
|
||||
expect(parsed).toMatchObject({ latestVersion: "1.2.3", updateAvailable: false, channel: "stable" });
|
||||
expect(process.exitCode).toBe(0);
|
||||
});
|
||||
@@ -462,11 +481,12 @@ describe("runUpdate", () => {
|
||||
expect(logSpy.mock.calls.flat().join("\n")).not.toContain("A newer beta");
|
||||
|
||||
logSpy.mockClear();
|
||||
resultSpy.mockClear();
|
||||
process.exitCode = 0;
|
||||
vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ json: vi.fn().mockResolvedValue({ "dist-tags": { latest: "1.2.3", beta: "1.3.0-beta.1" } }) }));
|
||||
await runUpdate({ check: true, json: true });
|
||||
|
||||
const output = logSpy.mock.calls[0]?.[0] as string;
|
||||
const output = resultSpy.mock.calls[0]?.[0] as string;
|
||||
expect(JSON.parse(output)).toEqual({
|
||||
currentVersion: "1.2.3",
|
||||
latestVersion: "1.2.3",
|
||||
@@ -475,6 +495,7 @@ describe("runUpdate", () => {
|
||||
channel: "stable",
|
||||
});
|
||||
expect(logSpy.mock.calls.flat().join("\n")).not.toContain("A newer beta");
|
||||
expect(resultSpy.mock.calls.flat().join("\n")).not.toContain("A newer beta");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -318,6 +318,13 @@ const NOT_SURFACED_ALLOWLIST: Record<string, string> = {
|
||||
ephemeralAgentsCanCreateTasks: "legacy compatibility input replaced by ephemeralAgentTaskCreationPolicy",
|
||||
// Global-only serve/dashboard LAN discovery switch; no Settings UI description field exists.
|
||||
localNetworkDiscoveryEnabled: "global-only LAN discovery runtime switch",
|
||||
/*
|
||||
FNXC:VoiceInput 2026-07-25-09:05:
|
||||
Nested Voice Input settings object. DEFAULT_SETTINGS stores voiceInput as undefined
|
||||
(opt-in object); the VoiceInputSection enable toggle documents Default: off for the
|
||||
nested enabled flag rather than a top-level plain description field.
|
||||
*/
|
||||
voiceInput: "nested Voice Input section object; enable toggle owns Default: off for voiceInput.enabled",
|
||||
// Moved to workflow settings (U4) — see MOVED_SETTINGS_KEYS in settings-schema.ts.
|
||||
workflowStepTimeoutMs: "moved to workflow settings (U4)",
|
||||
workflowStepScopeEnforcement: "moved to workflow settings (U4)",
|
||||
|
||||
@@ -6390,7 +6390,8 @@
|
||||
},
|
||||
"globalMcp": "MCP Servers · Global",
|
||||
"mcp": "MCP Servers · Project",
|
||||
"configVersions": "Configuration Versions"
|
||||
"configVersions": "Configuration Versions",
|
||||
"voiceInput": "Voice Input"
|
||||
},
|
||||
"nodeRouting": {
|
||||
"blockExecution": "Block execution",
|
||||
@@ -6896,6 +6897,25 @@
|
||||
"confirmTitle": "Roll back configuration?",
|
||||
"confirmMessage": "Restore this version? The rollback is recorded as a new version.",
|
||||
"confirmRollback": "Roll back"
|
||||
},
|
||||
"voiceInput": {
|
||||
"title": "Voice Input",
|
||||
"enable": "Enable voice input",
|
||||
"enableHelp": "Default: off. Voice dictation uses the operator-managed Parakeet v3 model.",
|
||||
"modelStatus": "Parakeet v3 model status",
|
||||
"modelStatusHelp": "The speech model is installed and managed locally on this device.",
|
||||
"modelActions": "Model management",
|
||||
"modelActionsHelp": "Download or remove the Parakeet v3 speech model.",
|
||||
"statusUnavailable": "Voice runtime status could not be determined; voice mode stays disabled.",
|
||||
"runtimeUnavailable": "Voice runtime unavailable",
|
||||
"unknown": "Status unavailable",
|
||||
"downloading": "Downloading model…",
|
||||
"downloadingProgress": "Downloading: {{progress}}%",
|
||||
"installed": "Installed",
|
||||
"notInstalled": "Not installed",
|
||||
"download": "Download",
|
||||
"remove": "Remove",
|
||||
"error": "Model error: {{message}}"
|
||||
}
|
||||
},
|
||||
"setup": {
|
||||
|
||||
@@ -6366,7 +6366,8 @@
|
||||
},
|
||||
"globalMcp": "MCP Servers · Global",
|
||||
"mcp": "MCP Servers · Project",
|
||||
"configVersions": "Versiones de configuración"
|
||||
"configVersions": "Versiones de configuración",
|
||||
"voiceInput": "Voice Input"
|
||||
},
|
||||
"nodeRouting": {
|
||||
"blockExecution": "",
|
||||
@@ -6886,6 +6887,25 @@
|
||||
"confirmTitle": "¿Revertir la configuración?",
|
||||
"confirmMessage": "¿Restaurar esta versión? La reversión se registra como una nueva versión.",
|
||||
"confirmRollback": "Revertir"
|
||||
},
|
||||
"voiceInput": {
|
||||
"title": "Voice Input",
|
||||
"enable": "Enable voice input",
|
||||
"enableHelp": "Default: off. Voice dictation uses the operator-managed Parakeet v3 model.",
|
||||
"modelStatus": "Parakeet v3 model status",
|
||||
"modelStatusHelp": "The speech model is installed and managed locally on this device.",
|
||||
"modelActions": "Model management",
|
||||
"modelActionsHelp": "Download or remove the Parakeet v3 speech model.",
|
||||
"statusUnavailable": "Voice runtime status could not be determined; voice mode stays disabled.",
|
||||
"runtimeUnavailable": "Voice runtime unavailable",
|
||||
"unknown": "Status unavailable",
|
||||
"downloading": "Downloading model…",
|
||||
"downloadingProgress": "Downloading: {{progress}}%",
|
||||
"installed": "Installed",
|
||||
"notInstalled": "Not installed",
|
||||
"download": "Download",
|
||||
"remove": "Remove",
|
||||
"error": "Model error: {{message}}"
|
||||
}
|
||||
},
|
||||
"setup": {
|
||||
|
||||
@@ -6366,7 +6366,8 @@
|
||||
},
|
||||
"globalMcp": "MCP Servers · Global",
|
||||
"mcp": "MCP Servers · Project",
|
||||
"configVersions": "Versions de configuration"
|
||||
"configVersions": "Versions de configuration",
|
||||
"voiceInput": "Voice Input"
|
||||
},
|
||||
"nodeRouting": {
|
||||
"blockExecution": "",
|
||||
@@ -6886,6 +6887,25 @@
|
||||
"confirmTitle": "Revenir à une configuration antérieure ?",
|
||||
"confirmMessage": "Restaurer cette version ? Le retour est enregistré comme une nouvelle version.",
|
||||
"confirmRollback": "Revenir en arrière"
|
||||
},
|
||||
"voiceInput": {
|
||||
"title": "Voice Input",
|
||||
"enable": "Enable voice input",
|
||||
"enableHelp": "Default: off. Voice dictation uses the operator-managed Parakeet v3 model.",
|
||||
"modelStatus": "Parakeet v3 model status",
|
||||
"modelStatusHelp": "The speech model is installed and managed locally on this device.",
|
||||
"modelActions": "Model management",
|
||||
"modelActionsHelp": "Download or remove the Parakeet v3 speech model.",
|
||||
"statusUnavailable": "Voice runtime status could not be determined; voice mode stays disabled.",
|
||||
"runtimeUnavailable": "Voice runtime unavailable",
|
||||
"unknown": "Status unavailable",
|
||||
"downloading": "Downloading model…",
|
||||
"downloadingProgress": "Downloading: {{progress}}%",
|
||||
"installed": "Installed",
|
||||
"notInstalled": "Not installed",
|
||||
"download": "Download",
|
||||
"remove": "Remove",
|
||||
"error": "Model error: {{message}}"
|
||||
}
|
||||
},
|
||||
"setup": {
|
||||
|
||||
@@ -6366,7 +6366,8 @@
|
||||
},
|
||||
"globalMcp": "MCP Servers · Global",
|
||||
"mcp": "MCP Servers · Project",
|
||||
"configVersions": "구성 버전"
|
||||
"configVersions": "구성 버전",
|
||||
"voiceInput": "Voice Input"
|
||||
},
|
||||
"nodeRouting": {
|
||||
"blockExecution": "",
|
||||
@@ -6886,6 +6887,25 @@
|
||||
"confirmTitle": "구성을 롤백할까요?",
|
||||
"confirmMessage": "이 버전을 복원할까요? 롤백은 새 버전으로 기록됩니다.",
|
||||
"confirmRollback": "롤백"
|
||||
},
|
||||
"voiceInput": {
|
||||
"title": "Voice Input",
|
||||
"enable": "Enable voice input",
|
||||
"enableHelp": "Default: off. Voice dictation uses the operator-managed Parakeet v3 model.",
|
||||
"modelStatus": "Parakeet v3 model status",
|
||||
"modelStatusHelp": "The speech model is installed and managed locally on this device.",
|
||||
"modelActions": "Model management",
|
||||
"modelActionsHelp": "Download or remove the Parakeet v3 speech model.",
|
||||
"statusUnavailable": "Voice runtime status could not be determined; voice mode stays disabled.",
|
||||
"runtimeUnavailable": "Voice runtime unavailable",
|
||||
"unknown": "Status unavailable",
|
||||
"downloading": "Downloading model…",
|
||||
"downloadingProgress": "Downloading: {{progress}}%",
|
||||
"installed": "Installed",
|
||||
"notInstalled": "Not installed",
|
||||
"download": "Download",
|
||||
"remove": "Remove",
|
||||
"error": "Model error: {{message}}"
|
||||
}
|
||||
},
|
||||
"setup": {
|
||||
|
||||
@@ -6366,7 +6366,8 @@
|
||||
},
|
||||
"globalMcp": "MCP Servers · Global",
|
||||
"mcp": "MCP Servers · Project",
|
||||
"configVersions": "配置版本"
|
||||
"configVersions": "配置版本",
|
||||
"voiceInput": "Voice Input"
|
||||
},
|
||||
"nodeRouting": {
|
||||
"blockExecution": "",
|
||||
@@ -6886,6 +6887,25 @@
|
||||
"confirmTitle": "回滚配置?",
|
||||
"confirmMessage": "恢复此版本?回滚会记录为新版本。",
|
||||
"confirmRollback": "回滚"
|
||||
},
|
||||
"voiceInput": {
|
||||
"title": "Voice Input",
|
||||
"enable": "Enable voice input",
|
||||
"enableHelp": "Default: off. Voice dictation uses the operator-managed Parakeet v3 model.",
|
||||
"modelStatus": "Parakeet v3 model status",
|
||||
"modelStatusHelp": "The speech model is installed and managed locally on this device.",
|
||||
"modelActions": "Model management",
|
||||
"modelActionsHelp": "Download or remove the Parakeet v3 speech model.",
|
||||
"statusUnavailable": "Voice runtime status could not be determined; voice mode stays disabled.",
|
||||
"runtimeUnavailable": "Voice runtime unavailable",
|
||||
"unknown": "Status unavailable",
|
||||
"downloading": "Downloading model…",
|
||||
"downloadingProgress": "Downloading: {{progress}}%",
|
||||
"installed": "Installed",
|
||||
"notInstalled": "Not installed",
|
||||
"download": "Download",
|
||||
"remove": "Remove",
|
||||
"error": "Model error: {{message}}"
|
||||
}
|
||||
},
|
||||
"setup": {
|
||||
|
||||
@@ -6366,7 +6366,8 @@
|
||||
},
|
||||
"globalMcp": "MCP Servers · Global",
|
||||
"mcp": "MCP Servers · Project",
|
||||
"configVersions": "設定版本"
|
||||
"configVersions": "設定版本",
|
||||
"voiceInput": "Voice Input"
|
||||
},
|
||||
"nodeRouting": {
|
||||
"blockExecution": "",
|
||||
@@ -6886,6 +6887,25 @@
|
||||
"confirmTitle": "回滾設定?",
|
||||
"confirmMessage": "還原此版本?回滾會記錄為新版本。",
|
||||
"confirmRollback": "回滾"
|
||||
},
|
||||
"voiceInput": {
|
||||
"title": "Voice Input",
|
||||
"enable": "Enable voice input",
|
||||
"enableHelp": "Default: off. Voice dictation uses the operator-managed Parakeet v3 model.",
|
||||
"modelStatus": "Parakeet v3 model status",
|
||||
"modelStatusHelp": "The speech model is installed and managed locally on this device.",
|
||||
"modelActions": "Model management",
|
||||
"modelActionsHelp": "Download or remove the Parakeet v3 speech model.",
|
||||
"statusUnavailable": "Voice runtime status could not be determined; voice mode stays disabled.",
|
||||
"runtimeUnavailable": "Voice runtime unavailable",
|
||||
"unknown": "Status unavailable",
|
||||
"downloading": "Downloading model…",
|
||||
"downloadingProgress": "Downloading: {{progress}}%",
|
||||
"installed": "Installed",
|
||||
"notInstalled": "Not installed",
|
||||
"download": "Download",
|
||||
"remove": "Remove",
|
||||
"error": "Model error: {{message}}"
|
||||
}
|
||||
},
|
||||
"setup": {
|
||||
|
||||
Reference in New Issue
Block a user