dsh-live-voice 0.2.2 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,25 @@
2
2
 
3
3
  All notable changes to DSH Live Voice are documented in this file.
4
4
 
5
+ ## [0.2.3] - 2026-09-21
6
+
7
+ ### Features
8
+
9
+ - Add an update notification in Live Voice settings when a newer GitHub release is available. The check runs at most once every 24 hours and the action opens the matching release.
10
+ - Show the installed Live Voice version and the tested DSH version in the settings header.
11
+ - Show Browser WebGPU Inference, sherpa-onnx Streaming, NVIDIA Parakeet, and Voxtral Realtime as disabled upcoming recognition engines.
12
+
13
+ ### Changes
14
+
15
+ - Rename recognition-engine options to describe the API they use: Browser SpeechRecognition, Qwen3 ASR HTTP API, and Whisper HTTP API.
16
+ - Show the expected HTTP URL or transcription route for the selected recognition engine.
17
+
18
+ ### Bug Fixes
19
+
20
+ - Avoid repeated GitHub requests when the release API is unavailable by caching failed checks for the same 24-hour interval.
21
+ - Handle release tags with a leading `v` and prerelease identifiers correctly when deciding whether an update is newer.
22
+ - Ignore unexpected release URLs and fall back to the repository releases page.
23
+
5
24
  ## [0.2.2] - 2026-09-19
6
25
 
7
26
  ### Features
@@ -106,7 +125,8 @@ This release expands conversation control, audio routing, speech filtering, and
106
125
  - Support browser speech synthesis and native macOS `say` output.
107
126
  - Add Live Voice controls, Whisper settings, build and browser-preview scripts, and an initial test suite.
108
127
 
109
- [0.2.2]: https://github.com/victorwads/dsh-live-voice/compare/v0.2.1...HEAD
128
+ [0.2.3]: https://github.com/victorwads/dsh-live-voice/compare/v0.2.2...HEAD
129
+ [0.2.2]: https://github.com/victorwads/dsh-live-voice/compare/v0.2.1...v0.2.2
110
130
  [0.2.1]: https://github.com/victorwads/dsh-live-voice/compare/v0.2.0...v0.2.1
111
131
  [0.2.0]: https://github.com/victorwads/dsh-live-voice/compare/v0.1.0...v0.2.0
112
132
  [0.1.0]: https://github.com/victorwads/dsh-live-voice/compare/v0.0.2...v0.1.0
