@goodandready/dsh-russian-lang 0.3.0 → 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.
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 */ }
725
+ }
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
+ })
568
749
  }
569
-
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,7 +1070,6 @@ 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': 'Предпросмотр визуализации',
@@ -871,175 +1083,162 @@ const exportSessionToMarkdown = (session, options = {}) => {
871
1083
  'Specific instructions, questions, or requirements for this specialist...': 'Конкретные инструкции, вопросы или требования для данного специалиста…',
872
1084
  }
873
1085
  const DOM_EN_TEXT = {
874
- 'Scheduled tasks': 'Задачи по расписанию',
875
- 'Today': 'Сегодня',
876
- 'Plugin Market': 'Магазин плагинов',
877
- 'Auto mode': 'Автоматический режим',
878
- 'Full access': 'Полный доступ',
879
- 'Read only': 'Только чтение',
880
- 'Side card': 'Боковая панель',
881
- 'panelName': 'Палитра команд',
882
- 'Hooks': 'Хуки',
883
- 'Manage what the side card shows and how it behaves': 'Настройка содержимого и поведения боковой панели',
884
- 'Inject the sidebar-open tool for the model': 'Предоставить модели инструмент sidebar-open',
885
- '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 (по умолчанию выключено)',
886
- 'Position compatibility mode': 'Режим совместимости расположения',
887
- 'Pick the title-bar compatibility scheme: auto-detect (default, conservative) / DSH official web / known desktop shells / custom (shift distance + custom CSS)': 'Выберите схему совместимости строки заголовка: автоопределение (по умолчанию) / официальный DSH Web / десктопные оболочки / пользовательский режим',
888
- 'Auto-detect': 'Автоопределение',
889
- 'Sidebar content': 'Содержимое боковой панели',
890
- 'Changes': 'Изменения',
891
- 'Tasks': 'Задачи',
892
- 'Time Machine': 'Машина времени',
893
- 'Live Canvas': 'Живой холст',
894
- 'Side Chat (beta)': 'Боковой чат (бета)',
895
- 'Terminal': 'Терминал',
896
- 'Feature settings': 'Настройки функции',
897
- 'Low': 'Низкий',
898
- 'Medium': 'Средний',
899
- 'High': 'Высокий',
900
- 'Effort': 'Рассуждения',
901
- 'Search engine (ModSearch)': 'Поисковая система (ModSearch)',
902
- 'Search engine provider configuration.': 'Настройка провайдера поисковой системы.',
903
- 'X search only': 'Только поиск в X',
904
- 'Security Auditor Shield': 'Защитный щит аудитора',
905
- 'Shield: Safe': 'Щит: Безопасно',
906
- 'Shield: Alert': 'Щит: Тревога',
907
- 'Lens': 'Линза',
908
- 'Gallery': 'Галерея',
909
- 'SSH: Local': 'SSH: Локально',
910
- 'SSH: Remote': 'SSH: Удалённо',
911
- '7/7 rot': '7/7 рот.',
912
- 'Smoke chat': 'Тестовый чат',
913
- 'Quick Launch Goal': 'Быстрый запуск цели',
914
- 'Define the objective for the agent in autonomous mode:': 'Сформулируйте задачу для автономной работы агента:',
915
- 'Fix Bug': 'Исправление бага',
916
- 'Refactor (YAGNI)': 'Рефакторинг (YAGNI)',
917
- 'Tests & Coverage': 'Тесты и покрытие',
918
- 'Code Review': 'Ревью кода',
919
- 'New Feature': 'Новая функция',
920
- 'Security Audit': 'Аудит безопасности',
921
- 'Docs & Contract': 'Документация и контракт',
922
- 'Upgrade Deps': 'Обновление зависимостей',
923
- 'Dead Code': 'Мёртвый код',
924
- 'Performance': 'Производительность',
925
- 'Start Goal': 'Запустить цель',
926
-
927
- 'Launch Multi-Agent Orchestrator': 'Запуск мультиагентного оркестратора',
928
- 'Full DAG Pipeline': 'Полный DAG-пайплайн',
929
- 'Direct Specialist Subagent': 'Прямой субагент-специалист',
930
- 'Objective Title': 'Название задачи',
931
- 'Scope & Requirements': 'Объём и требования',
932
- 'Topology Scenario': 'Топологический сценарий',
933
- 'Auto (Infer based on prompt complexity)': 'Авто (определить по сложности задачи)',
934
- 'Hotfix (1 Stage: Triage & Minimal Fix)': 'Хотфикс (1 этап: анализ и точечный фикс)',
935
- 'Simple (2 Stages: Spec + Exec)': 'Простой (2 этапа: ТЗ + реализация)',
936
- 'Medium (4 Stages: Spec -> Design -> Code -> QA)': 'Средний (4 этапа: ТЗ -> Дизайн -> Код -> Тестирование)',
937
- 'Complex (6 Stages: Full Engineering Lifecycle)': 'Сложный (6 этапов: полный инженерный цикл)',
938
- 'Enterprise (7 Stages: R&D Spike -> Fullstack -> Gate)': 'Enterprise (7 этапов: R&D исследование -> Фулстек -> Гейт приёмки)',
939
- 'Target Specialist Role': 'Роль целевого специалиста',
940
- 'UI/UX Interface Designer': 'UI/UX дизайнер интерфейсов',
941
- 'System Architect (DESIGN.md / ADR)': 'Системный архитектор (DESIGN.md / ADR)',
942
- 'Technical Spec Analyst': 'Аналитик технических спецификаций',
943
- 'Senior Frontend Developer': 'Ведущий frontend-разработчик',
944
- 'Senior Backend Developer': 'Ведущий backend-разработчик',
945
- 'QA Automation Engineer': 'Инженер автоматизации тестирования',
946
- 'Refactoring & Complexity Specialist': 'Специалист по рефакторингу и сложности',
947
- 'Hotfix & Diagnostic Engineer': 'Инженер хотфиксов и диагностики',
948
- 'Documentation Specialist': 'Технический писатель / Документация',
949
- 'Spike & R&D Researcher': 'Исследователь R&D и прототипирования',
950
- 'DevOps & Tooling Specialist': 'DevOps и инфраструктурный специалист',
951
- 'Instructions for Subagent': 'Инструкции для субагента',
952
- 'Start Pipeline': 'Запустить пайплайн',
953
- 'Delegate Subagent': 'Делегировать субагенту',
954
- 'Dispatching...': 'Отправка…',
955
-
956
- 'Discounted rate active': 'Действует сниженный тариф',
957
- 'off-peak': 'непиковый',
958
- 'peak': 'пиковый',
959
- 'Context cache saved:': 'Сэкономлено на кэше:',
960
- '1M tokens, $': '1 млн токенов, $',
961
- 'Input (cache hit)': 'Ввод (попадание в кэш)',
962
- 'Input (cache miss)': 'Ввод (промах кэша)',
963
- 'Output': 'Вывод',
964
- 'SESSION TOKENS': 'ТОКЕНЫ СЕССИИ',
965
- 'SPEND BY MODEL': 'РАСХОД ПО МОДЕЛЯМ',
966
- 'Session total:': 'Всего за сессию:',
967
- 'Copy Summary': 'Скопировать сводку',
968
-
969
- 'Context Lens': 'Линза контекста',
970
- 'Saved tokens': 'Сэкономлено токенов',
971
- '% Saved': '% экономии',
972
- 'ops': 'операций',
973
- 'Budget': 'Бюджет',
974
- 'Active focus': 'Активный фокус',
975
- 'No focus paths set': 'Пути фокусировки не заданы',
976
-
977
- 'Status': 'Статус',
978
- 'Graph & CI': 'Граф и CI',
979
- 'Events & PRs': 'События и PR',
980
- 'Branch': 'Ветка',
981
- 'Sync': 'Синхронизация',
982
- 'Up to date with remote (@{upstream})': 'Синхронизировано с удалённым репозиторием (@{upstream})',
983
- 'Up to date with remote': 'Синхронизировано с удалённым репозиторием',
984
- 'Clean': 'Чисто',
985
- 'No modified or untracked files in the working directory.': 'В рабочем каталоге нет изменённых или неотслеживаемых файлов.',
986
- 'RECENT COMMITS': 'ПОСЛЕДНИЕ КОММИТЫ',
987
-
988
- 'Active jobs': 'Активные задачи',
989
- 'No active jobs': 'Нет активных задач',
990
- 'Register in Models': 'Зарегистрировать в моделях',
991
- 'Base URL': 'Базовый URL',
992
- 'API key env / credential name': 'Имя переменной окружения / ключа API',
993
- 'Default model': 'Модель по умолчанию',
994
- 'Enabled': 'Включено',
995
- 'Save': 'Сохранить',
996
- 'Saved': 'Сохранено',
997
- 'Refresh': 'Обновить',
998
- 'Loading settings…': 'Загрузка настроек…',
999
- 'Put the ar-… key in DSH credentials or process env under this name. Never paste the key into this form.': 'Укажите ключ ar-… в учётных данных DSH или переменной окружения. Не вставляйте ключ в форму.',
1000
- 'Preferred engine': 'Предпочитаемый движок',
1001
- 'API key': 'API-ключ',
1002
- 'stored, leave empty to keep it': 'сохранён, оставьте пустым для сохранения',
1003
- 'Separate multiple keys with commas. ModSearch rotates to the next key after authentication, rate-limit, or quota failures.': 'Разделяйте несколько ключей запятыми. ModSearch переключается на следующий ключ при ошибках аутентификации или лимитов.',
1004
- 'Built-in official endpoint, leave blank to use it': 'Встроенная официальная конечная точка, оставьте пустым',
1005
- 'Automatic engine chain Checked engines may join failover. Only engines ready here are listed.': 'Автоматическая цепочка движков: отмеченные движки участвуют в отказоустойчивости.',
1006
- 'Discard': 'Сбросить',
1007
- 'Model synchronization': 'Синхронизация моделей',
1008
- 'Refresh model catalogs for API-key providers. A dry-run is the default; applying changes is explicit.': 'Обновление каталогов моделей провайдеров API-ключей. По умолчанию выполняется проверка (dry-run); применение изменений явное.',
1009
- 'All API-key providers': 'Все провайдеры API-ключей',
1010
- 'Preview only (dry-run)': 'Только предпросмотр (dry-run)',
1011
- 'Confirm stale removal': 'Подтверждать удаление устаревших',
1012
- 'Refresh status': 'Обновить статус',
1013
- 'Refresh all': 'Обновить всё',
1014
- 'Discover': 'Обнаружить',
1015
- 'Check availability': 'Проверить доступность',
1016
- 'Check credentials': 'Проверить учётные данные',
1017
- 'Choose models': 'Выбрать модели',
1018
- 'Manual model selection': 'Выбор моделей вручную',
1019
- '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.': 'Изображения в чате обрабатываются выбранной моделью зрения. Оставьте оба поля пустыми для автовыбора первой доступной модели.',
1020
- 'Mode': 'Режим',
1021
- 'Hybrid (auto-rewrite + tools)': 'Гибридный (авто-переписывание + инструменты)',
1022
- 'Describe strategy': 'Стратегия описания',
1023
- 'Auto (use vision LLM)': 'Авто (использовать Vision LLM)',
1024
- 'Escalation': 'Эскалация',
1025
- 'Simple only (one pass)': 'Только простая (один проход)',
1026
- 'Routing': 'Маршрутизация',
1027
- 'Channel order': 'Порядок каналов',
1028
- 'Issue Reporter': 'Репортёр проблем',
1029
- 'Turn a DSH plugin problem into a reviewable issue with automated diagnostics and previews.': 'Превратите проблему с плагином DSH в готовый issue с автоматической диагностикой.',
1030
- 'GitHub sign-in is not configured for this installation.': 'Вход через GitHub не настроен для этой установки.',
1031
- '0 plugins': '0 плагинов',
1032
- 'Catalog': 'Каталог',
1033
- 'Report Editor': 'Редактор отчёта',
1034
- 'My Reports': 'Мои отчёты',
1035
- 'Authorization': 'Авторизация',
1036
- 'Search installed plugins…': 'Поиск установленных плагинов…',
1037
- 'Search installed plugins...': 'Поиск установленных плагинов...',
1038
- 'Refresh inventory': 'Обновить список',
1039
- 'Loading inventory & status…': 'Загрузка списка и статуса…',
1040
- '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...': 'Загрузка списка и статуса...'
1041
1241
  }
