sqs-dsh-better-input 0.1.0

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.
Files changed (40) hide show
  1. package/LICENSE +27 -0
  2. package/README.md +218 -0
  3. package/assets/banner.png +0 -0
  4. package/cordis.patch.yml +6 -0
  5. package/lib/about.d.ts +65 -0
  6. package/lib/client/MicrophoneButton.d.ts +64 -0
  7. package/lib/client/OptimizeButton.d.ts +33 -0
  8. package/lib/client/VoiceRecognitionBar.d.ts +14 -0
  9. package/lib/client/index.d.ts +14 -0
  10. package/lib/client/settings-controller.d.ts +80 -0
  11. package/lib/client/settings.d.ts +15 -0
  12. package/lib/client/strings.d.ts +113 -0
  13. package/lib/client/templates-controller.d.ts +45 -0
  14. package/lib/client/templates-section.d.ts +17 -0
  15. package/lib/client/templates-source.d.ts +16 -0
  16. package/lib/client/voice-session.d.ts +35 -0
  17. package/lib/client/web-speech.d.ts +35 -0
  18. package/lib/client.d.ts +1 -0
  19. package/lib/client.js +2925 -0
  20. package/lib/client.js.map +1 -0
  21. package/lib/config.d.ts +100 -0
  22. package/lib/identity-CzaR1Taa.js +18 -0
  23. package/lib/identity-CzaR1Taa.js.map +1 -0
  24. package/lib/identity.d.ts +24 -0
  25. package/lib/index.d.ts +11 -0
  26. package/lib/index.js +920 -0
  27. package/lib/index.js.map +1 -0
  28. package/lib/polish/prompts.d.ts +37 -0
  29. package/lib/polish/service.d.ts +45 -0
  30. package/lib/remote-contract.d.ts +166 -0
  31. package/lib/remote.d.ts +56 -0
  32. package/lib/remote.js +226 -0
  33. package/lib/remote.js.map +1 -0
  34. package/lib/settings/store.d.ts +41 -0
  35. package/lib/templates/model.d.ts +30 -0
  36. package/lib/templates/store.d.ts +27 -0
  37. package/lib/typert.d.ts +107 -0
  38. package/lib/typert.js +468 -0
  39. package/lib/typert.js.map +1 -0
  40. package/package.json +116 -0