package/README.md CHANGED
@@ -1,8 +1,7 @@
1
1
  # DSH Live Voice
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/dsh-live-voice?logo=npm&label=npm&color=brightgreen)](https://www.npmjs.com/package/dsh-live-voice)
4
- [![dsh.pub registry status](https://dsh.pub/api/badges/victorwads/dsh-live-voice.svg)](https://dsh.pub/en/plugins/dsh-live-voice/)
5
- [![license](https://img.shields.io/badge/license-GPL--3.0--only-blue)](LICENSE)
4
+ [![DSH](https://img.shields.io/badge/DSH-v0.1.6--alpha.2-5c5cff?logo=deepseek&logoColor=white)](https://github.com/deepseek-ai/deepseek-harness/releases/tag/v0.1.6-alpha.2)
6
5
 
7
6
  **A local-first, hands-free voice assistant plugin for DeepSeek Harness (DSH). Speak, listen, answer prompts, and keep working without touching the computer.**
8
7
 
@@ -10,8 +9,6 @@ Looking for a DeepSeek Harness voice plugin, DSH microphone plugin, speech-to-te
10
9
 
11
10
  It coordinates the microphone, composer, assistant messages, structured questions, and speech output without letting listening and speaking compete. Once voice conversation mode is running, DSH can narrate responses and questions, capture your spoken answers, and continue the conversation while your hands stay free.
12
11
 
13
- [![DSH tested](https://img.shields.io/badge/DSH%20tested-0.1.6--alpha.2-5c5cff?logo=deepseek&logoColor=white)](https://github.com/deepseek-ai/deepseek-harness/releases/tag/v0.1.6-alpha.2)
14
-
15
12
  ## Built for daily use, maintained with you
16
13
 
17
14
  I use DSH Live Voice for at least eight hours a day. For me, it is the best voice plugin for DSH — and I am committed to making it better through real, everyday use.
package/lib/client.js CHANGED
@@ -2283,6 +2283,100 @@ function createQwenSettings(React2) {
2283
2283
  };
2284
2284
  }
2285
2285
 
2286
+ // src/client/releases.ts
2287
+ var CURRENT_VERSION = "0.2.3";
2288
+ var TESTED_DSH_VERSION = "0.1.6-alpha.2";
2289
+ var REPOSITORY_URL = "https://github.com/victorwads/dsh-live-voice";
2290
+ var RELEASES_URL = `${REPOSITORY_URL}/releases`;
2291
+ var LATEST_RELEASE_API_URL = "https://api.github.com/repos/victorwads/dsh-live-voice/releases/latest";
2292
+ var RELEASE_CHECK_STORAGE_KEY = "dsh-live-voice.latest-release-check";
2293
+ var RELEASE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
2294
+ var LIVE_VOICE_BADGE_URL = "https://cdn.simpleicons.org/npm/white";
2295
+ var DSH_BADGE_URL = "https://cdn.simpleicons.org/deepseek/white";
2296
+ var TESTED_DSH_RELEASE_URL = `https://github.com/deepseek-ai/deepseek-harness/releases/tag/v${TESTED_DSH_VERSION}`;
2297
+ function versionParts(version) {
2298
+ const normalized = String(version || "").trim().replace(/^v/i, "").split("+", 1)[0];
2299
+ const [core = "", prerelease = ""] = normalized.split("-", 2);
2300
+ const numbers = core.split(".").map((part) => {
2301
+ const match = part.match(/^\d+/);
2302
+ return match ? Number(match[0]) : 0;
2303
+ });
2304
+ while (numbers.length < 3) numbers.push(0);
2305
+ return { numbers, prerelease: prerelease ? prerelease.split(".") : [] };
2306
+ }
2307
+ function compareIdentifier(left, right) {
2308
+ const leftNumber = /^\d+$/.test(left) ? Number(left) : null;
2309
+ const rightNumber = /^\d+$/.test(right) ? Number(right) : null;
2310
+ if (leftNumber !== null && rightNumber !== null) return Math.sign(leftNumber - rightNumber);
2311
+ if (leftNumber !== null) return -1;
2312
+ if (rightNumber !== null) return 1;
2313
+ return left.localeCompare(right);
2314
+ }
2315
+ function compareVersions(left, right) {
2316
+ const a = versionParts(left);
2317
+ const b = versionParts(right);
2318
+ for (let index = 0; index < Math.max(a.numbers.length, b.numbers.length); index++) {
2319
+ const difference = (a.numbers[index] || 0) - (b.numbers[index] || 0);
2320
+ if (difference) return Math.sign(difference);
2321
+ }
2322
+ if (!a.prerelease.length && !b.prerelease.length) return 0;
2323
+ if (!a.prerelease.length) return 1;
2324
+ if (!b.prerelease.length) return -1;
2325
+ for (let index = 0; index < Math.max(a.prerelease.length, b.prerelease.length); index++) {
2326
+ if (a.prerelease[index] === void 0) return -1;
2327
+ if (b.prerelease[index] === void 0) return 1;
2328
+ const difference = compareIdentifier(a.prerelease[index], b.prerelease[index]);
2329
+ if (difference) return difference;
2330
+ }
2331
+ return 0;
2332
+ }
2333
+ function readCached(storage, now) {
2334
+ try {
2335
+ const cached = JSON.parse(storage?.getItem(RELEASE_CHECK_STORAGE_KEY) || "null");
2336
+ if (cached && Number.isFinite(cached.checkedAt) && now - cached.checkedAt >= 0 && now - cached.checkedAt < RELEASE_CHECK_INTERVAL_MS)
2337
+ return cached;
2338
+ } catch {
2339
+ }
2340
+ return null;
2341
+ }
2342
+ function writeCached(storage, value) {
2343
+ try {
2344
+ storage?.setItem(RELEASE_CHECK_STORAGE_KEY, JSON.stringify(value));
2345
+ } catch {
2346
+ }
2347
+ }
2348
+ async function checkLatestRelease({
2349
+ fetchImpl = globalThis.window?.fetch?.bind(globalThis.window),
2350
+ storage = globalThis.window?.localStorage,
2351
+ now = Date.now()
2352
+ } = {}) {
2353
+ const cached = readCached(storage, now);
2354
+ if (cached) return cached;
2355
+ const attempted = { checkedAt: now, release: null };
2356
+ writeCached(storage, attempted);
2357
+ if (typeof fetchImpl !== "function") return attempted;
2358
+ try {
2359
+ const response = await fetchImpl(LATEST_RELEASE_API_URL, {
2360
+ headers: { Accept: "application/vnd.github+json" }
2361
+ });
2362
+ if (!response.ok) return attempted;
2363
+ const body = await response.json();
2364
+ if (typeof body?.tag_name !== "string" || !body.tag_name.trim()) return attempted;
2365
+ const release = {
2366
+ tag: body.tag_name.trim(),
2367
+ url: typeof body.html_url === "string" && body.html_url.startsWith("https://github.com/") ? body.html_url : RELEASES_URL
2368
+ };
2369
+ const result = { checkedAt: now, release };
2370
+ writeCached(storage, result);
2371
+ return result;
2372
+ } catch {
2373
+ return attempted;
2374
+ }
2375
+ }
2376
+ function hasNewerRelease(release, currentVersion = CURRENT_VERSION) {
2377
+ return Boolean(release?.tag && compareVersions(release.tag, currentVersion) > 0);
2378
+ }
2379
+
2286
2380
  // src/client/components.ts
2287
2381
  function createComponents(React2) {
2288
2382
  const h = React2.createElement;
@@ -2576,6 +2670,17 @@ function createComponents(React2) {
2576
2670
  const settings = state.settings || {};
2577
2671
  const capabilities = state.capabilities || {};
2578
2672
  const tabsId = React2.useId();
2673
+ const [latestRelease, setLatestRelease] = React2.useState(null);
2674
+ React2.useEffect(() => {
2675
+ let active = true;
2676
+ void checkLatestRelease().then((result) => {
2677
+ if (active) setLatestRelease(result.release);
2678
+ });
2679
+ return () => {
2680
+ active = false;
2681
+ };
2682
+ }, []);
2683
+ const updateAvailable = hasNewerRelease(latestRelease);
2579
2684
  const tabs = [
2580
2685
  { id: "conversation", label: "Conversation" },
2581
2686
  { id: "speech", label: "Speech" },
@@ -2668,7 +2773,69 @@ function createComponents(React2) {
2668
2773
  return h(
2669
2774
  "section",
2670
2775
  { className: "dlv-settings", "aria-label": "Live Voice settings" },
2671
- h("h3", null, "Live Voice"),
2776
+ h(
2777
+ "div",
2778
+ { className: "dlv-settings-heading" },
2779
+ h("h3", null, "Live Voice"),
2780
+ h(
2781
+ "div",
2782
+ { className: "dlv-version-badges", "aria-label": "Version information" },
2783
+ h(
2784
+ "a",
2785
+ {
2786
+ className: "dlv-shields-badge",
2787
+ href: RELEASES_URL,
2788
+ target: "_blank",
2789
+ rel: "noreferrer",
2790
+ "aria-label": `DSH Live Voice v${CURRENT_VERSION}. Open releases`,
2791
+ title: `DSH Live Voice v${CURRENT_VERSION}`
2792
+ },
2793
+ h("img", { src: LIVE_VOICE_BADGE_URL, alt: "" }),
2794
+ h("span", null, `v${CURRENT_VERSION}`)
2795
+ ),
2796
+ h(
2797
+ "a",
2798
+ {
2799
+ className: "dlv-shields-badge",
2800
+ href: TESTED_DSH_RELEASE_URL,
2801
+ target: "_blank",
2802
+ rel: "noreferrer",
2803
+ "aria-label": `Compatible with DSH v${TESTED_DSH_VERSION}. Open release`,
2804
+ title: `Compatible with DSH v${TESTED_DSH_VERSION}`
2805
+ },
2806
+ h("img", { src: DSH_BADGE_URL, alt: "" }),
2807
+ h("span", null, `v${TESTED_DSH_VERSION}`)
2808
+ )
2809
+ ),
2810
+ h("span", { className: "dlv-heading-divider", "aria-hidden": true }),
2811
+ updateAvailable ? h(
2812
+ "a",
2813
+ {
2814
+ className: "dlv-version-badge dlv-update-badge",
2815
+ href: latestRelease.url,
2816
+ target: "_blank",
2817
+ rel: "noreferrer",
2818
+ "aria-label": `Update available: ${latestRelease.tag}. Open release`,
2819
+ title: `Update available: ${latestRelease.tag}`
2820
+ },
2821
+ h("span", { className: "dlv-update-icon", "aria-hidden": true }, "\u2191"),
2822
+ "Update available"
2823
+ ) : null,
2824
+ updateAvailable ? h("span", { className: "dlv-heading-divider", "aria-hidden": true }) : null,
2825
+ h(
2826
+ "a",
2827
+ {
2828
+ className: "dlv-version-badge dlv-star-badge",
2829
+ href: REPOSITORY_URL,
2830
+ target: "_blank",
2831
+ rel: "noreferrer",
2832
+ "aria-label": "Star DSH Live Voice on GitHub",
2833
+ title: "Star DSH Live Voice on GitHub"
2834
+ },
2835
+ h("span", { className: "dlv-star-icon", "aria-hidden": true }, "\u2605"),
2836
+ "Star Us on GitHub"
2837
+ )
2838
+ ),
2672
2839
  onClose ? h(Button, { label: "Close voice settings", icon: "close", onClick: onClose }) : null,
2673
2840
  h(
2674
2841
  "div",
@@ -2877,11 +3044,28 @@ function createComponents(React2) {
2877
3044
  event.target.value === "browser" && settings.recognitionLang === "auto" ? { recognitionEngine: "browser", recognitionLang: "pt-BR" } : { recognitionEngine: event.target.value }
2878
3045
  )
2879
3046
  },
2880
- h("option", { value: "qwen-http" }, "Qwen3 ASR \u2014 local MLX server"),
2881
- h("option", { value: "browser" }, "Browser SpeechRecognition"),
2882
- h("option", { value: "whisper-http" }, "Whisper HTTP \u2014 DSH host")
3047
+ h("option", { value: "browser" }, "Browser SpeechRecognition \u2014 Default option"),
3048
+ h("option", { value: "qwen-http" }, "Qwen3 ASR \u2014 HTTP API"),
3049
+ h("option", { value: "whisper-http" }, "Whisper \u2014 HTTP API"),
3050
+ h(
3051
+ "optgroup",
3052
+ { label: "Coming Soon \u2014 vote on repo issues" },
3053
+ h("option", { value: "webgpu", disabled: true }, "Browser WebGPU Inference \u2014 Soon"),
3054
+ h(
3055
+ "option",
3056
+ { value: "sherpa-onnx", disabled: true },
3057
+ "sherpa-onnx Streaming \u2014 Soon"
3058
+ ),
3059
+ h("option", { value: "parakeet", disabled: true }, "NVIDIA Parakeet \u2014 Soon"),
3060
+ h("option", { value: "voxtral", disabled: true }, "Voxtral Realtime \u2014 Soon")
3061
+ )
2883
3062
  )
2884
3063
  ),
3064
+ h(
3065
+ "small",
3066
+ { className: "dlv-recognition-engine-description" },
3067
+ settings.recognitionEngine === "qwen-http" ? "HTTP API at the configured base URL (default: http://127.0.0.1:8080/). Compatible with POST /v1/audio/transcriptions." : settings.recognitionEngine === "whisper-http" ? "HTTP API at the configured DSH host URL (default: http://127.0.0.1:8080/inference). Audio uses the authenticated DSH host transcription route." : "Uses the browser SpeechRecognition API. This is the default option."
3068
+ ),
2885
3069
  field("Input device", "inputDeviceId", deviceOptions("audioinput", "Microphone")),
2886
3070
  settings.recognitionEngine === "browser" ? h(
2887
3071
  "small",
@@ -3236,13 +3420,15 @@ var styles = `
3236
3420
  .dlv-error{color:var(--dsw-alias-state-error-primary);overflow-wrap:anywhere;font-size:13px;max-width:720px;margin:8px auto}
3237
3421
  .dlv-settings{box-sizing:border-box;padding:18px;width:100%;display:grid;gap:16px;max-width:640px;color:var(--dsw-alias-label-primary)}
3238
3422
  .dlv-settings h3,.dlv-settings p{margin:0}
3423
+ .dlv-settings-heading{display:flex;align-items:center;gap:8px;min-width:0}.dlv-settings-heading h3{white-space:nowrap}.dlv-heading-divider{height:1px;min-width:12px;flex:1;background:var(--dsw-alias-border-l1)}.dlv-settings-heading .dlv-star-badge{flex:none}
3424
+ .dlv-version-badges{display:flex;flex:none;gap:5px;align-items:center}.dlv-shields-badge{display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;height:20px;gap:4px;padding:3px 5px;border:1px solid var(--dsw-alias-border-l1);border-radius:5px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);font-size:10px;font-weight:650;line-height:12px;text-decoration:none}.dlv-shields-badge img{display:block;width:12px;height:12px;max-width:100%}.dlv-shields-badge:focus-visible{border-radius:4px;outline:2px solid var(--dsw-alias-label-primary);outline-offset:3px}.dlv-version-badge{display:inline-flex;align-items:center;min-height:24px;box-sizing:border-box;padding:3px 8px;border:1px solid var(--dsw-alias-border-l1);border-radius:999px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);font-size:11px;font-weight:650;line-height:16px;text-decoration:none}.dlv-version-badges a.dlv-version-badge:hover{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary)}.dlv-star-badge{padding:4px 10px;color:var(--dsw-alias-label-primary)}.dlv-update-icon{margin-right:4px;font-size:14px;line-height:1}.dlv-star-icon{margin-right:4px;color:#f59e0b;font-size:14px;line-height:1;text-shadow:0 0 8px rgba(245,158,11,.28)}.dlv-version-badges a.dlv-star-badge:hover{border-color:#f59e0b;background:rgba(245,158,11,.08)}.dlv-update-badge{border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}
3239
3425
  .dlv-settings-tabs{border-bottom:.5px solid var(--dsw-alias-border-l2);display:flex;align-items:flex-end;gap:22px;overflow-x:auto}.dlv-settings-tab{position:relative;flex:none;padding:7px 1px 9px;border:0;background:transparent;color:var(--dsw-alias-label-tertiary);font:inherit;font-size:13px;line-height:20px;cursor:pointer}.dlv-settings-tab:hover,.dlv-settings-tab[data-active=true]{color:var(--dsw-alias-label-primary)}.dlv-settings-tab[data-active=true]:after,.dlv-settings-tab:focus-visible:after{content:"";position:absolute;right:0;bottom:-1px;left:0;height:2px;border-radius:2px 2px 0 0;background:var(--dsw-alias-label-primary)}.dlv-settings-tab:focus-visible{border-radius:2px;outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px;color:var(--dsw-alias-label-primary)}.dlv-settings-panel{min-width:0;padding-top:2px}.dlv-settings-panel[hidden]{display:none}.dlv-settings-group{margin:0;border:1px solid var(--dsw-alias-border-l1);border-radius:10px;min-width:0;display:grid;gap:14px;padding:16px}.dlv-settings-group legend{padding:0 6px;font-weight:600;color:var(--dsw-alias-label-primary)}.dlv-settings-card-body{display:grid;gap:12px;padding:12px 0}.dlv-settings-subcard{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;min-width:0}.dlv-settings-subcard>summary{cursor:pointer;font-weight:650;list-style:none;display:flex;align-items:center;justify-content:space-between;padding:10px}.dlv-settings-subcard>summary::-webkit-details-marker{display:none}.dlv-settings-subcard>summary:after{content:"\u203A";transform:rotate(90deg);transition:transform .15s}.dlv-settings-subcard:not([open])>summary:after{transform:rotate(0)}.dlv-settings-subcard-body{display:grid;gap:10px;padding:0 10px 10px}
3240
3426
  .dlv-settings label{display:grid;gap:6px;font-size:14px}.dlv-settings label.dlv-check{display:flex;align-items:center;gap:8px}.dlv-settings label.dlv-check input{width:auto}
3241
3427
  .dlv-settings :is(input,select,textarea){box-sizing:border-box;width:100%;padding:8px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-1);color:inherit;font:inherit}
3242
3428
  .dlv-settings small,.dlv-settings p{font-size:13px;color:var(--dsw-alias-label-secondary);line-height:1.5}
3243
3429
  .dlv-preset-group{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px}.dlv-settings label.dlv-preset{display:flex;align-items:flex-start;gap:8px;padding:10px;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;cursor:pointer}.dlv-settings label.dlv-preset:has(input:checked){border-color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-1)}.dlv-settings label.dlv-preset input{width:auto;margin-top:3px}.dlv-preset span{display:grid;gap:3px}.dlv-vad-summary{font-weight:500}
3244
3430
  .dlv-settings-actions{display:flex;flex-wrap:wrap;gap:8px}.dlv-settings-actions button{padding:8px 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-1);color:inherit;cursor:pointer}.dlv-settings-actions button:disabled{opacity:.45;cursor:default}
3245
- @media(max-width:480px){.dlv-preset-group{grid-template-columns:1fr}.dlv-pill{flex-wrap:wrap}.dlv-wave{flex-basis:90px}.dlv-status{flex-basis:100px}}
3431
+ @media(max-width:480px){.dlv-settings-heading{gap:6px}.dlv-heading-divider{min-width:4px}.dlv-settings-heading .dlv-star-badge{padding:3px 6px;font-size:10px}.dlv-preset-group{grid-template-columns:1fr}.dlv-pill{flex-wrap:wrap}.dlv-wave{flex-basis:90px}.dlv-status{flex-basis:100px}}
3246
3432
  `;
3247
3433
 
3248
3434
  // src/client/chat.ts
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "dsh-live-voice",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
+ "dshTestedVersion": "0.1.6-alpha.2",
4
5
  "type": "module",
5
6
  "scripts": {
6
7
  "typecheck": "tsc --noEmit",
package/scripts/build.ts CHANGED
@@ -1,6 +1,12 @@
1
1
  // @ts-nocheck
2
2
  import { build, context, type BuildOptions } from 'esbuild';
3
- import { rm, mkdir } from 'node:fs/promises';
3
+ import { rm, mkdir, readFile } from 'node:fs/promises';
4
+
5
+ const packageMetadata = JSON.parse(await readFile('package.json', 'utf8'));
6
+ const versionDefine = {
7
+ __DLV_VERSION__: JSON.stringify(packageMetadata.version),
8
+ __DLV_TESTED_DSH_VERSION__: JSON.stringify(packageMetadata.dshTestedVersion),
9
+ };
4
10
 
5
11
  const client: BuildOptions = {
6
12
  entryPoints: ['src/client/index.ts'],
@@ -10,6 +16,7 @@ const client: BuildOptions = {
10
16
  platform: 'browser',
11
17
  target: ['es2022'],
12
18
  external: ['react', 'react-dom'],
19
+ define: versionDefine,
13
20
  banner: {
14
21
  js: 'window.__ModuleLoader__.load({id:"dsh-live-voice",factory:(require)=>{var module={exports:{}};var exports=module.exports;',
15
22
  },
@@ -35,6 +42,7 @@ const tests: BuildOptions = {
35
42
  platform: 'node',
36
43
  target: ['node22'],
37
44
  packages: 'external',
45
+ define: versionDefine,
38
46
  logLevel: 'silent',
39
47
  };
40
48
  const watch = process.argv.includes('--watch');
@@ -2,6 +2,17 @@
2
2
  import { createWhisperSettings } from './whisper-settings.ts';
3
3
  import { createQwenSettings } from './qwen-settings.ts';
4
4
  import { qwenVoices, usesPluginVoiceDetection, voiceDetectionPresets } from '../core/settings.ts';
5
+ import {
6
+ checkLatestRelease,
7
+ CURRENT_VERSION,
8
+ DSH_BADGE_URL,
9
+ hasNewerRelease,
10
+ LIVE_VOICE_BADGE_URL,
11
+ RELEASES_URL,
12
+ REPOSITORY_URL,
13
+ TESTED_DSH_RELEASE_URL,
14
+ TESTED_DSH_VERSION,
15
+ } from './releases.ts';
5
16
 
6
17
  // UI only: the controller owns capture, recognition, playback and policy.
7
18
  export function createComponents(React) {
@@ -383,6 +394,17 @@ export function createComponents(React) {
383
394
  const settings = state.settings || {};
384
395
  const capabilities = state.capabilities || {};
385
396
  const tabsId = React.useId();
397
+ const [latestRelease, setLatestRelease] = React.useState(null);
398
+ React.useEffect(() => {
399
+ let active = true;
400
+ void checkLatestRelease().then((result) => {
401
+ if (active) setLatestRelease(result.release);
402
+ });
403
+ return () => {
404
+ active = false;
405
+ };
406
+ }, []);
407
+ const updateAvailable = hasNewerRelease(latestRelease);
386
408
  const tabs = [
387
409
  { id: 'conversation', label: 'Conversation' },
388
410
  { id: 'speech', label: 'Speech' },
@@ -483,7 +505,73 @@ export function createComponents(React) {
483
505
  return h(
484
506
  'section',
485
507
  { className: 'dlv-settings', 'aria-label': 'Live Voice settings' },
486
- h('h3', null, 'Live Voice'),
508
+ h(
509
+ 'div',
510
+ { className: 'dlv-settings-heading' },
511
+ h('h3', null, 'Live Voice'),
512
+ h(
513
+ 'div',
514
+ { className: 'dlv-version-badges', 'aria-label': 'Version information' },
515
+ h(
516
+ 'a',
517
+ {
518
+ className: 'dlv-shields-badge',
519
+ href: RELEASES_URL,
520
+ target: '_blank',
521
+ rel: 'noreferrer',
522
+ 'aria-label': `DSH Live Voice v${CURRENT_VERSION}. Open releases`,
523
+ title: `DSH Live Voice v${CURRENT_VERSION}`,
524
+ },
525
+ h('img', { src: LIVE_VOICE_BADGE_URL, alt: '' }),
526
+ h('span', null, `v${CURRENT_VERSION}`),
527
+ ),
528
+ h(
529
+ 'a',
530
+ {
531
+ className: 'dlv-shields-badge',
532
+ href: TESTED_DSH_RELEASE_URL,
533
+ target: '_blank',
534
+ rel: 'noreferrer',
535
+ 'aria-label': `Compatible with DSH v${TESTED_DSH_VERSION}. Open release`,
536
+ title: `Compatible with DSH v${TESTED_DSH_VERSION}`,
537
+ },
538
+ h('img', { src: DSH_BADGE_URL, alt: '' }),
539
+ h('span', null, `v${TESTED_DSH_VERSION}`),
540
+ ),
541
+ ),
542
+ h('span', { className: 'dlv-heading-divider', 'aria-hidden': true }),
543
+ updateAvailable
544
+ ? h(
545
+ 'a',
546
+ {
547
+ className: 'dlv-version-badge dlv-update-badge',
548
+ href: latestRelease.url,
549
+ target: '_blank',
550
+ rel: 'noreferrer',
551
+ 'aria-label': `Update available: ${latestRelease.tag}. Open release`,
552
+ title: `Update available: ${latestRelease.tag}`,
553
+ },
554
+ h('span', { className: 'dlv-update-icon', 'aria-hidden': true }, '↑'),
555
+ 'Update available',
556
+ )
557
+ : null,
558
+ updateAvailable
559
+ ? h('span', { className: 'dlv-heading-divider', 'aria-hidden': true })
560
+ : null,
561
+ h(
562
+ 'a',
563
+ {
564
+ className: 'dlv-version-badge dlv-star-badge',
565
+ href: REPOSITORY_URL,
566
+ target: '_blank',
567
+ rel: 'noreferrer',
568
+ 'aria-label': 'Star DSH Live Voice on GitHub',
569
+ title: 'Star DSH Live Voice on GitHub',
570
+ },
571
+ h('span', { className: 'dlv-star-icon', 'aria-hidden': true }, '★'),
572
+ 'Star Us on GitHub',
573
+ ),
574
+ ),
487
575
  onClose
488
576
  ? h(Button, { label: 'Close voice settings', icon: 'close', onClick: onClose })
489
577
  : null,
@@ -710,11 +798,32 @@ export function createComponents(React) {
710
798
  : { recognitionEngine: event.target.value },
711
799
  ),
712
800
  },
713
- h('option', { value: 'qwen-http' }, 'Qwen3 ASR — local MLX server'),
714
- h('option', { value: 'browser' }, 'Browser SpeechRecognition'),
715
- h('option', { value: 'whisper-http' }, 'Whisper HTTP — DSH host'),
801
+ h('option', { value: 'browser' }, 'Browser SpeechRecognition — Default option'),
802
+ h('option', { value: 'qwen-http' }, 'Qwen3 ASR — HTTP API'),
803
+ h('option', { value: 'whisper-http' }, 'Whisper — HTTP API'),
804
+ h(
805
+ 'optgroup',
806
+ { label: 'Coming Soon — vote on repo issues' },
807
+ h('option', { value: 'webgpu', disabled: true }, 'Browser WebGPU Inference — Soon'),
808
+ h(
809
+ 'option',
810
+ { value: 'sherpa-onnx', disabled: true },
811
+ 'sherpa-onnx Streaming — Soon',
812
+ ),
813
+ h('option', { value: 'parakeet', disabled: true }, 'NVIDIA Parakeet — Soon'),
814
+ h('option', { value: 'voxtral', disabled: true }, 'Voxtral Realtime — Soon'),
815
+ ),
716
816
  ),
717
817
  ),
818
+ h(
819
+ 'small',
820
+ { className: 'dlv-recognition-engine-description' },
821
+ settings.recognitionEngine === 'qwen-http'
822
+ ? 'HTTP API at the configured base URL (default: http://127.0.0.1:8080/). Compatible with POST /v1/audio/transcriptions.'
823
+ : settings.recognitionEngine === 'whisper-http'
824
+ ? 'HTTP API at the configured DSH host URL (default: http://127.0.0.1:8080/inference). Audio uses the authenticated DSH host transcription route.'
825
+ : 'Uses the browser SpeechRecognition API. This is the default option.',
826
+ ),
718
827
  field('Input device', 'inputDeviceId', deviceOptions('audioinput', 'Microphone')),
719
828
  settings.recognitionEngine === 'browser'
720
829
  ? h(
@@ -0,0 +1,117 @@
1
+ declare const __DLV_VERSION__: string;
2
+ declare const __DLV_TESTED_DSH_VERSION__: string;
3
+
4
+ // Both values are injected from package.json by scripts/build.ts.
5
+ export const CURRENT_VERSION = __DLV_VERSION__;
6
+ export const TESTED_DSH_VERSION = __DLV_TESTED_DSH_VERSION__;
7
+ export const REPOSITORY_URL = 'https://github.com/victorwads/dsh-live-voice';
8
+ export const RELEASES_URL = `${REPOSITORY_URL}/releases`;
9
+ export const LATEST_RELEASE_API_URL =
10
+ 'https://api.github.com/repos/victorwads/dsh-live-voice/releases/latest';
11
+ export const RELEASE_CHECK_STORAGE_KEY = 'dsh-live-voice.latest-release-check';
12
+ export const RELEASE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
13
+
14
+ export const LIVE_VOICE_BADGE_URL = 'https://cdn.simpleicons.org/npm/white';
15
+ export const DSH_BADGE_URL = 'https://cdn.simpleicons.org/deepseek/white';
16
+ export const TESTED_DSH_RELEASE_URL = `https://github.com/deepseek-ai/deepseek-harness/releases/tag/v${TESTED_DSH_VERSION}`;
17
+
18
+ function versionParts(version) {
19
+ const normalized = String(version || '')
20
+ .trim()
21
+ .replace(/^v/i, '')
22
+ .split('+', 1)[0];
23
+ const [core = '', prerelease = ''] = normalized.split('-', 2);
24
+ const numbers = core.split('.').map((part) => {
25
+ const match = part.match(/^\d+/);
26
+ return match ? Number(match[0]) : 0;
27
+ });
28
+ while (numbers.length < 3) numbers.push(0);
29
+ return { numbers, prerelease: prerelease ? prerelease.split('.') : [] };
30
+ }
31
+
32
+ function compareIdentifier(left, right) {
33
+ const leftNumber = /^\d+$/.test(left) ? Number(left) : null;
34
+ const rightNumber = /^\d+$/.test(right) ? Number(right) : null;
35
+ if (leftNumber !== null && rightNumber !== null) return Math.sign(leftNumber - rightNumber);
36
+ if (leftNumber !== null) return -1;
37
+ if (rightNumber !== null) return 1;
38
+ return left.localeCompare(right);
39
+ }
40
+
41
+ export function compareVersions(left, right) {
42
+ const a = versionParts(left);
43
+ const b = versionParts(right);
44
+ for (let index = 0; index < Math.max(a.numbers.length, b.numbers.length); index++) {
45
+ const difference = (a.numbers[index] || 0) - (b.numbers[index] || 0);
46
+ if (difference) return Math.sign(difference);
47
+ }
48
+ if (!a.prerelease.length && !b.prerelease.length) return 0;
49
+ if (!a.prerelease.length) return 1;
50
+ if (!b.prerelease.length) return -1;
51
+ for (let index = 0; index < Math.max(a.prerelease.length, b.prerelease.length); index++) {
52
+ if (a.prerelease[index] === undefined) return -1;
53
+ if (b.prerelease[index] === undefined) return 1;
54
+ const difference = compareIdentifier(a.prerelease[index], b.prerelease[index]);
55
+ if (difference) return difference;
56
+ }
57
+ return 0;
58
+ }
59
+
60
+ function readCached(storage, now) {
61
+ try {
62
+ const cached = JSON.parse(storage?.getItem(RELEASE_CHECK_STORAGE_KEY) || 'null');
63
+ if (
64
+ cached &&
65
+ Number.isFinite(cached.checkedAt) &&
66
+ now - cached.checkedAt >= 0 &&
67
+ now - cached.checkedAt < RELEASE_CHECK_INTERVAL_MS
68
+ )
69
+ return cached;
70
+ } catch {}
71
+ return null;
72
+ }
73
+
74
+ function writeCached(storage, value) {
75
+ try {
76
+ storage?.setItem(RELEASE_CHECK_STORAGE_KEY, JSON.stringify(value));
77
+ } catch {}
78
+ }
79
+
80
+ export async function checkLatestRelease({
81
+ fetchImpl = globalThis.window?.fetch?.bind(globalThis.window),
82
+ storage = globalThis.window?.localStorage,
83
+ now = Date.now(),
84
+ } = {}) {
85
+ const cached = readCached(storage, now);
86
+ if (cached) return cached;
87
+
88
+ // Record the attempt before requesting so a failing public endpoint is not retried on every mount.
89
+ const attempted = { checkedAt: now, release: null };
90
+ writeCached(storage, attempted);
91
+ if (typeof fetchImpl !== 'function') return attempted;
92
+
93
+ try {
94
+ const response = await fetchImpl(LATEST_RELEASE_API_URL, {
95
+ headers: { Accept: 'application/vnd.github+json' },
96
+ });
97
+ if (!response.ok) return attempted;
98
+ const body = await response.json();
99
+ if (typeof body?.tag_name !== 'string' || !body.tag_name.trim()) return attempted;
100
+ const release = {
101
+ tag: body.tag_name.trim(),
102
+ url:
103
+ typeof body.html_url === 'string' && body.html_url.startsWith('https://github.com/')
104
+ ? body.html_url
105
+ : RELEASES_URL,
106
+ };
107
+ const result = { checkedAt: now, release };
108
+ writeCached(storage, result);
109
+ return result;
110
+ } catch {
111
+ return attempted;
112
+ }
113
+ }
114
+
115
+ export function hasNewerRelease(release, currentVersion = CURRENT_VERSION) {
116
+ return Boolean(release?.tag && compareVersions(release.tag, currentVersion) > 0);
117
+ }
@@ -29,12 +29,14 @@ export const styles = `
29
29
  .dlv-error{color:var(--dsw-alias-state-error-primary);overflow-wrap:anywhere;font-size:13px;max-width:720px;margin:8px auto}
30
30
  .dlv-settings{box-sizing:border-box;padding:18px;width:100%;display:grid;gap:16px;max-width:640px;color:var(--dsw-alias-label-primary)}
31
31
  .dlv-settings h3,.dlv-settings p{margin:0}
32
+ .dlv-settings-heading{display:flex;align-items:center;gap:8px;min-width:0}.dlv-settings-heading h3{white-space:nowrap}.dlv-heading-divider{height:1px;min-width:12px;flex:1;background:var(--dsw-alias-border-l1)}.dlv-settings-heading .dlv-star-badge{flex:none}
33
+ .dlv-version-badges{display:flex;flex:none;gap:5px;align-items:center}.dlv-shields-badge{display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;height:20px;gap:4px;padding:3px 5px;border:1px solid var(--dsw-alias-border-l1);border-radius:5px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);font-size:10px;font-weight:650;line-height:12px;text-decoration:none}.dlv-shields-badge img{display:block;width:12px;height:12px;max-width:100%}.dlv-shields-badge:focus-visible{border-radius:4px;outline:2px solid var(--dsw-alias-label-primary);outline-offset:3px}.dlv-version-badge{display:inline-flex;align-items:center;min-height:24px;box-sizing:border-box;padding:3px 8px;border:1px solid var(--dsw-alias-border-l1);border-radius:999px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);font-size:11px;font-weight:650;line-height:16px;text-decoration:none}.dlv-version-badges a.dlv-version-badge:hover{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary)}.dlv-star-badge{padding:4px 10px;color:var(--dsw-alias-label-primary)}.dlv-update-icon{margin-right:4px;font-size:14px;line-height:1}.dlv-star-icon{margin-right:4px;color:#f59e0b;font-size:14px;line-height:1;text-shadow:0 0 8px rgba(245,158,11,.28)}.dlv-version-badges a.dlv-star-badge:hover{border-color:#f59e0b;background:rgba(245,158,11,.08)}.dlv-update-badge{border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}
32
34
  .dlv-settings-tabs{border-bottom:.5px solid var(--dsw-alias-border-l2);display:flex;align-items:flex-end;gap:22px;overflow-x:auto}.dlv-settings-tab{position:relative;flex:none;padding:7px 1px 9px;border:0;background:transparent;color:var(--dsw-alias-label-tertiary);font:inherit;font-size:13px;line-height:20px;cursor:pointer}.dlv-settings-tab:hover,.dlv-settings-tab[data-active=true]{color:var(--dsw-alias-label-primary)}.dlv-settings-tab[data-active=true]:after,.dlv-settings-tab:focus-visible:after{content:"";position:absolute;right:0;bottom:-1px;left:0;height:2px;border-radius:2px 2px 0 0;background:var(--dsw-alias-label-primary)}.dlv-settings-tab:focus-visible{border-radius:2px;outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px;color:var(--dsw-alias-label-primary)}.dlv-settings-panel{min-width:0;padding-top:2px}.dlv-settings-panel[hidden]{display:none}.dlv-settings-group{margin:0;border:1px solid var(--dsw-alias-border-l1);border-radius:10px;min-width:0;display:grid;gap:14px;padding:16px}.dlv-settings-group legend{padding:0 6px;font-weight:600;color:var(--dsw-alias-label-primary)}.dlv-settings-card-body{display:grid;gap:12px;padding:12px 0}.dlv-settings-subcard{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;min-width:0}.dlv-settings-subcard>summary{cursor:pointer;font-weight:650;list-style:none;display:flex;align-items:center;justify-content:space-between;padding:10px}.dlv-settings-subcard>summary::-webkit-details-marker{display:none}.dlv-settings-subcard>summary:after{content:"›";transform:rotate(90deg);transition:transform .15s}.dlv-settings-subcard:not([open])>summary:after{transform:rotate(0)}.dlv-settings-subcard-body{display:grid;gap:10px;padding:0 10px 10px}
33
35
  .dlv-settings label{display:grid;gap:6px;font-size:14px}.dlv-settings label.dlv-check{display:flex;align-items:center;gap:8px}.dlv-settings label.dlv-check input{width:auto}
34
36
  .dlv-settings :is(input,select,textarea){box-sizing:border-box;width:100%;padding:8px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-1);color:inherit;font:inherit}
35
37
  .dlv-settings small,.dlv-settings p{font-size:13px;color:var(--dsw-alias-label-secondary);line-height:1.5}
36
38
  .dlv-preset-group{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px}.dlv-settings label.dlv-preset{display:flex;align-items:flex-start;gap:8px;padding:10px;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;cursor:pointer}.dlv-settings label.dlv-preset:has(input:checked){border-color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-1)}.dlv-settings label.dlv-preset input{width:auto;margin-top:3px}.dlv-preset span{display:grid;gap:3px}.dlv-vad-summary{font-weight:500}
37
39
  .dlv-settings-actions{display:flex;flex-wrap:wrap;gap:8px}.dlv-settings-actions button{padding:8px 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-1);color:inherit;cursor:pointer}.dlv-settings-actions button:disabled{opacity:.45;cursor:default}
38
- @media(max-width:480px){.dlv-preset-group{grid-template-columns:1fr}.dlv-pill{flex-wrap:wrap}.dlv-wave{flex-basis:90px}.dlv-status{flex-basis:100px}}
40
+ @media(max-width:480px){.dlv-settings-heading{gap:6px}.dlv-heading-divider{min-width:4px}.dlv-settings-heading .dlv-star-badge{padding:3px 6px;font-size:10px}.dlv-preset-group{grid-template-columns:1fr}.dlv-pill{flex-wrap:wrap}.dlv-wave{flex-basis:90px}.dlv-status{flex-basis:100px}}
39
41
  `;
40
42
  export default styles;