1042
-
1043
1242
  const ZH_WALKER = (root) => {
1044
1243
  try {
1045
1244
  const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT)
@@ -1089,7 +1288,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
1089
1288
  }
1090
1289
  } catch (err) { /* ignore */ }
1091
1290
  }
1092
-
1093
1291
  let zhObserver = null
1094
1292
  const syncZhDom = () => {
1095
1293
  try {
@@ -1116,8 +1314,7 @@ const exportSessionToMarkdown = (session, options = {}) => {
1116
1314
  }
1117
1315
  }, 'dsh-russian-lang: zh-dom')
1118
1316
  syncZhDom()
1119
-
1120
- const TYPO_YO_PAIRS = [["еще", "ещё"], ["ее", "её"], ["черный", "чёрный"], ["черная", "чёрная"], ["черные", "чёрные"], ["зеленый", "зелёный"], ["желтый", "жёлтый"], ["легкий", "лёгкий"], ["тяжелый", "тяжёлый"], ["надежный", "надёжный"], ["дешевый", "дешёвый"], ["идет", "идёт"], ["дает", "даёт"], ["ведет", "ведёт"], ["несет", "несёт"], ["живет", "живёт"], ["привел", "привёл"], ["шел", "шёл"], ["нее", "неё"], ["мое", "моё"], ["серьезно", "серьёзно"], ["придется", "придётся"], ["насчет", "насчёт"], ["твое", "твоё"], ["свое", "своё"], ["пойдем", "пойдём"], ["пришел", "пришёл"], ["нашел", "нашёл"], ["идем", "идём"], ["вперед", "вперёд"], ["пошел", "пошёл"], ["ребенка", "ребёнка"], ["ребенок", "ребёнок"], ["счет", "счёт"], ["своем", "своём"], ["придет", "придёт"], ["ушел", "ушёл"], ["ждет", "ждёт"], ["мертв", "мёртв"], ["твоем", "твоём"], ["вернется", "вернётся"], ["днем", "днём"], ["найдем", "найдём"], ["пойдет", "пойдёт"], ["начнем", "начнём"], ["вернемся", "вернёмся"], ["остается", "остаётся"], ["принес", "принёс"], ["идешь", "идёшь"], ["трех", "трёх"], ["ребенком", "ребёнком"], ["самолет", "самолёт"], ["определенно", "определённо"], ["пойдешь", "пойдёшь"], ["умрет", "умрёт"], ["прием", "приём"], ["прошел", "прошёл"], ["убьет", "убьёт"], ["тетя", "тётя"], ["провел", "провёл"], ["произойдет", "произойдёт"], ["семьей", "семьёй"], ["пойдемте", "пойдёмте"], ["пройдет", "пройдёт"], ["возьмем", "возьмём"], ["вернешься", "вернёшься"], ["найдешь", "найдёшь"], ["живешь", "живёшь"], ["отчет", "отчёт"], ["займет", "займёт"], ["ждем", "ждём"], ["сошел", "сошёл"], ["вдвоем", "вдвоём"], ["найдет", "найдёт"], ["вел", "вёл"], ["живем", "живём"], ["зашел", "зашёл"], ["начнется", "начнётся"], ["путем", "путём"], ["подойдет", "подойдёт"], ["режиссер", "режиссёр"], ["уйдет", "уйдёт"], ["придешь", "придёшь"], ["идемте", "идёмте"], ["ведешь", "ведёшь"], ["пес", "пёс"], ["умрешь", "умрёшь"], ["ждешь", "ждёшь"], ["найдете", "найдёте"], ["найдется", "найдётся"], ["клево", "клёво"], ["поймешь", "поймёшь"], ["разберемся", "разберёмся"], ["времен", "времён"], ["введен", "введён"], ["включен", "включён"], ["включенных", "включённых"], ["возьмется", "возьмётся"], ["завершен", "завершён"], ["завершенного", "завершённого"], ["задает", "задаёт"], ["задается", "задаётся"], ["заменен", "заменён"], ["звезд", "звёзд"], ["звезды", "звёзды"], ["изменен", "изменён"], ["истек", "истёк"], ["незавершенная", "незавершённая"], ["неподтвержденные", "неподтверждённые"], ["несохраненные", "несохранённые"], ["обновлен", "обновлён"], ["обновленные", "обновлённые"], ["определен", "определён"], ["отклонен", "отклонён"], ["отменен", "отменён"], ["очередность", "очерёдность"], ["поврежден", "повреждён"], ["повторен", "повторён"], ["подключен", "подключён"], ["подтвержденные", "подтверждённые"], ["приемки", "приёмки"], ["раздает", "раздаёт"], ["разрешенная", "разрешённая"], ["разрешенные", "разрешённые"], ["создаем", "создаём"], ["сойдется", "сойдётся"], ["сохранен", "сохранён"], ["сохраненный", "сохранённый"], ["счету", "счёту"], ["тяжелые", "тяжёлые"], ["темная", "тёмная"], ["уберет", "уберёт"], ["удален", "удалён"], ["учетные", "учётные"], ["учетных", "учётных"], ["емкости", "ёмкости"], ["емкость", "ёмкость"]]
1317
+ const TYPO_YO_PAIRS = [["еще","ещё"],["ее","её"],["черный","чёрный"],["черная","чёрная"],["черные","чёрные"],["зеленый","зелёный"],["желтый","жёлтый"],["легкий","лёгкий"],["тяжелый","тяжёлый"],["надежный","надёжный"],["дешевый","дешёвый"],["идет","идёт"],["дает","даёт"],["ведет","ведёт"],["несет","несёт"],["живет","живёт"],["привел","привёл"],["шел","шёл"],["нее","неё"],["мое","моё"],["серьезно","серьёзно"],["придется","придётся"],["насчет","насчёт"],["твое","твоё"],["свое","своё"],["пойдем","пойдём"],["пришел","пришёл"],["нашел","нашёл"],["идем","идём"],["вперед","вперёд"],["пошел","пошёл"],["ребенка","ребёнка"],["ребенок","ребёнок"],["счет","счёт"],["своем","своём"],["придет","придёт"],["ушел","ушёл"],["ждет","ждёт"],["мертв","мёртв"],["твоем","твоём"],["вернется","вернётся"],["днем","днём"],["найдем","найдём"],["пойдет","пойдёт"],["начнем","начнём"],["вернемся","вернёмся"],["остается","остаётся"],["принес","принёс"],["идешь","идёшь"],["трех","трёх"],["ребенком","ребёнком"],["самолет","самолёт"],["определенно","определённо"],["пойдешь","пойдёшь"],["умрет","умрёт"],["прием","приём"],["прошел","прошёл"],["убьет","убьёт"],["тетя","тётя"],["провел","провёл"],["произойдет","произойдёт"],["семьей","семьёй"],["пойдемте","пойдёмте"],["пройдет","пройдёт"],["возьмем","возьмём"],["вернешься","вернёшься"],["найдешь","найдёшь"],["живешь","живёшь"],["отчет","отчёт"],["займет","займёт"],["ждем","ждём"],["сошел","сошёл"],["вдвоем","вдвоём"],["найдет","найдёт"],["вел","вёл"],["живем","живём"],["зашел","зашёл"],["начнется","начнётся"],["путем","путём"],["подойдет","подойдёт"],["режиссер","режиссёр"],["уйдет","уйдёт"],["придешь","придёшь"],["идемте","идёмте"],["ведешь","ведёшь"],["пес","пёс"],["умрешь","умрёшь"],["ждешь","ждёшь"],["найдете","найдёте"],["найдется","найдётся"],["клево","клёво"],["поймешь","поймёшь"],["разберемся","разберёмся"],["времен","времён"],["введен","введён"],["включен","включён"],["включенных","включённых"],["возьмется","возьмётся"],["завершен","завершён"],["завершенного","завершённого"],["задает","задаёт"],["задается","задаётся"],["заменен","заменён"],["звезд","звёзд"],["звезды","звёзды"],["изменен","изменён"],["истек","истёк"],["незавершенная","незавершённая"],["неподтвержденные","неподтверждённые"],["несохраненные","несохранённые"],["обновлен","обновлён"],["обновленные","обновлённые"],["определен","определён"],["отклонен","отклонён"],["отменен","отменён"],["очередность","очерёдность"],["поврежден","повреждён"],["повторен","повторён"],["подключен","подключён"],["подтвержденные","подтверждённые"],["приемки","приёмки"],["раздает","раздаёт"],["разрешенная","разрешённая"],["разрешенные","разрешённые"],["создаем","создаём"],["сойдется","сойдётся"],["сохранен","сохранён"],["сохраненный","сохранённый"],["счету","счёту"],["тяжелые","тяжёлые"],["темная","тёмная"],["уберет","уберёт"],["удален","удалён"],["учетные","учётные"],["учетных","учётных"],["емкости","ёмкости"],["емкость","ёмкость"]]
1121
1318
  const typoYo = makeTypoYo(TYPO_YO_PAIRS)
