@goodandready/dsh-russian-lang 0.2.26 → 0.3.1

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 (52) hide show
  1. package/README.md +1 -1
  2. package/bin/dsh-i18n.mjs +247 -0
  3. package/lib/client.js +785 -410
  4. package/lib/index.js +69 -13
  5. package/lib/locales/plugins/01-token-dashboard.json +1 -1
  6. package/lib/locales/plugins/10-kanban.json +1 -1
  7. package/lib/locales/plugins/13-agent-teams.json +1 -1
  8. package/lib/locales/plugins/16-usage-stats.json +1 -1
  9. package/lib/locales/plugins/23-moa.json +1 -1
  10. package/lib/locales/plugins/24-personal-directive.json +1 -1
  11. package/lib/locales/plugins/33-messenger-gateway.json +1 -1
  12. package/lib/locales/plugins/35-gitea.json +1 -1
  13. package/lib/locales/plugins/36-cost-meter.json +1 -1
  14. package/lib/locales/plugins/37-time-machine.json +1 -1
  15. package/lib/locales/plugins/38-context-lens.json +1 -1
  16. package/lib/locales/plugins/39-shadow-auditor.json +1 -1
  17. package/lib/locales/plugins/40-clinebot.json +1 -1
  18. package/lib/locales/plugins/41-lanmode.json +1 -1
  19. package/lib/locales/plugins/42-plugin-console.json +1 -1
  20. package/lib/locales/plugins/46-memory-brain.json +1 -1
  21. package/lib/locales/plugins/48-task-tracker.json +1 -1
  22. package/lib/locales/plugins/49-dsml-guard.json +1 -1
  23. package/lib/locales/plugins/49-usage-guard.json +1 -1
  24. package/lib/locales/plugins/50-automode.json +1 -1
  25. package/lib/locales/plugins/54-workshop-market.json +1 -1
  26. package/lib/locales/plugins/56-cron.json +1 -1
  27. package/lib/locales/plugins/57-live-canvas.json +1 -1
  28. package/lib/locales/plugins/58-grok-xsearch.json +1 -1
  29. package/lib/locales/plugins/59-goal.json +1 -1
  30. package/lib/locales/plugins/60-model-sync.json +1 -1
  31. package/lib/locales/plugins/61-issue-reporter.json +1 -1
  32. package/lib/locales/plugins/62-model-search.json +1 -1
  33. package/lib/locales/plugins/63-agent-orchestrator.json +1 -1
  34. package/lib/locales/plugins/64-key-limits.json +1 -1
  35. package/lib/locales/plugins/65-subscriptions.json +1 -1
  36. package/lib/locales/plugins/66-tts.json +1 -1
  37. package/lib/locales/plugins/67-voice.json +1 -1
  38. package/lib/locales/plugins/68-image-gen.json +1 -1
  39. package/lib/locales/plugins/69-plugin-notify.json +1 -1
  40. package/lib/locales/plugins/70-hypotheses.json +1 -1
  41. package/lib/locales/plugins/71-memory-meter.json +1 -1
  42. package/lib/locales/plugins/72-server-monitor.json +1 -70
  43. package/lib/locales/plugins/73-agentrouter.json +1 -1
  44. package/lib/locales/plugins/75-test-pilot.json +1 -0
  45. package/lib/locales/plugins/76-visualize.json +1 -0
  46. package/lib/locales/plugins/77-github-ops.json +1 -0
  47. package/lib/locales/plugins/78-web-gateway.json +1 -0
  48. package/lib/locales/plugins/79-approval-gate.json +1 -0
  49. package/lib/locales.js +50 -12
  50. package/lib/pure.js +251 -13
  51. package/package.json +7 -3
  52. package/lib/locales/plugins/44-goal.json +0 -1