package/lib/client.js ADDED
@@ -0,0 +1,2925 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "sqs-dsh-better-input",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let react = require("react");
8
+ let react_jsx_runtime = require("react/jsx-runtime");
9
+ let react_dom = require("react-dom");
10
+ //#region src/identity.ts
11
+ /**
12
+ * Single source of truth for this package's identity.
13
+ *
14
+ * Every externally visible name (the Typert package key, the npm package name
15
+ * used by the update check, the settings namespace) derives from here so a
16
+ * rename never leaves a stale literal behind.
17
+ */
18
+ /** Stable identifier for this fork. Used as the Typert package key. */
19
+ const PACKAGE_NAME = "sqs-dsh-better-input";
20
+ //#endregion
21
+ //#region src/remote.ts
22
+ /**
23
+ * Build one strict codec for the CLIENT face.
24
+ *
25
+ * The client face exists only to describe the wire shape to
26
+ * `ctx.remote.$mount`; the actual byte validation happens on the Host. This is
27
+ * not an assumption — it is how dsh 0.1.7 is wired:
28
+ *
29
+ * - `@deepseek-ai/dsh-api-gateway/lib/index.js` (Host) is the ONLY place that
30
+ * runs `codec.create().parse(value)`. Validation lives in `decode()`.
31
+ * - `…/lib/client.js` contains ZERO `.create()` calls. Its `$mount` path only
32
+ * calls `requireStrictCodecs`, which inspects `codec.mode === 'strict'` and
33
+ * never reads `typeSymbol` or invokes the factory. `prepareInvocation()`
34
+ * forwards raw argument values straight to the RPC carrier.
35
+ *
36
+ * So materializing a real schema in the browser is pure dead weight: it pulls
37
+ * all of zod (~280 KB) into `lib/client.js` for a factory that is never called.
38
+ * The `create()` here is therefore lazy AND schema-free — it never imports zod.
39
+ *
40
+ * The eager `import { …Schema } from './remote-contract.js'` that used to sit
41
+ * at the top of this file was the single value-reachability edge that dragged
42
+ * zod into the browser bundle, because `src/client/index.ts` needs
43
+ * `TYPERT_REMOTE` as a value.
44
+ *
45
+ * The placeholder satisfies `TypertSchema` ({ parse(value) }) structurally, so
46
+ * the client face still typechecks. It is unreachable at runtime; if a future
47
+ * dsh ever validates on the client, `parse` throws loudly instead of silently
48
+ * accepting bad bytes. The Host manifest in `typert.ts` keeps the real zod
49
+ * schemas.
50
+ */
51
+ function codec(typeSymbol) {
52
+ return {
53
+ mode: "strict",
54
+ typeSymbol,
55
+ create: () => ({ parse(value) {
56
+ throw new Error(`sqs-dsh-better-input: client-face codec ${typeSymbol} was materialized. The client must never validate wire bytes (the Host does). If this fires, the client-face descriptor is being used for validation it should not own.`);
57
+ } })
58
+ };
59
+ }
60
+ /** Type symbol for one of this plugin's own wire types. */
61
+ const symbol = (name) => `${PACKAGE_NAME}#${name}`;
62
+ /** The client-face Typert manifest; mirrors the Host face in `typert.ts`. */
63
+ const TYPERT_REMOTE = {
64
+ package: PACKAGE_NAME,
65
+ descriptors: [
66
+ {
67
+ id: `${PACKAGE_NAME}#betterInput/getSettings`,
68
+ service: "BetterInputPolish",
69
+ namespace: "betterInput",
70
+ method: "getSettings",
71
+ invocation: { kind: "direct" },
72
+ parameters: [],
73
+ result: codec(symbol("BetterInputSettingsView"))
74
+ },
75
+ {
76
+ id: `${PACKAGE_NAME}#betterInput/updateSettings`,
77
+ service: "BetterInputPolish",
78
+ namespace: "betterInput",
79
+ method: "updateSettings",
80
+ invocation: { kind: "direct" },
81
+ parameters: [{
82
+ name: "patch",
83
+ wire: "patch",
84
+ source: "json",
85
+ codec: codec(symbol("BetterInputSettingsPatch"))
86
+ }],
87
+ cancellation: { parameter: "signal" },
88
+ result: codec(symbol("BetterInputSettingsView"))
89
+ },
90
+ {
91
+ id: `${PACKAGE_NAME}#betterInput/listRoutes`,
92
+ service: "BetterInputPolish",
93
+ namespace: "betterInput",
94
+ method: "listRoutes",
95
+ invocation: { kind: "direct" },
96
+ parameters: [],
97
+ result: codec(symbol("PolishRoute[]"))
98
+ },
99
+ {
100
+ id: `${PACKAGE_NAME}#betterInput/resolveModelEfforts`,
101
+ service: "BetterInputPolish",
102
+ namespace: "betterInput",
103
+ method: "resolveModelEfforts",
104
+ invocation: { kind: "direct" },
105
+ parameters: [{
106
+ name: "provider",
107
+ wire: "provider",
108
+ source: "json",
109
+ codec: codec("string")
110
+ }, {
111
+ name: "model",
112
+ wire: "model",
113
+ source: "json",
114
+ codec: codec("string")
115
+ }],
116
+ result: codec(symbol("ResolveModelEffortsResult"))
117
+ },
118
+ {
119
+ id: `${PACKAGE_NAME}#betterInput/getAbout`,
120
+ service: "BetterInputPolish",
121
+ namespace: "betterInput",
122
+ method: "getAbout",
123
+ invocation: { kind: "direct" },
124
+ parameters: [],
125
+ result: codec(symbol("AboutInfo"))
126
+ },
127
+ {
128
+ id: `${PACKAGE_NAME}#betterInput/checkForUpdate`,
129
+ service: "BetterInputPolish",
130
+ namespace: "betterInput",
131
+ method: "checkForUpdate",
132
+ invocation: { kind: "direct" },
133
+ parameters: [],
134
+ cancellation: { parameter: "signal" },
135
+ result: codec(symbol("UpdateCheckResult"))
136
+ },
137
+ {
138
+ id: `${PACKAGE_NAME}#betterInput/polish`,
139
+ service: "BetterInputPolish",
140
+ namespace: "betterInput",
141
+ method: "polish",
142
+ invocation: { kind: "direct" },
143
+ parameters: [
144
+ {
145
+ name: "transcript",
146
+ wire: "transcript",
147
+ source: "json",
148
+ codec: codec("string")
149
+ },
150
+ {
151
+ name: "provider",
152
+ wire: "provider",
153
+ source: "json",
154
+ codec: codec("string")
155
+ },
156
+ {
157
+ name: "model",
158
+ wire: "model",
159
+ source: "json",
160
+ codec: codec("string")
161
+ }
162
+ ],
163
+ cancellation: { parameter: "signal" },
164
+ result: codec("string")
165
+ },
166
+ {
167
+ id: `${PACKAGE_NAME}#betterInput/optimize`,
168
+ service: "BetterInputPolish",
169
+ namespace: "betterInput",
170
+ method: "optimize",
171
+ invocation: { kind: "direct" },
172
+ parameters: [
173
+ {
174
+ name: "text",
175
+ wire: "text",
176
+ source: "json",
177
+ codec: codec("string")
178
+ },
179
+ {
180
+ name: "provider",
181
+ wire: "provider",
182
+ source: "json",
183
+ codec: codec("string")
184
+ },
185
+ {
186
+ name: "model",
187
+ wire: "model",
188
+ source: "json",
189
+ codec: codec("string")
190
+ },
191
+ {
192
+ name: "context",
193
+ wire: "context",
194
+ source: "json",
195
+ codec: codec("string")
196
+ }
197
+ ],
198
+ cancellation: { parameter: "signal" },
199
+ result: codec("string")
200
+ },
201
+ {
202
+ id: `${PACKAGE_NAME}#betterInput/templatesList`,
203
+ service: "BetterInputPolish",
204
+ namespace: "betterInput",
205
+ method: "templatesList",
206
+ invocation: { kind: "direct" },
207
+ parameters: [],
208
+ result: codec(symbol("TemplateListResult"))
209
+ },
210
+ {
211
+ id: `${PACKAGE_NAME}#betterInput/templatesSave`,
212
+ service: "BetterInputPolish",
213
+ namespace: "betterInput",
214
+ method: "templatesSave",
215
+ invocation: { kind: "direct" },
216
+ parameters: [{
217
+ name: "template",
218
+ wire: "template",
219
+ source: "json",
220
+ codec: codec(symbol("TemplateInput"))
221
+ }],
222
+ cancellation: { parameter: "signal" },
223
+ result: codec(symbol("TemplateSaveResult"))
224
+ },
225
+ {
226
+ id: `${PACKAGE_NAME}#betterInput/templatesRemove`,
227
+ service: "BetterInputPolish",
228
+ namespace: "betterInput",
229
+ method: "templatesRemove",
230
+ invocation: { kind: "direct" },
231
+ parameters: [{
232
+ name: "id",
233
+ wire: "id",
234
+ source: "json",
235
+ codec: codec("string")
236
+ }],
237
+ cancellation: { parameter: "signal" },
238
+ result: codec(symbol("TemplateRemoveResult"))
239
+ }
240
+ ]
241
+ };
242
+ //#endregion
243
+ //#region src/client/strings.ts
244
+ const zh = {
245
+ voiceStart: "语音输入",
246
+ voiceStop: "停止语音输入",
247
+ voiceBusy: "正在处理…",
248
+ voiceUnavailable: "此浏览器不支持语音识别",
249
+ listening: "正在聆听…",
250
+ transcribing: "正在转写…",
251
+ polishing: "正在润色…",
252
+ voiceFailed: "语音输入失败",
253
+ polishNotConfigured: "未配置润色模型,请在设置页选择",
254
+ polishFailedKeepOriginal: "润色失败,已保留原文",
255
+ settingsTitle: "BetterInput 设置",
256
+ settingsDescription: "配置语音识别与 AI 润色。润色复用你在 dsh 设置里已配置的模型,无需额外 API key。",
257
+ loading: "加载中…",
258
+ saveFailed: "保存失败,请重试",
259
+ languageLabel: "识别语言",
260
+ languageHint: "留空时跟随浏览器语言。",
261
+ languagePlaceholder: "例如 zh-CN 或 en-US",
262
+ recordingLimitLabel: "单次录音上限(秒)",
263
+ recordingLimitHint: "1–600 秒。",
264
+ polishLabel: "AI 润色",
265
+ polishHint: "识别完成后用大模型清理转写文本(去口头禅、修正同音错字、加标点)。",
266
+ on: "开",
267
+ off: "关",
268
+ polishModelLabel: "润色模型",
269
+ polishModelHint: "选择 dsh 中已配置的模型路由。",
270
+ polishModelNone: "(未选择)",
271
+ polishEffortLabel: "润色思考强度",
272
+ polishEffortHint: "控制大模型的推理深度。默认即适配器最低档,适合大多数场景。",
273
+ polishPromptLabel: "自定义润色提示词",
274
+ polishPromptHint: "留空使用内置提示词。自定义提示词总是追加输出契约保护。",
275
+ polishPromptPlaceholder: "可选:粘贴自定义提示词…",
276
+ showDefaultPrompt: "查看内置提示词",
277
+ hideDefaultPrompt: "收起内置提示词",
278
+ defaultPromptLabel: "内置提示词",
279
+ effortDefaultLabel: "默认(关闭思考)",
280
+ effortLoadingLabel: "加载思考强度选项…",
281
+ routesStatus: "可用模型路由",
282
+ routesUnavailable: "不可用",
283
+ optimizeButton: "优化提示词",
284
+ optimizeBusy: "优化中…",
285
+ optimizeFailed: "优化失败,请重试",
286
+ optimizeEmpty: "输入框为空,无需优化",
287
+ optimizePanelTitle: "提示词优化结果",
288
+ optimizeOriginalLabel: "原文",
289
+ optimizeOptimizedLabel: "优化后",
290
+ optimizeAdopt: "采纳",
291
+ optimizeCancel: "取消",
292
+ optimizeNotConfigured: "未配置优化模型,请在设置页开启",
293
+ optimizeSectionLabel: "提示词优化",
294
+ optimizeModelLabel: "优化模型",
295
+ optimizeModelHint: "选择 dsh 中已配置的模型路由。",
296
+ optimizeEffortLabel: "优化思考强度",
297
+ optimizeEffortHint: "控制大模型的推理深度。默认即适配器最低档,适合大多数场景。",
298
+ optimizePromptLabel: "自定义优化提示词",
299
+ optimizePromptHint: "留空使用内置提示词。自定义提示词总是追加输出契约保护。",
300
+ optimizePromptPlaceholder: "可选:粘贴自定义提示词…",
301
+ contextTurnsLabel: "上下文引用轮数",
302
+ contextTurnsHint: "优化时引用最近 N 轮对话作为上下文,0 为禁用。默认 3 轮。",
303
+ aboutTitle: "关于与更新",
304
+ aboutVersionLabel: "当前版本",
305
+ aboutRepositoryLabel: "项目地址",
306
+ aboutChangelogLabel: "更新日志",
307
+ aboutLicenseLabel: "许可证",
308
+ checkUpdateButton: "检查更新",
309
+ checkingUpdate: "检查中…",
310
+ updateUpToDate: "当前已是最新版本。",
311
+ updateAvailable: "发现新版本",
312
+ updateUnpublished: "该版本未在 npm 上公开发布。",
313
+ updateCheckFailed: "检查更新失败",
314
+ updateCommandLabel: "已全局安装 dsh CLI,执行",
315
+ updateCommandNpxLabel: "未全局安装,改用 npx 执行",
316
+ updateCommandPick: "按你的安装方式二选一即可",
317
+ voiceSectionLabel: "语音识别",
318
+ polishSectionLabel: "提示词润色",
319
+ templatesTitle: "提示词模板",
320
+ templatesDescription: "把常用提示词存成模板,在输入框键入 / 即可搜索并插入。",
321
+ templatesNew: "新建模板",
322
+ templatesEdit: "编辑",
323
+ templatesSave: "保存",
324
+ templatesCancel: "取消",
325
+ templatesDelete: "删除",
326
+ templatesDeleteConfirm: "再次点击确认删除",
327
+ templatesEmpty: "还没有模板,点击「新建模板」创建第一个。",
328
+ templatesLoadFailed: "模板加载失败",
329
+ templatesRetry: "重试",
330
+ templatesActionFailed: "操作失败,请重试",
331
+ templatesNameLabel: "名称",
332
+ templatesNamePlaceholder: "例如:代码评审助手",
333
+ templatesDescriptionLabel: "描述",
334
+ templatesDescriptionPlaceholder: "可选:这个模板用来做什么",
335
+ templatesContentLabel: "内容",
336
+ templatesContentPlaceholder: "插入输入框的提示词正文…",
337
+ templatesTagsLabel: "标签",
338
+ templatesTagsHint: "逗号分隔,最多 8 个,用于菜单搜索。"
339
+ };
340
+ const en = {
341
+ voiceStart: "Voice input",
342
+ voiceStop: "Stop voice input",
343
+ voiceBusy: "Processing…",
344
+ voiceUnavailable: "Speech recognition is not supported in this browser",
345
+ listening: "Listening…",
346
+ transcribing: "Transcribing…",
347
+ polishing: "Polishing…",
348
+ voiceFailed: "Voice input failed",
349
+ polishNotConfigured: "No polish model configured, please choose one in Settings",
350
+ polishFailedKeepOriginal: "Polishing failed, original kept",
351
+ settingsTitle: "BetterInput Settings",
352
+ settingsDescription: "Configure voice recognition and AI polishing. Polishing reuses the models already configured in dsh — no extra API key needed.",
353
+ loading: "Loading…",
354
+ saveFailed: "Failed to save, please retry",
355
+ languageLabel: "Recognition language",
356
+ languageHint: "Empty follows the browser language.",
357
+ languagePlaceholder: "e.g. zh-CN or en-US",
358
+ recordingLimitLabel: "Recording limit (seconds)",
359
+ recordingLimitHint: "1–600 seconds.",
360
+ polishLabel: "AI polishing",
361
+ polishHint: "Clean the transcript with an LLM after recognition (fillers, homophone fixes, punctuation).",
362
+ on: "On",
363
+ off: "Off",
364
+ polishModelLabel: "Polish model",
365
+ polishModelHint: "Pick a model route already configured in dsh.",
366
+ polishModelNone: "(none)",
367
+ polishEffortLabel: "Polishing thinking effort",
368
+ polishEffortHint: "Controls the model inference depth. Default uses the adapter baseline (lightest tier).",
369
+ polishPromptLabel: "Custom polish prompt",
370
+ polishPromptHint: "Empty uses the built-in prompt. A custom prompt always keeps the output-contract guard.",
371
+ polishPromptPlaceholder: "Optional: paste a custom prompt…",
372
+ showDefaultPrompt: "Show the built-in prompt",
373
+ hideDefaultPrompt: "Hide the built-in prompt",
374
+ defaultPromptLabel: "Built-in prompt",
375
+ effortDefaultLabel: "Default (thinking off)",
376
+ effortLoadingLabel: "Loading reasoning effort options…",
377
+ routesStatus: "Available model routes",
378
+ routesUnavailable: "unavailable",
379
+ optimizeButton: "Optimize prompt",
380
+ optimizeBusy: "Optimizing…",
381
+ optimizeFailed: "Optimization failed, please retry",
382
+ optimizeEmpty: "Input is empty, nothing to optimize",
383
+ optimizePanelTitle: "Prompt optimization result",
384
+ optimizeOriginalLabel: "Original",
385
+ optimizeOptimizedLabel: "Optimized",
386
+ optimizeAdopt: "Adopt",
387
+ optimizeCancel: "Cancel",
388
+ optimizeNotConfigured: "No optimize model configured, enable it in Settings",
389
+ optimizeSectionLabel: "Prompt optimization",
390
+ optimizeModelLabel: "Optimize model",
391
+ optimizeModelHint: "Pick a model route already configured in dsh.",
392
+ optimizeEffortLabel: "Optimize thinking effort",
393
+ optimizeEffortHint: "Controls the model inference depth. Default uses the adapter baseline (lightest tier).",
394
+ optimizePromptLabel: "Custom optimize prompt",
395
+ optimizePromptHint: "Empty uses the built-in prompt. A custom prompt always keeps the output-contract guard.",
396
+ optimizePromptPlaceholder: "Optional: paste a custom prompt…",
397
+ contextTurnsLabel: "Context turns",
398
+ contextTurnsHint: "Include recent N turns as context for optimization. 0 = disabled. Default 3.",
399
+ aboutTitle: "About & Updates",
400
+ aboutVersionLabel: "Installed version",
401
+ aboutRepositoryLabel: "Repository",
402
+ aboutChangelogLabel: "Changelog",
403
+ aboutLicenseLabel: "License",
404
+ checkUpdateButton: "Check for updates",
405
+ checkingUpdate: "Checking…",
406
+ updateUpToDate: "You are up to date.",
407
+ updateAvailable: "A new version is available",
408
+ updateUnpublished: "This version is not published on npm.",
409
+ updateCheckFailed: "Update check failed",
410
+ updateCommandLabel: "With a global dsh CLI, run",
411
+ updateCommandNpxLabel: "Without a global dsh CLI, run via npx",
412
+ updateCommandPick: "Use either one depending on how you installed DSH",
413
+ voiceSectionLabel: "Voice Recognition",
414
+ polishSectionLabel: "Prompt Polishing",
415
+ templatesTitle: "Prompt Templates",
416
+ templatesDescription: "Save frequently used prompts as templates, then type / in the input box to search and insert.",
417
+ templatesNew: "New template",
418
+ templatesEdit: "Edit",
419
+ templatesSave: "Save",
420
+ templatesCancel: "Cancel",
421
+ templatesDelete: "Delete",
422
+ templatesDeleteConfirm: "Click again to confirm",
423
+ templatesEmpty: "No templates yet. Create your first one with \"New template\".",
424
+ templatesLoadFailed: "Failed to load templates",
425
+ templatesRetry: "Retry",
426
+ templatesActionFailed: "Action failed, please retry",
427
+ templatesNameLabel: "Name",
428
+ templatesNamePlaceholder: "e.g. Code review assistant",
429
+ templatesDescriptionLabel: "Description",
430
+ templatesDescriptionPlaceholder: "Optional: what this template is for",
431
+ templatesContentLabel: "Content",
432
+ templatesContentPlaceholder: "Prompt body inserted into the input box…",
433
+ templatesTagsLabel: "Tags",
434
+ templatesTagsHint: "Comma separated, up to 8, used for menu search."
435
+ };
436
+ /** Namespace owning every BetterInput surface string. Registered into the DSH
437
+ * locale runtime; slots declaring this namespace receive the typed `t`. */
438
+ const BETTER_INPUT_NS = "better-input";
439
+ /**
440
+ * Out-of-the-box defaults: every toggle ON so new users get the full
441
+ * experience immediately; reasoning effort left empty, which the Host
442
+ * translates to "thinking off" (the model's `off` tier when it exposes
443
+ * one, otherwise the adapter's own default).
444
+ * Provider/model stay empty and get auto-filled on first settings page
445
+ * load via SettingsController (first route returned by listRoutes).
446
+ */
447
+ const DEFAULT_SETTINGS = Object.freeze({
448
+ language: "",
449
+ maxRecordingSeconds: 120,
450
+ polishingEnabled: true,
451
+ polishProvider: "",
452
+ polishModel: "",
453
+ polishReasoningEffort: "",
454
+ polishPrompt: "",
455
+ optimizeEnabled: true,
456
+ optimizeProvider: "",
457
+ optimizeModel: "",
458
+ optimizeReasoningEffort: "",
459
+ optimizePrompt: "",
460
+ contextTurns: 3
461
+ });
462
+ /**
463
+ * Recognition language derived from the browser locale. An empty stored
464
+ * language follows the UI.
465
+ */
466
+ function recognitionLanguageFromBrowser() {
467
+ if (typeof navigator === "undefined") return "en-US";
468
+ return (navigator.language ?? "").toLowerCase().startsWith("zh") ? "zh-CN" : "en-US";
469
+ }
470
+ function effectiveRecognitionLanguage(stored) {
471
+ const value = stored.trim();
472
+ return value === "" ? recognitionLanguageFromBrowser() : value;
473
+ }
474
+ function isValidRecordingLimit(value) {
475
+ return Number.isSafeInteger(value) && value >= 1 && value <= 600;
476
+ }
477
+ /** Resolve the effective recording cap from stored settings. */
478
+ function effectiveRecordingSeconds(settings) {
479
+ return isValidRecordingLimit(settings.maxRecordingSeconds) ? settings.maxRecordingSeconds : 120;
480
+ }
481
+ //#endregion
482
+ //#region src/client/web-speech.ts
483
+ var WebSpeechSession = class {
484
+ recognition;
485
+ options;
486
+ active = false;
487
+ stopping = false;
488
+ finalText = "";
489
+ lastHeard = "";
490
+ ended = false;
491
+ silent = false;
492
+ constructor(options) {
493
+ const Recognition = getSpeechRecognitionConstructor();
494
+ if (Recognition === void 0) throw new Error("Speech recognition is unavailable in this browser");
495
+ this.options = options;
496
+ this.recognition = new Recognition();
497
+ this.recognition.lang = options.language;
498
+ this.recognition.continuous = true;
499
+ this.recognition.interimResults = true;
500
+ this.recognition.maxAlternatives = 1;
501
+ this.recognition.onstart = () => options.onStart?.();
502
+ this.recognition.onresult = (event) => this.handleResult(event);
503
+ this.recognition.onerror = (event) => this.handleError(event);
504
+ this.recognition.onend = () => this.handleEnd();
505
+ }
506
+ start() {
507
+ if (this.active || this.ended || this.silent) return;
508
+ this.active = true;
509
+ this.stopping = false;
510
+ try {
511
+ this.recognition.start();
512
+ } catch (error) {
513
+ this.fail(error instanceof Error ? error : new Error(String(error)));
514
+ }
515
+ }
516
+ stop() {
517
+ if (!this.active || this.ended || this.silent) return;
518
+ this.stopping = true;
519
+ this.active = false;
520
+ try {
521
+ this.recognition.stop();
522
+ } catch {
523
+ this.endOnce();
524
+ }
525
+ }
526
+ abort() {
527
+ if (this.ended || this.silent) return;
528
+ this.silent = true;
529
+ this.stopping = true;
530
+ this.active = false;
531
+ this.recognition.onstart = null;
532
+ this.recognition.onresult = null;
533
+ this.recognition.onerror = null;
534
+ this.recognition.onend = null;
535
+ try {
536
+ this.recognition.abort();
537
+ } catch {}
538
+ this.ended = true;
539
+ }
540
+ handleResult(event) {
541
+ if (this.silent || this.ended) return;
542
+ let finalChunk = "";
543
+ let interimChunk = "";
544
+ for (let index = event.resultIndex; index < event.results.length; index += 1) {
545
+ const result = event.results[index];
546
+ const transcript = result?.[0]?.transcript ?? "";
547
+ if (result?.isFinal === true) finalChunk = appendSpeech(finalChunk, transcript);
548
+ else interimChunk = appendSpeech(interimChunk, transcript);
549
+ }
550
+ if (finalChunk !== "") {
551
+ this.finalText = appendSpeech(this.finalText, finalChunk);
552
+ this.lastHeard = this.finalText;
553
+ this.options.onFinal(this.finalText);
554
+ }
555
+ const heard = appendSpeech(this.finalText, interimChunk);
556
+ if (heard !== "") this.lastHeard = heard;
557
+ this.options.onInterim(heard);
558
+ }
559
+ handleError(event) {
560
+ if (this.stopping || this.silent || this.ended) return;
561
+ const code = event.error ?? "unknown";
562
+ const detail = event.message === void 0 ? "" : `: ${event.message}`;
563
+ this.fail(/* @__PURE__ */ new Error(`Speech recognition failed (${code})${detail}`));
564
+ }
565
+ handleEnd() {
566
+ if (this.silent || this.ended) return;
567
+ if (this.active && !this.stopping) try {
568
+ this.recognition.start();
569
+ return;
570
+ } catch (error) {
571
+ this.fail(error instanceof Error ? error : new Error(String(error)));
572
+ return;
573
+ }
574
+ this.endOnce();
575
+ }
576
+ fail(error) {
577
+ if (this.stopping || this.silent || this.ended) return;
578
+ this.active = false;
579
+ this.stopping = true;
580
+ this.options.onError(error);
581
+ try {
582
+ this.recognition.abort();
583
+ } catch {}
584
+ this.endOnce();
585
+ }
586
+ endOnce() {
587
+ if (this.ended || this.silent) return;
588
+ this.ended = true;
589
+ this.options.onEnd(this.finalText !== "" ? this.finalText : this.lastHeard);
590
+ }
591
+ };
592
+ function appendSpeech(current, next) {
593
+ if (next === "") return current;
594
+ if (current === "") return next;
595
+ if (/\s$/.test(current) || /^\s/.test(next)) return current + next;
596
+ return `${current} ${next}`;
597
+ }
598
+ function getSpeechRecognitionConstructor() {
599
+ if (typeof window === "undefined") return void 0;
600
+ const speechWindow = window;
601
+ return speechWindow.SpeechRecognition ?? speechWindow.webkitSpeechRecognition;
602
+ }
603
+ //#endregion
604
+ //#region src/client/voice-session.ts
605
+ const NOTICE_STATES = /* @__PURE__ */ new Set(["error", "polish-error"]);
606
+ const VOICE_ERROR_DISMISS_MS = 2600;
607
+ /**
608
+ * Shared voice-input state for one session, written from scratch for
609
+ * sqs-dsh-better-input. The microphone button and the recognition bar both
610
+ * subscribe; the bar can request stop/cancel through the same instance.
611
+ */
612
+ var VoiceInputSession = class {
613
+ snapshot = {
614
+ state: "idle",
615
+ detail: ""
616
+ };
617
+ listeners = /* @__PURE__ */ new Set();
618
+ stopListeners = /* @__PURE__ */ new Set();
619
+ cancelListeners = /* @__PURE__ */ new Set();
620
+ epoch = 0;
621
+ errorTimer;
622
+ captureEpoch() {
623
+ return this.epoch;
624
+ }
625
+ isCurrentEpoch(epoch) {
626
+ return this.epoch === epoch;
627
+ }
628
+ getSnapshot = () => this.snapshot;
629
+ subscribe = (listener) => {
630
+ this.listeners.add(listener);
631
+ return () => this.listeners.delete(listener);
632
+ };
633
+ onStopRequested = (listener) => {
634
+ this.stopListeners.add(listener);
635
+ return () => this.stopListeners.delete(listener);
636
+ };
637
+ onCancelRequested = (listener) => {
638
+ this.cancelListeners.add(listener);
639
+ return () => this.cancelListeners.delete(listener);
640
+ };
641
+ setState(state, detail = "") {
642
+ const nextDetail = NOTICE_STATES.has(state) ? detail : "";
643
+ if (this.snapshot.state === state && this.snapshot.detail === nextDetail) return;
644
+ this.clearErrorTimer();
645
+ this.snapshot = {
646
+ state,
647
+ detail: nextDetail
648
+ };
649
+ this.emit();
650
+ if (NOTICE_STATES.has(state)) this.errorTimer = setTimeout(() => {
651
+ this.errorTimer = void 0;
652
+ if (NOTICE_STATES.has(this.snapshot.state)) this.setState("idle");
653
+ }, VOICE_ERROR_DISMISS_MS);
654
+ }
655
+ requestStop() {
656
+ for (const listener of this.stopListeners) listener();
657
+ }
658
+ requestCancel() {
659
+ if (this.snapshot.state !== "transcribing" && this.snapshot.state !== "polishing") return;
660
+ this.epoch += 1;
661
+ for (const listener of this.cancelListeners) listener();
662
+ this.setState("idle");
663
+ }
664
+ dispose() {
665
+ this.clearErrorTimer();
666
+ this.listeners.clear();
667
+ this.stopListeners.clear();
668
+ this.cancelListeners.clear();
669
+ }
670
+ clearErrorTimer() {
671
+ if (this.errorTimer === void 0) return;
672
+ clearTimeout(this.errorTimer);
673
+ this.errorTimer = void 0;
674
+ }
675
+ emit() {
676
+ for (const listener of this.listeners) listener();
677
+ }
678
+ };
679
+ function useVoiceInputSession(session) {
680
+ return (0, react.useSyncExternalStore)(session.subscribe, session.getSnapshot, session.getSnapshot);
681
+ }
682
+ //#endregion
683
+ //#region src/client/MicrophoneButton.tsx
684
+ /**
685
+ * The microphone button in the composer tool row. Click to start listening,
686
+ * click again to stop. Transcripts stream into the draft in real time; when
687
+ * polishing is enabled, the committed transcript is polished through the Host
688
+ * LLM route and replaces the draft (unless the user edited it meanwhile).
689
+ */
690
+ function MicrophoneButton({ useInput, inputActions, voiceSession, remote, useSettings, t }) {
691
+ const state = useVoiceInputSession(voiceSession).state;
692
+ const setState = (next, detail = "") => voiceSession.setState(next, detail);
693
+ const input = useInput((state) => state);
694
+ const speechRef = (0, react.useRef)(null);
695
+ const baseDraftRef = (0, react.useRef)("");
696
+ const mountedRef = (0, react.useRef)(true);
697
+ const stopRef = (0, react.useRef)(null);
698
+ const polishAbortRef = (0, react.useRef)(null);
699
+ const recordingTimerRef = (0, react.useRef)(null);
700
+ const settingsRef = (0, react.useRef)(null);
701
+ const settingsFace = useSettings();
702
+ if (settingsFace.status === "ready") settingsRef.current = settingsFace.settings;
703
+ const latestDraftRef = (0, react.useRef)(input.draft);
704
+ (0, react.useEffect)(() => {
705
+ latestDraftRef.current = input.draft;
706
+ }, [input.draft]);
707
+ (0, react.useEffect)(() => {
708
+ mountedRef.current = true;
709
+ const stop = voiceSession.onStopRequested(() => stopRef.current?.());
710
+ const cancel = voiceSession.onCancelRequested(() => {
711
+ speechRef.current?.abort();
712
+ polishAbortRef.current?.abort();
713
+ });
714
+ return () => {
715
+ mountedRef.current = false;
716
+ stop();
717
+ cancel();
718
+ speechRef.current?.abort();
719
+ speechRef.current = null;
720
+ polishAbortRef.current?.abort();
721
+ polishAbortRef.current = null;
722
+ clearRecordingTimer(recordingTimerRef);
723
+ voiceSession.setState("idle");
724
+ };
725
+ }, [voiceSession]);
726
+ const active = state === "starting" || state === "recording";
727
+ const busy = state === "transcribing" || state === "polishing";
728
+ const settings = settingsFace.status === "ready" ? settingsFace.settings : settingsRef.current;
729
+ const polishingEnabled = settings?.polishingEnabled ?? false;
730
+ const polishProvider = (settings?.polishProvider ?? "").trim();
731
+ const polishModel = (settings?.polishModel ?? "").trim();
732
+ const polishConfigured = polishingEnabled && polishProvider !== "" && polishModel !== "";
733
+ const startListening = () => {
734
+ if (active || busy) return;
735
+ const baseDraft = input.draft;
736
+ baseDraftRef.current = baseDraft;
737
+ let sessionDraft = baseDraft;
738
+ let failed = false;
739
+ setState("starting");
740
+ let session;
741
+ try {
742
+ session = new WebSpeechSession({
743
+ language: effectiveRecognitionLanguage(settingsRef.current?.language ?? ""),
744
+ onInterim: (text) => {
745
+ sessionDraft = updateDraft(baseDraft, text);
746
+ inputActions.setDraft(sessionDraft);
747
+ },
748
+ onFinal: (text) => {
749
+ sessionDraft = updateDraft(baseDraft, text);
750
+ inputActions.setDraft(sessionDraft);
751
+ },
752
+ onError: (error) => {
753
+ failed = true;
754
+ setState("error", error.message);
755
+ },
756
+ onEnd: (text) => {
757
+ speechRef.current = null;
758
+ clearRecordingTimer(recordingTimerRef);
759
+ if (!mountedRef.current) return;
760
+ const transcript = text.trim();
761
+ if (failed) return;
762
+ if (transcript === "") {
763
+ setState("idle");
764
+ return;
765
+ }
766
+ const draftAtStop = updateDraft(baseDraft, transcript);
767
+ inputActions.setDraft(draftAtStop);
768
+ latestDraftRef.current = draftAtStop;
769
+ const settings = settingsRef.current;
770
+ if (settings !== null && settings.polishingEnabled && settings.polishProvider.trim() !== "" && settings.polishModel.trim() !== "") polishDraft({
771
+ transcript,
772
+ baseDraft,
773
+ draftAtStop,
774
+ provider: settings.polishProvider,
775
+ model: settings.polishModel,
776
+ remote,
777
+ setState,
778
+ latestDraftRef,
779
+ actionsRef: { current: inputActions },
780
+ polishAbortRef
781
+ });
782
+ else setState("idle");
783
+ }
784
+ });
785
+ } catch (error) {
786
+ if (mountedRef.current) setState("error", error instanceof Error ? error.message : "Speech recognition is unavailable in this browser");
787
+ return;
788
+ }
789
+ speechRef.current = session;
790
+ session.start();
791
+ if (failed) return;
792
+ setState("recording");
793
+ armRecordingTimer(recordingTimerRef, effectiveRecordingSeconds(settingsRef.current ?? { maxRecordingSeconds: 120 }), () => {
794
+ speechRef.current?.stop();
795
+ });
796
+ };
797
+ const stopListening = () => {
798
+ clearRecordingTimer(recordingTimerRef);
799
+ if (!active) return;
800
+ speechRef.current?.stop();
801
+ };
802
+ stopRef.current = stopListening;
803
+ const tooltip = busy ? t("voiceBusy") : active ? t("voiceStop") : state === "polish-error" ? t("polishFailedKeepOriginal") : state === "error" ? t("voiceFailed") : !polishConfigured ? `${t("voiceStart")} — ${t("polishNotConfigured")}` : t("voiceStart");
804
+ const label = busy ? "…" : active ? t("voiceStop") : t("voiceStart");
805
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
806
+ type: "button",
807
+ "aria-label": label,
808
+ "aria-pressed": active,
809
+ disabled: busy,
810
+ title: tooltip,
811
+ onClick: active ? stopListening : startListening,
812
+ "data-better-input-state": state,
813
+ "data-better-input-polish-configured": polishConfigured ? "yes" : "no",
814
+ style: buttonStyle$2(active, busy, !polishConfigured),
815
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MicrophoneIcon, {})
816
+ });
817
+ }
818
+ async function polishDraft(options) {
819
+ if (options.provider.trim() === "" || options.model.trim() === "") return;
820
+ const controller = new AbortController();
821
+ options.polishAbortRef.current = controller;
822
+ options.setState("polishing");
823
+ try {
824
+ const result = await options.remote.polish(options.transcript, options.provider, options.model, controller.signal);
825
+ if (controller.signal.aborted) return;
826
+ if (!shouldApplyPolishResult(options.latestDraftRef.current, options.draftAtStop, options.baseDraft)) {
827
+ options.setState("idle");
828
+ return;
829
+ }
830
+ if (!result.ok) {
831
+ options.setState("polish-error", result.error.message);
832
+ return;
833
+ }
834
+ const text = result.value.trim() !== "" ? result.value.trim() : options.transcript;
835
+ const nextDraft = updateDraft(options.baseDraft, text);
836
+ options.latestDraftRef.current = nextDraft;
837
+ options.actionsRef.current.setDraft(nextDraft);
838
+ options.setState("idle");
839
+ } catch (error) {
840
+ if (controller.signal.aborted) return;
841
+ options.setState("polish-error", error instanceof Error ? error.message : "Polishing failed");
842
+ } finally {
843
+ if (options.polishAbortRef.current === controller) options.polishAbortRef.current = null;
844
+ }
845
+ }
846
+ /**
847
+ * Only replace the draft when the user has not edited it since the transcript
848
+ * landed. Both the transcript-at-stop and the base draft count as unchanged
849
+ * (the user may have reverted the interim edits).
850
+ */
851
+ function shouldApplyPolishResult(currentDraft, draftAtStop, baseDraft) {
852
+ const current = collapseDraft(currentDraft);
853
+ return current === collapseDraft(draftAtStop) || current === collapseDraft(baseDraft);
854
+ }
855
+ function collapseDraft(text) {
856
+ return text.replace(/\s+/g, " ").trim();
857
+ }
858
+ function buttonStyle$2(active, busy, polishUnconfigured) {
859
+ return {
860
+ display: "inline-flex",
861
+ alignItems: "center",
862
+ justifyContent: "center",
863
+ width: 28,
864
+ height: 28,
865
+ padding: 0,
866
+ border: "none",
867
+ borderRadius: 6,
868
+ background: active ? "var(--dsw-alias-state-business-primary, #4f8cff)" : "transparent",
869
+ color: active ? "#fff" : "var(--dsw-alias-label-primary, inherit)",
870
+ cursor: busy ? "default" : "pointer",
871
+ opacity: busy ? .5 : polishUnconfigured ? .75 : 1,
872
+ flex: "none"
873
+ };
874
+ }
875
+ function MicrophoneIcon() {
876
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
877
+ "aria-hidden": "true",
878
+ fill: "none",
879
+ height: "16",
880
+ viewBox: "0 0 16 16",
881
+ width: "16",
882
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
883
+ d: "M8 1.75a2.25 2.25 0 0 0-2.25 2.25v3.5a2.25 2.25 0 0 0 4.5 0V4A2.25 2.25 0 0 0 8 1.75Z",
884
+ stroke: "currentColor",
885
+ strokeLinejoin: "round",
886
+ strokeWidth: "1.4"
887
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
888
+ d: "M3.5 7.5a4.5 4.5 0 0 0 9 0M8 12.25V14M5.5 14h5",
889
+ stroke: "currentColor",
890
+ strokeLinecap: "round",
891
+ strokeLinejoin: "round",
892
+ strokeWidth: "1.4"
893
+ })]
894
+ });
895
+ }
896
+ /** Append transcript to a base draft with one space separator. */
897
+ function updateDraft(baseDraft, transcript) {
898
+ const text = transcript.trim();
899
+ if (text === "") return baseDraft;
900
+ if (baseDraft === "") return text;
901
+ if (/\s$/.test(baseDraft) || /^\s/.test(text)) return baseDraft + text;
902
+ return `${baseDraft} ${text}`;
903
+ }
904
+ function armRecordingTimer(timerRef, seconds, stop) {
905
+ clearRecordingTimer(timerRef);
906
+ timerRef.current = setTimeout(stop, Math.max(1, seconds) * 1e3);
907
+ }
908
+ function clearRecordingTimer(timerRef) {
909
+ if (timerRef.current === null) return;
910
+ clearTimeout(timerRef.current);
911
+ timerRef.current = null;
912
+ }
913
+ //#endregion
914
+ //#region src/client/OptimizeButton.tsx
915
+ /**
916
+ * The ✨ optimize button rendered above the composer card (in
917
+ * `conversation.input.dock`), right-aligned. Click reads the current draft,
918
+ * calls the Host LLM to optimize it, then shows a confirmation panel with
919
+ * the original and optimized text. The draft is replaced only when the user
920
+ * clicks "Adopt".
921
+ */
922
+ function OptimizeButton({ useChat, useInput, inputActions, remote, useSettings, t }) {
923
+ const [state, setState] = (0, react.useState)({ kind: "idle" });
924
+ const settingsFace = useSettings();
925
+ const abortRef = (0, react.useRef)(null);
926
+ const input = useInput((state) => state);
927
+ const chat = useChat((area) => area);
928
+ const draftRef = (0, react.useRef)(input.draft);
929
+ (0, react.useEffect)(() => {
930
+ draftRef.current = input.draft;
931
+ }, [input.draft]);
932
+ (0, react.useEffect)(() => {
933
+ return () => {
934
+ abortRef.current?.abort();
935
+ };
936
+ }, []);
937
+ const settings = settingsFace.status === "ready" ? settingsFace.settings : null;
938
+ const provider = settings?.optimizeProvider.trim() ?? "";
939
+ const model = settings?.optimizeModel.trim() ?? "";
940
+ const modelConfigured = provider !== "" && model !== "";
941
+ const handleClick = async () => {
942
+ if (state.kind === "optimizing") return;
943
+ const draft = draftRef.current.trim();
944
+ if (draft === "") {
945
+ setState({
946
+ kind: "error",
947
+ message: t("optimizeEmpty")
948
+ });
949
+ return;
950
+ }
951
+ if (!modelConfigured) {
952
+ setState({
953
+ kind: "error",
954
+ message: t("optimizeNotConfigured")
955
+ });
956
+ return;
957
+ }
958
+ abortRef.current?.abort();
959
+ const controller = new AbortController();
960
+ abortRef.current = controller;
961
+ setState({ kind: "optimizing" });
962
+ try {
963
+ const contextTurns = settings?.contextTurns ?? 3;
964
+ const context = contextTurns > 0 ? extractConversationContext(chat.legacy.nodes, contextTurns) : "";
965
+ const result = await remote.optimize(draft, provider, model, context, controller.signal);
966
+ if (controller.signal.aborted) return;
967
+ if (!result.ok) {
968
+ setState({
969
+ kind: "error",
970
+ message: result.error.message
971
+ });
972
+ return;
973
+ }
974
+ const optimized = result.value.trim();
975
+ if (optimized === "") {
976
+ setState({
977
+ kind: "error",
978
+ message: t("optimizeFailed")
979
+ });
980
+ return;
981
+ }
982
+ setState({
983
+ kind: "result",
984
+ original: draft,
985
+ optimized
986
+ });
987
+ } catch (error) {
988
+ if (controller.signal.aborted) return;
989
+ setState({
990
+ kind: "error",
991
+ message: error instanceof Error ? error.message : t("optimizeFailed")
992
+ });
993
+ } finally {
994
+ if (abortRef.current === controller) abortRef.current = null;
995
+ }
996
+ };
997
+ const handleAdopt = () => {
998
+ if (state.kind !== "result") return;
999
+ inputActions.setDraft(state.optimized);
1000
+ setState({ kind: "idle" });
1001
+ };
1002
+ const handleCancel = () => {
1003
+ setState({ kind: "idle" });
1004
+ };
1005
+ const busy = state.kind === "optimizing";
1006
+ const disabled = busy || !modelConfigured;
1007
+ const buttonTitle = modelConfigured ? t("optimizeButton") : t("optimizeNotConfigured");
1008
+ const errorToast = state.kind === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorToastPortal, {
1009
+ message: state.message,
1010
+ onDismiss: handleCancel
1011
+ }) : null;
1012
+ const confirmModal = state.kind === "result" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfirmModalPortal, {
1013
+ title: t("optimizePanelTitle"),
1014
+ originalLabel: t("optimizeOriginalLabel"),
1015
+ optimizedLabel: t("optimizeOptimizedLabel"),
1016
+ original: state.original,
1017
+ optimized: state.optimized,
1018
+ adoptLabel: t("optimizeAdopt"),
1019
+ cancelLabel: t("optimizeCancel"),
1020
+ onAdopt: handleAdopt,
1021
+ onCancel: handleCancel
1022
+ }) : null;
1023
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1024
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1025
+ style: containerStyle,
1026
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1027
+ type: "button",
1028
+ "aria-label": buttonTitle,
1029
+ title: buttonTitle,
1030
+ disabled,
1031
+ onClick: handleClick,
1032
+ style: buttonStyle$1(disabled),
1033
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SparkleIcon, {}), busy ? t("optimizeBusy") : ""]
1034
+ })
1035
+ }),
1036
+ errorToast,
1037
+ confirmModal
1038
+ ] });
1039
+ }
1040
+ /** Ctrl/Cmd+A inside a `<pre>` selects only that block's text. */
1041
+ function selectAllInPre(e) {
1042
+ if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "a") {
1043
+ e.preventDefault();
1044
+ const range = document.createRange();
1045
+ range.selectNodeContents(e.currentTarget);
1046
+ const selection = window.getSelection();
1047
+ if (selection) {
1048
+ selection.removeAllRanges();
1049
+ selection.addRange(range);
1050
+ }
1051
+ }
1052
+ }
1053
+ function OptimizeConfirmPanel(props) {
1054
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1055
+ style: modalContentStyle,
1056
+ children: [
1057
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1058
+ style: panelTitleStyle,
1059
+ children: props.title
1060
+ }),
1061
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1062
+ style: compareRowStyle,
1063
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1064
+ style: compareColStyle,
1065
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1066
+ style: compareLabelStyle,
1067
+ children: props.originalLabel
1068
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
1069
+ style: preStyle,
1070
+ tabIndex: 0,
1071
+ onKeyDown: selectAllInPre,
1072
+ children: props.original
1073
+ })]
1074
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1075
+ style: compareColStyle,
1076
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1077
+ style: compareLabelStyle,
1078
+ children: props.optimizedLabel
1079
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
1080
+ style: {
1081
+ ...preStyle,
1082
+ borderColor: "var(--dsw-alias-state-business-primary, #4f8cff)"
1083
+ },
1084
+ tabIndex: 0,
1085
+ onKeyDown: selectAllInPre,
1086
+ children: props.optimized
1087
+ })]
1088
+ })]
1089
+ }),
1090
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1091
+ style: panelActionsStyle,
1092
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1093
+ type: "button",
1094
+ style: cancelBtnStyle,
1095
+ onClick: props.onCancel,
1096
+ children: props.cancelLabel
1097
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1098
+ type: "button",
1099
+ style: adoptBtnStyle,
1100
+ onClick: props.onAdopt,
1101
+ children: props.adoptLabel
1102
+ })]
1103
+ })
1104
+ ]
1105
+ });
1106
+ }
1107
+ /**
1108
+ * Comparison modal portalled to `document.body`. DSH renders slot content
1109
+ * inside shadow roots whose ancestors may clip or contain fixed-position
1110
+ * children, so the overlay must escape the component tree entirely.
1111
+ */
1112
+ function ConfirmModalPortal(props) {
1113
+ return (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1114
+ style: modalOverlayStyle,
1115
+ onClick: (e) => {
1116
+ const selection = window.getSelection();
1117
+ if (selection && !selection.isCollapsed) return;
1118
+ props.onCancel();
1119
+ },
1120
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1121
+ onClick: (e) => e.stopPropagation(),
1122
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OptimizeConfirmPanel, { ...props })
1123
+ })
1124
+ }), document.body);
1125
+ }
1126
+ /**
1127
+ * Small error modal, also portalled to `document.body` for the same
1128
+ * stacking/escaping reasons as the comparison modal.
1129
+ */
1130
+ function ErrorToastPortal({ message, onDismiss }) {
1131
+ return (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1132
+ style: modalOverlayStyle,
1133
+ onClick: (e) => {
1134
+ const selection = window.getSelection();
1135
+ if (selection && !selection.isCollapsed) return;
1136
+ onDismiss();
1137
+ },
1138
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1139
+ style: errorModalStyle,
1140
+ onClick: (e) => e.stopPropagation(),
1141
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1142
+ style: { flex: 1 },
1143
+ children: message
1144
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1145
+ type: "button",
1146
+ style: errorDismissStyle,
1147
+ onClick: onDismiss,
1148
+ children: "×"
1149
+ })]
1150
+ })
1151
+ }), document.body);
1152
+ }
1153
+ function SparkleIcon() {
1154
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
1155
+ "aria-hidden": "true",
1156
+ fill: "none",
1157
+ height: "14",
1158
+ viewBox: "0 0 16 16",
1159
+ width: "14",
1160
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
1161
+ d: "M8 1.5l1.3 3.7L13 6.5l-3.7 1.3L8 11.5 6.7 7.8 3 6.5l3.7-1.3L8 1.5z",
1162
+ fill: "currentColor"
1163
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
1164
+ d: "M12.5 10.5l.6 1.7 1.7.6-1.7.6-.6 1.7-.6-1.7-1.7-.6 1.7-.6.6-1.7z",
1165
+ fill: "currentColor",
1166
+ opacity: "0.6"
1167
+ })]
1168
+ });
1169
+ }
1170
+ /**
1171
+ * Extract recent conversation context from the message array. Returns a
1172
+ * plain-text summary of the last N user/assistant turns, formatted as:
1173
+ *
1174
+ * User: <message text>
1175
+ * Assistant: <response text (truncated at 500 chars)>
1176
+ *
1177
+ * Returns an empty string when there are no user/assistant nodes.
1178
+ */
1179
+ function extractConversationContext(nodes, maxTurns) {
1180
+ const relevant = [];
1181
+ for (const node of nodes) if (node.kind === "user") {
1182
+ const text = node.content.filter((c) => c.type === "text").map((c) => c.text).join("");
1183
+ if (text.trim()) relevant.push({
1184
+ role: "user",
1185
+ text: text.trim()
1186
+ });
1187
+ } else if (node.kind === "assistant") {
1188
+ const text = node.blocks.filter((b) => b.kind === "text").map((b) => b.text).join("");
1189
+ if (text.trim()) relevant.push({
1190
+ role: "assistant",
1191
+ text: text.trim().slice(0, 500)
1192
+ });
1193
+ }
1194
+ const recent = relevant.slice(-maxTurns * 2);
1195
+ if (recent.length === 0) return "";
1196
+ return recent.map((entry) => entry.role === "user" ? `User: ${entry.text}` : `Assistant: ${entry.text}`).join("\n");
1197
+ }
1198
+ const containerStyle = {
1199
+ display: "inline-flex",
1200
+ alignItems: "center",
1201
+ height: 26,
1202
+ flex: "none"
1203
+ };
1204
+ const buttonStyle$1 = (disabled) => ({
1205
+ display: "inline-flex",
1206
+ alignItems: "center",
1207
+ gap: 4,
1208
+ height: 26,
1209
+ padding: "0 8px",
1210
+ border: "none",
1211
+ borderRadius: 6,
1212
+ background: "transparent",
1213
+ color: "var(--dsw-alias-label-secondary, inherit)",
1214
+ cursor: disabled ? "not-allowed" : "pointer",
1215
+ opacity: disabled ? .5 : 1,
1216
+ fontSize: 12,
1217
+ flex: "none"
1218
+ });
1219
+ const modalOverlayStyle = {
1220
+ position: "fixed",
1221
+ inset: 0,
1222
+ background: "rgba(0,0,0,0.35)",
1223
+ display: "flex",
1224
+ alignItems: "center",
1225
+ justifyContent: "center",
1226
+ zIndex: 99998
1227
+ };
1228
+ const modalContentStyle = {
1229
+ padding: 14,
1230
+ borderRadius: 10,
1231
+ background: "var(--dsw-alias-bg-layer-2, #fff)",
1232
+ border: "1px solid var(--dsw-alias-border-l2, #e0e0e0)",
1233
+ boxShadow: "0 8px 32px rgba(0,0,0,0.2)",
1234
+ width: "min(640px, 92vw)",
1235
+ zIndex: 99999
1236
+ };
1237
+ const panelTitleStyle = {
1238
+ fontSize: 14,
1239
+ fontWeight: 600,
1240
+ marginBottom: 10,
1241
+ color: "var(--dsw-alias-label-primary, inherit)"
1242
+ };
1243
+ const compareRowStyle = {
1244
+ display: "flex",
1245
+ gap: 10,
1246
+ maxHeight: 300
1247
+ };
1248
+ const compareColStyle = {
1249
+ flex: 1,
1250
+ display: "flex",
1251
+ flexDirection: "column",
1252
+ minWidth: 0
1253
+ };
1254
+ const compareLabelStyle = {
1255
+ fontSize: 11,
1256
+ color: "var(--dsw-alias-label-secondary, #888)",
1257
+ marginBottom: 4
1258
+ };
1259
+ const preStyle = {
1260
+ margin: 0,
1261
+ padding: 10,
1262
+ fontSize: 12,
1263
+ lineHeight: 1.6,
1264
+ whiteSpace: "pre-wrap",
1265
+ wordBreak: "break-word",
1266
+ overflow: "auto",
1267
+ maxHeight: 240,
1268
+ borderRadius: 6,
1269
+ border: "1px solid var(--dsw-alias-border-l2, #e8e8e8)",
1270
+ background: "var(--dsw-alias-bg-layer-1, #f9f9f9)",
1271
+ color: "var(--dsw-alias-label-primary, inherit)",
1272
+ fontFamily: "inherit"
1273
+ };
1274
+ const panelActionsStyle = {
1275
+ display: "flex",
1276
+ justifyContent: "flex-end",
1277
+ gap: 8,
1278
+ marginTop: 12
1279
+ };
1280
+ const adoptBtnStyle = {
1281
+ padding: "6px 16px",
1282
+ border: "none",
1283
+ borderRadius: 6,
1284
+ background: "var(--dsw-alias-state-business-primary, #4f8cff)",
1285
+ color: "#fff",
1286
+ cursor: "pointer",
1287
+ fontSize: 13
1288
+ };
1289
+ const cancelBtnStyle = {
1290
+ padding: "6px 16px",
1291
+ border: "1px solid var(--dsw-alias-border-l2, #ddd)",
1292
+ borderRadius: 6,
1293
+ background: "transparent",
1294
+ color: "var(--dsw-alias-label-primary, inherit)",
1295
+ cursor: "pointer",
1296
+ fontSize: 13
1297
+ };
1298
+ const errorModalStyle = {
1299
+ padding: "10px 14px",
1300
+ borderRadius: 8,
1301
+ background: "var(--dsw-alias-bg-layer-2, #fff)",
1302
+ border: "1px solid var(--dsw-alias-state-error-secondary, #f5c2c7)",
1303
+ boxShadow: "0 4px 16px rgba(0,0,0,0.15)",
1304
+ color: "var(--dsw-alias-state-error-primary, #c33)",
1305
+ fontSize: 13,
1306
+ display: "flex",
1307
+ alignItems: "center",
1308
+ gap: 10,
1309
+ zIndex: 99999,
1310
+ maxWidth: 420,
1311
+ width: "min(420px, 90vw)"
1312
+ };
1313
+ const errorDismissStyle = {
1314
+ border: "none",
1315
+ background: "transparent",
1316
+ color: "inherit",
1317
+ cursor: "pointer",
1318
+ fontSize: 18,
1319
+ padding: 0,
1320
+ lineHeight: 1,
1321
+ flex: "none"
1322
+ };
1323
+ //#endregion
1324
+ //#region src/client/VoiceRecognitionBar.tsx
1325
+ /**
1326
+ * The recognition status bar above the composer. Shows live state and a stop
1327
+ * button while recording; the bar renders nothing when idle.
1328
+ */
1329
+ function VoiceRecognitionBar({ voiceSession, t }) {
1330
+ const snapshot = useVoiceInputSession(voiceSession);
1331
+ const active = snapshot.state === "starting" || snapshot.state === "recording";
1332
+ const label = active ? t("listening") : snapshot.state === "transcribing" ? t("transcribing") : snapshot.state === "polishing" ? t("polishing") : snapshot.state === "polish-error" ? t("polishFailedKeepOriginal") : snapshot.state === "error" ? t("voiceFailed") : "";
1333
+ if (label === "") return null;
1334
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1335
+ "data-better-input-bar": "true",
1336
+ role: "status",
1337
+ style: {
1338
+ display: "flex",
1339
+ alignItems: "center",
1340
+ gap: 8,
1341
+ padding: "4px 10px",
1342
+ borderRadius: 8,
1343
+ fontSize: 12,
1344
+ lineHeight: "18px",
1345
+ background: "var(--dsw-alias-bg-layer-3, rgba(0,0,0,0.04))",
1346
+ color: "var(--dsw-alias-label-primary, inherit)"
1347
+ },
1348
+ children: [
1349
+ active ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PulsingDot, {}) : null,
1350
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label }),
1351
+ (snapshot.state === "error" || snapshot.state === "polish-error") && snapshot.detail !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1352
+ style: { opacity: .7 },
1353
+ children: snapshot.detail
1354
+ }) : null,
1355
+ active ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1356
+ type: "button",
1357
+ onClick: () => voiceSession.requestStop(),
1358
+ style: {
1359
+ marginLeft: "auto",
1360
+ padding: "2px 8px",
1361
+ border: "none",
1362
+ borderRadius: 6,
1363
+ fontSize: 12,
1364
+ background: "var(--dsw-alias-state-error-secondary, #e5484d)",
1365
+ color: "#fff",
1366
+ cursor: "pointer"
1367
+ },
1368
+ children: t("voiceStop")
1369
+ }) : null
1370
+ ]
1371
+ });
1372
+ }
1373
+ function PulsingDot() {
1374
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1375
+ "aria-hidden": "true",
1376
+ style: {
1377
+ width: 8,
1378
+ height: 8,
1379
+ borderRadius: "50%",
1380
+ background: "var(--dsw-alias-state-error-secondary, #e5484d)",
1381
+ animation: "sqs-dsh-better-input-pulse 1.2s ease-in-out infinite"
1382
+ }
1383
+ });
1384
+ }
1385
+ //#endregion
1386
+ //#region src/client/settings-controller.ts
1387
+ const EMPTY_ABOUT = {
1388
+ repository: "",
1389
+ repositorySlug: "",
1390
+ version: "",
1391
+ license: "",
1392
+ updateCommand: "",
1393
+ updateCommandNpx: ""
1394
+ };
1395
+ const EMPTY_VIEW = {
1396
+ available: false,
1397
+ writable: false,
1398
+ settings: { ...DEFAULT_SETTINGS },
1399
+ overridden: [],
1400
+ defaultPolishPrompt: "",
1401
+ defaultOptimizePrompt: ""
1402
+ };
1403
+ /**
1404
+ * Settings read/write controller for the settings page and the microphone
1405
+ * flow. Owns the remote calls and a simple external store so both the page
1406
+ * and the voice button observe the same values. Also caches per-model
1407
+ * reasoning-effort metadata loaded lazily through resolveModelEfforts so
1408
+ * opening the effort dropdown never double-fetches across renders.
1409
+ */
1410
+ var SettingsController = class {
1411
+ remote;
1412
+ settingsSnapshot = {
1413
+ status: "loading",
1414
+ view: EMPTY_VIEW,
1415
+ detail: ""
1416
+ };
1417
+ routesSnapshot = {
1418
+ status: "loading",
1419
+ routes: [],
1420
+ detail: ""
1421
+ };
1422
+ effortsSnapshot = {};
1423
+ aboutSnapshot = {
1424
+ status: "loading",
1425
+ about: EMPTY_ABOUT,
1426
+ detail: ""
1427
+ };
1428
+ updateSnapshot = {
1429
+ status: "idle",
1430
+ update: null,
1431
+ detail: ""
1432
+ };
1433
+ listeners = /* @__PURE__ */ new Set();
1434
+ disposed = false;
1435
+ constructor(remote) {
1436
+ this.remote = remote;
1437
+ }
1438
+ getSettingsSnapshot = () => this.settingsSnapshot;
1439
+ getRoutesSnapshot = () => this.routesSnapshot;
1440
+ getEffortsSnapshot = () => this.effortsSnapshot;
1441
+ getAboutSnapshot = () => this.aboutSnapshot;
1442
+ getUpdateSnapshot = () => this.updateSnapshot;
1443
+ subscribe = (listener) => {
1444
+ this.listeners.add(listener);
1445
+ return () => this.listeners.delete(listener);
1446
+ };
1447
+ async refreshSettings() {
1448
+ const result = await this.remote.getSettings();
1449
+ if (this.disposed) return;
1450
+ if (!result.ok) this.settingsSnapshot = {
1451
+ status: "error",
1452
+ view: EMPTY_VIEW,
1453
+ detail: result.error.message
1454
+ };
1455
+ else {
1456
+ this.settingsSnapshot = {
1457
+ status: "ready",
1458
+ view: result.value,
1459
+ detail: ""
1460
+ };
1461
+ this.autoPopulateDefaultRoutesIfNeeded(result.value.settings);
1462
+ }
1463
+ this.emit();
1464
+ }
1465
+ async refreshRoutes() {
1466
+ const result = await this.remote.listRoutes();
1467
+ if (this.disposed) return;
1468
+ if (!result.ok) this.routesSnapshot = {
1469
+ status: "error",
1470
+ routes: [],
1471
+ detail: result.error.message
1472
+ };
1473
+ else {
1474
+ this.routesSnapshot = {
1475
+ status: "ready",
1476
+ routes: result.value,
1477
+ detail: ""
1478
+ };
1479
+ if (this.settingsSnapshot.status === "ready") this.autoPopulateDefaultRoutesIfNeeded(this.settingsSnapshot.view.settings);
1480
+ }
1481
+ this.emit();
1482
+ }
1483
+ autoPopulateDefaultRoutesIfNeeded = async (settings) => {
1484
+ const polishEmpty = settings.polishProvider === "" || settings.polishModel === "";
1485
+ const optimizeEmpty = settings.optimizeProvider === "" || settings.optimizeModel === "";
1486
+ if (!polishEmpty && !optimizeEmpty) return;
1487
+ const first = (this.routesSnapshot.status === "ready" && this.routesSnapshot.routes.length > 0 ? this.routesSnapshot.routes : await (async () => {
1488
+ if (this.disposed) return [];
1489
+ const rs = await this.remote.listRoutes();
1490
+ if (this.disposed || !rs.ok) return [];
1491
+ this.routesSnapshot = {
1492
+ status: "ready",
1493
+ routes: rs.value,
1494
+ detail: ""
1495
+ };
1496
+ this.emit();
1497
+ return rs.value;
1498
+ })())[0];
1499
+ if (first === void 0) return;
1500
+ const patch = {};
1501
+ if (polishEmpty) {
1502
+ patch.polishProvider = first.provider;
1503
+ patch.polishModel = first.model;
1504
+ }
1505
+ if (optimizeEmpty) {
1506
+ patch.optimizeProvider = first.provider;
1507
+ patch.optimizeModel = first.model;
1508
+ }
1509
+ await this.update(patch);
1510
+ };
1511
+ async update(patch) {
1512
+ const result = await this.remote.updateSettings(patch);
1513
+ if (this.disposed) return false;
1514
+ if (!result.ok) return false;
1515
+ this.settingsSnapshot = {
1516
+ status: "ready",
1517
+ view: result.value,
1518
+ detail: ""
1519
+ };
1520
+ this.emit();
1521
+ return true;
1522
+ }
1523
+ /**
1524
+ * Lazily fetch reasoning efforts for a route. Results are cached in the
1525
+ * controller so changing the effort dropdown back and forth doesn't
1526
+ * re-trigger remote calls. Returns a snapshot entry immediately — the
1527
+ * caller subscribes via `useEffortsSnapshot` to re-render when the data
1528
+ * lands.
1529
+ */
1530
+ async ensureEffortsFor(provider, model) {
1531
+ if (provider === "" || model === "" || this.disposed) return;
1532
+ const key = `${provider}\u0000${model}`;
1533
+ if (this.effortsSnapshot[key] !== void 0) return;
1534
+ this.effortsSnapshot = {
1535
+ ...this.effortsSnapshot,
1536
+ [key]: {
1537
+ status: "loading",
1538
+ efforts: [],
1539
+ detail: ""
1540
+ }
1541
+ };
1542
+ this.emit();
1543
+ try {
1544
+ const result = await this.remote.resolveModelEfforts(provider, model);
1545
+ if (this.disposed) return;
1546
+ if (result.ok) this.effortsSnapshot = {
1547
+ ...this.effortsSnapshot,
1548
+ [key]: {
1549
+ status: "ready",
1550
+ efforts: result.value.efforts,
1551
+ defaultEffort: result.value.defaultEffort,
1552
+ detail: ""
1553
+ }
1554
+ };
1555
+ else this.effortsSnapshot = {
1556
+ ...this.effortsSnapshot,
1557
+ [key]: {
1558
+ status: "error",
1559
+ efforts: [],
1560
+ detail: result.error.message
1561
+ }
1562
+ };
1563
+ } catch (error) {
1564
+ if (this.disposed) return;
1565
+ this.effortsSnapshot = {
1566
+ ...this.effortsSnapshot,
1567
+ [key]: {
1568
+ status: "error",
1569
+ efforts: [],
1570
+ detail: error instanceof Error ? error.message : String(error)
1571
+ }
1572
+ };
1573
+ }
1574
+ this.emit();
1575
+ }
1576
+ async refreshAbout() {
1577
+ const result = await this.remote.getAbout();
1578
+ if (this.disposed) return;
1579
+ if (!result.ok) this.aboutSnapshot = {
1580
+ status: "error",
1581
+ about: EMPTY_ABOUT,
1582
+ detail: result.error.message
1583
+ };
1584
+ else this.aboutSnapshot = {
1585
+ status: "ready",
1586
+ about: result.value,
1587
+ detail: ""
1588
+ };
1589
+ this.emit();
1590
+ }
1591
+ async checkForUpdate() {
1592
+ if (this.disposed) return;
1593
+ if (this.updateSnapshot.status === "loading") return;
1594
+ this.updateSnapshot = {
1595
+ status: "loading",
1596
+ update: null,
1597
+ detail: ""
1598
+ };
1599
+ this.emit();
1600
+ try {
1601
+ const result = await this.remote.checkForUpdate();
1602
+ if (this.disposed) return;
1603
+ if (result.ok) this.updateSnapshot = {
1604
+ status: "ready",
1605
+ update: result.value,
1606
+ detail: ""
1607
+ };
1608
+ else this.updateSnapshot = {
1609
+ status: "error",
1610
+ update: null,
1611
+ detail: result.error.message
1612
+ };
1613
+ } catch (error) {
1614
+ if (this.disposed) return;
1615
+ this.updateSnapshot = {
1616
+ status: "error",
1617
+ update: null,
1618
+ detail: error instanceof Error ? error.message : String(error)
1619
+ };
1620
+ }
1621
+ this.emit();
1622
+ }
1623
+ dispose() {
1624
+ this.disposed = true;
1625
+ this.listeners.clear();
1626
+ }
1627
+ emit() {
1628
+ for (const listener of this.listeners) listener();
1629
+ }
1630
+ };
1631
+ function useSettingsSnapshot(controller) {
1632
+ return (0, react.useSyncExternalStore)(controller.subscribe, controller.getSettingsSnapshot, controller.getSettingsSnapshot);
1633
+ }
1634
+ function useRoutesSnapshot(controller) {
1635
+ return (0, react.useSyncExternalStore)(controller.subscribe, controller.getRoutesSnapshot, controller.getRoutesSnapshot);
1636
+ }
1637
+ function useEffortsSnapshot(controller) {
1638
+ return (0, react.useSyncExternalStore)(controller.subscribe, controller.getEffortsSnapshot, controller.getEffortsSnapshot);
1639
+ }
1640
+ function useAboutSnapshot(controller) {
1641
+ return (0, react.useSyncExternalStore)(controller.subscribe, controller.getAboutSnapshot, controller.getAboutSnapshot);
1642
+ }
1643
+ function useUpdateSnapshot(controller) {
1644
+ return (0, react.useSyncExternalStore)(controller.subscribe, controller.getUpdateSnapshot, controller.getUpdateSnapshot);
1645
+ }
1646
+ //#endregion
1647
+ //#region src/client/settings.tsx
1648
+ function ReasoningEffortSelect(props) {
1649
+ const { settingsController, provider, model, storedEffort, onChange, t } = props;
1650
+ const efforts = useEffortsSnapshot(settingsController);
1651
+ (0, react.useEffect)(() => {
1652
+ if (provider === "" || model === "") return;
1653
+ settingsController.ensureEffortsFor(provider, model);
1654
+ }, [
1655
+ settingsController,
1656
+ provider,
1657
+ model
1658
+ ]);
1659
+ if (provider === "" || model === "") return null;
1660
+ const entry = efforts[`${provider}\u0000${model}`];
1661
+ if (entry === void 0 || entry.status === "loading") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
1662
+ value: "",
1663
+ disabled: true,
1664
+ style: inputStyle$1,
1665
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1666
+ value: "",
1667
+ children: t("effortLoadingLabel")
1668
+ })
1669
+ });
1670
+ if (entry.status === "error" || entry.efforts.length === 0) return null;
1671
+ const items = entry.efforts;
1672
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1673
+ value: storedEffort,
1674
+ onChange: (event) => onChange(event.target.value),
1675
+ style: inputStyle$1,
1676
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1677
+ value: "",
1678
+ children: t("effortDefaultLabel")
1679
+ }), items.map((effort) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1680
+ value: effort.id,
1681
+ title: effort.description,
1682
+ children: effort.name
1683
+ }, effort.id))]
1684
+ });
1685
+ }
1686
+ /**
1687
+ * The BetterInput settings page. Renders the recognition and polishing
1688
+ * configuration; every field edits a local draft and saves on blur/change.
1689
+ */
1690
+ function BetterInputSettingsSection({ close, settingsController, t }) {
1691
+ const settings = useSettingsSnapshot(settingsController);
1692
+ const routes = useRoutesSnapshot(settingsController);
1693
+ const about = useAboutSnapshot(settingsController);
1694
+ const update = useUpdateSnapshot(settingsController);
1695
+ const [drafts, setDrafts] = (0, react.useState)({});
1696
+ const [saveFailed, setSaveFailed] = (0, react.useState)(false);
1697
+ const [showDefaultPrompt, setShowDefaultPrompt] = (0, react.useState)(false);
1698
+ const [showDefaultOptimizePrompt, setShowDefaultOptimizePrompt] = (0, react.useState)(false);
1699
+ const draftsRef = (0, react.useRef)(drafts);
1700
+ draftsRef.current = drafts;
1701
+ (0, react.useEffect)(() => {
1702
+ settingsController.refreshSettings();
1703
+ settingsController.refreshRoutes();
1704
+ settingsController.refreshAbout();
1705
+ }, [settingsController]);
1706
+ if (settings.status === "loading" || routes.status === "loading") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SectionFrame$1, {
1707
+ title: t("settingsTitle"),
1708
+ children: t("loading")
1709
+ });
1710
+ const field = (name, current) => ({
1711
+ text: drafts[name] ?? current,
1712
+ invalid: false
1713
+ });
1714
+ const setField = (name, value) => {
1715
+ setDrafts((prev) => ({
1716
+ ...prev,
1717
+ [name]: value
1718
+ }));
1719
+ };
1720
+ const save = async (patch) => {
1721
+ setSaveFailed(false);
1722
+ if (await settingsController.update(patch)) setDrafts((prev) => {
1723
+ const next = { ...prev };
1724
+ for (const key of Object.keys(patch)) delete next[key];
1725
+ return next;
1726
+ });
1727
+ else setSaveFailed(true);
1728
+ };
1729
+ const s = settings.view.settings;
1730
+ const languageField = field("language", s.language);
1731
+ const secondsField = field("maxRecordingSeconds", String(s.maxRecordingSeconds));
1732
+ const polishPromptField = field("polishPrompt", s.polishPrompt);
1733
+ const optimizePromptField = field("optimizePrompt", s.optimizePrompt);
1734
+ const contextTurnsField = field("contextTurns", String(s.contextTurns));
1735
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(SectionFrame$1, {
1736
+ title: t("settingsTitle"),
1737
+ children: [
1738
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1739
+ style: hintStyle$1,
1740
+ children: t("settingsDescription")
1741
+ }),
1742
+ saveFailed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1743
+ style: errorStyle$1,
1744
+ children: t("saveFailed")
1745
+ }) : null,
1746
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
1747
+ style: sectionTitleStyle,
1748
+ children: t("voiceSectionLabel")
1749
+ }),
1750
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
1751
+ label: t("languageLabel"),
1752
+ hint: t("languageHint"),
1753
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1754
+ type: "text",
1755
+ value: languageField.text,
1756
+ placeholder: t("languagePlaceholder"),
1757
+ onChange: (event) => setField("language", event.target.value),
1758
+ onBlur: () => void save({ language: languageField.text.trim() }),
1759
+ style: inputStyle$1
1760
+ })
1761
+ }),
1762
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
1763
+ label: t("recordingLimitLabel"),
1764
+ hint: t("recordingLimitHint"),
1765
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1766
+ type: "number",
1767
+ min: 1,
1768
+ max: 600,
1769
+ value: secondsField.text,
1770
+ onChange: (event) => setField("maxRecordingSeconds", event.target.value),
1771
+ onBlur: () => {
1772
+ const parsed = Number(secondsField.text);
1773
+ if (!Number.isSafeInteger(parsed) || parsed < 1 || parsed > 600) return;
1774
+ save({ maxRecordingSeconds: parsed });
1775
+ },
1776
+ style: inputStyle$1
1777
+ })
1778
+ }),
1779
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
1780
+ style: sectionTitleStyle,
1781
+ children: t("polishSectionLabel")
1782
+ }),
1783
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
1784
+ label: t("polishLabel"),
1785
+ hint: t("polishHint"),
1786
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1787
+ style: switchStyle,
1788
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1789
+ type: "checkbox",
1790
+ checked: s.polishingEnabled,
1791
+ onChange: (event) => void save({ polishingEnabled: event.target.checked })
1792
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: s.polishingEnabled ? t("on") : t("off") })]
1793
+ })
1794
+ }),
1795
+ s.polishingEnabled ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1796
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
1797
+ label: t("polishModelLabel"),
1798
+ hint: t("polishModelHint"),
1799
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1800
+ value: drafts.polishProvider !== void 0 || drafts.polishModel !== void 0 ? `${drafts.polishProvider ?? s.polishProvider}\u0000${drafts.polishModel ?? s.polishModel}` : `${s.polishProvider}\u0000${s.polishModel}`,
1801
+ onChange: (event) => {
1802
+ const [provider, model] = event.target.value.split("\0");
1803
+ save({
1804
+ polishProvider: provider ?? "",
1805
+ polishModel: model ?? ""
1806
+ });
1807
+ },
1808
+ style: inputStyle$1,
1809
+ disabled: routes.status !== "ready" || routes.routes.length === 0,
1810
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1811
+ value: "\0",
1812
+ children: t("polishModelNone")
1813
+ }), routes.status === "ready" && routes.routes.map((route) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
1814
+ value: `${route.provider}\u0000${route.model}`,
1815
+ children: [
1816
+ route.providerName,
1817
+ " / ",
1818
+ route.modelName
1819
+ ]
1820
+ }, `${route.provider}\u0000${route.model}`))]
1821
+ })
1822
+ }),
1823
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
1824
+ label: t("polishEffortLabel"),
1825
+ hint: t("polishEffortHint"),
1826
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ReasoningEffortSelect, {
1827
+ settingsController,
1828
+ provider: s.polishProvider,
1829
+ model: s.polishModel,
1830
+ storedEffort: s.polishReasoningEffort,
1831
+ onChange: (effortId) => void save({ polishReasoningEffort: effortId }),
1832
+ t
1833
+ })
1834
+ }),
1835
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Field, {
1836
+ label: t("polishPromptLabel"),
1837
+ hint: t("polishPromptHint"),
1838
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
1839
+ value: polishPromptField.text,
1840
+ rows: 5,
1841
+ placeholder: t("polishPromptPlaceholder"),
1842
+ onChange: (event) => setField("polishPrompt", event.target.value),
1843
+ onBlur: () => void save({ polishPrompt: polishPromptField.text }),
1844
+ style: {
1845
+ ...inputStyle$1,
1846
+ resize: "vertical",
1847
+ fontFamily: "monospace"
1848
+ }
1849
+ }), settings.view.defaultPolishPrompt !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1850
+ type: "button",
1851
+ onClick: () => setShowDefaultPrompt((prev) => !prev),
1852
+ style: toggleLinkStyle,
1853
+ children: showDefaultPrompt ? t("hideDefaultPrompt") : t("showDefaultPrompt")
1854
+ }), showDefaultPrompt ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
1855
+ style: {
1856
+ margin: "6px 0 0",
1857
+ padding: 8,
1858
+ maxHeight: 220,
1859
+ overflow: "auto",
1860
+ whiteSpace: "pre-wrap",
1861
+ wordBreak: "break-word",
1862
+ fontSize: 11,
1863
+ lineHeight: 1.5,
1864
+ background: "var(--dsw-alias-bg-layer-1, rgba(0,0,0,0.03))",
1865
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.3))",
1866
+ borderRadius: 6,
1867
+ fontFamily: "monospace"
1868
+ },
1869
+ children: settings.view.defaultPolishPrompt
1870
+ }) : null] }) : null]
1871
+ })
1872
+ ] }) : null,
1873
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
1874
+ style: {
1875
+ margin: "16px 0 0",
1876
+ fontSize: 14
1877
+ },
1878
+ children: t("optimizeSectionLabel")
1879
+ }),
1880
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
1881
+ label: t("optimizeModelLabel"),
1882
+ hint: t("optimizeModelHint"),
1883
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1884
+ value: drafts.optimizeProvider !== void 0 || drafts.optimizeModel !== void 0 ? `${drafts.optimizeProvider ?? s.optimizeProvider}\u0000${drafts.optimizeModel ?? s.optimizeModel}` : `${s.optimizeProvider}\u0000${s.optimizeModel}`,
1885
+ onChange: (event) => {
1886
+ const [provider, model] = event.target.value.split("\0");
1887
+ save({
1888
+ optimizeProvider: provider ?? "",
1889
+ optimizeModel: model ?? ""
1890
+ });
1891
+ },
1892
+ style: inputStyle$1,
1893
+ disabled: routes.status !== "ready" || routes.routes.length === 0,
1894
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1895
+ value: "\0",
1896
+ children: t("polishModelNone")
1897
+ }), routes.status === "ready" && routes.routes.map((route) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
1898
+ value: `${route.provider}\u0000${route.model}`,
1899
+ children: [
1900
+ route.providerName,
1901
+ " / ",
1902
+ route.modelName
1903
+ ]
1904
+ }, `${route.provider}\u0000${route.model}`))]
1905
+ })
1906
+ }),
1907
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
1908
+ label: t("optimizeEffortLabel"),
1909
+ hint: t("optimizeEffortHint"),
1910
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ReasoningEffortSelect, {
1911
+ settingsController,
1912
+ provider: s.optimizeProvider,
1913
+ model: s.optimizeModel,
1914
+ storedEffort: s.optimizeReasoningEffort,
1915
+ onChange: (effortId) => void save({ optimizeReasoningEffort: effortId }),
1916
+ t
1917
+ })
1918
+ }),
1919
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Field, {
1920
+ label: t("optimizePromptLabel"),
1921
+ hint: t("optimizePromptHint"),
1922
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
1923
+ value: optimizePromptField.text,
1924
+ rows: 5,
1925
+ placeholder: t("optimizePromptPlaceholder"),
1926
+ onChange: (event) => setField("optimizePrompt", event.target.value),
1927
+ onBlur: () => void save({ optimizePrompt: optimizePromptField.text }),
1928
+ style: {
1929
+ ...inputStyle$1,
1930
+ resize: "vertical",
1931
+ fontFamily: "monospace"
1932
+ }
1933
+ }), settings.view.defaultOptimizePrompt !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1934
+ type: "button",
1935
+ onClick: () => setShowDefaultOptimizePrompt((prev) => !prev),
1936
+ style: toggleLinkStyle,
1937
+ children: showDefaultOptimizePrompt ? t("hideDefaultPrompt") : t("showDefaultPrompt")
1938
+ }), showDefaultOptimizePrompt ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
1939
+ style: {
1940
+ margin: "6px 0 0",
1941
+ padding: 8,
1942
+ maxHeight: 220,
1943
+ overflow: "auto",
1944
+ whiteSpace: "pre-wrap",
1945
+ wordBreak: "break-word",
1946
+ fontSize: 11,
1947
+ lineHeight: 1.5,
1948
+ background: "var(--dsw-alias-bg-layer-1, rgba(0,0,0,0.03))",
1949
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.3))",
1950
+ borderRadius: 6,
1951
+ fontFamily: "monospace"
1952
+ },
1953
+ children: settings.view.defaultOptimizePrompt
1954
+ }) : null] }) : null]
1955
+ }),
1956
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
1957
+ label: t("contextTurnsLabel"),
1958
+ hint: t("contextTurnsHint"),
1959
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1960
+ type: "number",
1961
+ min: 0,
1962
+ max: 20,
1963
+ value: contextTurnsField.text,
1964
+ onChange: (event) => setField("contextTurns", event.target.value),
1965
+ onBlur: () => {
1966
+ const parsed = Number(contextTurnsField.text);
1967
+ if (!Number.isSafeInteger(parsed) || parsed < 0 || parsed > 20) return;
1968
+ save({ contextTurns: parsed });
1969
+ },
1970
+ style: inputStyle$1
1971
+ })
1972
+ }),
1973
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1974
+ style: hintStyle$1,
1975
+ children: [
1976
+ t("routesStatus"),
1977
+ ": ",
1978
+ routes.status === "ready" ? `${routes.routes.length}` : routes.detail || t("routesUnavailable")
1979
+ ]
1980
+ }),
1981
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("hr", { style: dividerStyle }),
1982
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AboutUpdateSection, {
1983
+ aboutStatus: about.status,
1984
+ version: about.about.version,
1985
+ repository: about.about.repository,
1986
+ license: about.about.license,
1987
+ update,
1988
+ t,
1989
+ onCheckUpdate: () => void settingsController.checkForUpdate()
1990
+ })
1991
+ ]
1992
+ });
1993
+ }
1994
+ function AboutUpdateSection(props) {
1995
+ const { aboutStatus, version, repository, license, update, t, onCheckUpdate } = props;
1996
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1997
+ style: {
1998
+ display: "flex",
1999
+ flexDirection: "column",
2000
+ gap: 8
2001
+ },
2002
+ children: [
2003
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
2004
+ style: {
2005
+ margin: 0,
2006
+ fontSize: 14
2007
+ },
2008
+ children: t("aboutTitle")
2009
+ }),
2010
+ aboutStatus === "ready" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2011
+ style: {
2012
+ display: "flex",
2013
+ flexDirection: "column",
2014
+ gap: 2,
2015
+ fontSize: 13,
2016
+ opacity: .85
2017
+ },
2018
+ children: [
2019
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
2020
+ t("aboutVersionLabel"),
2021
+ ": ",
2022
+ version || "—"
2023
+ ] }),
2024
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
2025
+ t("aboutLicenseLabel"),
2026
+ ": ",
2027
+ license || "—"
2028
+ ] }),
2029
+ repository !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("a", {
2030
+ href: repository,
2031
+ target: "_blank",
2032
+ rel: "noreferrer",
2033
+ style: { color: "var(--dsw-alias-state-business-primary, #4f8cff)" },
2034
+ children: [
2035
+ t("aboutRepositoryLabel"),
2036
+ ": ",
2037
+ repository
2038
+ ]
2039
+ }) : null,
2040
+ repository !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
2041
+ href: `${repository.replace(/\/+$/, "")}/blob/main/CHANGELOG.md`,
2042
+ target: "_blank",
2043
+ rel: "noreferrer",
2044
+ style: { color: "var(--dsw-alias-state-business-primary, #4f8cff)" },
2045
+ children: t("aboutChangelogLabel")
2046
+ }) : null
2047
+ ]
2048
+ }) : null,
2049
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2050
+ style: {
2051
+ display: "flex",
2052
+ flexDirection: "column",
2053
+ gap: 6
2054
+ },
2055
+ children: [
2056
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2057
+ type: "button",
2058
+ onClick: onCheckUpdate,
2059
+ disabled: update.status === "loading",
2060
+ style: {
2061
+ width: "fit-content",
2062
+ padding: "6px 12px",
2063
+ borderRadius: 6,
2064
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.4))",
2065
+ background: "var(--dsh-color-surface, transparent)",
2066
+ color: "var(--dsw-alias-label-primary, inherit)",
2067
+ fontSize: 13,
2068
+ cursor: update.status === "loading" ? "default" : "pointer"
2069
+ },
2070
+ children: update.status === "loading" ? t("checkingUpdate") : t("checkUpdateButton")
2071
+ }),
2072
+ update.status === "ready" && update.update !== null ? (() => {
2073
+ const status = update.update.status;
2074
+ if (status === "up-to-date") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2075
+ style: hintStyle$1,
2076
+ children: t("updateUpToDate")
2077
+ });
2078
+ if (status === "unpublished") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2079
+ style: hintStyle$1,
2080
+ children: t("updateUnpublished")
2081
+ });
2082
+ if (status === "update-available") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2083
+ style: {
2084
+ display: "flex",
2085
+ flexDirection: "column",
2086
+ gap: 4
2087
+ },
2088
+ children: [
2089
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
2090
+ style: {
2091
+ ...hintStyle$1,
2092
+ color: "var(--dsw-alias-state-error-primary, #e5484d)"
2093
+ },
2094
+ children: [
2095
+ t("updateAvailable"),
2096
+ ": ",
2097
+ update.update.installed,
2098
+ " → ",
2099
+ update.update.latest
2100
+ ]
2101
+ }),
2102
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2103
+ style: hintStyle$1,
2104
+ children: [t("updateCommandLabel"), ":"]
2105
+ }),
2106
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
2107
+ style: codeStyle,
2108
+ children: update.update.updateCommand
2109
+ }),
2110
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2111
+ style: hintStyle$1,
2112
+ children: [t("updateCommandNpxLabel"), ":"]
2113
+ }),
2114
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
2115
+ style: codeStyle,
2116
+ children: update.update.updateCommandNpx
2117
+ }),
2118
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2119
+ style: hintStyle$1,
2120
+ children: [t("updateCommandPick"), ":"]
2121
+ })
2122
+ ]
2123
+ });
2124
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2125
+ style: errorStyle$1,
2126
+ children: t("updateCheckFailed")
2127
+ });
2128
+ })() : null,
2129
+ update.status === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
2130
+ style: errorStyle$1,
2131
+ children: [
2132
+ t("updateCheckFailed"),
2133
+ ": ",
2134
+ update.detail
2135
+ ]
2136
+ }) : null
2137
+ ]
2138
+ })
2139
+ ]
2140
+ });
2141
+ }
2142
+ function SectionFrame$1({ title, children }) {
2143
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2144
+ style: {
2145
+ padding: 16,
2146
+ display: "flex",
2147
+ flexDirection: "column",
2148
+ gap: 12
2149
+ },
2150
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
2151
+ style: {
2152
+ margin: 0,
2153
+ fontSize: 16
2154
+ },
2155
+ children: title
2156
+ }), children]
2157
+ });
2158
+ }
2159
+ function Field({ label, hint, children }) {
2160
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2161
+ style: {
2162
+ display: "flex",
2163
+ flexDirection: "column",
2164
+ gap: 4
2165
+ },
2166
+ children: [
2167
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2168
+ style: {
2169
+ fontSize: 13,
2170
+ fontWeight: 600
2171
+ },
2172
+ children: label
2173
+ }),
2174
+ children,
2175
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2176
+ style: hintStyle$1,
2177
+ children: hint
2178
+ })
2179
+ ]
2180
+ });
2181
+ }
2182
+ const inputStyle$1 = {
2183
+ padding: "6px 8px",
2184
+ borderRadius: 6,
2185
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.4))",
2186
+ background: "var(--dsw-alias-bg-layer-1, #f9f9f9)",
2187
+ color: "var(--dsw-alias-label-primary, inherit)",
2188
+ fontSize: 13
2189
+ };
2190
+ const hintStyle$1 = {
2191
+ margin: 0,
2192
+ fontSize: 12,
2193
+ opacity: .7
2194
+ };
2195
+ const errorStyle$1 = {
2196
+ margin: 0,
2197
+ fontSize: 12,
2198
+ color: "var(--dsw-alias-state-error-primary, #e5484d)"
2199
+ };
2200
+ const switchStyle = {
2201
+ display: "flex",
2202
+ alignItems: "center",
2203
+ gap: 8,
2204
+ fontSize: 13
2205
+ };
2206
+ const toggleLinkStyle = {
2207
+ marginTop: 6,
2208
+ padding: 0,
2209
+ border: "none",
2210
+ background: "none",
2211
+ color: "var(--dsw-alias-state-business-primary, #4f8cff)",
2212
+ fontSize: 12,
2213
+ cursor: "pointer",
2214
+ textDecoration: "underline"
2215
+ };
2216
+ const dividerStyle = {
2217
+ margin: "8px 0",
2218
+ border: "none",
2219
+ borderTop: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.3))"
2220
+ };
2221
+ const sectionTitleStyle = {
2222
+ margin: "16px 0 0",
2223
+ fontSize: 14
2224
+ };
2225
+ const codeStyle = {
2226
+ padding: "6px 8px",
2227
+ overflow: "auto",
2228
+ whiteSpace: "pre-wrap",
2229
+ wordBreak: "break-all",
2230
+ fontSize: 12,
2231
+ fontFamily: "monospace",
2232
+ background: "var(--dsw-alias-bg-layer-1, rgba(0,0,0,0.03))",
2233
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.3))",
2234
+ borderRadius: 6
2235
+ };
2236
+ //#endregion
2237
+ //#region src/client/templates-source.ts
2238
+ /** Unique source name on the `/` trigger (the menu group title). */
2239
+ const TEMPLATES_SOURCE_NAME = "better-input-templates";
2240
+ const MAX_CANDIDATES = 50;
2241
+ /**
2242
+ * The input-trigger source that inserts stored prompt templates as literal
2243
+ * text on the `/` trigger.
2244
+ *
2245
+ * - `warm()` prefetches the template list when the input box mounts, so the
2246
+ * first `/` keystroke already shows entries.
2247
+ * - `candidates()` filters by name / description / tag substring.
2248
+ * - `onPick()` returns `{ text: content }` — the trigger token span is
2249
+ * replaced by the template body, so `/name` never persists in the draft
2250
+ * and no lexicon/codec registration is needed.
2251
+ */
2252
+ function createTemplatesSource(controller) {
2253
+ return {
2254
+ trigger: "/",
2255
+ name: TEMPLATES_SOURCE_NAME,
2256
+ order: 300,
2257
+ showGroupTitle: true,
2258
+ async candidates(_session, req) {
2259
+ const query = req.query.trim().toLowerCase();
2260
+ const templates = controller.getSnapshot().templates;
2261
+ return (query === "" ? templates : templates.filter((template) => template.name.toLowerCase().includes(query) || template.description !== "" && template.description.toLowerCase().includes(query) || template.tags.some((tag) => tag.toLowerCase().includes(query)))).slice(0, MAX_CANDIDATES).map((template) => ({
2262
+ name: template.name,
2263
+ ...template.description === "" ? {} : { description: template.description },
2264
+ ...template.tags.length === 0 ? {} : { hint: template.tags.join(" / ") },
2265
+ value: template.id
2266
+ }));
2267
+ },
2268
+ onPick(pick) {
2269
+ const id = pick.candidate.value ?? "";
2270
+ const template = controller.byId(id);
2271
+ if (template === void 0) return;
2272
+ return { text: template.content };
2273
+ },
2274
+ warm(_session) {
2275
+ controller.ensureLoaded();
2276
+ }
2277
+ };
2278
+ }
2279
+ //#endregion
2280
+ //#region src/client/templates-controller.ts
2281
+ /**
2282
+ * Client-side store for the prompt template library. Mirrors the settings
2283
+ * controller: an external-store class consumed via useSyncExternalStore,
2284
+ * one sticky initial fetch (also kicked off by the `/` trigger warm hook),
2285
+ * and optimistic list updates after save/remove so the menu and the settings
2286
+ * section stay in sync without refetching.
2287
+ */
2288
+ const EMPTY_SNAPSHOT = {
2289
+ status: "loading",
2290
+ templates: [],
2291
+ detail: ""
2292
+ };
2293
+ function sortByRecency(templates) {
2294
+ return [...templates].sort((left, right) => right.updatedAt - left.updatedAt);
2295
+ }
2296
+ /** Split on ASCII and full-width commas, trim, drop empties. */
2297
+ function parseTags(raw) {
2298
+ return raw.split(/[,,]/).map((tag) => tag.trim()).filter((tag) => tag !== "");
2299
+ }
2300
+ var TemplatesController = class {
2301
+ remote;
2302
+ snapshot = EMPTY_SNAPSHOT;
2303
+ listeners = /* @__PURE__ */ new Set();
2304
+ disposed = false;
2305
+ loadPromise;
2306
+ constructor(remote) {
2307
+ this.remote = remote;
2308
+ }
2309
+ getSnapshot = () => this.snapshot;
2310
+ subscribe = (listener) => {
2311
+ this.listeners.add(listener);
2312
+ return () => this.listeners.delete(listener);
2313
+ };
2314
+ byId = (id) => this.snapshot.templates.find((template) => template.id === id);
2315
+ /** Kick off the one-time initial fetch; safe to call repeatedly. */
2316
+ ensureLoaded = () => {
2317
+ if (this.loadPromise !== void 0 || this.disposed) return;
2318
+ this.loadPromise = this.refresh();
2319
+ };
2320
+ async refresh() {
2321
+ try {
2322
+ const result = await this.remote.templatesList();
2323
+ if (this.disposed) return;
2324
+ if (result.ok) this.snapshot = {
2325
+ status: "ready",
2326
+ templates: sortByRecency(result.value.templates),
2327
+ detail: ""
2328
+ };
2329
+ else this.snapshot = {
2330
+ status: "error",
2331
+ templates: [],
2332
+ detail: result.error.message
2333
+ };
2334
+ } catch (error) {
2335
+ if (this.disposed) return;
2336
+ this.snapshot = {
2337
+ status: "error",
2338
+ templates: [],
2339
+ detail: error instanceof Error ? error.message : String(error)
2340
+ };
2341
+ }
2342
+ this.emit();
2343
+ }
2344
+ /** Create or update one template; resolves false when the call failed. */
2345
+ async save(draft) {
2346
+ const tags = parseTags(draft.tags);
2347
+ const input = {
2348
+ name: draft.name,
2349
+ content: draft.content,
2350
+ ...draft.id === void 0 ? {} : { id: draft.id },
2351
+ ...draft.description === "" ? {} : { description: draft.description },
2352
+ ...tags.length === 0 ? {} : { tags }
2353
+ };
2354
+ try {
2355
+ const result = await this.remote.templatesSave(input);
2356
+ if (this.disposed || !result.ok) return false;
2357
+ const saved = result.value.template;
2358
+ const rest = this.snapshot.templates.filter((template) => template.id !== saved.id);
2359
+ this.snapshot = {
2360
+ ...this.snapshot,
2361
+ status: "ready",
2362
+ templates: sortByRecency([...rest, saved])
2363
+ };
2364
+ this.emit();
2365
+ return true;
2366
+ } catch {
2367
+ return false;
2368
+ }
2369
+ }
2370
+ /** Remove one template; resolves false when the call failed. */
2371
+ async remove(id) {
2372
+ try {
2373
+ const result = await this.remote.templatesRemove(id);
2374
+ if (this.disposed || !result.ok || !result.value.removed) return false;
2375
+ this.snapshot = {
2376
+ ...this.snapshot,
2377
+ templates: this.snapshot.templates.filter((template) => template.id !== id)
2378
+ };
2379
+ this.emit();
2380
+ return true;
2381
+ } catch {
2382
+ return false;
2383
+ }
2384
+ }
2385
+ dispose() {
2386
+ this.disposed = true;
2387
+ this.listeners.clear();
2388
+ }
2389
+ emit() {
2390
+ for (const listener of this.listeners) listener();
2391
+ }
2392
+ };
2393
+ function useTemplatesSnapshot(controller) {
2394
+ return (0, react.useSyncExternalStore)(controller.subscribe, controller.getSnapshot, controller.getSnapshot);
2395
+ }
2396
+ //#endregion
2397
+ //#region src/templates/model.ts
2398
+ const MAX_TEMPLATE_CONTENT_LENGTH = 8e3;
2399
+ //#endregion
2400
+ //#region src/client/templates-section.tsx
2401
+ /**
2402
+ * Settings section for the prompt template library: list saved templates,
2403
+ * create/edit through an inline form, and delete with a two-step confirm.
2404
+ * Mirrors the settings section conventions (framework-injected `t` + local
2405
+ * frame/field components + dsw CSS variables).
2406
+ */
2407
+ const EMPTY_DRAFT = {
2408
+ name: "",
2409
+ description: "",
2410
+ tags: "",
2411
+ content: ""
2412
+ };
2413
+ function draftFromTemplate(template) {
2414
+ return {
2415
+ id: template.id,
2416
+ name: template.name,
2417
+ description: template.description,
2418
+ tags: [...template.tags].join(", "),
2419
+ content: template.content
2420
+ };
2421
+ }
2422
+ function TemplatesSection({ t, templatesController }) {
2423
+ const snapshot = useTemplatesSnapshot(templatesController);
2424
+ const [editor, setEditor] = (0, react.useState)({ kind: "closed" });
2425
+ const [busy, setBusy] = (0, react.useState)(false);
2426
+ const [actionFailed, setActionFailed] = (0, react.useState)(false);
2427
+ const [armedDeleteId, setArmedDeleteId] = (0, react.useState)(null);
2428
+ (0, react.useEffect)(() => {
2429
+ templatesController.refresh();
2430
+ }, [templatesController]);
2431
+ if (snapshot.status === "loading") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SectionFrame, {
2432
+ title: t("templatesTitle"),
2433
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2434
+ style: hintStyle,
2435
+ children: t("loading")
2436
+ })
2437
+ });
2438
+ if (snapshot.status === "error") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(SectionFrame, {
2439
+ title: t("templatesTitle"),
2440
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
2441
+ style: errorStyle,
2442
+ children: [
2443
+ t("templatesLoadFailed"),
2444
+ ": ",
2445
+ snapshot.detail
2446
+ ]
2447
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2448
+ type: "button",
2449
+ style: buttonStyle,
2450
+ onClick: () => void templatesController.refresh(),
2451
+ children: t("templatesRetry")
2452
+ })]
2453
+ });
2454
+ const editing = editor.kind === "open" ? editor.draft : null;
2455
+ const saveDraft = async () => {
2456
+ if (editing === null || busy) return;
2457
+ if (editing.name.trim() === "" || editing.content.trim() === "") return;
2458
+ setBusy(true);
2459
+ setActionFailed(false);
2460
+ const ok = await templatesController.save(editing);
2461
+ setBusy(false);
2462
+ if (ok) setEditor({ kind: "closed" });
2463
+ else setActionFailed(true);
2464
+ };
2465
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(SectionFrame, {
2466
+ title: t("templatesTitle"),
2467
+ children: [
2468
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2469
+ style: hintStyle,
2470
+ children: t("templatesDescription")
2471
+ }),
2472
+ actionFailed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2473
+ style: errorStyle,
2474
+ children: t("templatesActionFailed")
2475
+ }) : null,
2476
+ editing === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2477
+ type: "button",
2478
+ style: buttonStyle,
2479
+ onClick: () => {
2480
+ setActionFailed(false);
2481
+ setArmedDeleteId(null);
2482
+ setEditor({
2483
+ kind: "open",
2484
+ draft: EMPTY_DRAFT
2485
+ });
2486
+ },
2487
+ children: t("templatesNew")
2488
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2489
+ style: editorStyle,
2490
+ children: [
2491
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2492
+ style: {
2493
+ display: "flex",
2494
+ flexDirection: "column",
2495
+ gap: 4
2496
+ },
2497
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2498
+ style: labelStyle,
2499
+ children: t("templatesNameLabel")
2500
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2501
+ value: editing.name,
2502
+ maxLength: 60,
2503
+ placeholder: t("templatesNamePlaceholder"),
2504
+ onChange: (event) => setEditor({
2505
+ kind: "open",
2506
+ draft: {
2507
+ ...editing,
2508
+ name: event.target.value
2509
+ }
2510
+ }),
2511
+ style: inputStyle
2512
+ })]
2513
+ }),
2514
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2515
+ style: {
2516
+ display: "flex",
2517
+ flexDirection: "column",
2518
+ gap: 4
2519
+ },
2520
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2521
+ style: labelStyle,
2522
+ children: t("templatesDescriptionLabel")
2523
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2524
+ value: editing.description,
2525
+ maxLength: 200,
2526
+ placeholder: t("templatesDescriptionPlaceholder"),
2527
+ onChange: (event) => setEditor({
2528
+ kind: "open",
2529
+ draft: {
2530
+ ...editing,
2531
+ description: event.target.value
2532
+ }
2533
+ }),
2534
+ style: inputStyle
2535
+ })]
2536
+ }),
2537
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2538
+ style: {
2539
+ display: "flex",
2540
+ flexDirection: "column",
2541
+ gap: 4
2542
+ },
2543
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2544
+ style: labelStyle,
2545
+ children: t("templatesTagsLabel")
2546
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2547
+ value: editing.tags,
2548
+ placeholder: t("templatesTagsHint"),
2549
+ onChange: (event) => setEditor({
2550
+ kind: "open",
2551
+ draft: {
2552
+ ...editing,
2553
+ tags: event.target.value
2554
+ }
2555
+ }),
2556
+ style: inputStyle
2557
+ })]
2558
+ }),
2559
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2560
+ style: {
2561
+ display: "flex",
2562
+ flexDirection: "column",
2563
+ gap: 4
2564
+ },
2565
+ children: [
2566
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2567
+ style: labelStyle,
2568
+ children: t("templatesContentLabel")
2569
+ }),
2570
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
2571
+ value: editing.content,
2572
+ rows: 6,
2573
+ maxLength: MAX_TEMPLATE_CONTENT_LENGTH,
2574
+ placeholder: t("templatesContentPlaceholder"),
2575
+ onChange: (event) => setEditor({
2576
+ kind: "open",
2577
+ draft: {
2578
+ ...editing,
2579
+ content: event.target.value
2580
+ }
2581
+ }),
2582
+ style: {
2583
+ ...inputStyle,
2584
+ resize: "vertical",
2585
+ fontFamily: "monospace"
2586
+ }
2587
+ }),
2588
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2589
+ style: hintStyle,
2590
+ children: [
2591
+ editing.content.length,
2592
+ " / ",
2593
+ MAX_TEMPLATE_CONTENT_LENGTH
2594
+ ]
2595
+ })
2596
+ ]
2597
+ }),
2598
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2599
+ style: {
2600
+ display: "flex",
2601
+ gap: 8
2602
+ },
2603
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2604
+ type: "button",
2605
+ style: buttonStyle,
2606
+ disabled: busy || editing.name.trim() === "" || editing.content.trim() === "",
2607
+ onClick: () => void saveDraft(),
2608
+ children: t("templatesSave")
2609
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2610
+ type: "button",
2611
+ style: buttonStyle,
2612
+ disabled: busy,
2613
+ onClick: () => {
2614
+ setEditor({ kind: "closed" });
2615
+ setActionFailed(false);
2616
+ },
2617
+ children: t("templatesCancel")
2618
+ })]
2619
+ })
2620
+ ]
2621
+ }),
2622
+ snapshot.templates.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2623
+ style: hintStyle,
2624
+ children: t("templatesEmpty")
2625
+ }) : null,
2626
+ snapshot.templates.map((template) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2627
+ style: cardStyle,
2628
+ children: [
2629
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2630
+ style: {
2631
+ display: "flex",
2632
+ alignItems: "baseline",
2633
+ justifyContent: "space-between",
2634
+ gap: 8
2635
+ },
2636
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", {
2637
+ style: { fontSize: 13 },
2638
+ children: template.name
2639
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2640
+ style: hintStyle,
2641
+ children: new Date(template.updatedAt).toLocaleDateString()
2642
+ })]
2643
+ }),
2644
+ template.description !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2645
+ style: hintStyle,
2646
+ children: template.description
2647
+ }) : null,
2648
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2649
+ style: previewStyle,
2650
+ children: template.content.length > 120 ? `${template.content.slice(0, 120)}…` : template.content
2651
+ }),
2652
+ template.tags.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2653
+ style: {
2654
+ display: "flex",
2655
+ flexWrap: "wrap",
2656
+ gap: 4
2657
+ },
2658
+ children: template.tags.map((tag) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2659
+ style: tagStyle,
2660
+ children: tag
2661
+ }, tag))
2662
+ }) : null,
2663
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2664
+ style: {
2665
+ display: "flex",
2666
+ gap: 8
2667
+ },
2668
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2669
+ type: "button",
2670
+ style: buttonStyle,
2671
+ onClick: () => {
2672
+ setActionFailed(false);
2673
+ setArmedDeleteId(null);
2674
+ setEditor({
2675
+ kind: "open",
2676
+ draft: draftFromTemplate(template)
2677
+ });
2678
+ },
2679
+ children: t("templatesEdit")
2680
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2681
+ type: "button",
2682
+ style: armedDeleteId === template.id ? deleteArmedStyle : buttonStyle,
2683
+ onClick: () => {
2684
+ if (armedDeleteId !== template.id) {
2685
+ setArmedDeleteId(template.id);
2686
+ return;
2687
+ }
2688
+ setArmedDeleteId(null);
2689
+ templatesController.remove(template.id).then((ok) => {
2690
+ if (!ok) setActionFailed(true);
2691
+ });
2692
+ },
2693
+ children: armedDeleteId === template.id ? t("templatesDeleteConfirm") : t("templatesDelete")
2694
+ })]
2695
+ })
2696
+ ]
2697
+ }, template.id))
2698
+ ]
2699
+ });
2700
+ }
2701
+ function SectionFrame({ title, children }) {
2702
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2703
+ style: {
2704
+ padding: 16,
2705
+ display: "flex",
2706
+ flexDirection: "column",
2707
+ gap: 12
2708
+ },
2709
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
2710
+ style: {
2711
+ margin: 0,
2712
+ fontSize: 16
2713
+ },
2714
+ children: title
2715
+ }), children]
2716
+ });
2717
+ }
2718
+ const editorStyle = {
2719
+ display: "flex",
2720
+ flexDirection: "column",
2721
+ gap: 8,
2722
+ padding: 12,
2723
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.4))",
2724
+ borderRadius: 6,
2725
+ background: "var(--dsw-alias-bg-layer-1, rgba(0,0,0,0.03))"
2726
+ };
2727
+ const cardStyle = {
2728
+ display: "flex",
2729
+ flexDirection: "column",
2730
+ gap: 6,
2731
+ padding: 12,
2732
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.3))",
2733
+ borderRadius: 6
2734
+ };
2735
+ const previewStyle = {
2736
+ margin: 0,
2737
+ fontSize: 12,
2738
+ fontFamily: "monospace",
2739
+ whiteSpace: "pre-wrap",
2740
+ wordBreak: "break-all",
2741
+ opacity: .8
2742
+ };
2743
+ const tagStyle = {
2744
+ padding: "1px 6px",
2745
+ fontSize: 11,
2746
+ borderRadius: 4,
2747
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.3))",
2748
+ opacity: .85
2749
+ };
2750
+ const labelStyle = {
2751
+ fontSize: 13,
2752
+ fontWeight: 600
2753
+ };
2754
+ const buttonStyle = {
2755
+ width: "fit-content",
2756
+ padding: "6px 12px",
2757
+ borderRadius: 6,
2758
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.4))",
2759
+ background: "var(--dsh-color-surface, transparent)",
2760
+ color: "var(--dsw-alias-label-primary, inherit)",
2761
+ fontSize: 13,
2762
+ cursor: "pointer"
2763
+ };
2764
+ const deleteArmedStyle = {
2765
+ ...buttonStyle,
2766
+ borderColor: "var(--dsw-alias-state-error-primary, #e5484d)",
2767
+ color: "var(--dsw-alias-state-error-primary, #e5484d)"
2768
+ };
2769
+ const inputStyle = {
2770
+ padding: "6px 8px",
2771
+ borderRadius: 6,
2772
+ border: "1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.4))",
2773
+ background: "var(--dsw-alias-bg-layer-1, #f9f9f9)",
2774
+ color: "var(--dsw-alias-label-primary, inherit)",
2775
+ fontSize: 13
2776
+ };
2777
+ const hintStyle = {
2778
+ margin: 0,
2779
+ fontSize: 12,
2780
+ opacity: .7
2781
+ };
2782
+ const errorStyle = {
2783
+ margin: 0,
2784
+ fontSize: 12,
2785
+ color: "var(--dsw-alias-state-error-primary, #e5484d)"
2786
+ };
2787
+ //#endregion
2788
+ //#region src/client/index.ts
2789
+ const PULSE_KEYFRAMES = `@keyframes sqs-dsh-better-input-pulse {
2790
+ 0%, 100% { opacity: 1; }
2791
+ 50% { opacity: 0.3; }
2792
+ }`;
2793
+ /**
2794
+ * Required Client services: the slot registry, the Typert remote hub, and
2795
+ * the DSH locale runtime. `remote.betterInput` is mounted by this plugin's
2796
+ * own apply() via `ctx.remote.$mount`, so it MUST NOT appear here — the
2797
+ * outer inject gates plugin activation and would deadlock waiting for
2798
+ * itself. It is declared only on the inner ctx.inject() below, which runs
2799
+ * after the mount. Settings reach the browser through `SettingsScopeBinder`
2800
+ * (provided by `@deepseek-ai/dsh-client-ui-settings`) and are read inside
2801
+ * the settings section slot itself, not via a top-level `settings` service
2802
+ * here.
2803
+ */
2804
+ const inject = [
2805
+ "slots",
2806
+ "remote",
2807
+ "locale",
2808
+ "inputTriggers",
2809
+ "conversation"
2810
+ ];
2811
+ async function apply(ctx) {
2812
+ const disposeRemote = await ctx.remote.$mount(TYPERT_REMOTE);
2813
+ const disposeLocaleDicts = ctx.locale.register(BETTER_INPUT_NS, {
2814
+ zh,
2815
+ en
2816
+ });
2817
+ await ctx.inject([
2818
+ "slots",
2819
+ "remote",
2820
+ "remote.betterInput",
2821
+ "inputTriggers",
2822
+ "conversation"
2823
+ ], async (remoteCtx) => {
2824
+ const remote = remoteCtx.remote.betterInput;
2825
+ const controller = new SettingsController(remote);
2826
+ const templatesController = new TemplatesController(remote);
2827
+ const voiceSessions = /* @__PURE__ */ new Map();
2828
+ const voiceSessionFor = (sessionId) => {
2829
+ let session = voiceSessions.get(sessionId);
2830
+ if (session === void 0) {
2831
+ session = new VoiceInputSession();
2832
+ voiceSessions.set(sessionId, session);
2833
+ }
2834
+ return session;
2835
+ };
2836
+ const disposeTemplatesSource = remoteCtx.inputTriggers.registerSource(createTemplatesSource(templatesController));
2837
+ remoteCtx.effect(() => () => {
2838
+ for (const session of voiceSessions.values()) session.dispose();
2839
+ voiceSessions.clear();
2840
+ disposeTemplatesSource();
2841
+ controller.dispose();
2842
+ templatesController.dispose();
2843
+ }, "sqs-dsh-better-input sessions lifecycle");
2844
+ remoteCtx.effect(() => {
2845
+ const styleTag = document.createElement("style");
2846
+ styleTag.dataset.plugin = "sqs-dsh-better-input";
2847
+ styleTag.textContent = PULSE_KEYFRAMES;
2848
+ document.head.appendChild(styleTag);
2849
+ return () => {
2850
+ styleTag.remove();
2851
+ };
2852
+ }, "sqs-dsh-better-input styles");
2853
+ controller.refreshSettings();
2854
+ controller.refreshRoutes();
2855
+ templatesController.ensureLoaded();
2856
+ const useSettings = () => {
2857
+ const snapshot = useSettingsSnapshot(controller);
2858
+ if (snapshot.status !== "ready") return {
2859
+ status: "loading",
2860
+ settings: snapshot.view.settings
2861
+ };
2862
+ return {
2863
+ status: "ready",
2864
+ settings: snapshot.view.settings
2865
+ };
2866
+ };
2867
+ remoteCtx.slots.inject("conversation.input.dock", () => remoteCtx.slots.register({
2868
+ name: "conversation.input.dock",
2869
+ id: "better-input-recognition-bar",
2870
+ order: 15,
2871
+ locale: BETTER_INPUT_NS,
2872
+ inject: (sessionId) => ({ voiceSession: voiceSessionFor(sessionId) })
2873
+ }, VoiceRecognitionBar));
2874
+ remoteCtx.slots.inject("conversation.input.right", () => remoteCtx.slots.register({
2875
+ name: "conversation.input.right",
2876
+ id: "better-input-optimize",
2877
+ order: 9998,
2878
+ locale: BETTER_INPUT_NS,
2879
+ inject: () => ({
2880
+ remote,
2881
+ useSettings
2882
+ })
2883
+ }, OptimizeButton));
2884
+ remoteCtx.slots.inject("conversation.input.right", () => remoteCtx.slots.register({
2885
+ name: "conversation.input.right",
2886
+ id: "better-input-voice",
2887
+ order: 9999,
2888
+ locale: BETTER_INPUT_NS,
2889
+ inject: (sessionId) => ({
2890
+ remote,
2891
+ voiceSession: voiceSessionFor(sessionId),
2892
+ useSettings
2893
+ })
2894
+ }, MicrophoneButton));
2895
+ remoteCtx.slots.inject("settings.section", () => remoteCtx.slots.register({
2896
+ name: "settings.section",
2897
+ id: "sqs-dsh-better-input",
2898
+ order: 16,
2899
+ label: () => ctx.locale.bind(BETTER_INPUT_NS)("settingsTitle"),
2900
+ locale: BETTER_INPUT_NS,
2901
+ inject: () => ({ settingsController: controller })
2902
+ }, BetterInputSettingsSection));
2903
+ remoteCtx.slots.inject("settings.section", () => remoteCtx.slots.register({
2904
+ name: "settings.section",
2905
+ id: "sqs-dsh-better-input-templates",
2906
+ order: 17,
2907
+ label: () => ctx.locale.bind(BETTER_INPUT_NS)("templatesTitle"),
2908
+ locale: BETTER_INPUT_NS,
2909
+ inject: () => ({ templatesController })
2910
+ }, TemplatesSection));
2911
+ return () => {};
2912
+ });
2913
+ return async () => {
2914
+ disposeLocaleDicts();
2915
+ await disposeRemote();
2916
+ };
2917
+ }
2918
+ //#endregion
2919
+ exports.apply = apply;
2920
+ exports.inject = inject;
2921
+ return module.exports;
2922
+ }
2923
+ });
2924
+
2925
+ //# sourceMappingURL=client.js.map