1122
1319
  const getTypoConf = () => {
1123
1320
  try {
@@ -1129,15 +1326,12 @@ const exportSessionToMarkdown = (session, options = {}) => {
1129
1326
  const typoNode = (node, conf) => {
1130
1327
  const before = node.nodeValue
1131
1328
  if (!before || !before.match || (before.match(/[\u0400-\u04FF]/g) || []).length < 3) return
1132
-
1133
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
1134
1330
  if (node.parentElement && node.parentElement.closest('.katex, [data-latex], math')) return
1135
1331
  if (/\$[^$\n]+\$/.test(before)) return
1136
-
1137
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"]')) {
1138
1333
  return
1139
1334
  }
1140
-
1141
1335
  const sel = typeof window !== 'undefined' && window.getSelection && window.getSelection()
1142
1336
  if (sel && !sel.isCollapsed && sel.rangeCount > 0) {
1143
1337
  try {
@@ -1147,7 +1341,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
1147
1341
  }
1148
1342
  } catch (e) { /* bestEffort */ void e; }
1149
1343
  }
1150
-
1151
1344
  let after = typoQuotes(before)
1152
1345
  after = typoDash(after)
1153
1346
  after = typoPunct(after)
@@ -1209,16 +1402,13 @@ const exportSessionToMarkdown = (session, options = {}) => {
1209
1402
  }
1210
1403
  }, 'dsh-russian-lang: typography')
1211
1404
  syncTypo()
