feat: add voice capability status reporting to client

This commit is contained in:
Mohamed Boudra
2026-02-14 13:14:57 +07:00
parent c4bebb97ab
commit a644adbf9b
16 changed files with 585 additions and 98 deletions
@@ -0,0 +1,118 @@
import { describe, expect, it } from "vitest";
import type { ServerCapabilities } from "@server/shared/messages";
import type { DaemonServerInfo } from "@/stores/session-store";
import {
getServerCapabilities,
getVoiceReadinessState,
resolveVoiceUnavailableMessage,
} from "./server-info-capabilities";
function buildServerInfo(capabilities?: ServerCapabilities): DaemonServerInfo {
return {
serverId: "srv-1",
hostname: "test-host",
...(capabilities ? { capabilities } : {}),
};
}
describe("server-info-capabilities", () => {
it("returns null capabilities when server_info does not include capability metadata", () => {
const serverInfo = buildServerInfo();
expect(getServerCapabilities({ serverInfo })).toBeNull();
});
it("returns the matching voice capability state by mode", () => {
const capabilities: ServerCapabilities = {
voice: {
dictation: {
enabled: true,
reason: "Dictation is warming up.",
},
voice: {
enabled: false,
reason: "Voice is disabled in daemon config.",
},
},
};
const serverInfo = buildServerInfo(capabilities);
expect(
getVoiceReadinessState({
serverInfo,
mode: "dictation",
})
).toEqual(capabilities.voice?.dictation);
expect(
getVoiceReadinessState({
serverInfo,
mode: "voice",
})
).toEqual(capabilities.voice?.voice);
});
it("returns null when capability is enabled and has no reason", () => {
const serverInfo = buildServerInfo({
voice: {
dictation: {
enabled: true,
reason: "",
},
voice: {
enabled: true,
reason: "",
},
},
});
expect(
resolveVoiceUnavailableMessage({
serverInfo,
mode: "dictation",
})
).toBeNull();
});
it("returns capability reason when present", () => {
const serverInfo = buildServerInfo({
voice: {
dictation: {
enabled: true,
reason: "Dictation models are still downloading.",
},
voice: {
enabled: true,
reason: "",
},
},
});
expect(
resolveVoiceUnavailableMessage({
serverInfo,
mode: "dictation",
})
).toBe("Dictation models are still downloading.");
});
it("returns null when capability reason is blank", () => {
const serverInfo = buildServerInfo({
voice: {
dictation: {
enabled: false,
reason: " ",
},
voice: {
enabled: true,
reason: "",
},
},
});
expect(
resolveVoiceUnavailableMessage({
serverInfo,
mode: "dictation",
})
).toBeNull();
});
});
@@ -0,0 +1,50 @@
import type { ServerCapabilityState } from "@server/shared/messages";
import type { DaemonServerInfo } from "@/stores/session-store";
export type VoiceReadinessMode = "dictation" | "voice";
export function getServerCapabilities(params: {
serverInfo: DaemonServerInfo | null | undefined;
}): DaemonServerInfo["capabilities"] | null {
const capabilities = params.serverInfo?.capabilities;
if (!capabilities) {
return null;
}
return capabilities;
}
export function getVoiceReadinessState(params: {
serverInfo: DaemonServerInfo | null | undefined;
mode: VoiceReadinessMode;
}): ServerCapabilityState | null {
const capabilities = getServerCapabilities({ serverInfo: params.serverInfo });
const voice = capabilities?.voice;
if (!voice) {
return null;
}
if (params.mode === "dictation") {
return voice.dictation;
}
return voice.voice;
}
export function resolveVoiceUnavailableMessage(params: {
serverInfo: DaemonServerInfo | null | undefined;
mode: VoiceReadinessMode;
}): string | null {
const readiness = getVoiceReadinessState({
serverInfo: params.serverInfo,
mode: params.mode,
});
if (!readiness) {
return null;
}
if (readiness.enabled && readiness.reason.trim().length === 0) {
return null;
}
const message = readiness.reason.trim();
if (message.length > 0) {
return message;
}
return null;
}
@@ -1,5 +1,6 @@
import { DaemonClient } from "@server/client/daemon-client";
import type { DaemonClientConfig } from "@server/client/daemon-client";
import { parseServerInfoStatusPayload } from "@server/shared/messages";
import type { HostConnection } from "@/contexts/daemon-registry-context";
import { buildDaemonWebSocketUrl, buildRelayWebSocketUrl } from "./daemon-endpoints";
import { createTauriWebSocketTransportFactory } from "./tauri-daemon-transport";
@@ -116,15 +117,10 @@ function connectAndProbe(
unsubscribeStatus = client.on("status", (message) => {
if (message.type !== "status") return;
const payload = message.payload as { status?: unknown; serverId?: unknown; hostname?: unknown };
if (payload?.status !== "server_info") return;
const raw = typeof payload.serverId === "string" ? payload.serverId.trim() : "";
if (!raw) return;
serverId = raw;
hostname = typeof payload.hostname === "string" ? payload.hostname.trim() : null;
if (hostname && hostname.length === 0) {
hostname = null;
}
const payload = parseServerInfoStatusPayload(message.payload);
if (!payload) return;
serverId = payload.serverId;
hostname = payload.hostname;
maybeFinishOk();
});