@goodandready/dsh-voice 0.8.36 → 0.8.37

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/lib/client.js CHANGED
@@ -2166,7 +2166,10 @@ window.__ModuleLoader__.load({
2166
2166
  // writes are dropped. The server does not share that restriction, and
2167
2167
  // dsh-lanmode rebuilds the same mechanics on the same calls. Prefer its
2168
2168
  // service when installed, otherwise the kernel one (fine on loopback).
2169
- const scope = ((ctx.get && ctx.get('lanSettings')) || ctx.configForms).get(NS)
2169
+ const settingsService = (ctx.get && ctx.get('lanSettings')) || ctx.configForms || ctx.settingsScope
2170
+ const scope = settingsService && typeof settingsService.get === 'function'
2171
+ ? settingsService.get(NS)
2172
+ : (settingsService && typeof settingsService.bind === 'function' ? settingsService.bind({ namespace: NS }) : null)
2170
2173
  const [snap, setSnap] = React.useState(null)
2171
2174
  const [draft, setDraft] = React.useState(null)
2172
2175
  const [saved, setSaved] = React.useState(false)
@@ -2190,11 +2193,11 @@ window.__ModuleLoader__.load({
2190
2193
 
2191
2194
  React.useEffect(() => {
2192
2195
  let alive = true
2193
- const render = () => { if (alive) setSnap(scope.getSnapshot()) }
2196
+ const render = () => { if (alive && scope && typeof scope.getSnapshot === 'function') setSnap(scope.getSnapshot()) }
2194
2197
  render()
2195
- const off = scope.subscribe(render)
2198
+ const off = (scope && typeof scope.subscribe === 'function') ? scope.subscribe(render) : () => {}
2196
2199
  return () => { alive = false; off() }
2197
- }, [])
2200
+ }, [scope])
2198
2201
 
2199
2202
  const [devices, setDevices] = React.useState([])
2200
2203
  React.useEffect(() => {
@@ -2214,7 +2217,13 @@ window.__ModuleLoader__.load({
2214
2217
  const timer = setInterval(() => {
2215
2218
  if (tries >= 15) { clearInterval(timer); return }
2216
2219
  tries += 1
2217
- try { ctx.configForms.describe().load() } catch (e) { /* service not up yet */ }
2220
+ try {
2221
+ if (ctx.configForms && typeof ctx.configForms.describe === 'function') {
2222
+ ctx.configForms.describe().load()
2223
+ } else if (ctx.settingsScope && typeof ctx.settingsScope.describe === 'function') {
2224
+ ctx.settingsScope.describe().load()
2225
+ }
2226
+ } catch (e) { /* service not up yet */ }
2218
2227
  }, 1000)
2219
2228
  return () => clearInterval(timer)
2220
2229
  }, [ready])
@@ -3125,7 +3134,7 @@ window.__ModuleLoader__.load({
3125
3134
  },
3126
3135
  PluginCard,
3127
3136
  ))
3128
- // Row seat and the legacy card stay as fallbacks.
3137
+ // Row seat (plugins.row.config): rendered in the plugin row in Plugins table.
3129
3138
  ctx.slots.inject('plugins.row.config', () => ctx.slots.register(
3130
3139
  {
3131
3140
  name: 'plugins.row.config',
@@ -3135,6 +3144,7 @@ window.__ModuleLoader__.load({
3135
3144
  },
3136
3145
  PluginCard,
3137
3146
  ))
3147
+ // Legacy seat fallback
3138
3148
  ctx.slots.inject('settings.plugin.item', () => ctx.slots.register(
3139
3149
  {
3140
3150
  name: 'settings.plugin.item',
@@ -0,0 +1,43 @@
1
+ // Decode modern volatile Config references while retaining stable identities for
2
+ // the request hot path. Legacy cores pass ordinary resolved configuration.
3
+ // If ctx is provided, invalidation listeners cache the unwrapped tree so the hot
4
+ // path never recursively walks volatile nodes on unchanged configuration.
5
+ export function createConfigReader(config, ctx) {
6
+ let previous
7
+ let dirty = true
8
+ if (ctx && typeof ctx.on === 'function') {
9
+ const markDirty = () => { dirty = true }
10
+ if (typeof ctx.effect === 'function') {
11
+ ctx.effect(() => ctx.on('settings/document-updated', markDirty))
12
+ ctx.effect(() => ctx.on('loader/volatile-update', markDirty))
13
+ ctx.effect(() => ctx.on('config', markDirty))
14
+ } else {
15
+ ctx.on('settings/document-updated', markDirty)
16
+ ctx.on('loader/volatile-update', markDirty)
17
+ ctx.on('config', markDirty)
18
+ }
19
+ }
20
+
21
+ const read = (value, held) => {
22
+ if (value && typeof value.get === 'function') value = value.get()
23
+ if (!value || typeof value !== 'object') return value
24
+ const keys = Object.keys(value)
25
+ const next = Array.isArray(value) ? [] : {}
26
+ let same = held && typeof held === 'object' && Array.isArray(value) === Array.isArray(held)
27
+ && keys.length === Object.keys(held).length
28
+ for (const key of keys) {
29
+ Object.defineProperty(next, key, { value: read(value[key], held?.[key]), writable: true, enumerable: true, configurable: true })
30
+ if (!held || !Object.hasOwn(held, key) || !Object.is(next[key], held[key])) same = false
31
+ }
32
+ return same ? held : next
33
+ }
34
+
35
+ return () => {
36
+ if (ctx && !dirty && previous !== undefined) {
37
+ return previous
38
+ }
39
+ previous = read(config, previous)
40
+ dirty = false
41
+ return previous
42
+ }
43
+ }
package/lib/index.js CHANGED
@@ -28,6 +28,7 @@ import { createUserMessage } from '@deepseek-ai/dsh-llm'
28
28
  import { buildProviderOrder, sessionCommand } from './transcribe-core.js'
29
29
  import { registerPluginUpdater } from './updater.js'
30
30
  import { registerSensevoiceInstaller } from './sensevoice-installer.js'
31
+ import { createConfigReader } from './config-compat.js'
31
32
 
32
33
  function isAutoLang(lang) {
33
34
  return !lang || lang === 'auto' || String(lang).includes(',')
@@ -65,7 +66,21 @@ const CustomProvider = z.object({
65
66
  + 'Empty means the built-in one.'),
66
67
  })
67
68
 
68
- export const Config = z.object({
69
+ function makeVolatile(schema) {
70
+ if (!schema || typeof schema.volatile !== 'function') return schema
71
+ if (schema.type === 'object' && schema.dict) {
72
+ const dict = {}
73
+ for (const [k, v] of Object.entries(schema.dict)) {
74
+ dict[k] = makeVolatile(v)
75
+ }
76
+ const res = z.object(dict)
77
+ if (schema.meta) Object.assign(res.meta, schema.meta)
78
+ return res
79
+ }
80
+ return schema.volatile()
81
+ }
82
+
83
+ const BaseConfig = z.object({
69
84
  dictation: z.object({
70
85
  chain: z.array(ChainEntry)
71
86
  .default([{ provider: 'deepgram', model: '' }, { provider: 'groq', model: '' }, { provider: 'local-whisper', model: '' }])
@@ -181,6 +196,10 @@ export const Config = z.object({
181
196
  .description('Target voice dictation and numeric choices to active DSH structured prompt modals.'),
182
197
  })
183
198
 
199
+ export const Config = typeof z.number().volatile === 'function'
200
+ ? makeVolatile(BaseConfig)
201
+ : BaseConfig
202
+
184
203
  const MIME_BY_EXT = {
185
204
  '.wav': 'audio/wav', '.mp3': 'audio/mpeg', '.m4a': 'audio/mp4', '.mp4': 'audio/mp4',
186
205
  '.ogg': 'audio/ogg', '.oga': 'audio/ogg', '.flac': 'audio/flac', '.webm': 'audio/webm', '.aac': 'audio/aac',
@@ -190,18 +209,22 @@ const MIME_BY_EXT = {
190
209
  export function apply(ctx, baseConfig) {
191
210
  let child = null
192
211
 
193
- // The settings card edits a namespace named after the plugin. Until the
194
- // host registers it via settings.register the snapshot is empty and
195
- // read-only. Reading through live() means edits apply to the next request
196
- // instead of requiring a process restart.
212
+ // The settings card edits a namespace named after the plugin.
213
+ // Using createConfigReader decodes volatile nodes on modern DSH cores
214
+ // while caching unwrapped values across requests.
215
+ const readEntryConfig = createConfigReader(baseConfig, ctx)
197
216
  let scope = null
198
- let getConfig = () => baseConfig
199
- const live = () => Config(structuredClone(getConfig() ?? {})) ?? baseConfig
217
+ let getConfig = readEntryConfig
218
+ const live = () => BaseConfig(structuredClone(getConfig() ?? {})) ?? readEntryConfig()
200
219
 
201
220
  ctx.inject(['settings'], (sctx) => {
202
- scope = sctx.settings.register(NS, Config, { base: baseConfig })
203
- getConfig = () => scope.get() ?? baseConfig
204
- sctx.effect(() => () => { getConfig = () => baseConfig; scope = null })
221
+ try {
222
+ scope = sctx.settings.register(NS, Config, { base: baseConfig })
223
+ if (scope && typeof scope.get === 'function') {
224
+ getConfig = () => scope.get() ?? readEntryConfig()
225
+ }
226
+ } catch { /* modern DSH handles settings via SettingsForms */ }
227
+ sctx.effect(() => () => { getConfig = readEntryConfig; scope = null })
205
228
  })
206
229
 
207
230
  async function resolveKey(ref) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-voice",
3
- "version": "0.8.36",
3
+ "version": "0.8.37",
4
4
  "description": "Voice input for DeepSeek Harness: dictation chunked by pauses and voice messages, each with its own provider fallback chain (Deepgram, Groq, HuggingFace, local whisper.cpp, plus any OpenAI-compatible API of your own).",
5
5
  "license": "MIT",
6
6
  "type": "module",