1212
-
1213
- const layout = makeLayout(new Set(["не", "что", "ты", "это", "на", "он", "мы", "как", "вы", "да", "мне", "нет", "меня", "так", "но", "его", "все", "она", "тебя", "если", "за", "бы", "тебе", "они", "чтобы", "же", "есть", "просто", "из", "для", "ну", "когда", "хорошо", "здесь", "по", "был", "знаю", "то", "только", "всё", "было", "вас", "может", "нас", "вот", "от", "быть", "кто", "будет", "почему", "вам", "их", "очень", "могу", "уже", "спасибо", "или", "нам", "еще", "там", "нужно", "сейчас", "где", "чем", "хочу", "ничего", "этого", "была", "мой", "ее", "ладно", "знаешь", "до", "этом", "потому", "теперь", "думаю", "больше", "её", "со", "раз", "ему", "надо", "время", "этот", "ли", "ещё", "пока", "даже", "привет", "сказал", "себя", "должен", "тоже", "хочешь", "давай", "никогда", "эй", "того", "тогда", "него", "ни", "тут", "были", "конечно", "правда", "об", "моя", "пожалуйста", "тобой", "сказать", "сегодня", "один", "лучше", "можешь", "сюда", "мной", "значит", "сделать", "всегда", "дело", "можно", "ей", "должны", "порядке", "без", "день", "том", "буду", "делать", "хотел", "чего", "эти", "много", "после", "этим", "всего", "во", "твой", "ним", "лет", "боже", "них", "сэр", "ведь", "мистер", "жизнь", "потом", "ней", "такой", "который", "всех", "через", "им", "возможно", "немного", "такое", "слишком", "себе", "зачем", "должна", "моей", "люди", "знаете", "этой", "думаешь", "свою", "точно", "человек", "твоя", "под", "сказала", "назад", "эту", "можем", "случилось", "мама", "мог", "вместе", "отец", "сделал", "мои", "кажется", "друг", "куда", "никто", "поэтому", "эта", "которые", "два", "тот", "сколько", "понимаю", "снова", "жизни", "нравится", "людей", "помочь", "видел", "люблю", "хочет", "место", "знать", "прости", "отлично", "похоже", "парень", "домой", "всем", "деньги", "иди", "времени", "дома", "именно", "доктор", "думал", "говорил", "делаешь", "будем", "прямо", "стоит", "поговорить", "найти", "разве", "слушай", "своей", "лишь", "ага", "можете", "простите", "хотела", "сам", "тем", "будешь", "прошу", "три", "деле", "хотите", "говорить", "давайте", "совсем", "знал", "знает", "какой", "моего", "скажи", "дом", "дела", "вами", "свои", "говорит", "несколько", "должно", "про", "ваш", "происходит", "жаль", "туда", "действительно", "папа", "завтра", "оно", "черт", "одна", "перед", "наш", "уверен", "отсюда", "нужна", "самом", "тех", "нужен", "свой", "мою", "кого", "верно", "работу", "каждый", "твоей", "будут", "хватит", "понял", "нее", "уж", "имя", "против", "пор", "чём", "раньше", "говорю", "более", "надеюсь", "итак", "при", "ваша", "вообще", "пошли", "мать", "нельзя", "наверное", "нами", "твои", "могли", "дай", "ради", "всю", "ребята", "ко", "хотя", "понимаешь", "идти", "этих", "откуда", "такая", "ясно", "другой", "извините", "вижу", "видеть", "над", "могут", "равно", "мисс", "скоро", "будто", "зовут", "виду", "наши", "думала", "послушай", "между", "своего", "вопрос", "этому", "почти", "года", "человека", "которая", "подожди", "руки", "нормально", "такие", "возьми", "минут", "извини", "вещи", "могла", "смотри", "хоть", "работа", "пару", "сын", "ваше", "дня", "пора", "неё", "жить", "видишь", "достаточно", "господи", "быстро", "твою", "весь", "убить", "ночь", "говоришь", "собой", "скажу", "готов", "слышал", "какая", "посмотри", "первый", "самое", "видела", "пусть", "месте", "нашли", "сказали", "плохо", "смогу", "ваши", "отца", "детей", "знаем", "рад", "прав", "никаких", "имею", "миссис", "иногда", "смерти", "своих", "пойду", "брат", "вроде", "рядом", "мир", "произошло", "которую", "сделала", "говорила", "знала", "мое", "одного", "таким", "помощь", "такого", "кем", "насчет", "вчера", "случае", "увидеть", "нашей", "говорят", "правильно", "убил", "одно", "пойти", "друга", "сама", "долго", "работать", "дверь", "делает", "женщина", "важно", "кроме", "будь", "собираюсь", "вашей", "нужны", "здорово", "номер", "проблема", "проблемы", "денег", "чёрт", "хороший", "твоего", "одну", "дальше", "вернуться", "давно", "последний", "ночи", "узнать", "уверена", "машину", "права", "пять", "моих", "также", "тому", "вечером", "дети", "опять", "серьезно", "работает", "своим", "сначала", "две", "одной", "моим", "те", "жена", "думаете", "помнишь", "глаза", "ними", "чувак", "ночью", "чувствую", "взять", "сразу", "утром", "часть", "придется", "стать", "идея", "дорогая", "прекрасно", "слова", "сделали", "видели", "наша", "сердце", "помню", "парня", "момент", "утро", "здравствуйте", "стал", "скорее", "ох", "большой", "часов", "год", "друзья", "сильно", "говори", "смысле", "которой", "уйти", "которого", "капитан", "работы", "любовь", "дочь", "новый", "боюсь", "имеет", "стой", "дней", "той", "голову", "ах", "столько", "понятно", "дать", "девушка", "довольно", "доме", "вечер", "оба", "пойдем", "увидимся", "шанс", "хорошая", "прежде", "сделаю", "какого", "быстрее", "понять", "рассказать", "вашего", "бог", "интересно", "затем", "умер", "обо", "никого", "парни", "самый", "твое", "вон", "пришел", "чтоб", "поверить", "слово", "будете", "нашел", "милая", "странно", "какие", "ой", "та", "однажды", "сможешь", "получить", "обычно", "ума", "полагаю", "алло", "посмотреть", "иначе", "наших", "тело", "хотели", "свое", "пришли", "моё", "другие", "выглядит", "подумал", "лицо", "приятно", "мам", "говорите", "обратно", "нашего", "телефон", "мире", "честь", "господин", "доброе", "план", "другом", "час", "джон", "пути", "делал", "типа", "посмотрим", "насколько", "пытался", "правду", "решил", "поздно", "нем", "работе", "любит", "хотят", "кому", "сестра", "места", "скажешь", "следующий", "других", "получил", "кстати", "говорили", "пришла", "безопасности", "рада", "город", "внимание", "кофе", "которое", "вся", "семья", "конец", "позже", "дал", "думать", "спать", "вернулся", "остаться", "некоторые", "кровь", "оружие", "пришлось", "хуже", "дорогой", "полиция", "сможем", "понимаете", "четыре", "которых", "ужасно", "ждать", "способ", "делаю", "готовы", "совершенно", "смотреть", "вернусь", "большое", "единственный", "вперед", "часа", "муж", "идет", "таких", "ребенка", "машина", "стороны", "смог", "сына", "играть", "сами", "верю", "честно", "нашла", "звучит", "угодно", "руку", "начала", "перевод", "какое", "взял", "станет", "пытаюсь", "внутри", "знали", "новости", "добрый", "подождите", "скажите", "ребенок", "вашу", "мальчик", "осталось", "делаете", "году", "мере", "поняла", "отношения", "далеко", "вокруг", "хотим", "слышала", "двух", "приятель", "никому", "мужчина", "неделю", "вернуть", "смерть", "сих", "ту", "сможет", "дайте", "двое", "видите", "долларов", "крови", "решение", "путь", "следует", "неужели", "около", "готова", "семьи", "идем", "смотрите", "круто", "история", "вдруг", "спросить", "твоих", "второй", "вина", "джек", "женщины", "нашу", "заткнись", "минуту", "собираешься", "твоим", "шоу", "прощения", "больно", "машины", "пап", "трудно", "постоянно", "поможет", "проблем", "например", "дерьмо", "полиции", "начать", "знают", "лучший", "добро", "спасти", "рождения", "оставить", "чарли", "любишь", "конце", "забыл", "месяцев", "часто", "мило", "комнате", "нему", "матери", "милый", "полностью", "школе", "леди", "серьёзно", "убили", "случай", "джо", "убийство", "послушайте", "посмотрите", "пошел", "никакого", "сделаем", "говоря", "неплохо", "использовать", "прошлой", "иду", "узнал", "прекрати", "чуть", "держи", "невозможно", "господа", "слушайте", "наше", "машине", "сэм", "свет", "образом", "иметь", "другое", "другого", "шесть", "людям", "погоди", "имени", "детка", "недели", "меньше", "легко", "крайней", "девочка", "придётся", "мало", "ненавижу", "последние", "насчёт", "выглядишь", "сообщение", "думает", "друзей", "обещаю", "агент", "вместо", "последнее", "самого", "настолько", "ответ", "детектив", "понятия", "родители", "показать", "поехали", "стало", "мэм", "своими", "всей", "очевидно", "помогите", "маленький", "школу", "пожаловать", "твоё", "либо", "забрать", "прошло", "особенно", "имеешь", "истории", "эм", "вопросы", "большая", "минутку", "пол", "остановить", "чему", "свидания", "обязательно", "городе", "убийца", "сделай", "ваших", "любви", "спокойной", "своё", "мадам", "идите", "выйти", "делают", "немедленно", "делай", "возможность", "человеком", "бывает", "ужин", "моем", "купить", "позвонить", "наконец", "стала", "любой", "клянусь", "смешно", "абсолютно", "всему", "переводчики", "забудь", "секунду", "выбор", "тысяч", "потерял", "майкл", "волнуйся", "ноги", "таком", "настоящий", "означает", "встретиться", "известно", "работаю", "замечательно", "любом", "оставь", "связи", "боль", "выпить", "представить", "убийства", "малыш", "умерла", "воды", "окей", "речь", "хорошие", "поводу", "перестань", "увидел", "повезло", "президент", "мужик", "которым", "прошлом", "дам", "десять", "умереть", "поговорим", "отцом", "искать", "тихо", "оставил", "пистолет", "начал", "сложно", "города", "неделе", "хотелось", "принять", "историю", "босс", "держать", "пойдём", "подумала", "фильм", "слышишь", "рано", "удачи", "еду", "словно", "причина", "пришёл", "компании", "спокойно", "работал", "подумать", "вести", "вещь", "брата", "конца", "получится", "блин", "право", "ввиду", "бога", "подарок", "комнату", "мира", "успокойся", "позволить", "моему", "идёт", "самая", "рассказал", "курсе", "намного", "мистера", "женщин", "тяжело", "ух", "предложение", "одним", "счет", "улице", "другим", "уходи", "майк", "среди", "помоги", "получилось", "месяц", "проверить", "генри", "голос", "позвольте", "стороне", "вероятно", "случится", "уверены", "мужа", "питер", "провести", "людьми", "неважно", "нашёл", "доктора", "месяца", "игра", "идём", "нём", "джордж", "познакомиться", "возьму", "слышали", "пошла", "корабль", "чувствуешь", "позволь", "сторону", "игры", "согласен", "письмо", "вниз", "молодец", "наверняка", "утра", "вашим", "нечего", "встречи", "вид", "никакой", "связь", "список", "такую", "весело", "сидеть", "гораздо", "собирался", "впервые", "находится", "объяснить", "полицию", "решили", "семью", "свидание", "отличная", "остальные", "прийти", "руках", "фбр", "чувство", "котором", "решила", "понадобится", "сынок", "найду", "фрэнк", "школы", "чувства", "дядя", "стали", "ушла", "первым", "поверь", "расскажи", "хочется", "девушки", "сон", "имел", "каким", "парнем", "многие", "части", "ехать", "считаю", "никуда", "голове", "старый", "оттуда", "называется", "вопросов", "фото", "пройти", "заниматься", "другу", "вернулась", "пара", "никак", "девочки", "благодарю", "своем", "недавно", "приехал", "глаз", "могло", "становится", "пыталась", "является", "брось", "богу", "нашим", "помощи", "волосы", "невероятно", "одном", "ушел", "джейн", "тела", "недель", "собирается", "защитить", "всеми", "существует", "жив", "первая", "позвоню", "благодаря", "вперёд", "навсегда", "помните", "земле", "делу", "вполне", "пытается", "будущее", "семь", "своему", "ок", "идиот", "слава", "стоило", "удалось", "делаем", "команда", "главное", "огонь", "дэнни", "замуж", "начали", "мужчины", "силы", "семье", "шеф", "суд", "слышу", "тюрьме", "попробовать", "друзьями", "алекс", "вашем", "новая", "маленькая", "черта", "придет", "страшно", "услышать", "новые", "общем", "звонил", "купил", "понравится", "близко", "самой", "смысл", "хорошее", "любил", "сара", "единственная", "делали", "макс", "мэри", "жду", "получается", "хорошего", "эми", "дамы", "жену", "гарри", "неправильно", "стоять", "плевать", "ключ", "верить", "лично", "слышать", "взяли", "поздравляю", "пить", "заставить", "получили", "рот", "делала", "преступления", "двери", "землю", "разумеется", "очередь", "первой", "разговор", "видимо", "звонок", "джеймс", "менее", "нового", "получила", "хм", "подальше", "сможете", "похож", "состоянии", "необходимо"]), new Set())
1405
+ const layout = makeLayout(new Set(["не","что","ты","это","на","он","мы","как","вы","да","мне","нет","меня","так","но","его","все","она","тебя","если","за","бы","тебе","они","чтобы","же","есть","просто","из","для"]), new Set())
1214
1406
  const layoutFixCandidate = layout.candidate