package/lib/client.js CHANGED
@@ -4,13 +4,48 @@ window.__ModuleLoader__.load({
4
4
  var module = { exports: {} }
5
5
  var React = null
6
6
  try { React = require('react') } catch (e) { /* карточка настроек необязательна */ }
7
-
8
-
9
-
10
7
  const numberFormat = new Intl.NumberFormat('ru-RU')
8
+ const compactNumberFormat = new Intl.NumberFormat('ru-RU', { notation: 'compact', compactDisplay: 'short' })
11
9
  const relativeTimeFormat = new Intl.RelativeTimeFormat('ru-RU', { numeric: 'auto' })
10
+ const datePresetFormats = {
11
+ short: new Intl.DateTimeFormat('ru-RU', { day: '2-digit', month: '2-digit', year: 'numeric' }),
12
+ shortDateTime: new Intl.DateTimeFormat('ru-RU', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }),
13
+ medium: new Intl.DateTimeFormat('ru-RU', { day: 'numeric', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit' }),
14
+ long: new Intl.DateTimeFormat('ru-RU', { day: 'numeric', month: 'long', year: 'numeric' }),
15
+ time: new Intl.DateTimeFormat('ru-RU', { hour: '2-digit', minute: '2-digit', second: '2-digit' }),
16
+ }
17
+ const formatDate = (val, style = 'short') => {
18
+ if (val === null || val === undefined || val === '') return ''
19
+ const d = val instanceof Date ? val : new Date(typeof val === 'number' && val < 1e12 ? val * 1000 : val)
20
+ if (isNaN(d.getTime())) return String(val)
21
+ if (typeof style === 'string' && datePresetFormats[style]) {
22
+ return datePresetFormats[style].format(d)
23
+ }
24
+ if (typeof style === 'object' && style !== null) {
25
+ try {
26
+ return new Intl.DateTimeFormat('ru-RU', style).format(d)
27
+ } catch (_) {
28
+ return datePresetFormats.short.format(d)
29
+ }
30
+ }
31
+ return datePresetFormats.short.format(d)
32
+ }
33
+ const formatCompactNumber = (val) => {
34
+ if (val === null || val === undefined || val === '') return ''
35
+ const n = typeof val === 'number' ? val : Number(val)
36
+ return isNaN(n) ? String(val) : compactNumberFormat.format(n)
37
+ }
38
+ const formatTokens = (count, compact = false) => {
39
+ if (count === null || count === undefined || count === '') return ''
40
+ const n = typeof count === 'number' ? count : Number(count)
41
+ if (isNaN(n)) return String(count)
42
+ const tokenWord = plural(n, ['токен', 'токена', 'токенов'])
43
+ if (compact && n >= 1000) {
44
+ return `${compactNumberFormat.format(n)} токенов`
45
+ }
46
+ return `${numberFormat.format(n)} ${tokenWord}`
47
+ }
12
48
  const currencyFormats = new Map()
13
-
14
49
  const getCurrencyFormat = (cur) => {
15
50
  const c = (cur || 'RUB').toUpperCase()
16
51
  if (!currencyFormats.has(c)) {
@@ -22,20 +57,17 @@ const getCurrencyFormat = (cur) => {
22
57
  }
23
58
  return currencyFormats.get(c)
24
59
  }
25
-
26
60
  const formatNumber = (val) => {
27
61
  if (val === null || val === undefined || val === '') return ''
28
62
  const n = typeof val === 'number' ? val : Number(val)
29
63
  return isNaN(n) ? String(val) : numberFormat.format(n)
30
64
  }
31
-
32
65
  const formatCurrency = (val, cur) => {
33
66
  if (val === null || val === undefined || val === '') return ''
34
67
  const n = typeof val === 'number' ? val : Number(val)
35
68
  if (isNaN(n)) return String(val)
36
69
  return getCurrencyFormat(cur).format(n)
37
70
  }
38
-
39
71
  const formatRelativeTime = (val, unit) => {
40
72
  if (val === null || val === undefined || val === '') return ''
41
73
  if (typeof val === 'number' && typeof unit === 'string') {
@@ -52,8 +84,6 @@ const formatRelativeTime = (val, unit) => {
52
84
  if (absSec < 31536000) return relativeTimeFormat.format(Math.round(diffSec / 2592000), 'month')
53
85
  return relativeTimeFormat.format(Math.round(diffSec / 31536000), 'year')
54
86
  }
55
-
56
-
57
87
  const INFLECT_CUSTOM = {
58
88
  'пользователь': { gen: 'пользователя', dat: 'пользователю', acc: 'пользователя', ins: 'пользователем', pre: 'пользователе' },
59
89
  'агент': { gen: 'агента', dat: 'агенту', acc: 'агента', ins: 'агентом', pre: 'агенте' },
@@ -66,9 +96,7 @@ const INFLECT_CUSTOM = {
66
96
  'файл': { gen: 'файла', dat: 'файлу', acc: 'файл', ins: 'файлом', pre: 'файле' },
67
97
  'папка': { gen: 'папки', dat: 'папке', acc: 'папку', ins: 'папкой', pre: 'папке' }
68
98
  }
69
-
70
99
  const keepCase = (src, out) => (src[0] === src[0].toUpperCase() ? out[0].toUpperCase() + out.slice(1) : out)
71
-
72
100
  const inflectWord = (word, cName) => {
73
101
  if (!word || typeof word !== 'string') return word
74
102
  const lower = word.toLowerCase()
@@ -77,7 +105,6 @@ const inflectWord = (word, cName) => {
77
105
  }
78
106
  if (/[a-zA-Z0-9_-]/.test(word) || /^[А-ЯЁ]{2,}$/.test(word)) return word
79
107
  if (/[оеиую]$/i.test(word) && !/(ко|ло|но|то|во|ро|до|по|со|мо|го)$/i.test(word)) return word
80
-
81
108
  const w = lower
82
109
  const endings = [
83
110
  ['ия', 2, (s) => ({ gen: s + 'ии', dat: s + 'ии', acc: s + 'ию', ins: s + 'ией', pre: s + 'ии' })],
@@ -98,35 +125,58 @@ const inflectWord = (word, cName) => {
98
125
  }
99
126
  return word
100
127
  }
101
-
102
128
  const INFLECT_CASES = new Set(['gen', 'dat', 'acc', 'ins', 'pre'])
103
-
104
129
  const inflect = (phrase, cName) => {
105
130
  if (!phrase || typeof phrase !== 'string') return phrase
106
131
  if (!INFLECT_CASES.has(cName)) return phrase
107
132
  return phrase.split(' ').map((w) => inflectWord(w, cName)).join(' ')
108
133
  }
109
-
110
-
134
+ const pluralRules = new Intl.PluralRules('ru-RU')
135
+ const pluralForm = (n) => pluralRules.select(n)
136
+ function plural(count, forms, withCount = false) {
137
+ const n = typeof count === 'number' ? count : (Number(count) || 0)
138
+ let one = '', few = '', many = ''
139
+ if (Array.isArray(forms)) {
140
+ one = forms[0] || ''
141
+ few = forms[1] || one
142
+ many = forms[2] || few || one
143
+ } else if (typeof forms === 'string') {
144
+ one = forms
145
+ few = arguments[2] || one
146
+ many = arguments[3] || few || one
147
+ withCount = arguments[4] || false
148
+ } else {
149
+ return ''
150
+ }
151
+ const form = pluralRules.select(n)
152
+ let word = many
153
+ if (form === 'one') word = one
154
+ else if (form === 'few') word = few
155
+ else if (form === 'many') word = many
156
+ return withCount ? `${formatNumber(n)} ${word}` : word
157
+ }
111
158
  const fill = (template, params) => {
112
159
  if (!params || typeof params !== 'object') return String(template)
113
- return String(template).replace(/\{(\w+)(?::(\w+))?\}/g, (match, name, spec) => {
160
+ return String(template).replace(/\{(\w+)(?::([^}]+))?\}/g, (match, name, spec) => {
114
161
  if (!(name in params)) return match
115
162
  const val = params[name]
163
+ if (!spec) return String(val)
116
164
  if (spec === 'number') return formatNumber(val)
165
+ if (spec === 'compact') return formatCompactNumber(val)
166
+ if (spec === 'tokens') return formatTokens(val)
167
+ if (spec === 'tokens:compact') return formatTokens(val, true)
117
168
  if (spec === 'reltime') return formatRelativeTime(val)
118
169
  if (spec === 'currency') return formatCurrency(val, params.currency || 'RUB')
170
+ if (spec === 'date') return formatDate(val)
171
+ if (spec.startsWith('date:')) return formatDate(val, spec.slice(5))
172
+ if (spec.startsWith('plural:')) {
173
+ const parts = spec.slice(7).split(',')
174
+ return plural(val, parts)
175
+ }
119
176
  if (INFLECT_CASES.has(spec)) return inflect(String(val), spec)
120
177
  return String(val)
121
178
  })
122
179
  }
123
-
124
- const pluralRules = new Intl.PluralRules('ru-RU')
125
-
126
- /** Русская форма числительного: one | few | many | other. */
127
- const pluralForm = (n) => pluralRules.select(n)
128
-
129
-
130
180
  const stemRussian = (word) => {
131
181
  if (!word || typeof word !== 'string') return ''
132
182
  let w = word.toLowerCase().trim()
@@ -138,15 +188,12 @@ const stemRussian = (word) => {
138
188
  w = w.replace(/(?:ами|ями|иями|ией|иям|ием|ах|ях|иях|ев|ов|ие|ье|ей|ой|ий|ям|ем|ам|ом|а|е|и|о|у|ы|ь|ю|я)$/, '')
139
189
  return w.length >= 2 ? w : word.toLowerCase()
140
190
  }
141
-
142
191
  const EN_RU_KEYS = {
143
192
  'q': 'й', 'w': 'ц', 'e': 'у', 'r': 'к', 't': 'е', 'y': 'н', 'u': 'г', 'i': 'ш', 'o': 'щ', 'p': 'з', '[': 'х', ']': 'ъ',
144
193
  'a': 'ф', 's': 'ы', 'd': 'в', 'f': 'а', 'g': 'п', 'h': 'р', 'j': 'о', 'k': 'л', 'l': 'д', ';': 'ж', "'": 'э',
145
194
  'z': 'я', 'x': 'ч', 'c': 'с', 'v': 'м', 'b': 'и', 'n': 'т', 'm': 'ь', ',': 'б', '.': 'ю'
146
195
  }
147
-
148
196
  const translitEnToRu = (str) => str.toLowerCase().split('').map((c) => EN_RU_KEYS[c] || c).join('')
149
-
150
197
  const fuzzyMatchRu = (query, target) => {
151
198
  if (!query || !target) return 0
152
199
  const q = query.toLowerCase().trim()
@@ -164,8 +211,6 @@ const fuzzyMatchRu = (query, target) => {
164
211
  if (matched > 0) return 50
165
212
  return 0
166
213
  }
167
-
168
-
169
214
  const ERROR_MAP = {
170
215
  ENOENT: { title: 'Файл не найден', message: 'Указанный файл или директория не существуют', hint: 'Проверьте правильность указанного пути к файлу.' },
171
216
  EACCES: { title: 'Отказано в доступе', message: 'Недостаточно прав для чтения или записи', hint: 'Проверьте права доступа к файлу или директории (chmod/chown).' },
@@ -184,13 +229,17 @@ const ERROR_MAP = {
184
229
  500: { title: 'Внутренняя ошибка сервера', message: 'На стороне сервера произошел сбой', hint: 'Попробуйте повторить запрос позже или проверьте серверные логи.' },
185
230
  502: { title: 'Ошибочный шлюз (Bad Gateway)', message: 'Промежуточный прокси не получил корректный ответ', hint: 'Проверьте работу нижележащей службы или upstream-сервера.' },
186
231
  503: { title: 'Служба временно недоступна', message: 'Сервер перегружен или находится на обслуживании', hint: 'Попробуйте повторить операцию через некоторое время.' },
187
- 504: { title: 'Шлюз не отвечает (Gateway Timeout)', message: 'Превышено время ожидания ответа от upstream-сервера', hint: 'Попробуйте повторить запрос позже.' }
232
+ 504: { title: 'Шлюз не отвечает (Gateway Timeout)', message: 'Превышено время ожидания ответа от upstream-сервера', hint: 'Попробуйте повторить запрос позже.' },
233
+ FETCH_FAILED: { title: 'Сетевой запрос не удался', message: 'Не удалось связаться с сервером (Failed to fetch)', hint: 'Проверьте сетевое подключение и доступность адреса.' },
234
+ CORS_ERROR: { title: 'Ошибка CORS', message: 'Запрос заблокирован политикой безопасности браузера', hint: 'Проверьте заголовки разрешённых источников на сервере.' },
235
+ WS_CLOSED: { title: 'Разрыв WebSocket', message: 'Связь с сервером по WebSocket прервана', hint: 'Проверьте работу сервера и сетевой статус.' },
236
+ QUOTA_EXCEEDED: { title: 'Исчерпана квота API', message: 'Превышен лимит запросов или баланс провайдера', hint: 'Проверьте баланс API-ключа в кабинете провайдера.' },
237
+ CONTEXT_OVERFLOW: { title: 'Превышен контекст', message: 'Объём диалога превышает размер контекстного окна', hint: 'Очистите или сожмите историю сообщений сессии.' },
238
+ MODEL_NOT_FOUND: { title: 'Модель не найдена', message: 'Указанная модель отсутствует или недоступна', hint: 'Проверьте идентификатор модели в настройках.' }
188
239
  }
189
-
190
240
  const fromMap = (key, rawMsg) => ({
191
241
  code: String(key), title: ERROR_MAP[key].title, message: ERROR_MAP[key].message, hint: ERROR_MAP[key].hint, raw: rawMsg
192
242
  })
193
-
194
243
  const humanizeError = (err) => {
195
244
  if (!err) return null
196
245
  const rawMsg = typeof err === 'string' ? err : (err.message || String(err))
@@ -200,12 +249,23 @@ const humanizeError = (err) => {
200
249
  if (lookupKey && ERROR_MAP[lookupKey]) return fromMap(lookupKey, rawMsg)
201
250
  if (/rate limit|too many requests/i.test(rawMsg)) return fromMap(429, rawMsg)
202
251
  if (/unauthorized|invalid token|invalid api key/i.test(rawMsg)) return fromMap(401, rawMsg)
252
+ if (/failed to fetch|network\s*error|load\s*failed|net::err_/i.test(rawMsg)) return fromMap('FETCH_FAILED', rawMsg)
253
+ if (/cors|cross-origin/i.test(rawMsg)) return fromMap('CORS_ERROR', rawMsg)
254
+ if (/websocket.*(?:closed|failed)/i.test(rawMsg)) return fromMap('WS_CLOSED', rawMsg)
255
+ if (/insufficient_quota|quota\s*exceeded/i.test(rawMsg)) return fromMap('QUOTA_EXCEEDED', rawMsg)
256
+ if (/context.*(?:exceeded|overflow|length)/i.test(rawMsg)) return fromMap('CONTEXT_OVERFLOW', rawMsg)
257
+ if (/model.*not.*found|model.*does not exist/i.test(rawMsg)) return fromMap('MODEL_NOT_FOUND', rawMsg)
203
258
  return { code: 'UNKNOWN', title: 'Ошибка операции', message: rawMsg, hint: 'Проверьте параметры операции и логи.', raw: rawMsg }
204
259
  }
205
-
206
-
207
- /** Версию передаёт вызывающий: в бандл её подставляет build.py из package.json,
208
- * чтобы строка не устаревала руками (в 0.1.31 здесь стояло «0.1.29»). */
260
+ const formatErrorToast = (err) => {
261
+ const h = humanizeError(err)
262
+ if (!h) return null
263
+ return {
264
+ type: 'error',
265
+ title: h.title,
266
+ description: h.message + (h.hint ? ' ' + h.hint : '')
267
+ }
268
+ }
209
269
  const makeIssueUrl = (opts = {}, version = '') => {
210
270
  const repo = 'GooDAnDReaDY/dsh-russian-lang'
211
271
  const title = opts.title || (opts.plugin
@@ -234,45 +294,31 @@ const makeIssueUrl = (opts = {}, version = '') => {
234
294
  params.set('body', bodyLines.join('\n'))
235
295
  return `https://github.com/${repo}/issues/new?` + params.toString()
236
296
  }
237
-
238
- /** Словари приходят параметром — в бандле это RU, в тесте фикстура. */
239
297
  const makePluginLocalizationStatus = (dicts) => (ns) => {
240
298
  const none = { status: 'none', count: 0, label: 'RU отсутствует' }
241
299
  if (!ns) return none
242
300
  const count = Object.keys((dicts && dicts[ns]) || {}).length
243
301
  return count > 0 ? { status: 'full', count, label: `RU: ${count} строк` } : none
244
302
  }
245
-
246
-
247
303
  const typoQuotes = (text) => text
248
304
  .replace(/"([^"\n]{1,200})"/g, '«$1»')
249
305
  .replace(/[“„]([^“”\n]{1,200})[”"]/g, '«$1»')
250
306
  .replace(/[『「]([^』」\n]{1,200})[』」]/g, '«$1»')
251
-
252
307
  const typoDash = (text) => text
253
308
  .replace(/(^|[\s(\[«])--(?=\s|$)/g, '$1—')
254
309
  .replace(/(^|[\s(\[«])-(?=\s)/g, '$1—')
255
-
256
310
  const typoPunct = (text) => text.replace(/\s+([,.:;!?])(?=\s|$)/g, '$1')
257
-
258
311
  const TYPO_SHORT = new Set(['в', 'с', 'к', 'о', 'у', 'а', 'и', 'но', 'не', 'ни', 'на', 'по', 'до', 'из', 'за', 'от', 'об'])
259
-
260
312
  const typoNbsp = (text) => text.replace(/(^|[\s(\[«])([а-яё]{1,2})(\s+)/g, (match, lead, word) => (
261
313
  TYPO_SHORT.has(word) ? lead + word + ' ' : match
262
314
  ))
263
-
264
315
  const YO_EDGE_L = '(?<![а-яёА-ЯЁ])'
265
316
  const YO_EDGE_R = '(?![а-яёА-ЯЁ])'
266
-
267
- /** Восстанавливает регистр совпадения: «ЕЩЕ» -> «ЕЩЁ», «Еще» -> «Ещё». */
268
317
  const yoCase = (src, repl) => {
269
318
  if (src === src.toUpperCase() && src !== src.toLowerCase()) return repl.toUpperCase()
270
319
  if (src[0] === src[0].toUpperCase()) return repl[0].toUpperCase() + repl.slice(1)
271
320
  return repl
272
321
  }
273
-
274
- /** pairs — [[«еще», «ещё»], ...] из build.py (ручные + корпусные, за вычетом
275
- * омографов). Регистр разбирает yoCase, поэтому пара нужна одна на слово. */
276
322
  const makeTypoYo = (pairs) => {
277
323
  const compiled = pairs.map((p) => [new RegExp(YO_EDGE_L + p[0] + YO_EDGE_R, 'gi'), p[1]])
278
324
  return (text) => {
@@ -280,25 +326,17 @@ const makeTypoYo = (pairs) => {
280
326
  return text
281
327
  }
282
328
  }
283
-
284
-
285
329
  const LAYOUT_LAT_TO_CYR = { ...EN_RU_KEYS, '/': '.', '`': 'ё' }
286
-
287
330
  const LAYOUT_CYR_TO_LAT = (() => {
288
331
  const out = {}
289
332
  for (const k in LAYOUT_LAT_TO_CYR) out[LAYOUT_LAT_TO_CYR[k]] = k
290
333
  return out
291
334
  })()
292
-
293
335
  const translit = (word, map) => {
294
336
  let out = ''
295
337
  for (const ch of word.toLowerCase()) out += map[ch] !== undefined ? map[ch] : ch
296
338
  return out
297
339
  }
298
-
299
- /** freq — частотный корпус, localDict — выученные за сессию слова (#67).
300
- * Оба приходят снаружи: в бандле это встроенный список и Set в памяти,
301
- * в тесте — фикстуры. */
302
340
  const makeLayout = (freq, localDict = new Set()) => {
303
341
  const ruWordFraction = (text) => {
304
342
  const words = text.toLowerCase().split(/[^а-яё]+/).filter(Boolean)
@@ -322,38 +360,21 @@ const makeLayout = (freq, localDict = new Set()) => {
322
360
  }
323
361
  return { ruWordFraction, candidate, learnWords, localDict }
324
362
  }
325
-
326
-
327
-
328
363
  const SHORT_WORDS_RE = /(^|[\s(«])([вВнНсСпПоОкКуУиИаА]|из|от|до|за|по|со|во|ко|об|на|под|над|при|про|без|для|не|ни|но)( )/g
329
-
330
- /**
331
- * Живая типографика для поля ввода DSH.
332
- * Заменяет кавычки "" на «», дефисы -- на тире —, многоточие ... на …,
333
- * и добавляет неразрывные пробелы после коротких предлогов.
334
- * ИГНОРИРУЕТ код внутри обратных кавычек `...` и блоков ```...```.
335
- */
336
364
  const formatInputLive = (text) => {
337
365
  if (!text || typeof text !== 'string') return text
338
-
339
366
  const parts = text.split(/(```[\s\S]*?```|`[^`\n]*`)/g)
340
367
  for (let i = 0; i < parts.length; i += 2) {
341
368
  let s = parts[i]
342
369
  if (!s) continue
343
-
344
370
  s = s.replace(/--/g, '—')
345
-
346
371
  s = s.replace(/(^|[\s([{-])"/g, '$1«')
347
372
  s = s.replace(/"/g, '»')
348
-
349
373
  s = s.replace(SHORT_WORDS_RE, '$1$2\u00A0')
350
-
351
374
  parts[i] = s
352
375
  }
353
376
  return parts.join('')
354
377
  }
355
-
356
-
357
378
  const RUSSIAN_SLASH_ALIASES = {
358
379
  '/цель': '/goal',
359
380
  '/справка': '/help',
@@ -371,11 +392,6 @@ const RUSSIAN_SLASH_ALIASES = {
371
392
  '/сессия': '/session',
372
393
  '/модель': '/model'
373
394
  }
374
-
375
- /**
376
- * Разворачивает русский алиас слэш-команды в каноническую команду DSH.
377
- * Например: "/цель сделать тесты" -> "/goal сделать тесты"
378
- */
379
395
  const expandSlashAlias = (input) => {
380
396
  if (!input || typeof input !== 'string' || !input.startsWith('/')) return input
381
397
  const match = input.match(/^(\s*\/[^\s]+)(.*)$/)
@@ -387,8 +403,6 @@ const expandSlashAlias = (input) => {
387
403
  }
388
404
  return input
389
405
  }
390
-
391
-
392
406
  const PHONETIC_LAT_TO_CYR = [
393
407
  ['shch', 'щ'], ['yo', 'ё'], ['zh', 'ж'], ['ch', 'ч'], ['sh', 'ш'],
394
408
  ['yu', 'ю'], ['ya', 'я'], ['ts', 'ц'],
@@ -397,7 +411,6 @@ const PHONETIC_LAT_TO_CYR = [
397
411
  ['n', 'н'], ['o', 'о'], ['p', 'п'], ['r', 'р'], ['s', 'с'], ['t', 'т'],
398
412
  ['u', 'у'], ['f', 'ф'], ['h', 'х'], ['c', 'ц'], ['y', 'ы'], ['x', 'кс']
399
413
  ]
400
-
401
414
  const PHONETIC_CYR_TO_LAT = [
402
415
  ['щ', 'shch'], ['ё', 'yo'], ['ж', 'zh'], ['ч', 'ch'], ['ш', 'sh'],
403
416
  ['ю', 'yu'], ['я', 'ya'], ['ц', 'ts'],
@@ -406,7 +419,6 @@ const PHONETIC_CYR_TO_LAT = [
406
419
  ['н', 'n'], ['о', 'o'], ['п', 'p'], ['р', 'r'], ['с', 's'], ['т', 't'],
407
420
  ['у', 'u'], ['ф', 'f'], ['х', 'h'], ['ы', 'y'], ['э', 'e'], ['ъ', ''], ['ь', '']
408
421
  ]
409
-
410
422
  const phoneticTranslit = (text, direction = 'lat2cyr') => {
411
423
  if (!text || typeof text !== 'string') return text
412
424
  let res = text
@@ -416,7 +428,6 @@ const phoneticTranslit = (text, direction = 'lat2cyr') => {
416
428
  const fromTitle = from[0].toUpperCase() + from.slice(1)
417
429
  const toUpper = to.toUpperCase()
418
430
  const toTitle = to[0] ? to[0].toUpperCase() + to.slice(1) : ''
419
-
420
431
  if (from.length > 1) {
421
432
  res = res.replaceAll(fromUpper, toUpper)
422
433
  res = res.replaceAll(fromTitle, toTitle)
@@ -428,11 +439,6 @@ const phoneticTranslit = (text, direction = 'lat2cyr') => {
428
439
  }
429
440
  return res
430
441
  }
431
-
432
-
433
- /**
434
- * Определяет преобладающий язык ввода в тексте: 'RU', 'EN' или null.
435
- */
436
442
  const detectInputLayout = (text) => {
437
443
  if (!text || typeof text !== 'string') return null
438
444
  const clean = text.replace(/`[^`]*`/g, '').replace(/https?:\/\/\S+/g, '')
@@ -447,33 +453,44 @@ const detectInputLayout = (text) => {
447
453
  if (lat > cyr && lat >= 2) return 'EN'
448
454
  return null
449
455
  }
450
-
451
-
452
456
  const SYSTEM_PROMPT_PRESETS = {
453
457
  technical_expert: {
454
458
  id: 'technical_expert',
455
459
  label: 'Технический эксперт',
456
- desc: 'Строгая русская инженерная терминология, чистый код и русские комментарии',
457
- text: 'Отвечай пользователю на русском языке. Используй точную русскую инженерную терминологию, пиши чистый, идиоматичный код с русскими комментариями и следуй лучшим практикам разработки. Если пользователь пишет на другом языке, отвечай на его языке.'
460
+ desc: 'Точная терминология, чистый код и русские комментарии',
461
+ text: 'Отвечай пользователю на русском языке. Используй точную инженерную терминологию, пиши чистый код и русские комментарии.'
458
462
  },
459
463
  tech_writer: {
460
464
  id: 'tech_writer',
461
465
  label: 'Технический писатель',
462
- desc: 'Структурированные тексты, Markdown, ГОСТ/RFC и выверенная типографика',
463
- text: 'Отвечай пользователю на русском языке. Оформляй документацию и пояснения в структурированном виде (Markdown, списки, таблицы), строго соблюдай правила русской типографики («кавычки-ёлочки», длинное тире, буква «ё») и требования к техническим текстам.'
466
+ desc: 'Структурированные тексты, Markdown и выверенная типографика',
467
+ text: 'Отвечай пользователю на русском языке. Оформляй документацию в Markdown, строго соблюдай правила русской типографики («ёлочки», тире, «ё»).'
464
468
  },
465
469
  concise: {
466
470
  id: 'concise',
467
471
  label: 'Лаконичный режим',
468
- desc: 'Краткие ёмкие ответы без лишней вводной воды и повторов',
469
- text: 'Отвечай пользователю на русском языке максимально кратко и ёмко. Без лишних вводных слов, повторов и пространных рассуждений. Сразу переходи к сути, коду или прямому решению задачи.'
472
+ desc: 'Краткие ёмкие ответы без вводных слов и воды',
473
+ text: 'Отвечай пользователю на русском языке максимально кратко и ёмко. Без вводных слов и воды, сразу код или решение.'
474
+ },
475
+ code_reviewer: {
476
+ id: 'code_reviewer',
477
+ label: 'Код-ревьюер',
478
+ desc: 'Поиск багов, безопасность и архитектурный аудит',
479
+ text: 'Анализируй код и отвечай на русском языке. Ищи уязвимости, граничные случаи и нарушения контрактов, предлагая исправления.'
480
+ },
481
+ architect: {
482
+ id: 'architect',
483
+ label: 'Системный архитектор',
484
+ desc: 'Контракты API/CLI, модульность и надёжность',
485
+ text: 'Отвечай на русском языке как архитектор. Проектируй модульные системы, чёткие контракты API/CLI и надёжные решения без лишней сложности.'
486
+ },
487
+ tutor: {
488
+ id: 'tutor',
489
+ label: 'Наставник (ментор)',
490
+ desc: 'Понятные объяснения сложных концепций и пошаговый разбор',
491
+ text: 'Отвечай на русском языке как наставник. Объясняй сложные темы простыми словами, наглядными аналогиями и пошаговыми примерами.'
470
492
  }
471
493
  }
472
-
473
-
474
- /**
475
- * Генерирует локализованный Markdown-отчёт по сессии диалога.
476
- */
477
494
  const exportSessionToMarkdown = (session, options = {}) => {
478
495
  if (!session) return ''
479
496
  const title = session.title || session.name || 'Диалог DSH'
@@ -482,21 +499,18 @@ const exportSessionToMarkdown = (session, options = {}) => {
482
499
  dateStyle: 'full',
483
500
  timeStyle: 'medium'
484
501
  }).format(date)
485
-
486
502
  const messages = Array.isArray(session.messages) ? session.messages : []
487
503
  let md = `# 💬 ${title}\n\n`
488
504
  md += `> **Дата экспорта:** ${dateStr}\n`
489
505
  if (session.model) md += `> **Модель:** \`${session.model}\`\n`
490
506
  if (session.workspace) md += `> **Рабочая область:** \`${session.workspace}\`\n`
491
507
  md += `> **Всего сообщений:** ${messages.length}\n\n---\n\n`
492
-
493
508
  for (let idx = 0; idx < messages.length; idx++) {
494
509
  const msg = messages[idx]
495
510
  const role = msg.role || 'unknown'
496
511
  const roleName = role === 'user' ? '👤 Пользователь' :
497
512
  role === 'assistant' ? '🤖 Ассистент' :
498
513
  role === 'system' ? '⚙️ Система' : `🔧 Инструмент (${role})`
499
-
500
514
  md += `### ${roleName}\n\n`
501
515
  if (msg.content) {
502
516
  md += `${msg.content}\n\n`
@@ -510,63 +524,264 @@ const exportSessionToMarkdown = (session, options = {}) => {
510
524
  }
511
525
  md += `---\n\n`
512
526
  }
513
-
514
527
  md += `*Сгенерировано с помощью [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) и @goodandready/dsh-russian-lang*\n`
515
528
  return md
516
529
  }
517
-
518
-
519
- /** namespace -> { ключ: перевод } */
520
- const RU = {"common":{"back":"Назад","brand.localBuild":"Локальная сборка DSH","cancel":"Отмена","close":"Закрыть","collapse":"Свернуть","copied":"Скопировано","copy":"Копировать","copy.compactJson":"Копировать компактный JSON","copy.failed":"Не удалось скопировать","copy.json":"Копировать JSON","copy.optionsHint":"{action}; правый клик для вариантов копирования","copy.path":"Копировать путь к свойству","copy.prettyJson":"Копировать форматированный JSON","copy.value":"Копировать значение","delete":"Удалить","edit":"Изменить","expand":"Развернуть","json.collapseNode":"Свернуть узел JSON","json.expandNode":"Развернуть узел JSON","json.label":"JSON","load.failed":"Не удалось загрузить","loading":"Загрузка…","markdown.footnotes":"Сноски","markdown.truncatedCharacters":"… обрезано на {total} символах","more":"Ещё","next":"Дальше","none":"Нет","number.million":"{value}M","number.thousand":"{value}K","ok":"ОК","previous":"Назад","retry":"Повторить","save":"Сохранить","search":"Поиск","skip":"Пропустить","submit":"Отправить","submitting":"Отправка…","truncated":"Обрезано","unknown":"Неизвестно"},"dsh-cron":{"sidebar.label":"Задачи по расписанию"},"dsh-usage-stats":{"footer.todayLabel":"Сегодня"},"pluginMarket":{"trigger":"Магазин плагинов"}}
521
-
522
- /** zh-строка -> ru-строка для DOM-перевода панелей вне locale-ядра. */
523
- const ZH_RU = {}
524
-
525
- RU['russian-lang'] = {"cardTitle": "Русская локализация", "cardSub": "Язык интерфейса, типографика, раскладка", "badgeRu": "🟢 RU активен", "badgeEn": "⚪ EN активен", "badgeCoverage": "🟢 100% (7,902 ключа)", "badgeSmartUx": "⚡ Smart UX активен", "secLanguage": "🌐 Язык интерфейса", "secLanguageDesc": "Нативное переключение языка интерфейса DSH на русский без перезагрузки страницы.", "enabled": "Русский язык включён", "enabledDesc": "Переключает язык интерфейса DeepSeek Harness на русский.", "quickSwitchNote": "Быстрый переключатель RU ⇄ EN доступен в шапке сессии рядом с кнопками диалога.", "secTypography": "✍️ Умная типографика и ввод (Smart UX)", "secTypographyDesc": "Автоматическое улучшение текстов по нормам русской типографики во время диалога и набора.", "typography": "Типографика вывода", "typographyDesc": "Исправляет типографику ответов модели: кавычки-«ёлочки», тире («—») вместо дефисов, неразрывные пробелы после предлогов.", "yo": "Буква «ё»", "yoDesc": "Восстанавливать «ё» в частых словах (ещё, чёрный, идёт и др.), написанных через «е». Неоднозначные слова (все/всё) не трогаются.", "liveInput": "Живая типографика инпута", "liveInputDesc": "Автоматически заменять \"\" на «» и -- на — прямо во время набора промпта (код в бэктиках игнорируется).", "slashAliases": "Русские алиасы команд", "slashAliasesDesc": "Поддержка русских команд: /цель -> /goal, /сжать -> /compact, /план -> /plan, /справка -> /help, /память -> /memory.", "altLHintText": "Мгновенная конвертация раскладки текущего поля (ghbdtn ⇄ привет, /vjltkm ⇄ /model). В углу поля ввода также отображается метка раскладки.", "secAgentPrompt": "🤖 Системный промпт агента", "secAgentPromptDesc": "Официальная секция расширения DSH systemPrompt для ведения диалога на русском языке.", "agentPrompt": "Русский промпт агента", "agentPromptDesc": "Добавляет в системный промпт инструкцию отвечать по-русски в выбранном стиле.", "agentPromptPreset": "Стиль ответов агента", "presetExpert": "Технический эксперт (строгая терминология, чистый код)", "presetWriter": "Технический писатель (Markdown, таблицы, ГОСТ)", "presetConcise": "Лаконичный режим (кратко, без лишней воды)", "secUpdater": "🔄 Обновление языкового пакета", "secUpdaterDesc": "Проверка наличия новых релизов в реестре npm и обновление в один клик.", "updaterCurrent": "Текущая версия: v{version}", "updaterLatest": "Доступна новая версия: v{version}", "updaterUpToDate": "Установлена актуальная версия", "updaterChecking": "Проверка…", "updaterCheckBtn": "Проверить обновления", "updaterBtn": "Обновить до v{version} в 1 клик", "updaterUpdating": "Установка обновления…", "updaterSuccess": "✅ Плагин успешно обновлён! Перезапустите DSH для применения.", "updaterFailed": "❌ Не удалось проверить/обновить плагин. Проверьте сеть или логи сервера.", "badgeUpdateAvailable": "Доступно обновление", "badgeUpToDate": "Актуальная версия", "secTranslator": "🌐 Перевод сообщений ассистента", "secTranslatorDesc": "Настройка перевода ответов модели в чате на русский язык (локально в Docker или онлайн).", "translateEngine": "Движок перевода", "engineOff": "Выключен (по умолчанию, без сетевых вызовов)", "engineLocal": "Локальный LibreTranslate (приватно, ~600 МБ RAM)", "engineGoogle": "Google Translate (онлайн)", "googleWarn": "⚠️ Внимание: при онлайн-переводе текст сообщений ассистента передаётся на публичные серверы Google для перевода на лету. Не используйте для конфиденциальных данных.", "localStatusRunning": "🟢 Контейнер LibreTranslate активен", "localStatusStopped": "⚪ Контейнер LibreTranslate не запущен", "localStartBtn": "Запустить LibreTranslate в Docker (~600 МБ RAM)", "localStarting": "Запуск контейнера…", "localInfo": "LibreTranslate запускается изолированно в Docker на сервере, потребляет ~500–700 МБ RAM и переводит на 100% локально.", "secSupport": "📊 Покрытие экосистемы и поддержка", "secSupportDesc": "Словари синхронизированы с DSH v0.1.6-alpha.1. 100.0% UI-покрытие ядра и всех установленных плагинов (7,902 ключа) без черновых машинных переводов.", "statNamespaces": "Пространств имён", "statCoreKeys": "Ключей ядра", "statPluginKeys": "Ключей плагинов", "overridesCount": "Своих переопределений", "statusLoading": "Настройки загружаются…", "statusUnavailable": "Настройки недоступны на этом хосте", "translateTurn": "Перевести на русский", "reportIssue": "Сообщить о неточности перевода", "exportMdHint": "Экспорт диалога в Markdown доступен по кнопке [ 📥 MD ] в шапке сессии.", "translateTurnHint": "Перевод ответов ассистента на русский доступен по кнопке [ RU ↗ ] на блоках сообщений."}
526
-
530
+ const findTranslationKey = (text, dicts = {}, overrides = {}, zhRu = {}) => {
531
+ if (!text || typeof text !== 'string') return null
532
+ const trimmed = text.trim()
533
+ if (!trimmed) return null
534
+ if (overrides && typeof overrides === 'object') {
535
+ for (const [k, v] of Object.entries(overrides)) {
536
+ if (v === text || (typeof v === 'string' && v.trim() === trimmed)) {
537
+ return { key: k, value: v, source: 'override' }
538
+ }
539
+ }
540
+ }
541
+ if (dicts && typeof dicts === 'object') {
542
+ for (const [ns, map] of Object.entries(dicts)) {
543
+ if (!map || typeof map !== 'object') continue
544
+ for (const [k, v] of Object.entries(map)) {
545
+ if (v === text || (typeof v === 'string' && v.trim() === trimmed)) {
546
+ return { ns, key: k, value: v, source: 'dictionary' }
547
+ }
548
+ }
549
+ }
550
+ }
551
+ if (zhRu && typeof zhRu === 'object') {
552
+ for (const [zh, ru] of Object.entries(zhRu)) {
553
+ if (ru === text || (typeof ru === 'string' && ru.trim() === trimmed)) {
554
+ return { zh, key: zh, value: ru, source: 'dom_zh' }
555
+ }
556
+ }
557
+ }
558
+ return null
559
+ }
560
+ const bilingualCommandMatch = (query, command, options = {}) => {
561
+ if (!query || !command) return { matched: false, score: 0 }
562
+ const q = String(query).trim().toLowerCase()
563
+ if (!q) return { matched: false, score: 0 }
564
+ const cmdObj = typeof command === 'string' ? { title: command } : command
565
+ const candidates = [
566
+ cmdObj.title,
567
+ cmdObj.label,
568
+ cmdObj.name,
569
+ cmdObj.originalTitle,
570
+ cmdObj.enTitle,
571
+ cmdObj.id,
572
+ cmdObj.description,
573
+ cmdObj.category,
574
+ ...(Array.isArray(cmdObj.keywords) ? cmdObj.keywords : [])
575
+ ].filter((c) => typeof c === 'string' && c.trim())
576
+ if (!candidates.length) return { matched: false, score: 0 }
577
+ const queryVariants = [{ text: q, kind: 'direct' }]
578
+ const lat2cyr = translit(q, LAYOUT_LAT_TO_CYR)
579
+ if (lat2cyr !== q) queryVariants.push({ text: lat2cyr, kind: 'layout' })
580
+ const cyr2lat = translit(q, LAYOUT_CYR_TO_LAT)
581
+ if (cyr2lat !== q) queryVariants.push({ text: cyr2lat, kind: 'layout' })
582
+ const phonLat2Cyr = phoneticTranslit(q, 'lat2cyr')
583
+ if (phonLat2Cyr && phonLat2Cyr !== q && phonLat2Cyr !== lat2cyr) {
584
+ queryVariants.push({ text: phonLat2Cyr, kind: 'phonetic' })
585
+ }
586
+ const phonCyr2Lat = phoneticTranslit(q, 'cyr2lat')
587
+ if (phonCyr2Lat && phonCyr2Lat !== q && phonCyr2Lat !== cyr2lat) {
588
+ queryVariants.push({ text: phonCyr2Lat, kind: 'phonetic' })
589
+ }
590
+ let bestScore = 0
591
+ let bestVariant = null
592
+ let bestCandidate = null
593
+ for (const variant of queryVariants) {
594
+ for (const cand of candidates) {
595
+ const score = fuzzyMatchRu(variant.text, cand)
596
+ if (score > bestScore) {
597
+ bestScore = score
598
+ bestVariant = variant
599
+ bestCandidate = cand
600
+ }
601
+ }
602
+ }
603
+ const threshold = options.threshold ?? 50
604
+ return {
605
+ matched: bestScore >= threshold,
606
+ score: bestScore,
607
+ matchedText: bestCandidate,
608
+ variant: bestVariant ? bestVariant.text : q,
609
+ kind: bestVariant ? bestVariant.kind : 'none'
610
+ }
611
+ }
612
+ const filterCommands = (query, commands = [], options = {}) => {
613
+ if (!Array.isArray(commands)) return []
614
+ if (!query || !String(query).trim()) return commands
615
+ const results = []
616
+ for (const cmd of commands) {
617
+ const res = bilingualCommandMatch(query, cmd, options)
618
+ if (res.matched) {
619
+ results.push({ item: cmd, ...res })
620
+ }
621
+ }
622
+ return results.sort((a, b) => b.score - a.score).map((r) => r.item)
623
+ }
624
+ const RU = {"common":{"back":"Назад","brand.localBuild":"Локальная сборка DSH","cancel":"Отмена","close":"Закрыть","collapse":"Свернуть","copied":"Скопировано","copy":"Копировать","copy.compactJson":"Копировать компактный JSON","copy.failed":"Не удалось скопировать","copy.json":"Копировать JSON","copy.optionsHint":"{action}; правый клик для вариантов копирования","copy.path":"Копировать путь к свойству","copy.prettyJson":"Копировать форматированный JSON","copy.value":"Копировать значение","delete":"Удалить","edit":"Изменить","expand":"Развернуть","json.collapseNode":"Свернуть узел JSON","json.expandNode":"Развернуть узел JSON","json.label":"JSON","load.failed":"Не удалось загрузить","loading":"Загрузка…","markdown.footnotes":"Сноски","markdown.truncatedCharacters":"… обрезано на {total} символах","more":"Ещё","next":"Дальше","none":"Нет","number.million":"{value}M","number.thousand":"{value}K","ok":"ОК","previous":"Назад","retry":"Повторить","save":"Сохранить","search":"Поиск","skip":"Пропустить","submit":"Отправить","submitting":"Отправка…","truncated":"Обрезано","unknown":"Неизвестно"},"dsh-cron":{"sidebar.label":"Задачи по расписанию"},"dsh-usage-stats":{"footer.todayLabel":"Сегодня"},"pluginMarket":{"trigger":"Магазин плагинов"}}
625
+ const ZH_RU = {}
626
+ RU['russian-lang'] = {"cardTitle":"Русская локализация","cardSub":"Язык интерфейса, типографика, раскладка","badgeRu":"🟢 RU активен","badgeEn":"⚪ EN активен","badgeCoverage":"🟢 100% (7,902 ключа)","badgeSmartUx":"⚡ Smart UX активен","secLanguage":"🌐 Язык интерфейса","secLanguageDesc":"Нативное переключение языка интерфейса DSH на русский без перезагрузки страницы.","enabled":"Русский язык включён","enabledDesc":"Переключает язык интерфейса DeepSeek Harness на русский.","quickSwitchNote":"Быстрый переключатель RU ⇄ EN доступен в шапке сессии рядом с кнопками диалога.","secTypography":"✍️ Умная типографика и ввод (Smart UX)","secTypographyDesc":"Автоматическое улучшение текстов по нормам русской типографики во время диалога и набора.","typography":"Типографика вывода","typographyDesc":"Исправляет типографику ответов модели: кавычки-«ёлочки», тире («—») вместо дефисов, неразрывные пробелы после предлогов.","yo":"Буква «ё»","yoDesc":"Восстанавливать «ё» в частых словах (ещё, чёрный, идёт и др.), написанных через «е». Неоднозначные слова (все/всё) не трогаются.","liveInput":"Живая типографика инпута","liveInputDesc":"Автоматически заменять \"\" на «» и -- на — прямо во время набора промпта (код в бэктиках игнорируется).","slashAliases":"Русские алиасы команд","slashAliasesDesc":"Поддержка русских команд: /цель -> /goal, /сжать -> /compact, /план -> /plan, /справка -> /help, /память -> /memory.","altLHintText":"Мгновенная конвертация раскладки текущего поля (ghbdtn ⇄ привет, /vjltkm ⇄ /model). В углу поля ввода также отображается метка раскладки.","secAgentPrompt":"🤖 Системный промпт агента","secAgentPromptDesc":"Официальная секция расширения DSH systemPrompt для ведения диалога на русском языке.","agentPrompt":"Русский промпт агента","agentPromptDesc":"Добавляет в системный промпт инструкцию отвечать по-русски в выбранном стиле.","agentPromptPreset":"Стиль ответов агента","presetExpert":"Технический эксперт (строгая терминология, чистый код)","presetWriter":"Технический писатель (Markdown, таблицы, ГОСТ)","presetConcise":"Лаконичный режим (кратко, без лишней воды)","presetReviewer":"Код-ревьюер (поиск багов, безопасность, аудит)","presetArchitect":"Системный архитектор (контракты, масштабируемость)","presetTutor":"Наставник (понятные объяснения, пошаговый разбор)","secUpdater":"🔄 Обновление языкового пакета","secUpdaterDesc":"Проверка наличия новых релизов в реестре npm и обновление в один клик.","updaterCurrent":"Текущая версия: v{version}","updaterLatest":"Доступна новая версия: v{version}","updaterUpToDate":"Установлена актуальная версия","updaterChecking":"Проверка…","updaterCheckBtn":"Проверить обновления","updaterBtn":"Обновить до v{version} в 1 клик","updaterUpdating":"Установка обновления…","updaterSuccess":"✅ Плагин успешно обновлён! Перезапустите DSH для применения.","updaterFailed":"❌ Не удалось проверить/обновить плагин. Проверьте сеть или логи сервера.","badgeUpdateAvailable":"Доступно обновление","badgeUpToDate":"Актуальная версия","secTranslator":"🌐 Перевод сообщений ассистента","secTranslatorDesc":"Настройка перевода ответов модели в чате на русский язык (локально в Docker или онлайн).","translateEngine":"Движок перевода","engineOff":"Выключен (по умолчанию, без сетевых вызовов)","engineLocal":"Локальный LibreTranslate (приватно, ~600 МБ RAM)","engineGoogle":"Google Translate (онлайн)","googleWarn":"⚠️ Внимание: при онлайн-переводе текст сообщений ассистента передаётся на публичные серверы Google для перевода на лету. Не используйте для конфиденциальных данных.","localStatusRunning":"🟢 Контейнер LibreTranslate активен","localStatusStopped":"⚪ Контейнер LibreTranslate не запущен","localStartBtn":"Запустить LibreTranslate в Docker (~600 МБ RAM)","localStarting":"Запуск контейнера…","localInfo":"LibreTranslate запускается изолированно в Docker на сервере, потребляет ~500–700 МБ RAM и переводит на 100% локально.","secOverrides":"✍️ Пользовательские переопределения (Custom Overrides)","secOverridesDesc":"Возможность заменить любую формулировку интерфейса DSH на собственную. Переопределения сохраняются в настройках и имеют наивысший приоритет.","overrideKeyPlaceholder":"Ключ (например, common.back или settings.title)","overrideValuePlaceholder":"Ваш русский перевод","overrideAddBtn":"Добавить","overrideEmpty":"Переопределений пока нет.","overrideDelete":"Удалить","overrideTip":"💡 Совет: зажмите Alt и кликните на любой переведённый элемент интерфейса, чтобы открыть всплывающий инспектор перевода и сразу изменить формулировку.","secSupport":"📊 Покрытие экосистемы и поддержка","secSupportDesc":"Словари синхронизированы с DSH v0.1.6-alpha.1. 100.0% UI-покрытие ядра и всех установленных плагинов (7,902 ключа) без черновых машинных переводов.","statNamespaces":"Пространств имён","statCoreKeys":"Ключей ядра","statPluginKeys":"Ключей плагинов","overridesCount":"Своих переопределений","statusLoading":"Настройки загружаются…","statusUnavailable":"Настройки недоступны на этом хосте","translateTurn":"Перевести на русский","reportIssue":"Сообщить о неточности перевода","exportMdHint":"Экспорт диалога в Markdown доступен по кнопке [ 📥 MD ] в шапке сессии.","translateTurnHint":"Перевод ответов ассистента на русский доступен по кнопке [ RU ↗ ] на блоках сообщений."}
527
627
  const SETTINGS_NS_NAME = 'russian-lang'
528
628
  const ROW_CONFIG_KEY = '@goodandready/dsh-russian-lang#russian-lang'
529
-
530
629
  function apply(ctx) {
531
630
  const runtime = ctx.locale
532
631
  const scope = ctx.settingsScope.bind({ namespace: SETTINGS_NS_NAME })
533
-
534
632
  for (const ns of Object.keys(RU)) {
535
633
  ctx.effect(() => {
536
634
  try { return ctx.locale.register(ns, 'ru', RU[ns]) }
537
635
  catch (err) { return () => {} }
538
636
  }, 'dsh-russian-lang: ' + ns)
539
637
  }
540
-
541
- if (typeof fetch === 'function') {
542
- fetch('/api/dsh-russian-lang/dict/all', { headers: { 'Accept': 'application/json' } })
543
- .then((res) => res.ok ? res.json() : null)
544
- .then((data) => {
545
- if (!data) return
546
- const allDicts = Object.assign({}, data.core || {}, data.plugins || {}, data)
547
- if (data.zhRu && typeof ZH_RU === 'object') {
548
- Object.assign(ZH_RU, data.zhRu)
549
- if (typeof updateZhRu === 'function') updateZhRu(data.zhRu)
638
+ const registerDictMap = (dictMap) => {
639
+ if (!dictMap || typeof dictMap !== 'object') return
640
+ for (const ns of Object.keys(dictMap)) {
641
+ if (ns === 'zhRu' || ns === 'plugins' || ns === 'core') continue
642
+ const dict = dictMap[ns]
643
+ if (typeof dict !== 'object' || !dict) continue
644
+ if (!RU[ns]) {
645
+ RU[ns] = dict
646
+ ctx.effect(() => {
647
+ try { return ctx.locale.register(ns, 'ru', dict) }
648
+ catch (err) { return () => {} }
649
+ }, 'dsh-russian-lang: ' + ns)
650
+ } else {
651
+ Object.assign(RU[ns], dict)
652
+ }
653
+ }
654
+ }
655
+ const CACHE_NAME = 'dsh-ru-cache-v1'
656
+ const loadLocalDict = (key) => {
657
+ try {
658
+ const raw = typeof localStorage !== 'undefined' ? localStorage.getItem('dsh_ru_' + key) : null
659
+ return raw ? JSON.parse(raw) : null
660
+ } catch (_) { return null }
661
+ }
662
+ const saveLocalDict = (key, etag, data) => {
663
+ try {
664
+ if (typeof localStorage !== 'undefined') {
665
+ localStorage.setItem('dsh_ru_' + key, JSON.stringify({ etag, data }))
666
+ }
667
+ } catch (_e) { /* noop */ }
668
+ }
669
+ try {
670
+ const cachedCore = loadLocalDict('core')
671
+ if (cachedCore && cachedCore.data && cachedCore.data.core) {
672
+ registerDictMap(cachedCore.data.core)
673
+ if (cachedCore.data.zhRu && typeof ZH_RU === 'object') {
674
+ Object.assign(ZH_RU, cachedCore.data.zhRu)
675
+ }
676
+ }
677
+ } catch (_e) { /* noop */ }
678
+ const fetchCachedResource = async (url, cacheKey, onData) => {
679
+ let etag = null
680
+ let delivered = false
681
+ if (typeof caches !== 'undefined') {
682
+ try {
683
+ const cache = await caches.open(CACHE_NAME)
684
+ const matched = await cache.match(url)
685
+ if (matched) {
686
+ etag = matched.headers.get('etag')
687
+ const parsed = await matched.json()
688
+ if (parsed) {
689
+ delivered = true
690
+ onData(parsed)
691
+ }
550
692
  }
551
- for (const ns of Object.keys(allDicts)) {
552
- if (ns === 'zhRu' || ns === 'plugins' || ns === 'core') continue
553
- const dict = allDicts[ns]
554
- if (typeof dict !== 'object' || !dict) continue
555
- if (!RU[ns]) {
556
- RU[ns] = dict
557
- ctx.effect(() => {
558
- try { return ctx.locale.register(ns, 'ru', dict) }
559
- catch (err) { return () => {} }
560
- }, 'dsh-russian-lang: ' + ns)
561
- } else {
562
- Object.assign(RU[ns], dict)
693
+ } catch (_e) { /* noop */ }
694
+ }
695
+ if (!delivered && cacheKey) {
696
+ const local = loadLocalDict(cacheKey)
697
+ if (local && local.data) {
698
+ etag = local.etag
699
+ delivered = true
700
+ onData(local.data)
701
+ }
702
+ }
703
+ if (typeof fetch !== 'function') return
704
+ try {
705
+ const headers = { 'Accept': 'application/json' }
706
+ if (etag) headers['If-None-Match'] = etag
707
+ const res = await fetch(url, { headers })
708
+ if (res.status === 304) return
709
+ if (res.ok) {
710
+ const newEtag = res.headers.get('etag')
711
+ const cloned = res.clone()
712
+ const data = await res.json()
713
+ if (data) {
714
+ onData(data)
715
+ if (typeof caches !== 'undefined') {
716
+ try {
717
+ const cache = await caches.open(CACHE_NAME)
718
+ await cache.put(url, cloned)
719
+ } catch (_e) { /* noop */ }
563
720
  }
721
+ if (cacheKey) saveLocalDict(cacheKey, newEtag, data)
564
722
  }
565
- if (typeof syncZhDom === 'function') syncZhDom()
566
- })
567
- .catch(() => {})
723
+ }
724
+ } catch (_e) { /* noop */ }
568
725
  }
569
-
726
+ const loadedPluginNames = new Set()
727
+ const pendingPluginLoads = new Set()
728
+ let pluginBatchTimer = null
729
+ const loadPluginDictionaries = (names) => {
730
+ if (typeof fetch !== 'function') return
731
+ const toLoad = []
732
+ for (const raw of names) {
733
+ const n = String(raw).trim()
734
+ if (!n || loadedPluginNames.has(n) || pendingPluginLoads.has(n)) continue
735
+ toLoad.push(n)
736
+ pendingPluginLoads.add(n)
737
+ }
738
+ const qs = toLoad.length > 0 ? ('?names=' + encodeURIComponent(toLoad.join(','))) : ''
739
+ const url = '/api/dsh-russian-lang/dict/plugins' + qs
740
+ const cacheKey = toLoad.length > 0 ? ('plugins_' + toLoad.sort().join('_')) : 'plugins_all'
741
+ fetchCachedResource(url, cacheKey, (data) => {
742
+ if (data && data.plugins) {
743
+ registerDictMap(data.plugins)
744
+ for (const n of toLoad) loadedPluginNames.add(n)
745
+ }
746
+ }).catch(() => {}).finally(() => {
747
+ for (const n of toLoad) pendingPluginLoads.delete(n)
748
+ })
749
+ }
750
+ const schedulePluginLoad = (name) => {
751
+ if (!name || loadedPluginNames.has(name) || pendingPluginLoads.has(name)) return
752
+ pendingPluginLoads.add(name)
753
+ if (pluginBatchTimer) clearTimeout(pluginBatchTimer)
754
+ pluginBatchTimer = setTimeout(() => {
755
+ const batch = Array.from(pendingPluginLoads).filter((n) => !loadedPluginNames.has(n))
756
+ if (batch.length > 0) loadPluginDictionaries(batch)
757
+ }, 50)
758
+ }
759
+ fetchCachedResource('/api/dsh-russian-lang/dict/core', 'core', (data) => {
760
+ if (!data) return
761
+ if (data.core) registerDictMap(data.core)
762
+ if (data.zhRu && typeof ZH_RU === 'object') {
763
+ Object.assign(ZH_RU, data.zhRu)
764
+ if (typeof updateZhRu === 'function') updateZhRu(data.zhRu)
765
+ }
766
+ if (typeof syncZhDom === 'function') syncZhDom()
767
+ setTimeout(() => { loadPluginDictionaries([]) }, 100)
768
+ }).catch(() => {
769
+ if (typeof fetch === 'function') {
770
+ fetch('/api/dsh-russian-lang/dict/all', { headers: { 'Accept': 'application/json' } })
771
+ .then((res) => res.ok ? res.json() : null)
772
+ .then((data) => {
773
+ if (!data) return
774
+ const allDicts = Object.assign({}, data.core || {}, data.plugins || {}, data)
775
+ if (data.zhRu && typeof ZH_RU === 'object') {
776
+ Object.assign(ZH_RU, data.zhRu)
777
+ if (typeof updateZhRu === 'function') updateZhRu(data.zhRu)
778
+ }
779
+ registerDictMap(allDicts)
780
+ if (typeof syncZhDom === 'function') syncZhDom()
781
+ })
782
+ .catch(() => {})
783
+ }
784
+ })
570
785
  const origTranslate = runtime.translate.bind(runtime)
571
786
  const getOverrides = () => {
572
787
  try {
@@ -577,6 +792,9 @@ const exportSessionToMarkdown = (session, options = {}) => {
577
792
  const getPluginLocalizationStatus = makePluginLocalizationStatus(RU)
578
793
  try {
579
794
  runtime.formatNumber = formatNumber
795
+ runtime.formatCompactNumber = formatCompactNumber
796
+ runtime.formatDate = formatDate
797
+ runtime.formatTokens = formatTokens
580
798
  runtime.formatRelativeTime = formatRelativeTime
581
799
  runtime.formatCurrency = formatCurrency
582
800
  runtime.inflect = inflect
@@ -584,6 +802,8 @@ const exportSessionToMarkdown = (session, options = {}) => {
584
802
  runtime.stemRussian = stemRussian
585
803
  runtime.fuzzyMatchRu = fuzzyMatchRu
586
804
  runtime.humanizeError = humanizeError
805
+ runtime.plural = plural
806
+ runtime.pluralForm = pluralForm
587
807
  } catch (err) { /* ignore */ }
588
808
  const lookupChain = () => {
589
809
  try {
@@ -592,11 +812,13 @@ const exportSessionToMarkdown = (session, options = {}) => {
592
812
  } catch (err) { /* ignore */ }
593
813
  return [runtime.getLocale().active]
594
814
  }
815
+ const translationRegistry = new Map()
595
816
  const lookup = (ns, key) => {
596
817
  const chain = lookupChain()
597
818
  return runtime.lookup.length >= 3 ? runtime.lookup(ns, key, chain) : runtime.lookup(ns, key)
598
819
  }
599
820
  runtime.translate = function (ns, key, params) {
821
+ if (ns && !RU[ns] && !loadedPluginNames.has(ns)) schedulePluginLoad(ns)
600
822
  const overrides = getOverrides()
601
823
  if (overrides[key] !== undefined) {
602
824
  return params ? fill(overrides[key], params) : overrides[key]
@@ -623,9 +845,12 @@ const exportSessionToMarkdown = (session, options = {}) => {
623
845
  }
624
846
  }
625
847
  }
626
- return origTranslate(ns, key, params)
848
+ const res = origTranslate(ns, key, params)
849
+ if (typeof res === 'string' && res && res.length < 300) {
850
+ translationRegistry.set(res.trim(), { ns, key, params, value: res })
851
+ }
852
+ return res
627
853
  }
628
-
629
854
  const syncLang = () => {
630
855
  try {
631
856
  if (typeof document !== 'undefined' && document.documentElement
@@ -634,15 +859,11 @@ const exportSessionToMarkdown = (session, options = {}) => {
634
859
  }
635
860
  } catch (err) { /* ignore */ }
636
861
  }
637
-
638
862
  const native = runtime.getLocale().locales.some((l) => l.id === 'ru')
639
-
640
863
  if (!native) {
641
864
  runtime.addLanguage({ id: 'ru', label: 'Русский', fallback: 'en' })
642
865
  syncLang()
643
-
644
866
  }
645
-
646
867
  const syncFlag = () => {
647
868
  try {
648
869
  const wantRu = runtime.getLocale().active === 'ru'
@@ -663,7 +884,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
663
884
  try { return runtime.subscribe(syncFlag) }
664
885
  catch (err) { return undefined }
665
886
  }, 'dsh-russian-lang: sync-flag')
666
-
667
887
  const activate = () => {
668
888
  try {
669
889
  if (runtime.getLocale().active === 'ru') return
@@ -680,11 +900,9 @@ const exportSessionToMarkdown = (session, options = {}) => {
680
900
  }
681
901
  ctx.effect(() => scope.subscribe(tryBoot), 'dsh-russian-lang: boot')
682
902
  tryBoot()
683
-
684
903
  const unsubscribeLang = runtime.subscribe(syncLang)
685
904
  ctx.effect(() => unsubscribeLang, 'dsh-russian-lang: html-lang')
686
905
  syncLang()
687
-
688
906
  const SPELL_ON = 'data-russian-lang-spell-on'
689
907
  const SPELL_WAS = 'data-russian-lang-spell-was'
690
908
  const LANG_WAS = 'data-russian-lang-lang-was'
@@ -747,63 +965,60 @@ const exportSessionToMarkdown = (session, options = {}) => {
747
965
  }
748
966
  }, 'dsh-russian-lang: spellcheck')
749
967
  syncSpell()
750
-
751
968
  const ZH_CJK = /[\u3400-\u9fff\uf900-\ufaff]/
752
969
  const ZH_RE_ESC = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
753
970
  const ZH_EXACT = new Map()
754
971
  const ZH_PATTERNS = []
755
-
756
972
  const CORE_ZH_PRESETS = {
757
- '请求批准': 'Запрашивать подтверждение',
758
- '完全放开': 'Полный доступ',
759
- '只读': 'Только чтение',
760
- '只能读,任何写入都需要审批。': 'Только чтение, любая запись требует подтверждения.',
761
- '工作区内可写;工作区外的操作请求人工审批。': 'Запись в рабочей области разрешена; операции вне рабочей области требуют подтверждения.',
762
- '全放行,不弹审批。': 'Полный доступ, запросы на подтверждение не выводятся.',
763
- '确定性规则 + 两阶段分类器自动决定;危险或故障时 fail-closed。': 'Детерминированные правила + двухэтапный классификатор; при рисках — безопасная блокировка.',
764
- '手动测试': 'Ручное тестирование',
765
- '事件': 'Событие',
766
- '模拟 (看匹配)': 'Симуляция (проверка)',
767
- '执行 (真实触发)': 'Выполнение (реальный триггер)',
768
- '通知渠道测试': 'Тест каналов уведомлений',
769
- '渠道': 'Канал',
770
- 'Slack 风格单行摘要': 'Сводка в стиле Slack',
771
- '发送测试通知': 'Отправить тестовое уведомление',
772
- '飞书通知': 'Уведомления Feishu',
773
- '扫码连接飞书': 'Подключить Feishu по QR-коду',
774
- '将创建名为 [DSH 通知机器人] 的飞书应用': 'Будет создано приложение Feishu [DSH 通知机器人]',
775
- '卡片截断长度': 'Длина обрезки карточки',
776
- '复制 YAML': 'Копировать YAML',
777
- '去抖': 'Дебаунс',
778
- '已断开': 'Отключено',
779
- '已连接': 'Подключено',
780
- '已保存': 'Сохранено',
781
- '保存失败': 'Не удалось сохранить',
782
- '编辑': 'Редактировать',
783
- '取消编辑': 'Отмена',
784
- '启用': 'Включить',
785
- '停用': 'Отключить',
786
- '重试': 'Повторить',
787
- '刷新': 'Обновить',
788
- '网络请求失败': 'Сетевой запрос не удался',
789
- '如未立即生效请重启': 'Если изменения не применились, перезапустите DSH',
790
- '通知机器人': 'Бот уведомлений',
791
- '飞书扫码授权二维码': 'QR-код авторизации Feishu',
792
- '在浏览器中打开飞书授权链接': 'Открыть ссылку авторизации Feishu в браузере',
793
- '重新扫码会覆盖现有应用凭据与本': 'Повторное сканирование перезапишет учётные данные',
794
- '扫码者本人接收通知卡片': 'Получатель карточки уведомлений — авторизованный пользователь',
795
- 'tool (可选)': 'Инструмент (опц.)',
796
- 'runningSubagents (可选)': 'Подагенты (опц.)',
797
- 'durationMs (可选)': 'Длительность мс (опц.)',
798
- 'usage 输入 (可选)': 'Входные токены (опц.)',
799
- 'usage 输出 (可选)': 'Выходные токены (опц.)',
800
- 'profile (写入哪个 profile 的 cordis.patch.yml)': 'Профиль (куда записать cordis.patch.yml)',
801
- 'URL (留空用 DSH_HOOKS_WEBHOOK_URL)': 'URL (по умолчанию DSH_HOOKS_WEBHOOK_URL)',
802
- '当前已配置的 Hook 规则列表': 'Список текущих настроенных правил хуков',
803
- '测试通道': 'Тест канала'
973
+ '请求批准': 'Запрашивать подтверждение',
974
+ '完全放开': 'Полный доступ',
975
+ '只读': 'Только чтение',
976
+ '只能读,任何写入都需要审批。': 'Только чтение, любая запись требует подтверждения.',
977
+ '工作区内可写;工作区外的操作请求人工审批。': 'Запись в рабочей области разрешена; операции вне рабочей области требуют подтверждения.',
978
+ '全放行,不弹审批。': 'Полный доступ, запросы на подтверждение не выводятся.',
979
+ '确定性规则 + 两阶段分类器自动决定;危险或故障时 fail-closed。': 'Детерминированные правила + двухэтапный классификатор; при рисках — безопасная блокировка.',
980
+ '手动测试': 'Ручное тестирование',
981
+ '事件': 'Событие',
982
+ '模拟 (看匹配)': 'Симуляция (проверка)',
983
+ '执行 (真实触发)': 'Выполнение (реальный триггер)',
984
+ '通知渠道测试': 'Тест каналов уведомлений',
985
+ '渠道': 'Канал',
986
+ 'Slack 风格单行摘要': 'Сводка в стиле Slack',
987
+ '发送测试通知': 'Отправить тестовое уведомление',
988
+ '飞书通知': 'Уведомления Feishu',
989
+ '扫码连接飞书': 'Подключить Feishu по QR-коду',
990
+ '将创建名为 [DSH 通知机器人] 的飞书应用': 'Будет создано приложение Feishu [DSH 通知机器人]',
991
+ '卡片截断长度': 'Длина обрезки карточки',
992
+ '复制 YAML': 'Копировать YAML',
993
+ '去抖': 'Дебаунс',
994
+ '已断开': 'Отключено',
995
+ '已连接': 'Подключено',
996
+ '已保存': 'Сохранено',
997
+ '保存失败': 'Не удалось сохранить',
998
+ '编辑': 'Редактировать',
999
+ '取消编辑': 'Отмена',
1000
+ '启用': 'Включить',
1001
+ '停用': 'Отключить',
1002
+ '重试': 'Повторить',
1003
+ '刷新': 'Обновить',
1004
+ '网络请求失败': 'Сетевой запрос не удался',
1005
+ '如未立即生效请重启': 'Если изменения не применились, перезапустите DSH',
1006
+ '通知机器人': 'Бот уведомлений',
1007
+ '飞书扫码授权二维码': 'QR-код авторизации Feishu',
1008
+ '在浏览器中打开飞书授权链接': 'Открыть ссылку авторизации Feishu в браузере',
1009
+ '重新扫码会覆盖现有应用凭据与本': 'Повторное сканирование перезапишет учётные данные',
1010
+ '扫码者本人接收通知卡片': 'Получатель карточки уведомлений — авторизованный пользователь',
1011
+ 'tool (可选)': 'Инструмент (опц.)',
1012
+ 'runningSubagents (可选)': 'Подагенты (опц.)',
1013
+ 'durationMs (可选)': 'Длительность мс (опц.)',
1014
+ 'usage 输入 (可选)': 'Входные токены (опц.)',
1015
+ 'usage 输出 (可选)': 'Выходные токены (опц.)',
1016
+ 'profile (写入哪个 profile 的 cordis.patch.yml)': 'Профиль (куда записать cordis.patch.yml)',
1017
+ 'URL (留空用 DSH_HOOKS_WEBHOOK_URL)': 'URL (по умолчанию DSH_HOOKS_WEBHOOK_URL)',
1018
+ '当前已配置的 Hook 规则列表': 'Список текущих настроенных правил хуков',
1019
+ '测试通道': 'Тест канала'
804
1020
  }
805
1021
  for (const [k, v] of Object.entries(CORE_ZH_PRESETS)) ZH_EXACT.set(k, v)
806
-
807
1022
  const zhSortedExact = []
808
1023
  const rebuildZhSorted = () => {
809
1024
  zhSortedExact.length = 0
@@ -811,7 +1026,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
811
1026
  zhSortedExact.sort((a, b) => b[0].length - a[0].length)
812
1027
  }
813
1028
  rebuildZhSorted()
814
-
815
1029
  const updateZhRu = (entries) => {
816
1030
  if (!entries || typeof entries !== 'object') return
817
1031
  for (const [zhText, ruText] of Object.entries(entries)) {
@@ -828,7 +1042,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
828
1042
  rebuildZhSorted()
829
1043
  }
830
1044
  updateZhRu(ZH_RU)
831
-
832
1045
  const zhTranslateText = (text) => {
833
1046
  if (!ZH_CJK.test(text)) return null
834
1047
  const exact = ZH_EXACT.get(text)
@@ -857,107 +1070,175 @@ const exportSessionToMarkdown = (session, options = {}) => {
857
1070
  if (changed) return replaced
858
1071
  return null
859
1072
  }
860
-
861
1073
  const DOM_EN_ATTRS = {
862
1074
  'Streaming preview': 'Предпросмотр стриминга',
863
1075
  'Visualization streaming preview': 'Предпросмотр визуализации',
864
1076
  'Security Auditor Shield': 'Защитный щит аудитора',
865
1077
  'Shield: Safe': 'Щит: Безопасно',
866
- 'Shield: Alert': 'Щит: Тревога'
1078
+ 'Shield: Alert': 'Щит: Тревога',
1079
+ 'e.g. Implement report export and cover with unit tests': 'Например: Реализовать экспорт отчёта и покрыть юнит-тестами',
1080
+ 'e.g. Implement report export and cover with unit tests...': 'Например: Реализовать экспорт отчёта и покрыть юнит-тестами…',
1081
+ 'e.g. Implement user profile settings card with theme tokens': 'Например: Реализовать карточку настроек профиля с токенами темы',
1082
+ 'Detailed functional specs, acceptance criteria, constraints...': 'Детальная функциональная спецификация, критерии приёмки, ограничения…',
1083
+ 'Specific instructions, questions, or requirements for this specialist...': 'Конкретные инструкции, вопросы или требования для данного специалиста…',
867
1084
  }
868
1085
  const DOM_EN_TEXT = {
869
- 'Scheduled tasks': 'Задачи по расписанию',
870
- 'Today': 'Сегодня',
871
- 'Plugin Market': 'Магазин плагинов',
872
- 'Auto mode': 'Автоматический режим',
873
- 'Full access': 'Полный доступ',
874
- 'Read only': 'Только чтение',
875
- 'Side card': 'Боковая панель',
876
- 'panelName': 'Палитра команд',
877
- 'Hooks': 'Хуки',
878
- 'Manage what the side card shows and how it behaves': 'Настройка содержимого и поведения боковой панели',
879
- 'Inject the sidebar-open tool for the model': 'Предоставить модели инструмент sidebar-open',
880
- 'When enabled, the model can actively open files, folders, and HTTP(S) pages in the sidebar through the sidebar_open tool (off by default)': 'Если включено, модель может открывать файлы, папки и веб-страницы в боковой панели через инструмент sidebar_open (по умолчанию выключено)',
881
- 'Position compatibility mode': 'Режим совместимости расположения',
882
- 'Pick the title-bar compatibility scheme: auto-detect (default, conservative) / DSH official web / known desktop shells / custom (shift distance + custom CSS)': 'Выберите схему совместимости строки заголовка: автоопределение (по умолчанию) / официальный DSH Web / десктопные оболочки / пользовательский режим',
883
- 'Auto-detect': 'Автоопределение',
884
- 'Sidebar content': 'Содержимое боковой панели',
885
- 'Changes': 'Изменения',
886
- 'Tasks': 'Задачи',
887
- 'Time Machine': 'Машина времени',
888
- 'Live Canvas': 'Живой холст',
889
- 'Side Chat (beta)': 'Боковой чат (бета)',
890
- 'Terminal': 'Терминал',
891
- 'Feature settings': 'Настройки функции',
892
- 'Low': 'Низкий',
893
- 'Medium': 'Средний',
894
- 'High': 'Высокий',
895
- 'Effort': 'Рассуждения',
896
- 'Search engine (ModSearch)': 'Поисковая система (ModSearch)',
897
- 'Search engine provider configuration.': 'Настройка провайдера поисковой системы.',
898
- 'X search only': 'Только поиск в X',
899
- 'Security Auditor Shield': 'Защитный щит аудитора',
900
- 'Shield: Safe': 'Щит: Безопасно',
901
- 'Shield: Alert': 'Щит: Тревога',
902
- 'Lens': 'Линза',
903
- 'Gallery': 'Галерея',
904
- 'SSH: Local': 'SSH: Локально',
905
- 'SSH: Remote': 'SSH: Удалённо',
906
- '7/7 rot': '7/7 рот.',
907
- 'Smoke chat': 'Тестовый чат',
908
- 'Register in Models': 'Зарегистрировать в моделях',
909
- 'Base URL': 'Базовый URL',
910
- 'API key env / credential name': 'Имя переменной окружения / ключа API',
911
- 'Default model': 'Модель по умолчанию',
912
- 'Enabled': 'Включено',
913
- 'Save': 'Сохранить',
914
- 'Saved': 'Сохранено',
915
- 'Refresh': 'Обновить',
916
- 'Loading settings…': 'Загрузка настроек…',
917
- 'Put the ar-… key in DSH credentials or process env under this name. Never paste the key into this form.': 'Укажите ключ ar-… в учётных данных DSH или переменной окружения. Не вставляйте ключ в форму.',
918
- 'Preferred engine': 'Предпочитаемый движок',
919
- 'API key': 'API-ключ',
920
- 'stored, leave empty to keep it': 'сохранён, оставьте пустым для сохранения',
921
- 'Separate multiple keys with commas. ModSearch rotates to the next key after authentication, rate-limit, or quota failures.': 'Разделяйте несколько ключей запятыми. ModSearch переключается на следующий ключ при ошибках аутентификации или лимитов.',
922
- 'Built-in official endpoint, leave blank to use it': 'Встроенная официальная конечная точка, оставьте пустым',
923
- 'Automatic engine chain Checked engines may join failover. Only engines ready here are listed.': 'Автоматическая цепочка движков: отмеченные движки участвуют в отказоустойчивости.',
924
- 'Discard': 'Сбросить',
925
- 'Model synchronization': 'Синхронизация моделей',
926
- 'Refresh model catalogs for API-key providers. A dry-run is the default; applying changes is explicit.': 'Обновление каталогов моделей провайдеров API-ключей. По умолчанию выполняется проверка (dry-run); применение изменений явное.',
927
- 'All API-key providers': 'Все провайдеры API-ключей',
928
- 'Preview only (dry-run)': 'Только предпросмотр (dry-run)',
929
- 'Confirm stale removal': 'Подтверждать удаление устаревших',
930
- 'Refresh status': 'Обновить статус',
931
- 'Refresh all': 'Обновить всё',
932
- 'Discover': 'Обнаружить',
933
- 'Check availability': 'Проверить доступность',
934
- 'Check credentials': 'Проверить учётные данные',
935
- 'Choose models': 'Выбрать модели',
936
- 'Manual model selection': 'Выбор моделей вручную',
937
- 'Images in chat are processed by the vision model you choose here. Leave both fields empty to auto-pick the first vision-capable model from the catalog.': 'Изображения в чате обрабатываются выбранной моделью зрения. Оставьте оба поля пустыми для автовыбора первой доступной модели.',
938
- 'Mode': 'Режим',
939
- 'Hybrid (auto-rewrite + tools)': 'Гибридный (авто-переписывание + инструменты)',
940
- 'Describe strategy': 'Стратегия описания',
941
- 'Auto (use vision LLM)': 'Авто (использовать Vision LLM)',
942
- 'Escalation': 'Эскалация',
943
- 'Simple only (one pass)': 'Только простая (один проход)',
944
- 'Routing': 'Маршрутизация',
945
- 'Channel order': 'Порядок каналов',
946
- 'Issue Reporter': 'Репортёр проблем',
947
- 'Turn a DSH plugin problem into a reviewable issue with automated diagnostics and previews.': 'Превратите проблему с плагином DSH в готовый issue с автоматической диагностикой.',
948
- 'GitHub sign-in is not configured for this installation.': 'Вход через GitHub не настроен для этой установки.',
949
- '0 plugins': '0 плагинов',
950
- 'Catalog': 'Каталог',
951
- 'Report Editor': 'Редактор отчёта',
952
- 'My Reports': 'Мои отчёты',
953
- 'Authorization': 'Авторизация',
954
- 'Search installed plugins…': 'Поиск установленных плагинов…',
955
- 'Search installed plugins...': 'Поиск установленных плагинов...',
956
- 'Refresh inventory': 'Обновить список',
957
- 'Loading inventory & status…': 'Загрузка списка и статуса…',
958
- 'Loading inventory & status...': 'Загрузка списка и статуса...'
1086
+ 'Scheduled tasks': 'Задачи по расписанию',
1087
+ 'Today': 'Сегодня',
1088
+ 'Plugin Market': 'Магазин плагинов',
1089
+ 'GitHub ops': 'Операции с GitHub',
1090
+ 'Auto mode': 'Автоматический режим',
1091
+ 'Full access': 'Полный доступ',
1092
+ 'Read only': 'Только чтение',
1093
+ 'Side card': 'Боковая панель',
1094
+ 'panelName': 'Палитра команд',
1095
+ 'Hooks': 'Хуки',
1096
+ 'Manage what the side card shows and how it behaves': 'Настройка содержимого и поведения боковой панели',
1097
+ 'Inject the sidebar-open tool for the model': 'Предоставить модели инструмент sidebar-open',
1098
+ 'Position compatibility mode': 'Режим совместимости расположения',
1099
+ 'Auto-detect': 'Автоопределение',
1100
+ 'Sidebar content': 'Содержимое боковой панели',
1101
+ 'Changes': 'Изменения',
1102
+ 'Tasks': 'Задачи',
1103
+ 'Time Machine': 'Машина времени',
1104
+ 'Live Canvas': 'Живой холст',
1105
+ 'Side Chat (beta)': 'Боковой чат (бета)',
1106
+ 'Terminal': 'Терминал',
1107
+ 'Feature settings': 'Настройки функции',
1108
+ 'Low': 'Низкий',
1109
+ 'Medium': 'Средний',
1110
+ 'High': 'Высокий',
1111
+ 'Effort': 'Рассуждения',
1112
+ 'Search engine (ModSearch)': 'Поисковая система (ModSearch)',
1113
+ 'Search engine provider configuration.': 'Настройка провайдера поисковой системы.',
1114
+ 'X search only': 'Только поиск в X',
1115
+ 'Security Auditor Shield': 'Защитный щит аудитора',
1116
+ 'Shield: Safe': 'Щит: Безопасно',
1117
+ 'Shield: Alert': 'Щит: Тревога',
1118
+ 'Lens': 'Линза',
1119
+ 'Gallery': 'Галерея',
1120
+ 'SSH: Local': 'SSH: Локально',
1121
+ 'SSH: Remote': 'SSH: Удалённо',
1122
+ '7/7 rot': '7/7 рот.',
1123
+ 'Smoke chat': 'Тестовый чат',
1124
+ 'Quick Launch Goal': 'Быстрый запуск цели',
1125
+ 'Define the objective for the agent in autonomous mode:': 'Сформулируйте задачу для автономной работы агента:',
1126
+ 'Fix Bug': 'Исправление бага',
1127
+ 'Refactor (YAGNI)': 'Рефакторинг (YAGNI)',
1128
+ 'Tests & Coverage': 'Тесты и покрытие',
1129
+ 'Code Review': 'Ревью кода',
1130
+ 'New Feature': 'Новая функция',
1131
+ 'Security Audit': 'Аудит безопасности',
1132
+ 'Docs & Contract': 'Документация и контракт',
1133
+ 'Upgrade Deps': 'Обновление зависимостей',
1134
+ 'Dead Code': 'Мёртвый код',
1135
+ 'Performance': 'Производительность',
1136
+ 'Start Goal': 'Запустить цель',
1137
+ 'Launch Multi-Agent Orchestrator': 'Запуск мультиагентного оркестратора',
1138
+ 'Full DAG Pipeline': 'Полный DAG-пайплайн',
1139
+ 'Direct Specialist Subagent': 'Прямой субагент-специалист',
1140
+ 'Objective Title': 'Название задачи',
1141
+ 'Scope & Requirements': 'Объём и требования',
1142
+ 'Topology Scenario': 'Топологический сценарий',
1143
+ 'Auto (Infer based on prompt complexity)': 'Авто (определить по сложности задачи)',
1144
+ 'Hotfix (1 Stage: Triage & Minimal Fix)': 'Хотфикс (1 этап: анализ и точечный фикс)',
1145
+ 'Simple (2 Stages: Spec + Exec)': 'Простой (2 этапа: ТЗ + реализация)',
1146
+ 'Medium (4 Stages: Spec -> Design -> Code -> QA)': 'Средний (4 этапа: ТЗ -> Дизайн -> Код -> Тестирование)',
1147
+ 'Complex (6 Stages: Full Engineering Lifecycle)': 'Сложный (6 этапов: полный инженерный цикл)',
1148
+ 'Enterprise (7 Stages: R&D Spike -> Fullstack -> Gate)': 'Enterprise (7 этапов: R&D исследование -> Фулстек -> Гейт приёмки)',
1149
+ 'Target Specialist Role': 'Роль целевого специалиста',
1150
+ 'UI/UX Interface Designer': 'UI/UX дизайнер интерфейсов',
1151
+ 'System Architect (DESIGN.md / ADR)': 'Системный архитектор (DESIGN.md / ADR)',
1152
+ 'Technical Spec Analyst': 'Аналитик технических спецификаций',
1153
+ 'Senior Frontend Developer': 'Ведущий frontend-разработчик',
1154
+ 'Senior Backend Developer': 'Ведущий backend-разработчик',
1155
+ 'QA Automation Engineer': 'Инженер автоматизации тестирования',
1156
+ 'Refactoring & Complexity Specialist': 'Специалист по рефакторингу и сложности',
1157
+ 'Hotfix & Diagnostic Engineer': 'Инженер хотфиксов и диагностики',
1158
+ 'Documentation Specialist': 'Технический писатель / Документация',
1159
+ 'Spike & R&D Researcher': 'Исследователь R&D и прототипирования',
1160
+ 'DevOps & Tooling Specialist': 'DevOps и инфраструктурный специалист',
1161
+ 'Instructions for Subagent': 'Инструкции для субагента',
1162
+ 'Start Pipeline': 'Запустить пайплайн',
1163
+ 'Delegate Subagent': 'Делегировать субагенту',
1164
+ 'Dispatching...': 'Отправка…',
1165
+ 'Discounted rate active': 'Действует сниженный тариф',
1166
+ 'off-peak': 'непиковый',
1167
+ 'peak': 'пиковый',
1168
+ 'Context cache saved:': 'Сэкономлено на кэше:',
1169
+ '1M tokens, $': '1 млн токенов, $',
1170
+ 'Input (cache hit)': 'Ввод (попадание в кэш)',
1171
+ 'Input (cache miss)': 'Ввод (промах кэша)',
1172
+ 'Output': 'Вывод',
1173
+ 'SESSION TOKENS': 'ТОКЕНЫ СЕССИИ',
1174
+ 'SPEND BY MODEL': 'РАСХОД ПО МОДЕЛЯМ',
1175
+ 'Session total:': 'Всего за сессию:',
1176
+ 'Copy Summary': 'Скопировать сводку',
1177
+ 'Context Lens': 'Линза контекста',
1178
+ 'Saved tokens': 'Сэкономлено токенов',
1179
+ '% Saved': '% экономии',
1180
+ 'ops': 'операций',
1181
+ 'Budget': 'Бюджет',
1182
+ 'Active focus': 'Активный фокус',
1183
+ 'No focus paths set': 'Пути фокусировки не заданы',
1184
+ 'Status': 'Статус',
1185
+ 'Graph & CI': 'Граф и CI',
1186
+ 'Events & PRs': 'События и PR',
1187
+ 'Branch': 'Ветка',
1188
+ 'Sync': 'Синхронизация',
1189
+ 'Up to date with remote (@{upstream})': 'Синхронизировано с удалённым репозиторием (@{upstream})',
1190
+ 'Up to date with remote': 'Синхронизировано с удалённым репозиторием',
1191
+ 'Clean': 'Чисто',
1192
+ 'No modified or untracked files in the working directory.': 'В рабочем каталоге нет изменённых или неотслеживаемых файлов.',
1193
+ 'RECENT COMMITS': 'ПОСЛЕДНИЕ КОММИТЫ',
1194
+ 'Active jobs': 'Активные задачи',
1195
+ 'No active jobs': 'Нет активных задач',
1196
+ 'Register in Models': 'Зарегистрировать в моделях',
1197
+ 'Base URL': 'Базовый URL',
1198
+ 'API key env / credential name': 'Имя переменной окружения / ключа API',
1199
+ 'Default model': 'Модель по умолчанию',
1200
+ 'Enabled': 'Включено',
1201
+ 'Save': 'Сохранить',
1202
+ 'Saved': 'Сохранено',
1203
+ 'Refresh': 'Обновить',
1204
+ 'Loading settings…': 'Загрузка настроек…',
1205
+ 'Preferred engine': 'Предпочитаемый движок',
1206
+ 'API key': 'API-ключ',
1207
+ 'stored, leave empty to keep it': 'сохранён, оставьте пустым для сохранения',
1208
+ 'Built-in official endpoint, leave blank to use it': 'Встроенная официальная конечная точка, оставьте пустым',
1209
+ 'Discard': 'Сбросить',
1210
+ 'Model synchronization': 'Синхронизация моделей',
1211
+ 'All API-key providers': 'Все провайдеры API-ключей',
1212
+ 'Preview only (dry-run)': 'Только предпросмотр (dry-run)',
1213
+ 'Confirm stale removal': 'Подтверждать удаление устаревших',
1214
+ 'Refresh status': 'Обновить статус',
1215
+ 'Refresh all': 'Обновить всё',
1216
+ 'Discover': 'Обнаружить',
1217
+ 'Check availability': 'Проверить доступность',
1218
+ 'Check credentials': 'Проверить учётные данные',
1219
+ 'Choose models': 'Выбрать модели',
1220
+ 'Manual model selection': 'Выбор моделей вручную',
1221
+ 'Mode': 'Режим',
1222
+ 'Hybrid (auto-rewrite + tools)': 'Гибридный (авто-переписывание + инструменты)',
1223
+ 'Describe strategy': 'Стратегия описания',
1224
+ 'Auto (use vision LLM)': 'Авто (использовать Vision LLM)',
1225
+ 'Escalation': 'Эскалация',
1226
+ 'Simple only (one pass)': 'Только простая (один проход)',
1227
+ 'Routing': 'Маршрутизация',
1228
+ 'Channel order': 'Порядок каналов',
1229
+ 'Issue Reporter': 'Репортёр проблем',
1230
+ 'GitHub sign-in is not configured for this installation.': 'Вход через GitHub не настроен для этой установки.',
1231
+ '0 plugins': '0 плагинов',
1232
+ 'Catalog': 'Каталог',
1233
+ 'Report Editor': 'Редактор отчёта',
1234
+ 'My Reports': 'Мои отчёты',
1235
+ 'Authorization': 'Авторизация',
1236
+ 'Search installed plugins…': 'Поиск установленных плагинов…',
1237
+ 'Search installed plugins...': 'Поиск установленных плагинов...',
1238
+ 'Refresh inventory': 'Обновить список',
1239
+ 'Loading inventory & status…': 'Загрузка списка и статуса…',
1240
+ 'Loading inventory & status...': 'Загрузка списка и статуса...'
959
1241
  }
960
-
961
1242
  const ZH_WALKER = (root) => {
962
1243
  try {
963
1244
  const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT)
@@ -1007,7 +1288,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
1007
1288
  }
1008
1289
  } catch (err) { /* ignore */ }
1009
1290
  }
1010
-
1011
1291
  let zhObserver = null
1012
1292
  const syncZhDom = () => {
1013
1293
  try {
@@ -1034,8 +1314,7 @@ const exportSessionToMarkdown = (session, options = {}) => {
1034
1314
  }
1035
1315
  }, 'dsh-russian-lang: zh-dom')
1036
1316
  syncZhDom()
1037
-
1038
- const TYPO_YO_PAIRS = [["еще", "ещё"], ["ее", "её"], ["черный", "чёрный"], ["черная", "чёрная"], ["черные", "чёрные"], ["зеленый", "зелёный"], ["желтый", "жёлтый"], ["легкий", "лёгкий"], ["тяжелый", "тяжёлый"], ["надежный", "надёжный"], ["дешевый", "дешёвый"], ["идет", "идёт"], ["дает", "даёт"], ["ведет", "ведёт"], ["несет", "несёт"], ["живет", "живёт"], ["привел", "привёл"], ["шел", "шёл"], ["нее", "неё"], ["мое", "моё"], ["серьезно", "серьёзно"], ["придется", "придётся"], ["насчет", "насчёт"], ["твое", "твоё"], ["свое", "своё"], ["пойдем", "пойдём"], ["пришел", "пришёл"], ["нашел", "нашёл"], ["идем", "идём"], ["вперед", "вперёд"], ["пошел", "пошёл"], ["ребенка", "ребёнка"], ["ребенок", "ребёнок"], ["счет", "счёт"], ["своем", "своём"], ["придет", "придёт"], ["ушел", "ушёл"], ["ждет", "ждёт"], ["мертв", "мёртв"], ["твоем", "твоём"], ["вернется", "вернётся"], ["днем", "днём"], ["найдем", "найдём"], ["пойдет", "пойдёт"], ["начнем", "начнём"], ["вернемся", "вернёмся"], ["остается", "остаётся"], ["принес", "принёс"], ["идешь", "идёшь"], ["трех", "трёх"], ["ребенком", "ребёнком"], ["самолет", "самолёт"], ["определенно", "определённо"], ["пойдешь", "пойдёшь"], ["умрет", "умрёт"], ["прием", "приём"], ["прошел", "прошёл"], ["убьет", "убьёт"], ["тетя", "тётя"], ["провел", "провёл"], ["произойдет", "произойдёт"], ["семьей", "семьёй"], ["пойдемте", "пойдёмте"], ["пройдет", "пройдёт"], ["возьмем", "возьмём"], ["вернешься", "вернёшься"], ["найдешь", "найдёшь"], ["живешь", "живёшь"], ["отчет", "отчёт"], ["займет", "займёт"], ["ждем", "ждём"], ["сошел", "сошёл"], ["вдвоем", "вдвоём"], ["найдет", "найдёт"], ["вел", "вёл"], ["живем", "живём"], ["зашел", "зашёл"], ["начнется", "начнётся"], ["путем", "путём"], ["подойдет", "подойдёт"], ["режиссер", "режиссёр"], ["уйдет", "уйдёт"], ["придешь", "придёшь"], ["идемте", "идёмте"], ["ведешь", "ведёшь"], ["пес", "пёс"], ["умрешь", "умрёшь"], ["ждешь", "ждёшь"], ["найдете", "найдёте"], ["найдется", "найдётся"], ["клево", "клёво"], ["поймешь", "поймёшь"], ["разберемся", "разберёмся"], ["времен", "времён"], ["введен", "введён"], ["включен", "включён"], ["включенных", "включённых"], ["возьмется", "возьмётся"], ["завершен", "завершён"], ["завершенного", "завершённого"], ["задает", "задаёт"], ["задается", "задаётся"], ["заменен", "заменён"], ["звезд", "звёзд"], ["звезды", "звёзды"], ["изменен", "изменён"], ["истек", "истёк"], ["незавершенная", "незавершённая"], ["неподтвержденные", "неподтверждённые"], ["несохраненные", "несохранённые"], ["обновлен", "обновлён"], ["обновленные", "обновлённые"], ["определен", "определён"], ["отклонен", "отклонён"], ["отменен", "отменён"], ["очередность", "очерёдность"], ["поврежден", "повреждён"], ["повторен", "повторён"], ["подключен", "подключён"], ["подтвержденные", "подтверждённые"], ["приемки", "приёмки"], ["раздает", "раздаёт"], ["разрешенная", "разрешённая"], ["разрешенные", "разрешённые"], ["создаем", "создаём"], ["сойдется", "сойдётся"], ["сохранен", "сохранён"], ["сохраненный", "сохранённый"], ["счету", "счёту"], ["тяжелые", "тяжёлые"], ["темная", "тёмная"], ["уберет", "уберёт"], ["удален", "удалён"], ["учетные", "учётные"], ["учетных", "учётных"], ["емкости", "ёмкости"], ["емкость", "ёмкость"]]
1317
+ const TYPO_YO_PAIRS = [["еще","ещё"],["ее","её"],["черный","чёрный"],["черная","чёрная"],["черные","чёрные"],["зеленый","зелёный"],["желтый","жёлтый"],["легкий","лёгкий"],["тяжелый","тяжёлый"],["надежный","надёжный"],["дешевый","дешёвый"],["идет","идёт"],["дает","даёт"],["ведет","ведёт"],["несет","несёт"],["живет","живёт"],["привел","привёл"],["шел","шёл"],["нее","неё"],["мое","моё"],["серьезно","серьёзно"],["придется","придётся"],["насчет","насчёт"],["твое","твоё"],["свое","своё"],["пойдем","пойдём"],["пришел","пришёл"],["нашел","нашёл"],["идем","идём"],["вперед","вперёд"],["пошел","пошёл"],["ребенка","ребёнка"],["ребенок","ребёнок"],["счет","счёт"],["своем","своём"],["придет","придёт"],["ушел","ушёл"],["ждет","ждёт"],["мертв","мёртв"],["твоем","твоём"],["вернется","вернётся"],["днем","днём"],["найдем","найдём"],["пойдет","пойдёт"],["начнем","начнём"],["вернемся","вернёмся"],["остается","остаётся"],["принес","принёс"],["идешь","идёшь"],["трех","трёх"],["ребенком","ребёнком"],["самолет","самолёт"],["определенно","определённо"],["пойдешь","пойдёшь"],["умрет","умрёт"],["прием","приём"],["прошел","прошёл"],["убьет","убьёт"],["тетя","тётя"],["провел","провёл"],["произойдет","произойдёт"],["семьей","семьёй"],["пойдемте","пойдёмте"],["пройдет","пройдёт"],["возьмем","возьмём"],["вернешься","вернёшься"],["найдешь","найдёшь"],["живешь","живёшь"],["отчет","отчёт"],["займет","займёт"],["ждем","ждём"],["сошел","сошёл"],["вдвоем","вдвоём"],["найдет","найдёт"],["вел","вёл"],["живем","живём"],["зашел","зашёл"],["начнется","начнётся"],["путем","путём"],["подойдет","подойдёт"],["режиссер","режиссёр"],["уйдет","уйдёт"],["придешь","придёшь"],["идемте","идёмте"],["ведешь","ведёшь"],["пес","пёс"],["умрешь","умрёшь"],["ждешь","ждёшь"],["найдете","найдёте"],["найдется","найдётся"],["клево","клёво"],["поймешь","поймёшь"],["разберемся","разберёмся"],["времен","времён"],["введен","введён"],["включен","включён"],["включенных","включённых"],["возьмется","возьмётся"],["завершен","завершён"],["завершенного","завершённого"],["задает","задаёт"],["задается","задаётся"],["заменен","заменён"],["звезд","звёзд"],["звезды","звёзды"],["изменен","изменён"],["истек","истёк"],["незавершенная","незавершённая"],["неподтвержденные","неподтверждённые"],["несохраненные","несохранённые"],["обновлен","обновлён"],["обновленные","обновлённые"],["определен","определён"],["отклонен","отклонён"],["отменен","отменён"],["очередность","очерёдность"],["поврежден","повреждён"],["повторен","повторён"],["подключен","подключён"],["подтвержденные","подтверждённые"],["приемки","приёмки"],["раздает","раздаёт"],["разрешенная","разрешённая"],["разрешенные","разрешённые"],["создаем","создаём"],["сойдется","сойдётся"],["сохранен","сохранён"],["сохраненный","сохранённый"],["счету","счёту"],["тяжелые","тяжёлые"],["темная","тёмная"],["уберет","уберёт"],["удален","удалён"],["учетные","учётные"],["учетных","учётных"],["емкости","ёмкости"],["емкость","ёмкость"]]
1039
1318
  const typoYo = makeTypoYo(TYPO_YO_PAIRS)
1040
1319
  const getTypoConf = () => {
1041
1320
  try {
@@ -1047,15 +1326,12 @@ const exportSessionToMarkdown = (session, options = {}) => {
1047
1326
  const typoNode = (node, conf) => {
1048
1327
  const before = node.nodeValue
1049
1328
  if (!before || !before.match || (before.match(/[\u0400-\u04FF]/g) || []).length < 3) return
1050
-
1051
1329
  if (node.parentElement && node.parentElement.closest('code, pre, a, script, style, textarea, input, select, button, kbd, samp, [contenteditable="true"], [data-composer-input], [role="textbox"]')) return
1052
1330
  if (node.parentElement && node.parentElement.closest('.katex, [data-latex], math')) return
1053
1331
  if (/\$[^$\n]+\$/.test(before)) return
1054
-
1055
1332
  if (node.parentElement && node.parentElement.closest('[data-chat-flow-status="running"], [data-turn-running], [data-turn-tail], [data-streaming="true"], .dsw-turn-running, [class*="streaming"], [class*="Streaming"]')) {
1056
1333
  return
1057
1334
  }
1058
-
1059
1335
  const sel = typeof window !== 'undefined' && window.getSelection && window.getSelection()
1060
1336
  if (sel && !sel.isCollapsed && sel.rangeCount > 0) {
1061
1337
  try {
@@ -1065,7 +1341,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
1065
1341
  }
1066
1342
  } catch (e) { /* bestEffort */ void e; }
1067
1343
  }
1068
-
1069
1344
  let after = typoQuotes(before)
1070
1345
  after = typoDash(after)
1071
1346
  after = typoPunct(after)
@@ -1127,16 +1402,13 @@ const exportSessionToMarkdown = (session, options = {}) => {
1127
1402
  }
1128
1403
  }, 'dsh-russian-lang: typography')
1129
1404
  syncTypo()
1130
-
1131
- const layout = makeLayout(new Set(["не", "что", "ты", "это", "на", "он", "мы", "как", "вы", "да", "мне", "нет", "меня", "так", "но", "его", "все", "она", "тебя", "если", "за", "бы", "тебе", "они", "чтобы", "же", "есть", "просто", "из", "для", "ну", "когда", "хорошо", "здесь", "по", "был", "знаю", "то", "только", "всё", "было", "вас", "может", "нас", "вот", "от", "быть", "кто", "будет", "почему", "вам", "их", "очень", "могу", "уже", "спасибо", "или", "нам", "еще", "там", "нужно", "сейчас", "где", "чем", "хочу", "ничего", "этого", "была", "мой", "ее", "ладно", "знаешь", "до", "этом", "потому", "теперь", "думаю", "больше", "её", "со", "раз", "ему", "надо", "время", "этот", "ли", "ещё", "пока", "даже", "привет", "сказал", "себя", "должен", "тоже", "хочешь", "давай", "никогда", "эй", "того", "тогда", "него", "ни", "тут", "были", "конечно", "правда", "об", "моя", "пожалуйста", "тобой", "сказать", "сегодня", "один", "лучше", "можешь", "сюда", "мной", "значит", "сделать", "всегда", "дело", "можно", "ей", "должны", "порядке", "без", "день", "том", "буду", "делать", "хотел", "чего", "эти", "много", "после", "этим", "всего", "во", "твой", "ним", "лет", "боже", "них", "сэр", "ведь", "мистер", "жизнь", "потом", "ней", "такой", "который", "всех", "через", "им", "возможно", "немного", "такое", "слишком", "себе", "зачем", "должна", "моей", "люди", "знаете", "этой", "думаешь", "свою", "точно", "человек", "твоя", "под", "сказала", "назад", "эту", "можем", "случилось", "мама", "мог", "вместе", "отец", "сделал", "мои", "кажется", "друг", "куда", "никто", "поэтому", "эта", "которые", "два", "тот", "сколько", "понимаю", "снова", "жизни", "нравится", "людей", "помочь", "видел", "люблю", "хочет", "место", "знать", "прости", "отлично", "похоже", "парень", "домой", "всем", "деньги", "иди", "времени", "дома", "именно", "доктор", "думал", "говорил", "делаешь", "будем", "прямо", "стоит", "поговорить", "найти", "разве", "слушай", "своей", "лишь", "ага", "можете", "простите", "хотела", "сам", "тем", "будешь", "прошу", "три", "деле", "хотите", "говорить", "давайте", "совсем", "знал", "знает", "какой", "моего", "скажи", "дом", "дела", "вами", "свои", "говорит", "несколько", "должно", "про", "ваш", "происходит", "жаль", "туда", "действительно", "папа", "завтра", "оно", "черт", "одна", "перед", "наш", "уверен", "отсюда", "нужна", "самом", "тех", "нужен", "свой", "мою", "кого", "верно", "работу", "каждый", "твоей", "будут", "хватит", "понял", "нее", "уж", "имя", "против", "пор", "чём", "раньше", "говорю", "более", "надеюсь", "итак", "при", "ваша", "вообще", "пошли", "мать", "нельзя", "наверное", "нами", "твои", "могли", "дай", "ради", "всю", "ребята", "ко", "хотя", "понимаешь", "идти", "этих", "откуда", "такая", "ясно", "другой", "извините", "вижу", "видеть", "над", "могут", "равно", "мисс", "скоро", "будто", "зовут", "виду", "наши", "думала", "послушай", "между", "своего", "вопрос", "этому", "почти", "года", "человека", "которая", "подожди", "руки", "нормально", "такие", "возьми", "минут", "извини", "вещи", "могла", "смотри", "хоть", "работа", "пару", "сын", "ваше", "дня", "пора", "неё", "жить", "видишь", "достаточно", "господи", "быстро", "твою", "весь", "убить", "ночь", "говоришь", "собой", "скажу", "готов", "слышал", "какая", "посмотри", "первый", "самое", "видела", "пусть", "месте", "нашли", "сказали", "плохо", "смогу", "ваши", "отца", "детей", "знаем", "рад", "прав", "никаких", "имею", "миссис", "иногда", "смерти", "своих", "пойду", "брат", "вроде", "рядом", "мир", "произошло", "которую", "сделала", "говорила", "знала", "мое", "одного", "таким", "помощь", "такого", "кем", "насчет", "вчера", "случае", "увидеть", "нашей", "говорят", "правильно", "убил", "одно", "пойти", "друга", "сама", "долго", "работать", "дверь", "делает", "женщина", "важно", "кроме", "будь", "собираюсь", "вашей", "нужны", "здорово", "номер", "проблема", "проблемы", "денег", "чёрт", "хороший", "твоего", "одну", "дальше", "вернуться", "давно", "последний", "ночи", "узнать", "уверена", "машину", "права", "пять", "моих", "также", "тому", "вечером", "дети", "опять", "серьезно", "работает", "своим", "сначала", "две", "одной", "моим", "те", "жена", "думаете", "помнишь", "глаза", "ними", "чувак", "ночью", "чувствую", "взять", "сразу", "утром", "часть", "придется", "стать", "идея", "дорогая", "прекрасно", "слова", "сделали", "видели", "наша", "сердце", "помню", "парня", "момент", "утро", "здравствуйте", "стал", "скорее", "ох", "большой", "часов", "год", "друзья", "сильно", "говори", "смысле", "которой", "уйти", "которого", "капитан", "работы", "любовь", "дочь", "новый", "боюсь", "имеет", "стой", "дней", "той", "голову", "ах", "столько", "понятно", "дать", "девушка", "довольно", "доме", "вечер", "оба", "пойдем", "увидимся", "шанс", "хорошая", "прежде", "сделаю", "какого", "быстрее", "понять", "рассказать", "вашего", "бог", "интересно", "затем", "умер", "обо", "никого", "парни", "самый", "твое", "вон", "пришел", "чтоб", "поверить", "слово", "будете", "нашел", "милая", "странно", "какие", "ой", "та", "однажды", "сможешь", "получить", "обычно", "ума", "полагаю", "алло", "посмотреть", "иначе", "наших", "тело", "хотели", "свое", "пришли", "моё", "другие", "выглядит", "подумал", "лицо", "приятно", "мам", "говорите", "обратно", "нашего", "телефон", "мире", "честь", "господин", "доброе", "план", "другом", "час", "джон", "пути", "делал", "типа", "посмотрим", "насколько", "пытался", "правду", "решил", "поздно", "нем", "работе", "любит", "хотят", "кому", "сестра", "места", "скажешь", "следующий", "других", "получил", "кстати", "говорили", "пришла", "безопасности", "рада", "город", "внимание", "кофе", "которое", "вся", "семья", "конец", "позже", "дал", "думать", "спать", "вернулся", "остаться", "некоторые", "кровь", "оружие", "пришлось", "хуже", "дорогой", "полиция", "сможем", "понимаете", "четыре", "которых", "ужасно", "ждать", "способ", "делаю", "готовы", "совершенно", "смотреть", "вернусь", "большое", "единственный", "вперед", "часа", "муж", "идет", "таких", "ребенка", "машина", "стороны", "смог", "сына", "играть", "сами", "верю", "честно", "нашла", "звучит", "угодно", "руку", "начала", "перевод", "какое", "взял", "станет", "пытаюсь", "внутри", "знали", "новости", "добрый", "подождите", "скажите", "ребенок", "вашу", "мальчик", "осталось", "делаете", "году", "мере", "поняла", "отношения", "далеко", "вокруг", "хотим", "слышала", "двух", "приятель", "никому", "мужчина", "неделю", "вернуть", "смерть", "сих", "ту", "сможет", "дайте", "двое", "видите", "долларов", "крови", "решение", "путь", "следует", "неужели", "около", "готова", "семьи", "идем", "смотрите", "круто", "история", "вдруг", "спросить", "твоих", "второй", "вина", "джек", "женщины", "нашу", "заткнись", "минуту", "собираешься", "твоим", "шоу", "прощения", "больно", "машины", "пап", "трудно", "постоянно", "поможет", "проблем", "например", "дерьмо", "полиции", "начать", "знают", "лучший", "добро", "спасти", "рождения", "оставить", "чарли", "любишь", "конце", "забыл", "месяцев", "часто", "мило", "комнате", "нему", "матери", "милый", "полностью", "школе", "леди", "серьёзно", "убили", "случай", "джо", "убийство", "послушайте", "посмотрите", "пошел", "никакого", "сделаем", "говоря", "неплохо", "использовать", "прошлой", "иду", "узнал", "прекрати", "чуть", "держи", "невозможно", "господа", "слушайте", "наше", "машине", "сэм", "свет", "образом", "иметь", "другое", "другого", "шесть", "людям", "погоди", "имени", "детка", "недели", "меньше", "легко", "крайней", "девочка", "придётся", "мало", "ненавижу", "последние", "насчёт", "выглядишь", "сообщение", "думает", "друзей", "обещаю", "агент", "вместо", "последнее", "самого", "настолько", "ответ", "детектив", "понятия", "родители", "показать", "поехали", "стало", "мэм", "своими", "всей", "очевидно", "помогите", "маленький", "школу", "пожаловать", "твоё", "либо", "забрать", "прошло", "особенно", "имеешь", "истории", "эм", "вопросы", "большая", "минутку", "пол", "остановить", "чему", "свидания", "обязательно", "городе", "убийца", "сделай", "ваших", "любви", "спокойной", "своё", "мадам", "идите", "выйти", "делают", "немедленно", "делай", "возможность", "человеком", "бывает", "ужин", "моем", "купить", "позвонить", "наконец", "стала", "любой", "клянусь", "смешно", "абсолютно", "всему", "переводчики", "забудь", "секунду", "выбор", "тысяч", "потерял", "майкл", "волнуйся", "ноги", "таком", "настоящий", "означает", "встретиться", "известно", "работаю", "замечательно", "любом", "оставь", "связи", "боль", "выпить", "представить", "убийства", "малыш", "умерла", "воды", "окей", "речь", "хорошие", "поводу", "перестань", "увидел", "повезло", "президент", "мужик", "которым", "прошлом", "дам", "десять", "умереть", "поговорим", "отцом", "искать", "тихо", "оставил", "пистолет", "начал", "сложно", "города", "неделе", "хотелось", "принять", "историю", "босс", "держать", "пойдём", "подумала", "фильм", "слышишь", "рано", "удачи", "еду", "словно", "причина", "пришёл", "компании", "спокойно", "работал", "подумать", "вести", "вещь", "брата", "конца", "получится", "блин", "право", "ввиду", "бога", "подарок", "комнату", "мира", "успокойся", "позволить", "моему", "идёт", "самая", "рассказал", "курсе", "намного", "мистера", "женщин", "тяжело", "ух", "предложение", "одним", "счет", "улице", "другим", "уходи", "майк", "среди", "помоги", "получилось", "месяц", "проверить", "генри", "голос", "позвольте", "стороне", "вероятно", "случится", "уверены", "мужа", "питер", "провести", "людьми", "неважно", "нашёл", "доктора", "месяца", "игра", "идём", "нём", "джордж", "познакомиться", "возьму", "слышали", "пошла", "корабль", "чувствуешь", "позволь", "сторону", "игры", "согласен", "письмо", "вниз", "молодец", "наверняка", "утра", "вашим", "нечего", "встречи", "вид", "никакой", "связь", "список", "такую", "весело", "сидеть", "гораздо", "собирался", "впервые", "находится", "объяснить", "полицию", "решили", "семью", "свидание", "отличная", "остальные", "прийти", "руках", "фбр", "чувство", "котором", "решила", "понадобится", "сынок", "найду", "фрэнк", "школы", "чувства", "дядя", "стали", "ушла", "первым", "поверь", "расскажи", "хочется", "девушки", "сон", "имел", "каким", "парнем", "многие", "части", "ехать", "считаю", "никуда", "голове", "старый", "оттуда", "называется", "вопросов", "фото", "пройти", "заниматься", "другу", "вернулась", "пара", "никак", "девочки", "благодарю", "своем", "недавно", "приехал", "глаз", "могло", "становится", "пыталась", "является", "брось", "богу", "нашим", "помощи", "волосы", "невероятно", "одном", "ушел", "джейн", "тела", "недель", "собирается", "защитить", "всеми", "существует", "жив", "первая", "позвоню", "благодаря", "вперёд", "навсегда", "помните", "земле", "делу", "вполне", "пытается", "будущее", "семь", "своему", "ок", "идиот", "слава", "стоило", "удалось", "делаем", "команда", "главное", "огонь", "дэнни", "замуж", "начали", "мужчины", "силы", "семье", "шеф", "суд", "слышу", "тюрьме", "попробовать", "друзьями", "алекс", "вашем", "новая", "маленькая", "черта", "придет", "страшно", "услышать", "новые", "общем", "звонил", "купил", "понравится", "близко", "самой", "смысл", "хорошее", "любил", "сара", "единственная", "делали", "макс", "мэри", "жду", "получается", "хорошего", "эми", "дамы", "жену", "гарри", "неправильно", "стоять", "плевать", "ключ", "верить", "лично", "слышать", "взяли", "поздравляю", "пить", "заставить", "получили", "рот", "делала", "преступления", "двери", "землю", "разумеется", "очередь", "первой", "разговор", "видимо", "звонок", "джеймс", "менее", "нового", "получила", "хм", "подальше", "сможете", "похож", "состоянии", "необходимо"]), new Set())
1405
+ const layout = makeLayout(new Set(["не","что","ты","это","на","он","мы","как","вы","да","мне","нет","меня","так","но","его","все","она","тебя","если","за","бы","тебе","они","чтобы","же","есть","просто","из","для"]), new Set())
1132
1406
  const layoutFixCandidate = layout.candidate
1133
1407
  const learnWords = layout.learnWords
1134
-
1135
1408
  function setNativeInputValue(el, value, cursorStart, cursorEnd) {
1136
1409
  if (!el) return
1137
1410
  const oldVal = el.value || ''
1138
1411
  if (oldVal === value) return
1139
-
1140
1412
  const proto = el.tagName === 'TEXTAREA' ? window.HTMLTextAreaElement.prototype : window.HTMLInputElement.prototype
1141
1413
  const desc = Object.getOwnPropertyDescriptor(proto, 'value')
1142
1414
  if (desc && desc.set) {
@@ -1144,31 +1416,26 @@ const exportSessionToMarkdown = (session, options = {}) => {
1144
1416
  } else {
1145
1417
  el.value = value
1146
1418
  }
1147
-
1148
1419
  if (el._valueTracker) {
1149
1420
  el._valueTracker.setValue(value === '' ? '__force__' : '')
1150
1421
  }
1151
-
1152
1422
  try {
1153
1423
  const propsKey = Object.keys(el).find((k) => k.startsWith('__reactProps$') || k.startsWith('__reactEventHandlers$'))
1154
1424
  if (propsKey && el[propsKey] && typeof el[propsKey].onChange === 'function') {
1155
1425
  el[propsKey].onChange({ target: el, currentTarget: el })
1156
1426
  }
1157
1427
  } catch (e) { /* bestEffort */ void e; }
1158
-
1159
1428
  try {
1160
1429
  el.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true }))
1161
1430
  } catch (e) {
1162
1431
  el.dispatchEvent(new Event('input', { bubbles: true }))
1163
1432
  }
1164
1433
  el.dispatchEvent(new Event('change', { bubbles: true }))
1165
-
1166
1434
  if (typeof cursorStart === 'number' && typeof el.setSelectionRange === 'function') {
1167
1435
  const end = typeof cursorEnd === 'number' ? cursorEnd : cursorStart
1168
1436
  try { el.setSelectionRange(cursorStart, end) } catch (e) { /* bestEffort */ void e; }
1169
1437
  }
1170
1438
  }
1171
-
1172
1439
  let layoutHintEl = null
1173
1440
  const layoutCurrentInput = (ev) => {
1174
1441
  if (ev && ev.target) {
@@ -1191,7 +1458,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
1191
1458
  if (focused) return focused
1192
1459
  return null
1193
1460
  }
1194
-
1195
1461
  function getCaretCharacterOffset(root) {
1196
1462
  if (!root) return -1
1197
1463
  const sel = typeof window !== 'undefined' && window.getSelection && window.getSelection()
@@ -1207,7 +1473,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
1207
1473
  return -1
1208
1474
  }
1209
1475
  }
1210
-
1211
1476
  function setCaretCharacterOffset(root, offset) {
1212
1477
  if (!root || offset < 0) return
1213
1478
  const sel = typeof window !== 'undefined' && window.getSelection && window.getSelection()
@@ -1237,7 +1502,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
1237
1502
  }
1238
1503
  } catch (e) { /* bestEffort */ void e; }
1239
1504
  }
1240
-
1241
1505
  const isCaretInCode = (el, value, caretOffset) => {
1242
1506
  if (!el) return false
1243
1507
  if (caretOffset == null || caretOffset < 0) caretOffset = (value || '').length
@@ -1253,7 +1517,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
1253
1517
  if (single && single.length % 2 === 1) return true
1254
1518
  return false
1255
1519
  }
1256
-
1257
1520
  const getComposerText = (el) => {
1258
1521
  if (!el) return ''
1259
1522
  if (el.tagName === 'TEXTAREA' || el.tagName === 'INPUT') {
@@ -1263,17 +1526,14 @@ const exportSessionToMarkdown = (session, options = {}) => {
1263
1526
  const raw = host.innerText !== undefined ? host.innerText : (host.textContent || '')
1264
1527
  return raw.replace(/\r/g, '').replace(/[\u200B\uFEFF]/g, '').replace(/\n+$/, '')
1265
1528
  }
1266
-
1267
1529
  const setComposerText = (el, value, cursorStart, cursorEnd) => {
1268
1530
  if (!el) return
1269
1531
  if (el.tagName === 'TEXTAREA' || el.tagName === 'INPUT') {
1270
1532
  setNativeInputValue(el, value, cursorStart, cursorEnd)
1271
1533
  return
1272
1534
  }
1273
-
1274
1535
  const host = (el.closest && el.closest('[data-composer-input], [contenteditable="true"]')) || el
1275
1536
  const initialOffset = typeof cursorStart === 'number' ? cursorStart : getCaretCharacterOffset(host)
1276
-
1277
1537
  try {
1278
1538
  host.focus()
1279
1539
  const sel = typeof window !== 'undefined' && window.getSelection && window.getSelection()
@@ -1284,12 +1544,10 @@ const exportSessionToMarkdown = (session, options = {}) => {
1284
1544
  sel.addRange(range)
1285
1545
  }
1286
1546
  document.execCommand('insertText', false, value)
1287
-
1288
1547
  const targetOffset = typeof cursorStart === 'number' ? cursorStart : (initialOffset >= 0 ? Math.min(initialOffset, value.length) : value.length)
1289
1548
  setCaretCharacterOffset(host, targetOffset)
1290
1549
  } catch (e) { /* bestEffort */ void e; }
1291
1550
  }
1292
-
1293
1551
  const layoutDismiss = () => {
1294
1552
  if (layoutHintEl) { layoutHintEl.remove(); layoutHintEl = null }
1295
1553
  }
@@ -1298,9 +1556,10 @@ const exportSessionToMarkdown = (session, options = {}) => {
1298
1556
  layoutHintEl = document.createElement('div')
1299
1557
  layoutHintEl.dataset.russianLangLayout = '1'
1300
1558
  Object.assign(layoutHintEl.style, {
1301
- position: 'fixed', zIndex: '99999', background: '#fff', color: '#000',
1302
- border: '1px solid #888', borderRadius: '8px', padding: '6px 10px',
1303
- fontSize: '13px', boxShadow: '0 2px 8px rgba(0,0,0,.2)', cursor: 'pointer'
1559
+ position: 'fixed', zIndex: '99999', background: 'var(--dsw-alias-bg-layer-3)',
1560
+ color: 'var(--dsw-alias-label-primary)',
1561
+ border: '1px solid var(--dsw-alias-border-l2)', borderRadius: '8px', padding: '6px 10px',
1562
+ fontSize: '13px', boxShadow: 'var(--dsw-alias-shadow-l2)', cursor: 'pointer'
1304
1563
  })
1305
1564
  const label = direction === 'cyr2lat' ? 'Команда, не та раскладка' : 'Не та раскладка'
1306
1565
  layoutHintEl.textContent = label + ': ' + converted
@@ -1315,7 +1574,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
1315
1574
  layoutHintEl.style.left = (r.left + 8) + 'px'
1316
1575
  layoutHintEl.style.bottom = (window.innerHeight - r.top + 6) + 'px'
1317
1576
  }
1318
-
1319
1577
  let layoutBadgeEl = null
1320
1578
  const layoutBadge = (el) => {
1321
1579
  const value = getComposerText(el)
@@ -1329,8 +1587,8 @@ const exportSessionToMarkdown = (session, options = {}) => {
1329
1587
  layoutBadgeEl.type = 'button'
1330
1588
  layoutBadgeEl.dataset.russianLangLayoutBadge = '1'
1331
1589
  Object.assign(layoutBadgeEl.style, {
1332
- position: 'fixed', zIndex: '99998', background: 'var(--dsw-alias-bg-layer-3, #fff)',
1333
- color: 'var(--dsw-alias-label-secondary, #666)', border: '1px solid var(--dsw-alias-border-l2, #888)',
1590
+ position: 'fixed', zIndex: '99998', background: 'var(--dsw-alias-bg-layer-3)',
1591
+ color: 'var(--dsw-alias-label-secondary)', border: '1px solid var(--dsw-alias-border-l2)',
1334
1592
  borderRadius: '6px', padding: '1px 6px', fontSize: '11px', cursor: 'pointer',
1335
1593
  fontFamily: 'monospace', lineHeight: '1.4', fontWeight: '600'
1336
1594
  })
@@ -1354,7 +1612,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
1354
1612
  const layoutBadgeHide = () => {
1355
1613
  if (layoutBadgeEl) { layoutBadgeEl.remove(); layoutBadgeEl = null }
1356
1614
  }
1357
-
1358
1615
  let isFormatting = false
1359
1616
  const layoutOnInput = (ev) => {
1360
1617
  if (isFormatting) return
@@ -1363,8 +1620,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
1363
1620
  if (!el) { layoutDismiss(); layoutBadgeHide(); return }
1364
1621
  layoutBadge(el) // #66: метка раскладки
1365
1622
  const value = getComposerText(el)
1366
-
1367
-
1368
1623
  if (value.trim().length < 4) { layoutDismiss(); return }
1369
1624
  const latCount = (value.match(/[a-z]/g) || []).length
1370
1625
  const cyrCount = (value.match(/[\u0430-\u044f\u0451]/g) || []).length
@@ -1380,18 +1635,14 @@ const exportSessionToMarkdown = (session, options = {}) => {
1380
1635
  } catch (err) { /* ignore */ }
1381
1636
  }
1382
1637
  const unsubscribeLayout = runtime.subscribe(layoutOnInput)
1383
-
1384
1638
  const layoutOnKeydown = (ev) => {
1385
1639
  const el = layoutCurrentInput(ev)
1386
1640
  if (!el) return
1387
-
1388
1641
  const value = getComposerText(el)
1389
-
1390
1642
  const isL = ev.code === 'KeyL' || ev.key.toLowerCase() === 'l' || ev.key.toLowerCase() === 'д'
1391
1643
  if (ev.altKey && !ev.ctrlKey && !ev.metaKey && isL) {
1392
1644
  const sel = typeof window !== 'undefined' && window.getSelection && window.getSelection()
1393
1645
  const selectedText = (sel && !sel.isCollapsed) ? sel.toString() : ''
1394
-
1395
1646
  if (selectedText) {
1396
1647
  const c = layoutFixCandidate(selectedText, 'lat2cyr') || layoutFixCandidate(selectedText, 'cyr2lat')
1397
1648
  if (c) {
@@ -1413,7 +1664,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
1413
1664
  return
1414
1665
  }
1415
1666
  }
1416
-
1417
1667
  const c = layoutFixCandidate(value, 'lat2cyr') || layoutFixCandidate(value, 'cyr2lat')
1418
1668
  if (c) {
1419
1669
  ev.preventDefault()
@@ -1435,7 +1685,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
1435
1685
  }
1436
1686
  return
1437
1687
  }
1438
-
1439
1688
  if (ev.key === ' ' || ev.key === 'Enter') {
1440
1689
  if (value && value.startsWith('/') && typeof expandSlashAlias === 'function') {
1441
1690
  const expanded = expandSlashAlias(value)
@@ -1461,18 +1710,15 @@ const exportSessionToMarkdown = (session, options = {}) => {
1461
1710
  }
1462
1711
  }
1463
1712
  }
1464
-
1465
1713
  try {
1466
1714
  const snapVal = scope ? (scope.getSnapshot().value || {}) : {}
1467
1715
  const typoLive = snapVal.typography ? snapVal.typography.liveInput !== false : true
1468
1716
  if (typoLive && !ev.ctrlKey && !ev.altKey && !ev.metaKey) {
1469
1717
  const isTextarea = el.tagName === 'TEXTAREA' || el.tagName === 'INPUT'
1470
-
1471
1718
  if (isTextarea) {
1472
1719
  const caretPos = el.selectionStart || 0
1473
1720
  if (!isCaretInCode(el, value, caretPos)) {
1474
1721
  const textBefore = caretPos >= 0 ? value.slice(0, caretPos) : value
1475
-
1476
1722
  if (ev.key === '-' && textBefore.endsWith('-')) {
1477
1723
  ev.preventDefault()
1478
1724
  isFormatting = true
@@ -1484,7 +1730,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
1484
1730
  }
1485
1731
  return
1486
1732
  }
1487
-
1488
1733
  if (ev.key === '"') {
1489
1734
  ev.preventDefault()
1490
1735
  isFormatting = true
@@ -1504,6 +1749,104 @@ const exportSessionToMarkdown = (session, options = {}) => {
1504
1749
  }
1505
1750
  } catch (e) { /* bestEffort */ void e; }
1506
1751
  }
1752
+ const openInspectorModal = (meta, rawText) => {
1753
+ if (typeof document === 'undefined') return
1754
+ const old = document.getElementById('dsh-ru-inspector-modal')
1755
+ if (old) old.remove()
1756
+ const modal = document.createElement('div')
1757
+ modal.id = 'dsh-ru-inspector-modal'
1758
+ modal.className = 'rl-modal-mask'
1759
+ const k = meta ? (meta.key || (meta.ns ? meta.ns + '.' + meta.key : '')) : ''
1760
+ const src = meta ? (meta.source === 'override' ? 'Оверрайд' : meta.source === 'dom_zh' ? 'DOM ZH' : 'Словарь ' + (meta.ns || '')) : 'Текст'
1761
+ const v = (meta && meta.value) || rawText
1762
+ modal.innerHTML = '<div class="rl-modal-box">' +
1763
+ '<div style="display:flex;justify-content:space-between;align-items:center;">' +
1764
+ '<div style="font-weight:600;font-size:14px;">🔍 Инспектор перевода</div>' +
1765
+ '<button type="button" class="rl-btn-close" style="background:none;border:none;color:var(--dsw-alias-label-secondary);font-size:18px;cursor:pointer;">&times;</button>' +
1766
+ '</div>' +
1767
+ '<div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap;font-size:12px;">' +
1768
+ '<span style="color:var(--dsw-alias-label-secondary);">Ключ:</span>' +
1769
+ '<code style="background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-state-brand-primary);padding:2px 6px;border-radius:4px;">' + (k || '(не опознан)') + '</code>' +
1770
+ '<span style="padding:1px 6px;border-radius:4px;font-size:10px;background:var(--dsw-alias-bg-layer-3);">' + src + '</span>' +
1771
+ '</div>' +
1772
+ '<div style="font-size:12px;color:var(--dsw-alias-label-secondary);word-break:break-word;">Текущий текст: <b class="rl-raw-text" style="color:var(--dsw-alias-label-primary)"></b></div>' +
1773
+ '<div style="display:flex;flex-direction:column;gap:6px;">' +
1774
+ '<label style="font-size:12px;color:var(--dsw-alias-label-secondary);">Новый перевод (оверрайд):</label>' +
1775
+ '<textarea class="rl-edit-val rl-select" style="min-height:50px;font-family:inherit;width:100%;resize:vertical;"></textarea>' +
1776
+ '</div>' +
1777
+ '<div style="display:flex;justify-content:flex-end;gap:8px;">' +
1778
+ '<button type="button" class="rl-btn rl-cancel-btn">Отмена</button>' +
1779
+ '<button type="button" class="rl-btn rl-btn-primary rl-save-btn">Сохранить оверрайд</button>' +
1780
+ '</div>' +
1781
+ '</div>'
1782
+ modal.querySelector('.rl-raw-text').textContent = rawText
1783
+ const textarea = modal.querySelector('.rl-edit-val')
1784
+ textarea.value = v
1785
+ modal.querySelector('.rl-btn-close').onclick = () => modal.remove()
1786
+ modal.querySelector('.rl-cancel-btn').onclick = () => modal.remove()
1787
+ modal.onclick = (e) => { if (e.target === modal) modal.remove() }
1788
+ const saveBtn = modal.querySelector('.rl-save-btn')
1789
+ saveBtn.onclick = () => {
1790
+ const val = textarea.value.trim()
1791
+ const targetKey = k || rawText
1792
+ if (!targetKey || !val) return
1793
+ const cur = getOverrides()
1794
+ try { scope.set('overrides', Object.assign({}, cur, { [targetKey]: val })) } catch (_e) { /* noop */ }
1795
+ saveBtn.textContent = '✓ Сохранено'
1796
+ saveBtn.disabled = true
1797
+ setTimeout(() => modal.remove(), 600)
1798
+ }
1799
+ document.body.appendChild(modal)
1800
+ setTimeout(() => textarea.focus(), 50)
1801
+ }
1802
+ const inspectorOnClick = (e) => {
1803
+ if (!e.altKey) return
1804
+ if (e.target && e.target.closest && e.target.closest('#dsh-ru-inspector-modal')) return
1805
+ const raw = (e.target && (e.target.innerText || e.target.textContent) || '').trim()
1806
+ if (!raw) return
1807
+ e.preventDefault()
1808
+ e.stopPropagation()
1809
+ const meta = translationRegistry.get(raw) || (typeof findTranslationKey === 'function' ? findTranslationKey(raw, RU, getOverrides(), ZH_RU) : null)
1810
+ openInspectorModal(meta, raw)
1811
+ }
1812
+ ctx.effect(() => {
1813
+ document.addEventListener('click', inspectorOnClick, true)
1814
+ return () => {
1815
+ document.removeEventListener('click', inspectorOnClick, true)
1816
+ const m = document.getElementById('dsh-ru-inspector-modal')
1817
+ if (m) m.remove()
1818
+ }
1819
+ }, 'dsh-russian-lang: inspector')
1820
+ runtime.findTranslationKey = (text) => (typeof findTranslationKey === 'function' ? findTranslationKey(text, RU, getOverrides(), ZH_RU) : null)
1821
+ runtime.openInspector = openInspectorModal
1822
+ runtime.translationRegistry = translationRegistry
1823
+ runtime.bilingualCommandMatch = (query, cmd, opts) => (typeof bilingualCommandMatch === 'function' ? bilingualCommandMatch(query, cmd, opts) : { matched: false, score: 0 })
1824
+ runtime.filterCommands = (query, cmds, opts) => (typeof filterCommands === 'function' ? filterCommands(query, cmds, opts) : cmds)
1825
+ runtime.humanizeError = (err) => (typeof humanizeError === 'function' ? humanizeError(err) : null)
1826
+ runtime.formatErrorToast = (err) => (typeof formatErrorToast === 'function' ? formatErrorToast(err) : null)
1827
+ const toastObserver = typeof MutationObserver !== 'undefined' ? new MutationObserver((mutations) => {
1828
+ for (const m of mutations) {
1829
+ for (const node of m.addedNodes) {
1830
+ if (!node || node.nodeType !== 1) continue
1831
+ if (node.matches && (node.matches('.dsw-toast, [role="alert"], .toast, .ant-message-notice'))) {
1832
+ const text = (node.innerText || node.textContent || '').trim()
1833
+ if (/failed to fetch|network error|econnrefused|etimedout/i.test(text)) {
1834
+ const h = typeof humanizeError === 'function' ? humanizeError(text) : null
1835
+ if (h && h.title) {
1836
+ const targetEl = node.querySelector('.toast-body, .ant-message-custom-content') || node
1837
+ targetEl.textContent = h.title + ': ' + h.message
1838
+ }
1839
+ }
1840
+ }
1841
+ }
1842
+ }
1843
+ }) : null
1844
+ ctx.effect(() => {
1845
+ if (toastObserver && typeof document !== 'undefined' && document.body) {
1846
+ toastObserver.observe(document.body, { childList: true, subtree: true })
1847
+ }
1848
+ return () => { if (toastObserver) toastObserver.disconnect() }
1849
+ }, 'dsh-russian-lang: toast-humanizer')
1507
1850
  ctx.effect(() => {
1508
1851
  document.addEventListener('input', layoutOnInput, true)
1509
1852
  document.addEventListener('keydown', layoutOnKeydown, true)
@@ -1515,7 +1858,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
1515
1858
  layoutBadgeHide()
1516
1859
  }
1517
1860
  }, 'dsh-russian-lang: layout')
1518
-
1519
1861
  if (!ctx.slots || !React) return
1520
1862
  const toggleRu = (wantRu) => {
1521
1863
  try {
@@ -1524,6 +1866,16 @@ const exportSessionToMarkdown = (session, options = {}) => {
1524
1866
  } catch (err) { console.warn('dsh-russian-lang: toggle failed', err) }
1525
1867
  }
1526
1868
  try {
1869
+ ctx.slots.inject('plugins.item', () =>
1870
+ ctx.slots.register({
1871
+ name: 'plugins.item',
1872
+ id: SETTINGS_NS_NAME,
1873
+ order: 60,
1874
+ label: () => 'Russian language',
1875
+ locale: SETTINGS_NS_NAME,
1876
+ inject: () => ({ scope, runtime, toggleRu }),
1877
+ }, SettingsCard),
1878
+ )
1527
1879
  ctx.slots.inject('plugins.row.config', () =>
1528
1880
  ctx.slots.register({
1529
1881
  name: 'plugins.row.config',
@@ -1541,7 +1893,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
1541
1893
  }, SettingsCard),
1542
1894
  )
1543
1895
  } catch (err) { console.warn('dsh-russian-lang: settings slot unavailable', err) }
1544
-
1545
1896
  try {
1546
1897
  ctx.slots.inject('conversation.session.header.utilities', () =>
1547
1898
  ctx.slots.register({
@@ -1553,7 +1904,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
1553
1904
  }, QuickLangSwitch),
1554
1905
  )
1555
1906
  } catch (err) { /* ignore if slot not declared */ }
1556
-
1557
1907
  try {
1558
1908
  ctx.slots.inject('conversation.chat.assistant-actions', () =>
1559
1909
  ctx.slots.register({
@@ -1565,7 +1915,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
1565
1915
  }, TranslateTurnAction),
1566
1916
  )
1567
1917
  } catch (err) { /* ignore if slot not declared */ }
1568
-
1569
1918
  try {
1570
1919
  ctx.slots.inject('conversation.session.header.utilities', () =>
1571
1920
  ctx.slots.register({
@@ -1578,7 +1927,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
1578
1927
  )
1579
1928
  } catch (err) { /* ignore if slot not declared */ }
1580
1929
  }
1581
-
1582
1930
  function QuickLangSwitch(props) {
1583
1931
  const inj = typeof props.inject === 'function' ? (props.inject() || {}) : (props.inject || {})
1584
1932
  const runtime = props.runtime || inj.runtime
@@ -1598,7 +1946,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
1598
1946
  onClick: () => { if (toggleRu) toggleRu(!isRu) },
1599
1947
  }, React.createElement('span', { className: 'rl-lang-text' }, isRu ? 'RU' : 'EN'))
1600
1948
  }
1601
-
1602
1949
  function ExportMarkdownButton(props) {
1603
1950
  const [done, setDone] = React.useState(false)
1604
1951
  const onExport = () => {
@@ -1606,11 +1953,9 @@ const exportSessionToMarkdown = (session, options = {}) => {
1606
1953
  const titleEl = document.querySelector('.dsw-session-title, [data-session-title], header h1, header h2, [class*="title"]')
1607
1954
  const rawTitle = (titleEl && titleEl.textContent.trim()) || document.title || 'Диалог DSH'
1608
1955
  const title = rawTitle.replace(/\s*—\s*DeepSeek Harness\s*$/, '').trim() || 'Диалог DSH'
1609
-
1610
1956
  const flow = document.querySelector('[data-chat-flow]') || document.querySelector('[data-chat-flow-scroll]') || document.body
1611
1957
  const flowItems = Array.from(flow.querySelectorAll('[data-chat-flow-kind]'))
1612
1958
  const messages = []
1613
-
1614
1959
  if (flowItems.length > 0) {
1615
1960
  flowItems.forEach((node) => {
1616
1961
  const kind = node.getAttribute('data-chat-flow-kind')
@@ -1628,7 +1973,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
1628
1973
  }
1629
1974
  })
1630
1975
  }
1631
-
1632
1976
  if (messages.length === 0) {
1633
1977
  const allElements = Array.from(document.querySelectorAll('[class*="userRow"], [class*="UserRow"], [class*="assistant-step"], [class*="AssistantMarkdown"], .dsw-turn-node, [data-role]'))
1634
1978
  const seen = new Set()
@@ -1643,7 +1987,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
1643
1987
  messages.push({ role, content: text })
1644
1988
  })
1645
1989
  }
1646
-
1647
1990
  const session = {
1648
1991
  title,
1649
1992
  createdAt: new Date().toISOString(),
@@ -1668,7 +2011,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
1668
2011
  console.warn('dsh-russian-lang: export md failed', err)
1669
2012
  }
1670
2013
  }
1671
-
1672
2014
  return React.createElement('button', {
1673
2015
  type: 'button',
1674
2016
  className: 'rl-lang-chip rl-export-md-btn',
@@ -1676,7 +2018,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
1676
2018
  onClick: onExport,
1677
2019
  }, React.createElement('span', { className: 'rl-lang-text' }, done ? '✓ MD' : '📥 MD'))
1678
2020
  }
1679
-
1680
2021
  async function translateTurnContent(text) {
1681
2022
  if (!text || typeof text !== 'string') return { error: 'Текст для перевода пуст.' }
1682
2023
  try {
@@ -1694,19 +2035,17 @@ const exportSessionToMarkdown = (session, options = {}) => {
1694
2035
  return { error: 'Не удалось связаться с хостом DSH: ' + (err.message || err) }
1695
2036
  }
1696
2037
  }
1697
-
1698
2038
  function TranslateTurnAction(props) {
1699
2039
  const t = typeof props.t === 'function' ? props.t : ((k) => k)
1700
2040
  const [loading, setLoading] = React.useState(false)
1701
2041
  const [open, setOpen] = React.useState(false)
1702
2042
  const [upStatus, setUpStatus] = React.useState({
1703
- currentVersion: '0.2.25',
2043
+ currentVersion: '0.3.1',
1704
2044
  latestVersion: undefined,
1705
2045
  updateAvailable: false
1706
2046
  })
1707
2047
  const [upLoading, setUpLoading] = React.useState(false)
1708
2048
  const [upMsg, setUpMsg] = React.useState(null)
1709
-
1710
2049
  const checkUpdate = () => {
1711
2050
  setUpLoading(true)
1712
2051
  setUpMsg(null)
@@ -1721,7 +2060,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
1721
2060
  setUpMsg({ type: 'err', text: t('updaterFailed') })
1722
2061
  })
1723
2062
  }
1724
-
1725
2063
  const triggerUpdate = () => {
1726
2064
  setUpLoading(true)
1727
2065
  setUpMsg(null)
@@ -1744,18 +2082,15 @@ const exportSessionToMarkdown = (session, options = {}) => {
1744
2082
  setUpMsg({ type: 'err', text: t('updaterFailed') })
1745
2083
  })
1746
2084
  }
1747
-
1748
2085
  const [transStatus, setTransStatus] = React.useState(null)
1749
2086
  const [transStarting, setTransStarting] = React.useState(false)
1750
2087
  const [transMsg, setTransMsg] = React.useState(null)
1751
-
1752
2088
  const fetchTransStatus = () => {
1753
2089
  fetch('/api/dsh-russian-lang/translator/status')
1754
2090
  .then((r) => r.json())
1755
2091
  .then((data) => setTransStatus(data))
1756
2092
  .catch(() => setTransStatus(null))
1757
2093
  }
1758
-
1759
2094
  const startLibreTranslate = () => {
1760
2095
  setTransStarting(true)
1761
2096
  setTransMsg(null)
@@ -1775,15 +2110,12 @@ const exportSessionToMarkdown = (session, options = {}) => {
1775
2110
  setTransMsg({ type: 'err', text: 'Ошибка: ' + (err.message || err) })
1776
2111
  })
1777
2112
  }
1778
-
1779
2113
  React.useEffect(() => {
1780
2114
  if (open) {
1781
2115
  checkUpdate()
1782
2116
  fetchTransStatus()
1783
2117
  }
1784
2118
  }, [open])
1785
-
1786
-
1787
2119
  return React.createElement('button', {
1788
2120
  type: 'button',
1789
2121
  className: 'rl-action-btn' + (open ? ' rl-action-btn-active' : ''),
@@ -1794,7 +2126,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
1794
2126
  const btn = ev.currentTarget
1795
2127
  const tailFlowItem = btn.closest('[data-chat-flow-kind="turn-tail"]') || btn.closest('[data-chat-flow-kind]') || btn.closest('[data-turn-tail]')?.closest('[data-chat-flow-kind]') || btn.closest('[data-turn-tail]')
1796
2128
  if (!tailFlowItem) return
1797
-
1798
2129
  const parentContainer = tailFlowItem.parentElement
1799
2130
  let box = parentContainer ? parentContainer.querySelector('.rl-turn-translation[data-tail-key="' + (tailFlowItem.getAttribute('data-chat-flow-key') || '') + '"]') : null
1800
2131
  if (!box) {
@@ -1805,7 +2136,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
1805
2136
  setOpen(box.style.display !== 'none')
1806
2137
  return
1807
2138
  }
1808
-
1809
2139
  setLoading(true)
1810
2140
  try {
1811
2141
  const assistantNodes = []
@@ -1816,7 +2146,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
1816
2146
  }
1817
2147
  prev = prev.previousElementSibling
1818
2148
  }
1819
-
1820
2149
  let rawText = ''
1821
2150
  if (assistantNodes.length > 0) {
1822
2151
  rawText = assistantNodes.map((node) => {
@@ -1836,19 +2165,16 @@ const exportSessionToMarkdown = (session, options = {}) => {
1836
2165
  }).filter(Boolean).join('\n\n')
1837
2166
  }
1838
2167
  }
1839
-
1840
2168
  if (!rawText) {
1841
2169
  const prose = tailFlowItem.querySelector('.dsw-prose, [data-block-kind="text"], .dsw-markdown-view, p')
1842
2170
  if (prose) rawText = prose.innerText.trim()
1843
2171
  }
1844
-
1845
2172
  let transResult = { error: 'Не удалось обнаружить текст сообщения ассистента для перевода.' }
1846
2173
  if (rawText) {
1847
2174
  transResult = await translateTurnContent(rawText)
1848
2175
  }
1849
2176
  const isErr = !!transResult.error
1850
2177
  const displayText = transResult.translatedText || transResult.error || ''
1851
-
1852
2178
  box = document.createElement('div')
1853
2179
  box.className = 'rl-turn-translation'
1854
2180
  const key = tailFlowItem.getAttribute('data-chat-flow-key')
@@ -1860,9 +2186,8 @@ const exportSessionToMarkdown = (session, options = {}) => {
1860
2186
  '<button type="button" class="rl-trans-btn rl-btn-close" title="Закрыть">✕</button>' +
1861
2187
  '</div>' +
1862
2188
  '</div>' +
1863
- '<div class="rl-trans-body"' + (isErr ? ' style="color: var(--dsw-alias-color-warning, #eab308); font-size: 13px;"' : '') + '></div>'
2189
+ '<div class="rl-trans-body"' + (isErr ? ' style="color: var(--dsw-alias-state-warning-primary); font-size: 13px;"' : '') + '></div>'
1864
2190
  box.querySelector('.rl-trans-body').textContent = displayText
1865
-
1866
2191
  const copyBtn = box.querySelector('.rl-btn-copy')
1867
2192
  if (copyBtn) {
1868
2193
  copyBtn.addEventListener('click', (e) => {
@@ -1872,14 +2197,12 @@ const exportSessionToMarkdown = (session, options = {}) => {
1872
2197
  setTimeout(() => { copyBtn.textContent = '📋 Копировать' }, 2000)
1873
2198
  })
1874
2199
  }
1875
-
1876
2200
  const closeBtn = box.querySelector('.rl-btn-close')
1877
2201
  closeBtn.addEventListener('click', (e) => {
1878
2202
  e.stopPropagation()
1879
2203
  box.style.display = 'none'
1880
2204
  setOpen(false)
1881
2205
  })
1882
-
1883
2206
  if (parentContainer) {
1884
2207
  parentContainer.insertBefore(box, tailFlowItem)
1885
2208
  } else {
@@ -1894,7 +2217,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
1894
2217
  },
1895
2218
  }, React.createElement('span', null, loading ? '...' : (open ? 'RU ✓' : 'RU ↗')))
1896
2219
  }
1897
-
1898
2220
  function SettingsCard(props) {
1899
2221
  const inj = typeof props.inject === 'function'
1900
2222
  ? (props.inject() || {})
@@ -1903,7 +2225,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
1903
2225
  const runtime = props.runtime || inj.runtime
1904
2226
  const toggleRu = props.toggleRu || inj.toggleRu
1905
2227
  const t = typeof props.t === 'function' ? props.t : ((k) => k)
1906
-
1907
2228
  const [open, setOpen] = React.useState(false)
1908
2229
  const [snap, setSnap] = React.useState(
1909
2230
  () => (scope && scope.getSnapshot ? scope.getSnapshot() : { status: 'loading', value: {} }))
@@ -1911,7 +2232,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
1911
2232
  () => { try { return runtime.getLocale().active === 'ru' } catch (e) { return false } })
1912
2233
  const [typo, setTypoState] = React.useState(() =>
1913
2234
  (snap.value && snap.value.typography) || {})
1914
-
1915
2235
  React.useEffect(() => {
1916
2236
  if (!scope || !scope.subscribe) return undefined
1917
2237
  const un = scope.subscribe(() => {
@@ -1930,12 +2250,13 @@ const exportSessionToMarkdown = (session, options = {}) => {
1930
2250
  return un
1931
2251
  } catch (e) { return undefined }
1932
2252
  }, [runtime])
1933
-
1934
2253
  const status = snap.status || 'loading'
1935
2254
  const value = snap.value || {}
1936
2255
  const typography = typo
1937
- const overridesCount = Object.keys(value.overrides || {}).length
1938
-
2256
+ const overrides = value.overrides || {}
2257
+ const overridesCount = Object.keys(overrides).length
2258
+ const [newKey, setNewKey] = React.useState('')
2259
+ const [newVal, setNewVal] = React.useState('')
1939
2260
  const setTypo = (patch) => {
1940
2261
  const next = Object.assign({}, typo, patch)
1941
2262
  setTypoState(next)
@@ -1949,24 +2270,20 @@ const exportSessionToMarkdown = (session, options = {}) => {
1949
2270
  }
1950
2271
  }
1951
2272
  const onEnabled = (ev) => { if (toggleRu) toggleRu(ev.target.checked) }
1952
-
1953
2273
  const checkbox = (checked, onChange, disabled) =>
1954
2274
  React.createElement('input', {
1955
2275
  type: 'checkbox', checked: !!checked, disabled: !!disabled,
1956
2276
  className: 'rl-check', onChange: (ev) => onChange(ev),
1957
2277
  })
1958
-
1959
2278
  const disabled = status !== 'ready'
1960
2279
  const statusLine = status === 'ready'
1961
2280
  ? ''
1962
2281
  : (status === 'unavailable' ? t('statusUnavailable') : t('statusLoading'))
1963
-
1964
2282
  let ChevronIcon = null
1965
2283
  try {
1966
2284
  const primitives = require('@deepseek-ai/dsh-client-ui-primitives')
1967
2285
  ChevronIcon = primitives && primitives.IconChevronDownOutline14
1968
2286
  } catch (_) { ChevronIcon = null }
1969
-
1970
2287
  const FallbackChevron = () => React.createElement('svg', {
1971
2288
  width: 14, height: 14, viewBox: '0 0 14 14', fill: 'none',
1972
2289
  'aria-hidden': 'true',
@@ -1974,17 +2291,13 @@ const exportSessionToMarkdown = (session, options = {}) => {
1974
2291
  d: 'M3.5 5.25 7 8.75l3.5-3.5', stroke: 'currentColor',
1975
2292
  strokeWidth: 1.5, strokeLinecap: 'round', strokeLinejoin: 'round',
1976
2293
  }))
1977
-
1978
2294
  const Chevron = ChevronIcon || FallbackChevron
1979
-
1980
2295
  const presetKey = value.agentPromptPreset || 'technical_expert'
1981
2296
  const presetInfo = typeof SYSTEM_PROMPT_PRESETS !== 'undefined' ? SYSTEM_PROMPT_PRESETS[presetKey] : null
1982
-
1983
2297
  if (props && props.view === 'summary') {
1984
2298
  return React.createElement('div', { className: 'rl-sub' }, statusLine || t('cardSub'))
1985
2299
  }
1986
2300
  const page = !!(props && props.view === 'page')
1987
-
1988
2301
  return React.createElement('div', { className: page ? 'rl-page-seat' : 'rl-card' },
1989
2302
  React.createElement('button', {
1990
2303
  type: 'button',
@@ -2007,7 +2320,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
2007
2320
  }, React.createElement(Chevron, null))),
2008
2321
  (page || open) && React.createElement('div', { className: 'rl-body' },
2009
2322
  React.createElement('div', { className: 'rl-page' },
2010
-
2011
2323
  React.createElement('div', { className: 'rl-section-card' },
2012
2324
  React.createElement('div', { className: 'rl-section-title' },
2013
2325
  React.createElement('span', null, t('secLanguage')),
@@ -2028,7 +2340,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
2028
2340
  overridesCount > 0 ? React.createElement('div', { className: 'rl-badge rl-badge-dim', style: { alignSelf: 'flex-start' } },
2029
2341
  t('overridesCount') + ': ' + overridesCount) : null
2030
2342
  ),
2031
-
2032
2343
  React.createElement('div', { className: 'rl-section-card' },
2033
2344
  React.createElement('div', { className: 'rl-section-title' },
2034
2345
  React.createElement('span', null, t('secTypography')),
@@ -2084,7 +2395,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
2084
2395
  React.createElement('span', null, t('altLHintText'))
2085
2396
  )
2086
2397
  ),
2087
-
2088
2398
  React.createElement('div', { className: 'rl-section-card' },
2089
2399
  React.createElement('div', { className: 'rl-section-title' },
2090
2400
  React.createElement('span', null, t('secAgentPrompt')),
@@ -2126,7 +2436,10 @@ const exportSessionToMarkdown = (session, options = {}) => {
2126
2436
  },
2127
2437
  React.createElement('option', { value: 'technical_expert' }, t('presetExpert')),
2128
2438
  React.createElement('option', { value: 'tech_writer' }, t('presetWriter')),
2129
- React.createElement('option', { value: 'concise' }, t('presetConcise'))
2439
+ React.createElement('option', { value: 'concise' }, t('presetConcise')),
2440
+ React.createElement('option', { value: 'code_reviewer' }, t('presetReviewer')),
2441
+ React.createElement('option', { value: 'architect' }, t('presetArchitect')),
2442
+ React.createElement('option', { value: 'tutor' }, t('presetTutor'))
2130
2443
  )
2131
2444
  ),
2132
2445
  presetInfo ? React.createElement('div', { className: 'rl-preview-box' },
@@ -2135,7 +2448,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
2135
2448
  ) : null
2136
2449
  ) : null
2137
2450
  ),
2138
-
2139
2451
  React.createElement('div', { className: 'rl-section-card' },
2140
2452
  React.createElement('div', { className: 'rl-section-title' },
2141
2453
  React.createElement('span', null, t('secUpdater')),
@@ -2147,9 +2459,9 @@ const exportSessionToMarkdown = (session, options = {}) => {
2147
2459
  React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: '8px' } },
2148
2460
  React.createElement('div', null,
2149
2461
  React.createElement('div', { style: { fontWeight: 600, color: 'var(--dsw-alias-label-primary)' } },
2150
- t('updaterCurrent').replace('{version}', upStatus.currentVersion || '0.2.25')),
2462
+ t('updaterCurrent').replace('{version}', upStatus.currentVersion || '0.3.1')),
2151
2463
  upStatus.updateAvailable
2152
- ? React.createElement('div', { style: { color: 'var(--dsw-alias-color-warning, #eab308)', marginTop: '2px', fontWeight: 500 } },
2464
+ ? React.createElement('div', { style: { color: 'var(--dsw-alias-state-warning-primary)', marginTop: '2px', fontWeight: 500 } },
2153
2465
  t('updaterLatest').replace('{version}', upStatus.latestVersion || ''))
2154
2466
  : React.createElement('div', { style: { color: 'var(--dsw-alias-label-secondary)', marginTop: '2px' } },
2155
2467
  t('updaterUpToDate'))
@@ -2171,13 +2483,12 @@ const exportSessionToMarkdown = (session, options = {}) => {
2171
2483
  upMsg ? React.createElement('div', {
2172
2484
  style: {
2173
2485
  marginTop: '10px', padding: '8px 12px', borderRadius: '8px',
2174
- background: upMsg.type === 'ok' ? 'rgba(34, 197, 94, 0.12)' : 'rgba(239, 68, 68, 0.12)',
2175
- color: upMsg.type === 'ok' ? '#16a34a' : '#dc2626', fontSize: '12px', fontWeight: 500
2486
+ background: upMsg.type === 'ok' ? 'color-mix(in srgb, var(--dsw-alias-state-success-primary) 12%, transparent)' : 'color-mix(in srgb, var(--dsw-alias-state-error-primary) 12%, transparent)',
2487
+ color: upMsg.type === 'ok' ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-state-error-primary)', fontSize: '12px', fontWeight: 500
2176
2488
  }
2177
2489
  }, upMsg.text) : null
2178
2490
  )
2179
2491
  ),
2180
-
2181
2492
  React.createElement('div', { className: 'rl-section-card' },
2182
2493
  React.createElement('div', { className: 'rl-section-title' },
2183
2494
  React.createElement('span', null, t('secTranslator')),
@@ -2208,8 +2519,8 @@ const exportSessionToMarkdown = (session, options = {}) => {
2208
2519
  value.translateEngine === 'google' ? React.createElement('div', {
2209
2520
  style: {
2210
2521
  marginTop: '10px', padding: '8px 12px', borderRadius: '8px',
2211
- background: 'rgba(234, 179, 8, 0.12)', border: '1px solid rgba(234, 179, 8, 0.3)',
2212
- color: 'var(--dsw-alias-color-warning, #eab308)', fontSize: '12px', fontWeight: 500, lineHeight: 1.4
2522
+ background: 'color-mix(in srgb, var(--dsw-alias-state-warning-primary) 12%, transparent)', border: '1px solid color-mix(in srgb, var(--dsw-alias-state-warning-primary) 30%, transparent)',
2523
+ color: 'var(--dsw-alias-state-warning-primary)', fontSize: '12px', fontWeight: 500, lineHeight: 1.4
2213
2524
  }
2214
2525
  }, t('googleWarn')) : null,
2215
2526
  value.translateEngine === 'local' ? React.createElement('div', {
@@ -2230,14 +2541,78 @@ const exportSessionToMarkdown = (session, options = {}) => {
2230
2541
  transMsg ? React.createElement('div', {
2231
2542
  style: {
2232
2543
  marginTop: '6px', padding: '8px 12px', borderRadius: '8px',
2233
- background: transMsg.type === 'ok' ? 'rgba(34, 197, 94, 0.12)' : 'rgba(239, 68, 68, 0.12)',
2234
- color: transMsg.type === 'ok' ? '#16a34a' : '#dc2626', fontSize: '12px', fontWeight: 500
2544
+ background: transMsg.type === 'ok' ? 'color-mix(in srgb, var(--dsw-alias-state-success-primary) 12%, transparent)' : 'color-mix(in srgb, var(--dsw-alias-state-error-primary) 12%, transparent)',
2545
+ color: transMsg.type === 'ok' ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-state-error-primary)', fontSize: '12px', fontWeight: 500
2235
2546
  }
2236
2547
  }, transMsg.text) : null
2237
2548
  ) : null
2238
2549
  )
2239
2550
  ),
2240
-
2551
+ React.createElement('div', { className: 'rl-section-card' },
2552
+ React.createElement('div', { className: 'rl-section-title' },
2553
+ React.createElement('span', null, t('secOverrides')),
2554
+ React.createElement('span', { className: 'rl-badge rl-badge-dim' },
2555
+ String(overridesCount) + ' ' + (typeof plural === 'function' ? plural(overridesCount, ['запись', 'записи', 'записей']) : 'записей'))
2556
+ ),
2557
+ React.createElement('div', { className: 'rl-section-desc' }, t('secOverridesDesc')),
2558
+ React.createElement('div', { style: { display: 'flex', gap: '8px', flexWrap: 'wrap' } },
2559
+ React.createElement('input', {
2560
+ className: 'rl-select',
2561
+ style: { flex: '1 1 180px', maxWidth: 'none' },
2562
+ placeholder: t('overrideKeyPlaceholder'),
2563
+ value: newKey,
2564
+ onChange: (e) => setNewKey(e.target.value),
2565
+ }),
2566
+ React.createElement('input', {
2567
+ className: 'rl-select',
2568
+ style: { flex: '2 1 240px', maxWidth: 'none' },
2569
+ placeholder: t('overrideValuePlaceholder'),
2570
+ value: newVal,
2571
+ onChange: (e) => setNewVal(e.target.value),
2572
+ }),
2573
+ React.createElement('button', {
2574
+ type: 'button',
2575
+ className: 'rl-btn rl-btn-primary',
2576
+ disabled: !newKey.trim() || !newVal.trim(),
2577
+ onClick: () => {
2578
+ const k = newKey.trim()
2579
+ const v = newVal.trim()
2580
+ if (!k || !v) return
2581
+ const next = Object.assign({}, overrides, { [k]: v })
2582
+ try { scope.set('overrides', next) } catch (_e) { /* noop */ }
2583
+ setNewKey('')
2584
+ setNewVal('')
2585
+ },
2586
+ }, t('overrideAddBtn'))
2587
+ ),
2588
+ overridesCount === 0
2589
+ ? React.createElement('div', { className: 'rl-hint-text', style: { fontStyle: 'italic', padding: '4px 0' } }, t('overrideEmpty'))
2590
+ : React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: '6px', marginTop: '6px' } },
2591
+ Object.keys(overrides).map((k) =>
2592
+ React.createElement('div', {
2593
+ key: k,
2594
+ style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '6px 10px', background: 'var(--dsw-alias-bg-layer-3)', borderRadius: '6px', border: '1px solid var(--dsw-alias-border-l2)', fontSize: '12px' },
2595
+ },
2596
+ React.createElement('div', { style: { display: 'flex', gap: '8px', alignItems: 'center', overflow: 'hidden' } },
2597
+ React.createElement('code', { style: { color: 'var(--dsw-alias-state-brand-primary)', fontWeight: 600 } }, k),
2598
+ React.createElement('span', { style: { color: 'var(--dsw-alias-label-secondary)' } }, '→'),
2599
+ React.createElement('span', { style: { color: 'var(--dsw-alias-label-primary)' } }, overrides[k])
2600
+ ),
2601
+ React.createElement('button', {
2602
+ type: 'button',
2603
+ className: 'rl-btn',
2604
+ style: { padding: '2px 8px', height: '24px', fontSize: '11px', color: 'var(--dsw-alias-state-error-primary)' },
2605
+ onClick: () => {
2606
+ const next = Object.assign({}, overrides)
2607
+ delete next[k]
2608
+ try { scope.set('overrides', next) } catch (_e) { /* noop */ }
2609
+ },
2610
+ }, t('overrideDelete'))
2611
+ )
2612
+ )
2613
+ ),
2614
+ React.createElement('div', { className: 'rl-hint-text', style: { marginTop: '8px' } }, t('overrideTip'))
2615
+ ),
2241
2616
  React.createElement('div', { className: 'rl-section-card' },
2242
2617
  React.createElement('div', { className: 'rl-section-title' },
2243
2618
  React.createElement('span', null, t('secSupport')),
@@ -2260,7 +2635,7 @@ const exportSessionToMarkdown = (session, options = {}) => {
2260
2635
  ),
2261
2636
  React.createElement('div', { className: 'rl-actions-row' },
2262
2637
  React.createElement('a', {
2263
- href: makeIssueUrl({}, '0.2.25'),
2638
+ href: makeIssueUrl({}, '0.3.1'),
2264
2639
  target: '_blank',
2265
2640
  rel: 'noopener noreferrer',
2266
2641
  className: 'rl-btn rl-btn-primary'
@@ -2274,8 +2649,9 @@ const exportSessionToMarkdown = (session, options = {}) => {
2274
2649
  )
2275
2650
  )
2276
2651
  }
2277
-
2278
2652
  const RL_CSS = [
2653
+ '.rl-modal-mask{position:fixed;top:0;left:0;width:100vw;height:100vh;background:var(--dsw-alias-bg-mask);display:flex;align-items:center;justify-content:center;z-index:99999;backdrop-filter:blur(2px)}',
2654
+ '.rl-modal-box{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l1);border-radius:10px;width:calc(100vw - 40px);max-width:500px;box-shadow:var(--dsw-alias-shadow-l3);display:flex;flex-direction:column;gap:12px;padding:18px}',
2279
2655
  '.rl-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;overflow:hidden;transition:border-color .15s ease}',
2280
2656
  '.rl-head{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;display:flex;align-items:center;gap:12px;padding:14px 18px}',
2281
2657
  '.rl-head:hover{background:var(--dsw-alias-bg-layer-2)}',
@@ -2292,8 +2668,8 @@ const exportSessionToMarkdown = (session, options = {}) => {
2292
2668
  '.rl-grid-2{display:grid;grid-template-columns:repeat(auto-fit, minmax(280px, 1fr));gap:10px}',
2293
2669
  '.rl-grid-3{display:grid;grid-template-columns:repeat(auto-fit, minmax(140px, 1fr));gap:10px}',
2294
2670
  '.rl-badge{font-size:11px;padding:2px 8px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);display:inline-flex;align-items:center;gap:4px;font-weight:500}',
2295
- '.rl-badge-ok{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary);background:rgba(16,185,129,0.08)}',
2296
- '.rl-badge-warn{border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary);background:rgba(245,158,11,0.08)}',
2671
+ '.rl-badge-ok{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary);background:color-mix(in srgb, var(--dsw-alias-state-success-primary) 8%, transparent)}',
2672
+ '.rl-badge-warn{border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary);background:color-mix(in srgb, var(--dsw-alias-state-warning-primary) 8%, transparent)}',
2297
2673
  '.rl-badge-dim{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-3)}',
2298
2674
  '.rl-item-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:8px;padding:10px 12px;display:flex;flex-direction:column;gap:4px}',
2299
2675
  '.rl-item-head{display:flex;align-items:center;justify-content:space-between;gap:8px}',
@@ -2340,8 +2716,7 @@ const exportSessionToMarkdown = (session, options = {}) => {
2340
2716
  tag.textContent = RL_CSS
2341
2717
  document.head.appendChild(tag)
2342
2718
  }
2343
-
2344
2719
  module.exports = { apply, inject: ['locale', 'connection', 'remote', 'settingsScope', 'slots'] }
2345
2720
  return module.exports
2346
2721
  },
2347
- })
2722
+ })