1215
1407
  const learnWords = layout.learnWords
1216
-
1217
1408
  function setNativeInputValue(el, value, cursorStart, cursorEnd) {
1218
1409
  if (!el) return
1219
1410
  const oldVal = el.value || ''
1220
1411
  if (oldVal === value) return
1221
-
1222
1412
  const proto = el.tagName === 'TEXTAREA' ? window.HTMLTextAreaElement.prototype : window.HTMLInputElement.prototype
1223
1413
  const desc = Object.getOwnPropertyDescriptor(proto, 'value')
1224
1414
  if (desc && desc.set) {
@@ -1226,31 +1416,26 @@ const exportSessionToMarkdown = (session, options = {}) => {
1226
1416
  } else {
1227
1417
  el.value = value
1228
1418
  }
1229
-
1230
1419
  if (el._valueTracker) {
1231
1420
  el._valueTracker.setValue(value === '' ? '__force__' : '')
1232
1421
  }
1233
-
1234
1422
  try {
1235
1423
  const propsKey = Object.keys(el).find((k) => k.startsWith('__reactProps$') || k.startsWith('__reactEventHandlers$'))
1236
1424
  if (propsKey && el[propsKey] && typeof el[propsKey].onChange === 'function') {
1237
1425
  el[propsKey].onChange({ target: el, currentTarget: el })
1238
1426
  }
1239
1427
  } catch (e) { /* bestEffort */ void e; }
1240
-
1241
1428
  try {
1242
1429
  el.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true }))
1243
1430
  } catch (e) {
1244
1431
  el.dispatchEvent(new Event('input', { bubbles: true }))
1245
1432
  }
1246
1433
  el.dispatchEvent(new Event('change', { bubbles: true }))
1247
-
1248
1434
  if (typeof cursorStart === 'number' && typeof el.setSelectionRange === 'function') {
1249
1435
  const end = typeof cursorEnd === 'number' ? cursorEnd : cursorStart
1250
1436
  try { el.setSelectionRange(cursorStart, end) } catch (e) { /* bestEffort */ void e; }
1251
1437
  }
1252
1438
  }
1253
-
1254
1439
  let layoutHintEl = null
1255
1440
  const layoutCurrentInput = (ev) => {
1256
1441
  if (ev && ev.target) {
@@ -1273,7 +1458,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
1273
1458
  if (focused) return focused
1274
1459
  return null
1275
1460
  }
1276
-
1277
1461
  function getCaretCharacterOffset(root) {
1278
1462
  if (!root) return -1
1279
1463
  const sel = typeof window !== 'undefined' && window.getSelection && window.getSelection()
@@ -1289,7 +1473,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
1289
1473
  return -1
1290
1474
  }
1291
1475
  }
1292
-
1293
1476
  function setCaretCharacterOffset(root, offset) {
1294
1477
  if (!root || offset < 0) return
1295
1478
  const sel = typeof window !== 'undefined' && window.getSelection && window.getSelection()
@@ -1319,7 +1502,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
1319
1502
  }
1320
1503
  } catch (e) { /* bestEffort */ void e; }
1321
1504
  }
1322
-
1323
1505
  const isCaretInCode = (el, value, caretOffset) => {
1324
1506
  if (!el) return false
1325
1507
  if (caretOffset == null || caretOffset < 0) caretOffset = (value || '').length
@@ -1335,7 +1517,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
1335
1517
  if (single && single.length % 2 === 1) return true
1336
1518
  return false
1337
1519
  }
1338
-
1339
1520
  const getComposerText = (el) => {
1340
1521
  if (!el) return ''
1341
1522
  if (el.tagName === 'TEXTAREA' || el.tagName === 'INPUT') {
@@ -1345,17 +1526,14 @@ const exportSessionToMarkdown = (session, options = {}) => {
1345
1526
  const raw = host.innerText !== undefined ? host.innerText : (host.textContent || '')
1346
1527
  return raw.replace(/\r/g, '').replace(/[\u200B\uFEFF]/g, '').replace(/\n+$/, '')
1347
1528
  }
1348
-
1349
1529
  const setComposerText = (el, value, cursorStart, cursorEnd) => {
1350
1530
  if (!el) return
1351
1531
  if (el.tagName === 'TEXTAREA' || el.tagName === 'INPUT') {
1352
1532
  setNativeInputValue(el, value, cursorStart, cursorEnd)
1353
1533
  return
1354
1534
  }
1355
-
1356
1535
  const host = (el.closest && el.closest('[data-composer-input], [contenteditable="true"]')) || el
1357
1536
  const initialOffset = typeof cursorStart === 'number' ? cursorStart : getCaretCharacterOffset(host)
1358
-
1359
1537
  try {
1360
1538
  host.focus()
1361
1539
  const sel = typeof window !== 'undefined' && window.getSelection && window.getSelection()
@@ -1366,12 +1544,10 @@ const exportSessionToMarkdown = (session, options = {}) => {
1366
1544
  sel.addRange(range)
1367
1545
  }
1368
1546
  document.execCommand('insertText', false, value)
1369
-
1370
1547
  const targetOffset = typeof cursorStart === 'number' ? cursorStart : (initialOffset >= 0 ? Math.min(initialOffset, value.length) : value.length)
1371
1548
  setCaretCharacterOffset(host, targetOffset)
1372
1549
  } catch (e) { /* bestEffort */ void e; }
1373
1550
  }
1374
-
1375
1551
  const layoutDismiss = () => {
1376
1552
  if (layoutHintEl) { layoutHintEl.remove(); layoutHintEl = null }
1377
1553
  }
@@ -1398,7 +1574,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
1398
1574
  layoutHintEl.style.left = (r.left + 8) + 'px'
1399
1575
  layoutHintEl.style.bottom = (window.innerHeight - r.top + 6) + 'px'
1400
1576
  }
1401
-
1402
1577
  let layoutBadgeEl = null
1403
1578
  const layoutBadge = (el) => {
1404
1579
  const value = getComposerText(el)
@@ -1437,7 +1612,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
1437
1612
  const layoutBadgeHide = () => {
1438
1613
  if (layoutBadgeEl) { layoutBadgeEl.remove(); layoutBadgeEl = null }
1439
1614
  }
1440
-
1441
1615
  let isFormatting = false
1442
1616
  const layoutOnInput = (ev) => {
1443
1617
  if (isFormatting) return
@@ -1446,8 +1620,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
1446
1620
  if (!el) { layoutDismiss(); layoutBadgeHide(); return }
1447
1621
  layoutBadge(el) // #66: метка раскладки
1448
1622
  const value = getComposerText(el)
1449
-
1450
-
1451
1623
  if (value.trim().length < 4) { layoutDismiss(); return }
1452
1624
  const latCount = (value.match(/[a-z]/g) || []).length
1453
1625
  const cyrCount = (value.match(/[\u0430-\u044f\u0451]/g) || []).length
@@ -1463,18 +1635,14 @@ const exportSessionToMarkdown = (session, options = {}) => {
1463
1635
  } catch (err) { /* ignore */ }
1464
1636
  }
1465
1637
  const unsubscribeLayout = runtime.subscribe(layoutOnInput)
1466
-
1467
1638
  const layoutOnKeydown = (ev) => {
1468
1639
  const el = layoutCurrentInput(ev)
1469
1640
  if (!el) return
1470
-
1471
1641
  const value = getComposerText(el)
1472
-
1473
1642
  const isL = ev.code === 'KeyL' || ev.key.toLowerCase() === 'l' || ev.key.toLowerCase() === 'д'
1474
1643
  if (ev.altKey && !ev.ctrlKey && !ev.metaKey && isL) {
1475
1644
  const sel = typeof window !== 'undefined' && window.getSelection && window.getSelection()
1476
1645
  const selectedText = (sel && !sel.isCollapsed) ? sel.toString() : ''
1477
-
1478
1646
  if (selectedText) {
1479
1647
  const c = layoutFixCandidate(selectedText, 'lat2cyr') || layoutFixCandidate(selectedText, 'cyr2lat')
1480
1648
  if (c) {
@@ -1496,7 +1664,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
1496
1664
  return
1497
1665
  }
1498
1666
  }
1499
-
1500
1667
  const c = layoutFixCandidate(value, 'lat2cyr') || layoutFixCandidate(value, 'cyr2lat')
1501
1668
  if (c) {
1502
1669
  ev.preventDefault()
@@ -1518,7 +1685,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
1518
1685
  }
1519
1686
  return
1520
1687
  }
1521
-
1522
1688
  if (ev.key === ' ' || ev.key === 'Enter') {
1523
1689
  if (value && value.startsWith('/') && typeof expandSlashAlias === 'function') {
1524
1690
  const expanded = expandSlashAlias(value)
@@ -1544,18 +1710,15 @@ const exportSessionToMarkdown = (session, options = {}) => {
1544
1710
  }
1545
1711
  }
1546
1712
  }
1547
-
1548
1713
  try {
1549
1714
  const snapVal = scope ? (scope.getSnapshot().value || {}) : {}
1550
1715
  const typoLive = snapVal.typography ? snapVal.typography.liveInput !== false : true
1551
1716
  if (typoLive && !ev.ctrlKey && !ev.altKey && !ev.metaKey) {
1552
1717
  const isTextarea = el.tagName === 'TEXTAREA' || el.tagName === 'INPUT'
1553
-
1554
1718
  if (isTextarea) {
1555
1719
  const caretPos = el.selectionStart || 0
1556
1720
  if (!isCaretInCode(el, value, caretPos)) {
1557
1721
  const textBefore = caretPos >= 0 ? value.slice(0, caretPos) : value
1558
-
1559
1722
  if (ev.key === '-' && textBefore.endsWith('-')) {
1560
1723
  ev.preventDefault()
1561
1724
  isFormatting = true
@@ -1567,7 +1730,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
1567
1730
  }
1568
1731
  return
1569
1732
  }
1570
-
1571
1733
  if (ev.key === '"') {
1572
1734
  ev.preventDefault()
1573
1735
  isFormatting = true
@@ -1587,6 +1749,104 @@ const exportSessionToMarkdown = (session, options = {}) => {
1587
1749
  }
1588
1750
  } catch (e) { /* bestEffort */ void e; }
1589
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')
1590
1850
  ctx.effect(() => {
1591
1851
  document.addEventListener('input', layoutOnInput, true)
1592
1852
  document.addEventListener('keydown', layoutOnKeydown, true)
@@ -1598,7 +1858,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
1598
1858
  layoutBadgeHide()
1599
1859
  }
1600
1860
  }, 'dsh-russian-lang: layout')
1601
-
1602
1861
  if (!ctx.slots || !React) return
1603
1862
  const toggleRu = (wantRu) => {
1604
1863
  try {
@@ -1634,7 +1893,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
1634
1893
  }, SettingsCard),
1635
1894
  )
1636
1895
  } catch (err) { console.warn('dsh-russian-lang: settings slot unavailable', err) }
1637
-
1638
1896
  try {
1639
1897
  ctx.slots.inject('conversation.session.header.utilities', () =>
1640
1898
  ctx.slots.register({
@@ -1646,7 +1904,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
1646
1904
  }, QuickLangSwitch),
1647
1905
  )
1648
1906
  } catch (err) { /* ignore if slot not declared */ }
1649
-
1650
1907
  try {
1651
1908
  ctx.slots.inject('conversation.chat.assistant-actions', () =>
1652
1909
  ctx.slots.register({
@@ -1658,7 +1915,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
1658
1915
  }, TranslateTurnAction),
1659
1916
  )
1660
1917
  } catch (err) { /* ignore if slot not declared */ }
1661
-
1662
1918
  try {
1663
1919
  ctx.slots.inject('conversation.session.header.utilities', () =>
1664
1920
  ctx.slots.register({
@@ -1671,7 +1927,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
1671
1927
  )
1672
1928
  } catch (err) { /* ignore if slot not declared */ }
1673
1929
  }
1674
-
1675
1930
  function QuickLangSwitch(props) {
1676
1931
  const inj = typeof props.inject === 'function' ? (props.inject() || {}) : (props.inject || {})
1677
1932
  const runtime = props.runtime || inj.runtime
@@ -1691,7 +1946,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
1691
1946
  onClick: () => { if (toggleRu) toggleRu(!isRu) },
1692
1947
  }, React.createElement('span', { className: 'rl-lang-text' }, isRu ? 'RU' : 'EN'))
1693
1948
  }
1694
-
1695
1949
  function ExportMarkdownButton(props) {
1696
1950
  const [done, setDone] = React.useState(false)
1697
1951
  const onExport = () => {
@@ -1699,11 +1953,9 @@ const exportSessionToMarkdown = (session, options = {}) => {
1699
1953
  const titleEl = document.querySelector('.dsw-session-title, [data-session-title], header h1, header h2, [class*="title"]')
1700
1954
  const rawTitle = (titleEl && titleEl.textContent.trim()) || document.title || 'Диалог DSH'
1701
1955
  const title = rawTitle.replace(/\s*—\s*DeepSeek Harness\s*$/, '').trim() || 'Диалог DSH'
1702
-
1703
1956
  const flow = document.querySelector('[data-chat-flow]') || document.querySelector('[data-chat-flow-scroll]') || document.body
1704
1957
  const flowItems = Array.from(flow.querySelectorAll('[data-chat-flow-kind]'))
1705
1958
  const messages = []
1706
-
1707
1959
  if (flowItems.length > 0) {
1708
1960
  flowItems.forEach((node) => {
1709
1961
  const kind = node.getAttribute('data-chat-flow-kind')
@@ -1721,7 +1973,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
1721
1973
  }
1722
1974
  })
1723
1975
  }
1724
-
1725
1976
  if (messages.length === 0) {
1726
1977
  const allElements = Array.from(document.querySelectorAll('[class*="userRow"], [class*="UserRow"], [class*="assistant-step"], [class*="AssistantMarkdown"], .dsw-turn-node, [data-role]'))
1727
1978
  const seen = new Set()
@@ -1736,7 +1987,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
1736
1987
  messages.push({ role, content: text })
1737
1988
  })
1738
1989
  }
1739
-
1740
1990
  const session = {
1741
1991
  title,
1742
1992
  createdAt: new Date().toISOString(),
@@ -1761,7 +2011,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
1761
2011
  console.warn('dsh-russian-lang: export md failed', err)
1762
2012
  }
1763
2013
  }
1764
-
1765
2014
  return React.createElement('button', {
1766
2015
  type: 'button',
1767
2016
  className: 'rl-lang-chip rl-export-md-btn',
@@ -1769,7 +2018,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
1769
2018
  onClick: onExport,
1770
2019
  }, React.createElement('span', { className: 'rl-lang-text' }, done ? '✓ MD' : '📥 MD'))
1771
2020
  }
1772
-
1773
2021
  async function translateTurnContent(text) {
1774
2022
  if (!text || typeof text !== 'string') return { error: 'Текст для перевода пуст.' }
1775
2023
  try {
@@ -1787,19 +2035,17 @@ const exportSessionToMarkdown = (session, options = {}) => {
1787
2035
  return { error: 'Не удалось связаться с хостом DSH: ' + (err.message || err) }
1788
2036
  }
1789
2037
  }
1790
-
1791
2038
  function TranslateTurnAction(props) {
1792
2039
  const t = typeof props.t === 'function' ? props.t : ((k) => k)
1793
2040
  const [loading, setLoading] = React.useState(false)
1794
2041
  const [open, setOpen] = React.useState(false)
1795
2042
  const [upStatus, setUpStatus] = React.useState({
1796
- currentVersion: '0.2.26',
2043
+ currentVersion: '0.3.1',
1797
2044
  latestVersion: undefined,
1798
2045
  updateAvailable: false
1799
2046
  })
1800
2047
  const [upLoading, setUpLoading] = React.useState(false)
1801
2048
  const [upMsg, setUpMsg] = React.useState(null)
1802
-
1803
2049
  const checkUpdate = () => {
1804
2050
  setUpLoading(true)
1805
2051
  setUpMsg(null)
@@ -1814,7 +2060,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
1814
2060
  setUpMsg({ type: 'err', text: t('updaterFailed') })
1815
2061
  })
1816
2062
  }
1817
-
1818
2063
  const triggerUpdate = () => {
1819
2064
  setUpLoading(true)
1820
2065
  setUpMsg(null)
@@ -1837,18 +2082,15 @@ const exportSessionToMarkdown = (session, options = {}) => {
1837
2082
  setUpMsg({ type: 'err', text: t('updaterFailed') })
1838
2083
  })
1839
2084
  }
1840
-
1841
2085
  const [transStatus, setTransStatus] = React.useState(null)
1842
2086
  const [transStarting, setTransStarting] = React.useState(false)
1843
2087
  const [transMsg, setTransMsg] = React.useState(null)
1844
-
1845
2088
  const fetchTransStatus = () => {
1846
2089
  fetch('/api/dsh-russian-lang/translator/status')
1847
2090
  .then((r) => r.json())
1848
2091
  .then((data) => setTransStatus(data))
1849
2092
  .catch(() => setTransStatus(null))
1850
2093
  }
1851
-
1852
2094
  const startLibreTranslate = () => {
1853
2095
  setTransStarting(true)
1854
2096
  setTransMsg(null)
@@ -1868,15 +2110,12 @@ const exportSessionToMarkdown = (session, options = {}) => {
1868
2110
  setTransMsg({ type: 'err', text: 'Ошибка: ' + (err.message || err) })
1869
2111
  })
1870
2112
  }
1871
-
1872
2113
  React.useEffect(() => {
1873
2114
  if (open) {
1874
2115
  checkUpdate()
1875
2116
  fetchTransStatus()
1876
2117
  }
1877
2118
  }, [open])
1878
-
1879
-
1880
2119
  return React.createElement('button', {
1881
2120
  type: 'button',
1882
2121
  className: 'rl-action-btn' + (open ? ' rl-action-btn-active' : ''),
@@ -1887,7 +2126,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
1887
2126
  const btn = ev.currentTarget
1888
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]')
1889
2128
  if (!tailFlowItem) return
1890
-
1891
2129
  const parentContainer = tailFlowItem.parentElement
1892
2130
  let box = parentContainer ? parentContainer.querySelector('.rl-turn-translation[data-tail-key="' + (tailFlowItem.getAttribute('data-chat-flow-key') || '') + '"]') : null
1893
2131
  if (!box) {
@@ -1898,7 +2136,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
1898
2136
  setOpen(box.style.display !== 'none')
1899
2137
  return
1900
2138
  }
1901
-
1902
2139
  setLoading(true)
1903
2140
  try {
1904
2141
  const assistantNodes = []
@@ -1909,7 +2146,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
1909
2146
  }
1910
2147
  prev = prev.previousElementSibling
1911
2148
  }
1912
-
1913
2149
  let rawText = ''
1914
2150
  if (assistantNodes.length > 0) {
1915
2151
  rawText = assistantNodes.map((node) => {
@@ -1929,19 +2165,16 @@ const exportSessionToMarkdown = (session, options = {}) => {
1929
2165
  }).filter(Boolean).join('\n\n')
1930
2166
  }
1931
2167
  }
1932
-
1933
2168
  if (!rawText) {
1934
2169
  const prose = tailFlowItem.querySelector('.dsw-prose, [data-block-kind="text"], .dsw-markdown-view, p')
1935
2170
  if (prose) rawText = prose.innerText.trim()
1936
2171
  }
1937
-
1938
2172
  let transResult = { error: 'Не удалось обнаружить текст сообщения ассистента для перевода.' }
1939
2173
  if (rawText) {
1940
2174
  transResult = await translateTurnContent(rawText)
1941
2175
  }
1942
2176
  const isErr = !!transResult.error
1943
2177
  const displayText = transResult.translatedText || transResult.error || ''
1944
-
1945
2178
  box = document.createElement('div')
1946
2179
  box.className = 'rl-turn-translation'
1947
2180
  const key = tailFlowItem.getAttribute('data-chat-flow-key')
@@ -1955,7 +2188,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
1955
2188
  '</div>' +
1956
2189
  '<div class="rl-trans-body"' + (isErr ? ' style="color: var(--dsw-alias-state-warning-primary); font-size: 13px;"' : '') + '></div>'
1957
2190
  box.querySelector('.rl-trans-body').textContent = displayText
1958
-
1959
2191
  const copyBtn = box.querySelector('.rl-btn-copy')
1960
2192
  if (copyBtn) {
1961
2193
  copyBtn.addEventListener('click', (e) => {
@@ -1965,14 +2197,12 @@ const exportSessionToMarkdown = (session, options = {}) => {
1965
2197
  setTimeout(() => { copyBtn.textContent = '📋 Копировать' }, 2000)
1966
2198
  })
1967
2199
  }
1968
-
1969
2200
  const closeBtn = box.querySelector('.rl-btn-close')
1970
2201
  closeBtn.addEventListener('click', (e) => {
1971
2202
  e.stopPropagation()
1972
2203
  box.style.display = 'none'
1973
2204
  setOpen(false)
1974
2205
  })
1975
-
1976
2206
  if (parentContainer) {
1977
2207
  parentContainer.insertBefore(box, tailFlowItem)
1978
2208
  } else {
@@ -1987,7 +2217,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
1987
2217
  },
1988
2218
  }, React.createElement('span', null, loading ? '...' : (open ? 'RU ✓' : 'RU ↗')))
1989
2219
  }
1990
-
1991
2220
  function SettingsCard(props) {
1992
2221
  const inj = typeof props.inject === 'function'
1993
2222
  ? (props.inject() || {})
@@ -1996,7 +2225,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
1996
2225
  const runtime = props.runtime || inj.runtime
1997
2226
  const toggleRu = props.toggleRu || inj.toggleRu
1998
2227
  const t = typeof props.t === 'function' ? props.t : ((k) => k)
1999
-
2000
2228
  const [open, setOpen] = React.useState(false)
2001
2229
  const [snap, setSnap] = React.useState(
2002
2230
  () => (scope && scope.getSnapshot ? scope.getSnapshot() : { status: 'loading', value: {} }))
@@ -2004,7 +2232,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
2004
2232
  () => { try { return runtime.getLocale().active === 'ru' } catch (e) { return false } })
2005
2233
  const [typo, setTypoState] = React.useState(() =>
2006
2234
  (snap.value && snap.value.typography) || {})
2007
-
2008
2235
  React.useEffect(() => {
2009
2236
  if (!scope || !scope.subscribe) return undefined
2010
2237
  const un = scope.subscribe(() => {
@@ -2023,12 +2250,13 @@ const exportSessionToMarkdown = (session, options = {}) => {
2023
2250
  return un
2024
2251
  } catch (e) { return undefined }
2025
2252
  }, [runtime])
2026
-
2027
2253
  const status = snap.status || 'loading'
2028
2254
  const value = snap.value || {}
2029
2255
  const typography = typo
2030
- const overridesCount = Object.keys(value.overrides || {}).length
2031
-
2256
+ const overrides = value.overrides || {}
2257
+ const overridesCount = Object.keys(overrides).length
2258
+ const [newKey, setNewKey] = React.useState('')
2259
+ const [newVal, setNewVal] = React.useState('')
2032
2260
  const setTypo = (patch) => {
2033
2261
  const next = Object.assign({}, typo, patch)
2034
2262
  setTypoState(next)
@@ -2042,24 +2270,20 @@ const exportSessionToMarkdown = (session, options = {}) => {
2042
2270
  }
2043
2271
  }
2044
2272
  const onEnabled = (ev) => { if (toggleRu) toggleRu(ev.target.checked) }
2045
-
2046
2273
  const checkbox = (checked, onChange, disabled) =>
2047
2274
  React.createElement('input', {
2048
2275
  type: 'checkbox', checked: !!checked, disabled: !!disabled,
2049
2276
  className: 'rl-check', onChange: (ev) => onChange(ev),
2050
2277
  })
2051
-
2052
2278
  const disabled = status !== 'ready'
2053
2279
  const statusLine = status === 'ready'
2054
2280
  ? ''
2055
2281
  : (status === 'unavailable' ? t('statusUnavailable') : t('statusLoading'))
2056
-
2057
2282
  let ChevronIcon = null
2058
2283
  try {
2059
2284
  const primitives = require('@deepseek-ai/dsh-client-ui-primitives')
2060
2285
  ChevronIcon = primitives && primitives.IconChevronDownOutline14
2061
2286
  } catch (_) { ChevronIcon = null }
2062
-
2063
2287
  const FallbackChevron = () => React.createElement('svg', {
2064
2288
  width: 14, height: 14, viewBox: '0 0 14 14', fill: 'none',
2065
2289
  'aria-hidden': 'true',
@@ -2067,17 +2291,13 @@ const exportSessionToMarkdown = (session, options = {}) => {
2067
2291
  d: 'M3.5 5.25 7 8.75l3.5-3.5', stroke: 'currentColor',
2068
2292
  strokeWidth: 1.5, strokeLinecap: 'round', strokeLinejoin: 'round',
2069
2293
  }))
2070
-
2071
2294
  const Chevron = ChevronIcon || FallbackChevron
2072
-
2073
2295
  const presetKey = value.agentPromptPreset || 'technical_expert'
2074
2296
  const presetInfo = typeof SYSTEM_PROMPT_PRESETS !== 'undefined' ? SYSTEM_PROMPT_PRESETS[presetKey] : null
2075
-
2076
2297
  if (props && props.view === 'summary') {
2077
2298
  return React.createElement('div', { className: 'rl-sub' }, statusLine || t('cardSub'))
2078
2299
  }
2079
2300
  const page = !!(props && props.view === 'page')
2080
-
2081
2301
  return React.createElement('div', { className: page ? 'rl-page-seat' : 'rl-card' },
2082
2302
  React.createElement('button', {
2083
2303
  type: 'button',
@@ -2100,7 +2320,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
2100
2320
  }, React.createElement(Chevron, null))),
2101
2321
  (page || open) && React.createElement('div', { className: 'rl-body' },
2102
2322
  React.createElement('div', { className: 'rl-page' },
2103
-
2104
2323
  React.createElement('div', { className: 'rl-section-card' },
2105
2324
  React.createElement('div', { className: 'rl-section-title' },
2106
2325
  React.createElement('span', null, t('secLanguage')),
@@ -2121,7 +2340,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
2121
2340
  overridesCount > 0 ? React.createElement('div', { className: 'rl-badge rl-badge-dim', style: { alignSelf: 'flex-start' } },
2122
2341
  t('overridesCount') + ': ' + overridesCount) : null
2123
2342
  ),
2124
-
2125
2343
  React.createElement('div', { className: 'rl-section-card' },
2126
2344
  React.createElement('div', { className: 'rl-section-title' },
2127
2345
  React.createElement('span', null, t('secTypography')),
@@ -2177,7 +2395,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
2177
2395
  React.createElement('span', null, t('altLHintText'))
2178
2396
  )
2179
2397
  ),
2180
-
2181
2398
  React.createElement('div', { className: 'rl-section-card' },
2182
2399
  React.createElement('div', { className: 'rl-section-title' },
2183
2400
  React.createElement('span', null, t('secAgentPrompt')),
@@ -2219,7 +2436,10 @@ const exportSessionToMarkdown = (session, options = {}) => {
2219
2436
  },
2220
2437
  React.createElement('option', { value: 'technical_expert' }, t('presetExpert')),
2221
2438
  React.createElement('option', { value: 'tech_writer' }, t('presetWriter')),
2222
- 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'))
2223
2443
  )
2224
2444
  ),
2225
2445
  presetInfo ? React.createElement('div', { className: 'rl-preview-box' },
@@ -2228,7 +2448,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
2228
2448
  ) : null
2229
2449
  ) : null
2230
2450
  ),
2231
-
2232
2451
  React.createElement('div', { className: 'rl-section-card' },
2233
2452
  React.createElement('div', { className: 'rl-section-title' },
2234
2453
  React.createElement('span', null, t('secUpdater')),
@@ -2240,7 +2459,7 @@ const exportSessionToMarkdown = (session, options = {}) => {
2240
2459
  React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: '8px' } },
2241
2460
  React.createElement('div', null,
2242
2461
  React.createElement('div', { style: { fontWeight: 600, color: 'var(--dsw-alias-label-primary)' } },
2243
- t('updaterCurrent').replace('{version}', upStatus.currentVersion || '0.2.26')),
2462
+ t('updaterCurrent').replace('{version}', upStatus.currentVersion || '0.3.1')),
2244
2463
  upStatus.updateAvailable
2245
2464
  ? React.createElement('div', { style: { color: 'var(--dsw-alias-state-warning-primary)', marginTop: '2px', fontWeight: 500 } },
2246
2465
  t('updaterLatest').replace('{version}', upStatus.latestVersion || ''))
@@ -2270,7 +2489,6 @@ const exportSessionToMarkdown = (session, options = {}) => {
2270
2489
  }, upMsg.text) : null
2271
2490
  )
2272
2491
  ),
2273
-
2274
2492
  React.createElement('div', { className: 'rl-section-card' },
2275
2493
  React.createElement('div', { className: 'rl-section-title' },
2276
2494
  React.createElement('span', null, t('secTranslator')),
@@ -2330,7 +2548,71 @@ const exportSessionToMarkdown = (session, options = {}) => {
2330
2548
  ) : null
2331
2549
  )
2332
2550
  ),
2333
-
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
+ ),
2334
2616
  React.createElement('div', { className: 'rl-section-card' },
2335
2617
  React.createElement('div', { className: 'rl-section-title' },
2336
2618
  React.createElement('span', null, t('secSupport')),
@@ -2353,7 +2635,7 @@ const exportSessionToMarkdown = (session, options = {}) => {
2353
2635
  ),
2354
2636
  React.createElement('div', { className: 'rl-actions-row' },
2355
2637
  React.createElement('a', {
2356
- href: makeIssueUrl({}, '0.2.26'),
2638
+ href: makeIssueUrl({}, '0.3.1'),
2357
2639
  target: '_blank',
2358
2640
  rel: 'noopener noreferrer',
2359
2641
  className: 'rl-btn rl-btn-primary'
@@ -2367,8 +2649,9 @@ const exportSessionToMarkdown = (session, options = {}) => {
2367
2649
  )
2368
2650
  )
2369
2651
  }
2370
-
2371
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}',
2372
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}',
2373
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}',
2374
2657
  '.rl-head:hover{background:var(--dsw-alias-bg-layer-2)}',
@@ -2433,8 +2716,7 @@ const exportSessionToMarkdown = (session, options = {}) => {
2433
2716
  tag.textContent = RL_CSS
2434
2717
  document.head.appendChild(tag)
2435
2718
  }
2436
-
2437
2719
  module.exports = { apply, inject: ['locale', 'connection', 'remote', 'settingsScope', 'slots'] }
2438
2720
  return module.exports
2439
2721
  },
2440
- })
2722
+ })