@goodandready/dsh-russian-lang 0.2.8 → 0.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +3 -3
  2. package/lib/client.js +337 -40
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  <h3>Полная русская локализация, умная типографика и исправление раскладки клавиатуры для DeepSeek Harness</h3>
6
6
 
7
7
  <p align="center">
8
- <a href="https://www.npmjs.com/package/@goodandready/dsh-russian-lang"><img src="https://img.shields.io/badge/npm-v0.2.8-6366f1.svg?style=for-the-badge&labelColor=1e1b4b" alt="npm version"></a>
8
+ <a href="https://www.npmjs.com/package/@goodandready/dsh-russian-lang"><img src="https://img.shields.io/badge/npm-v0.2.9-6366f1.svg?style=for-the-badge&labelColor=1e1b4b" alt="npm version"></a>
9
9
  <a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-10b981.svg?style=for-the-badge&color=10b981&labelColor=064e3b" alt="license"></a>
10
10
  <a href="https://github.com/topics/dsh-plugin"><img src="https://img.shields.io/badge/DSH-Plugin-8b5cf6.svg?style=for-the-badge&labelColor=2e1065" alt="DSH Plugin"></a>
11
11
  <a href="https://nodejs.org"><img src="https://img.shields.io/badge/Node-20%2B-f59e0b.svg?style=for-the-badge&labelColor=451a03" alt="Node version"></a>
@@ -169,9 +169,9 @@ russian-lang:
169
169
  MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
170
170
 
171
171
 
172
- ## ⚡ Новые возможности v0.2.8 — Smart Russian UX
172
+ ## ⚡ Новые возможности v0.2.9 — Smart Russian UX
173
173
 
174
- Версия **v0.2.8** расширяет функционал плагина от чистой локализации до полноценной адаптации пользовательского опыта работы с моделями на русском языке:
174
+ Версия **v0.2.9** расширяет функционал плагина от чистой локализации до полноценной адаптации пользовательского опыта работы с моделями на русском языке:
175
175
 
176
176
  1. **Живая экранная типографика в поле ввода (Live Input Typography)**:
177
177
  - Автоматическая подстановка кавычек-ёлочек («...») вместо машинных программистских кавычек (`""`).
package/lib/client.js CHANGED
@@ -1019,10 +1019,35 @@ const exportSessionToMarkdown = (session, options = {}) => {
1019
1019
 
1020
1020
  // Отвечаем на real input: input / input_event, слушаем на document.
1021
1021
  // Читаем value у поля, где курсор (textarea/input), не трогая contenteditable.
1022
+ function setNativeInputValue(el, value) {
1023
+ if (!el) return
1024
+ const proto = el.tagName === 'TEXTAREA' ? window.HTMLTextAreaElement.prototype : window.HTMLInputElement.prototype
1025
+ const desc = Object.getOwnPropertyDescriptor(proto, 'value')
1026
+ if (desc && desc.set) {
1027
+ desc.set.call(el, value)
1028
+ } else {
1029
+ el.value = value
1030
+ }
1031
+ if (el._valueTracker) {
1032
+ try { el._valueTracker.setValue(value) } catch (e) {}
1033
+ }
1034
+ el.dispatchEvent(new Event('input', { bubbles: true }))
1035
+ el.dispatchEvent(new Event('change', { bubbles: true }))
1036
+ }
1037
+
1022
1038
  let layoutHintEl = null
1023
- const layoutCurrentInput = () => {
1039
+ const layoutCurrentInput = (ev) => {
1040
+ if (ev && ev.target && (ev.target.tagName === 'TEXTAREA' || (ev.target.tagName === 'INPUT' && (ev.target.type === 'text' || !ev.target.type)))) {
1041
+ return ev.target
1042
+ }
1024
1043
  const el = document.activeElement
1025
- if (el && (el.tagName === 'TEXTAREA' || (el.tagName === 'INPUT' && el.type === 'text'))) return el
1044
+ if (el && (el.tagName === 'TEXTAREA' || (el.tagName === 'INPUT' && (ev.target ? ev.target.type === 'text' : true)))) return el
1045
+ if (el && typeof el.querySelector === 'function') {
1046
+ const inner = el.querySelector('textarea, input[type="text"]')
1047
+ if (inner) return inner
1048
+ }
1049
+ const focused = document.querySelector('textarea:focus, input[type="text"]:focus')
1050
+ if (focused) return focused
1026
1051
  return null
1027
1052
  }
1028
1053
  const layoutDismiss = () => {
@@ -1041,8 +1066,7 @@ const exportSessionToMarkdown = (session, options = {}) => {
1041
1066
  layoutHintEl.textContent = label + ': ' + converted
1042
1067
  layoutHintEl.addEventListener('mousedown', (ev) => {
1043
1068
  ev.preventDefault()
1044
- inputEl.value = converted
1045
- inputEl.dispatchEvent(new Event('input', { bubbles: true }))
1069
+ setNativeInputValue(inputEl, converted)
1046
1070
  learnWords(converted) // #67: запомнить принятые слова
1047
1071
  layoutDismiss()
1048
1072
  })
@@ -1078,7 +1102,10 @@ const exportSessionToMarkdown = (session, options = {}) => {
1078
1102
  ev.preventDefault()
1079
1103
  const v = el.value || ''
1080
1104
  const c = layoutFixCandidate(v, 'lat2cyr') || layoutFixCandidate(v, 'cyr2lat')
1081
- if (c) { el.value = c.converted; el.dispatchEvent(new Event('input', { bubbles: true })) }
1105
+ if (c) {
1106
+ setNativeInputValue(el, c.converted)
1107
+ learnWords(c.converted)
1108
+ }
1082
1109
  })
1083
1110
  document.body.appendChild(layoutBadgeEl)
1084
1111
  }
@@ -1090,10 +1117,13 @@ const exportSessionToMarkdown = (session, options = {}) => {
1090
1117
  const layoutBadgeHide = () => {
1091
1118
  if (layoutBadgeEl) { layoutBadgeEl.remove(); layoutBadgeEl = null }
1092
1119
  }
1093
- const layoutOnInput = () => {
1120
+
1121
+ let isFormatting = false
1122
+ const layoutOnInput = (ev) => {
1123
+ if (isFormatting) return
1094
1124
  try {
1095
1125
  if (runtime.getLocale().active !== 'ru') { layoutDismiss(); layoutBadgeHide(); return }
1096
- const el = layoutCurrentInput()
1126
+ const el = layoutCurrentInput(ev)
1097
1127
  if (!el) { layoutDismiss(); layoutBadgeHide(); return }
1098
1128
  layoutBadge(el) // #66: метка раскладки
1099
1129
  const value = el.value || ''
@@ -1105,11 +1135,43 @@ const exportSessionToMarkdown = (session, options = {}) => {
1105
1135
  if (typoLive && value && typeof formatInputLive === 'function') {
1106
1136
  const formatted = formatInputLive(value)
1107
1137
  if (formatted !== value) {
1108
- const sStart = el.selectionStart
1109
- const sEnd = el.selectionEnd
1110
- const diff = formatted.length - value.length
1111
- el.value = formatted
1112
- if (sStart !== null && sEnd !== null) el.setSelectionRange(sStart + diff, sEnd + diff)
1138
+ isFormatting = true
1139
+ try {
1140
+ const sStart = el.selectionStart
1141
+ const sEnd = el.selectionEnd
1142
+ const diff = formatted.length - value.length
1143
+ setNativeInputValue(el, formatted)
1144
+ if (sStart !== null && sEnd !== null) {
1145
+ const nextPos = Math.max(0, sStart + diff)
1146
+ el.setSelectionRange(nextPos, nextPos)
1147
+ }
1148
+ } finally {
1149
+ isFormatting = false
1150
+ }
1151
+ }
1152
+ }
1153
+ } catch (e) { /* ignore */ }
1154
+
1155
+ // #158: Русские алиасы слэш-команд при вводе пробела после команды (например "/цель ")
1156
+ try {
1157
+ const snapVal = scope ? (scope.getSnapshot().value || {}) : {}
1158
+ const allowAliases = snapVal.slashAliases !== false
1159
+ if (allowAliases && value.startsWith('/') && typeof expandSlashAlias === 'function') {
1160
+ const expanded = expandSlashAlias(value)
1161
+ if (expanded !== value) {
1162
+ isFormatting = true
1163
+ try {
1164
+ const sStart = el.selectionStart
1165
+ const sEnd = el.selectionEnd
1166
+ const diff = expanded.length - value.length
1167
+ setNativeInputValue(el, expanded)
1168
+ if (sStart !== null && sEnd !== null) {
1169
+ const nextPos = Math.max(0, sStart + diff)
1170
+ el.setSelectionRange(nextPos, nextPos)
1171
+ }
1172
+ } finally {
1173
+ isFormatting = false
1174
+ }
1113
1175
  }
1114
1176
  }
1115
1177
  } catch (e) { /* ignore */ }
@@ -1132,41 +1194,76 @@ const exportSessionToMarkdown = (session, options = {}) => {
1132
1194
  }
1133
1195
  const unsubscribeLayout = runtime.subscribe(layoutOnInput)
1134
1196
  document.addEventListener('input', layoutOnInput, true)
1197
+
1135
1198
  const layoutOnKeydown = (ev) => {
1199
+ const el = layoutCurrentInput(ev)
1200
+ if (!el) return
1201
+
1136
1202
  // Alt+L (клавиша KeyL, Latin 'l' или русская 'д'): ручной конверт текущего инпута
1137
1203
  const isL = ev.code === 'KeyL' || ev.key.toLowerCase() === 'l' || ev.key.toLowerCase() === 'д'
1138
1204
  if (ev.altKey && !ev.ctrlKey && !ev.metaKey && isL) {
1139
- const el = layoutCurrentInput()
1140
- if (el) {
1141
- const value = el.value || ''
1142
- const c = layoutFixCandidate(value, 'lat2cyr') || layoutFixCandidate(value, 'cyr2lat')
1143
- if (c) {
1144
- ev.preventDefault()
1145
- el.value = c.converted
1146
- el.dispatchEvent(new Event('input', { bubbles: true }))
1205
+ const value = el.value || ''
1206
+ const c = layoutFixCandidate(value, 'lat2cyr') || layoutFixCandidate(value, 'cyr2lat')
1207
+ if (c) {
1208
+ ev.preventDefault()
1209
+ isFormatting = true
1210
+ try {
1211
+ setNativeInputValue(el, c.converted)
1147
1212
  learnWords(c.converted) // #67
1213
+ } finally {
1214
+ isFormatting = false
1148
1215
  }
1149
1216
  }
1150
1217
  }
1218
+
1151
1219
  // #158: Alt+T: фонетическая транслитерация (privet <-> привет)
1152
1220
  const isT = ev.code === 'KeyT' || ev.key.toLowerCase() === 't' || ev.key.toLowerCase() === 'е'
1153
1221
  if (ev.altKey && !ev.ctrlKey && !ev.metaKey && isT) {
1154
- const el = layoutCurrentInput()
1155
- if (el && el.value && typeof phoneticTranslit === 'function') {
1222
+ if (el.value && typeof phoneticTranslit === 'function') {
1156
1223
  ev.preventDefault()
1157
- const dir = /[а-яё]/i.test(el.value) ? 'cyr2lat' : 'lat2cyr'
1158
- el.value = phoneticTranslit(el.value, dir)
1159
- el.dispatchEvent(new Event('input', { bubbles: true }))
1224
+ isFormatting = true
1225
+ try {
1226
+ const sStart = el.selectionStart
1227
+ const sEnd = el.selectionEnd
1228
+ if (sStart !== null && sEnd !== null && sStart !== sEnd) {
1229
+ const sel = el.value.slice(sStart, sEnd)
1230
+ const dir = /[а-яё]/i.test(sel) ? 'cyr2lat' : 'lat2cyr'
1231
+ const converted = phoneticTranslit(sel, dir)
1232
+ const next = el.value.slice(0, sStart) + converted + el.value.slice(sEnd)
1233
+ setNativeInputValue(el, next)
1234
+ el.setSelectionRange(sStart, sStart + converted.length)
1235
+ } else {
1236
+ const dir = /[а-яё]/i.test(el.value) ? 'cyr2lat' : 'lat2cyr'
1237
+ const converted = phoneticTranslit(el.value, dir)
1238
+ setNativeInputValue(el, converted)
1239
+ }
1240
+ } finally {
1241
+ isFormatting = false
1242
+ }
1160
1243
  }
1161
1244
  }
1245
+
1162
1246
  // #158: Разворачивание русских алиасов слэш-команд (/цель -> /goal)
1163
1247
  if (ev.key === ' ' || ev.key === 'Enter') {
1164
- const el = layoutCurrentInput()
1165
- if (el && el.value && el.value.startsWith('/') && typeof expandSlashAlias === 'function') {
1248
+ if (el.value && el.value.startsWith('/') && typeof expandSlashAlias === 'function') {
1166
1249
  const expanded = expandSlashAlias(el.value)
1167
1250
  if (expanded !== el.value) {
1168
- el.value = expanded
1169
- el.dispatchEvent(new Event('input', { bubbles: true }))
1251
+ if (ev.key === ' ') {
1252
+ ev.preventDefault()
1253
+ isFormatting = true
1254
+ try {
1255
+ setNativeInputValue(el, expanded + ' ')
1256
+ } finally {
1257
+ isFormatting = false
1258
+ }
1259
+ } else if (ev.key === 'Enter') {
1260
+ isFormatting = true
1261
+ try {
1262
+ setNativeInputValue(el, expanded)
1263
+ } finally {
1264
+ isFormatting = false
1265
+ }
1266
+ }
1170
1267
  }
1171
1268
  }
1172
1269
  }
@@ -1229,6 +1326,19 @@ const exportSessionToMarkdown = (session, options = {}) => {
1229
1326
  }, TranslateTurnAction),
1230
1327
  )
1231
1328
  } catch (err) { /* ignore if slot not declared */ }
1329
+
1330
+ // #158: Экспорт сессии в Markdown
1331
+ try {
1332
+ ctx.slots.inject('conversation.session.header.utilities', () =>
1333
+ ctx.slots.register({
1334
+ name: 'conversation.session.header.utilities',
1335
+ id: 'dsh-russian-lang-md-export',
1336
+ order: 101,
1337
+ locale: SETTINGS_NS_NAME,
1338
+ inject: () => ({ runtime }),
1339
+ }, ExportMarkdownButton),
1340
+ )
1341
+ } catch (err) { /* ignore if slot not declared */ }
1232
1342
  }
1233
1343
 
1234
1344
  // Карточка настроек: React-компонент вне apply.
@@ -1260,25 +1370,178 @@ const exportSessionToMarkdown = (session, options = {}) => {
1260
1370
  }, React.createElement('span', { className: 'rl-lang-text' }, isRu ? 'RU' : 'EN'))
1261
1371
  }
1262
1372
 
1373
+ // #158: Кнопка экспорта в Markdown
1374
+ function ExportMarkdownButton(props) {
1375
+ const [done, setDone] = React.useState(false)
1376
+ const onExport = () => {
1377
+ try {
1378
+ const titleEl = document.querySelector('.dsw-session-title, [data-session-title], header h1, header h2')
1379
+ const title = (titleEl && titleEl.textContent.trim()) || document.title || 'Диалог DSH'
1380
+ const messageNodes = document.querySelectorAll(
1381
+ '[data-turn-id], [data-message-role], .dsw-turn-node, .dsw-chat-message, [data-turn-tail]'
1382
+ )
1383
+ const messages = []
1384
+ const seen = new Set()
1385
+ messageNodes.forEach((node) => {
1386
+ let role = 'assistant'
1387
+ const roleAttr = node.getAttribute('data-message-role')
1388
+ if (roleAttr === 'user' || node.classList.contains('dsw-user-message') || node.querySelector('[data-role="user"]')) {
1389
+ role = 'user'
1390
+ }
1391
+ const prose = node.querySelector('.dsw-prose, [data-block-kind="text"], .dsw-markdown-view, p')
1392
+ let text = (prose ? prose.innerText : node.innerText) || ''
1393
+ text = text.trim()
1394
+ if (!text || seen.has(text)) return
1395
+ seen.add(text)
1396
+ messages.push({ role, content: text })
1397
+ })
1398
+
1399
+ const session = {
1400
+ title,
1401
+ createdAt: new Date().toISOString(),
1402
+ messages
1403
+ }
1404
+ const md = exportSessionToMarkdown(session)
1405
+ const blob = new Blob([md], { type: 'text/markdown;charset=utf-8' })
1406
+ const url = URL.createObjectURL(blob)
1407
+ const a = document.createElement('a')
1408
+ const safeTitle = (title || 'dialog').replace(/[/\\?%*:|"<>]/g, '-').slice(0, 50)
1409
+ a.href = url
1410
+ a.download = safeTitle + '-' + new Date().toISOString().slice(0, 10) + '.md'
1411
+ document.body.appendChild(a)
1412
+ a.click()
1413
+ document.body.removeChild(a)
1414
+ URL.revokeObjectURL(url)
1415
+ setDone(true)
1416
+ setTimeout(() => setDone(false), 2000)
1417
+ } catch (err) {
1418
+ console.warn('dsh-russian-lang: export md failed', err)
1419
+ }
1420
+ }
1421
+
1422
+ return React.createElement('button', {
1423
+ type: 'button',
1424
+ className: 'rl-lang-chip rl-export-md-btn',
1425
+ title: 'Экспорт диалога в Markdown (.md)',
1426
+ onClick: onExport,
1427
+ }, React.createElement('span', { className: 'rl-lang-text' }, done ? '✓ MD' : '📥 MD'))
1428
+ }
1429
+
1430
+ async function translateTurnContent(text) {
1431
+ if (!text || typeof text !== 'string') return ''
1432
+ const cyr = (text.match(/[\u0430-\u044f\u0451]/gi) || []).length
1433
+ const lat = (text.match(/[a-z]/gi) || []).length
1434
+ if (cyr > lat && cyr > 15) return text
1435
+
1436
+ const parts = text.split(/(```[\s\S]*?```)/g)
1437
+ for (let i = 0; i < parts.length; i += 2) {
1438
+ const chunk = parts[i].trim()
1439
+ if (!chunk) continue
1440
+ const paragraphs = chunk.split(/\n\n+/)
1441
+ const translatedParas = []
1442
+ for (const para of paragraphs) {
1443
+ const pTrim = para.trim()
1444
+ if (!pTrim) continue
1445
+ try {
1446
+ const url = 'https://api.mymemory.translated.net/get?q=' + encodeURIComponent(pTrim) + '&langpair=en|ru'
1447
+ const res = await fetch(url)
1448
+ if (res.ok) {
1449
+ const data = await res.json()
1450
+ if (data && data.responseData && data.responseData.translatedText) {
1451
+ translatedParas.push(data.responseData.translatedText)
1452
+ continue
1453
+ }
1454
+ }
1455
+ } catch (err) {
1456
+ try {
1457
+ const gurl = 'https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=ru&dt=t&q=' + encodeURIComponent(pTrim)
1458
+ const gres = await fetch(gurl)
1459
+ if (gres.ok) {
1460
+ const gdata = await gres.json()
1461
+ if (Array.isArray(gdata) && Array.isArray(gdata[0])) {
1462
+ translatedParas.push(gdata[0].map((it) => it[0]).join(''))
1463
+ continue
1464
+ }
1465
+ }
1466
+ } catch (e2) {}
1467
+ }
1468
+ translatedParas.push(pTrim)
1469
+ }
1470
+ parts[i] = translatedParas.join('\n\n')
1471
+ }
1472
+ return parts.join('\n\n')
1473
+ }
1474
+
1263
1475
  // #158: Кнопка перевода реплики на русский
1264
1476
  function TranslateTurnAction(props) {
1265
1477
  const t = typeof props.t === 'function' ? props.t : ((k) => k)
1266
- const [done, setDone] = React.useState(false)
1478
+ const [loading, setLoading] = React.useState(false)
1479
+ const [open, setOpen] = React.useState(false)
1480
+
1267
1481
  return React.createElement('button', {
1268
1482
  type: 'button',
1269
- className: 'rl-action-btn',
1483
+ className: 'rl-action-btn' + (open ? ' rl-action-btn-active' : ''),
1270
1484
  title: t('translateTurn'),
1271
- onClick: (ev) => {
1485
+ onClick: async (ev) => {
1272
1486
  ev.stopPropagation()
1273
- const turn = ev.currentTarget.closest('[data-turn-id], .dsw-turn-node, [data-message-role="assistant"]')
1274
- if (turn) {
1275
- turn.style.outline = '1px dashed var(--dsw-alias-label-primary)'
1276
- setTimeout(() => { turn.style.outline = '' }, 1500)
1487
+ const btn = ev.currentTarget
1488
+ const turn = btn.closest('[data-turn-tail], [data-turn-id], .osXY9a_root, [data-message-role="assistant"], .dsw-turn-node') || btn.parentElement.parentElement
1489
+ if (!turn) return
1490
+
1491
+ let box = turn.querySelector('.rl-turn-translation')
1492
+ if (box) {
1493
+ box.style.display = box.style.display === 'none' ? 'block' : 'none'
1494
+ setOpen(box.style.display !== 'none')
1495
+ return
1496
+ }
1497
+
1498
+ setLoading(true)
1499
+ try {
1500
+ const prose = turn.querySelector('.dsw-prose, [data-block-kind="text"], .dsw-markdown-view, p')
1501
+ const rawText = (prose ? prose.innerText : turn.innerText) || ''
1502
+ const translated = await translateTurnContent(rawText)
1503
+
1504
+ box = document.createElement('div')
1505
+ box.className = 'rl-turn-translation'
1506
+ box.innerHTML = '<div class="rl-trans-head">' +
1507
+ '<span class="rl-trans-title">🌐 Перевод на русский</span>' +
1508
+ '<div class="rl-trans-tools">' +
1509
+ '<button type="button" class="rl-trans-btn rl-btn-copy" title="Скопировать перевод">📋 Копировать</button>' +
1510
+ '<button type="button" class="rl-trans-btn rl-btn-close" title="Закрыть">✕</button>' +
1511
+ '</div>' +
1512
+ '</div>' +
1513
+ '<div class="rl-trans-body"></div>'
1514
+ box.querySelector('.rl-trans-body').textContent = translated
1515
+
1516
+ const copyBtn = box.querySelector('.rl-btn-copy')
1517
+ copyBtn.addEventListener('click', (e) => {
1518
+ e.stopPropagation()
1519
+ try { navigator.clipboard.writeText(translated) } catch (err) {}
1520
+ copyBtn.textContent = '✓ Скопировано'
1521
+ setTimeout(() => { copyBtn.textContent = '📋 Копировать' }, 2000)
1522
+ })
1523
+
1524
+ const closeBtn = box.querySelector('.rl-btn-close')
1525
+ closeBtn.addEventListener('click', (e) => {
1526
+ e.stopPropagation()
1527
+ box.style.display = 'none'
1528
+ setOpen(false)
1529
+ })
1530
+
1531
+ const actionsBar = btn.closest('.dsw-turn-actions, [data-turn-actions]') || btn.parentElement
1532
+ if (actionsBar && actionsBar.parentNode) {
1533
+ actionsBar.parentNode.insertBefore(box, actionsBar)
1534
+ } else {
1535
+ turn.appendChild(box)
1536
+ }
1537
+ setOpen(true)
1538
+ } catch (err) {
1539
+ console.warn('dsh-russian-lang: translate turn failed', err)
1540
+ } finally {
1541
+ setLoading(false)
1277
1542
  }
1278
- setDone(true)
1279
- setTimeout(() => setDone(false), 2000)
1280
1543
  }
1281
- }, React.createElement('span', null, done ? '✓' : 'RU ↗'))
1544
+ }, React.createElement('span', null, loading ? '⏳' : (open ? 'RU ✓' : 'RU ↗')))
1282
1545
  }
1283
1546
 
1284
1547
  function SettingsCard(props) {
@@ -1387,8 +1650,14 @@ const exportSessionToMarkdown = (session, options = {}) => {
1387
1650
  row(t('enabled'), checkbox(ruActive, onEnabled, false), null),
1388
1651
  row(t('typography'), checkbox(typography.enabled !== false && ruActive,
1389
1652
  (ev) => setTypo({ enabled: ev.target.checked }), !ruActive), t('typographyDesc')),
1653
+ row(t('liveInput'), checkbox(typography.liveInput !== false && ruActive,
1654
+ (ev) => setTypo({ liveInput: ev.target.checked }), !ruActive), t('liveInputDesc')),
1390
1655
  row(t('yo'), checkbox(typography.yo === true,
1391
1656
  (ev) => setTypo({ yo: ev.target.checked }), !ruActive), t('yoDesc')),
1657
+ row(t('slashAliases'), checkbox(value.slashAliases !== false && ruActive,
1658
+ (ev) => {
1659
+ try { scope.set('slashAliases', ev.target.checked) } catch (err) {}
1660
+ }, !ruActive), t('slashAliasesDesc')),
1392
1661
  row(t('agentPrompt'), checkbox(value.agentPrompt === true,
1393
1662
  (ev) => {
1394
1663
  try {
@@ -1400,12 +1669,31 @@ const exportSessionToMarkdown = (session, options = {}) => {
1400
1669
  console.warn('dsh-russian-lang: scope.set agentPrompt sync threw', err && err.message || err)
1401
1670
  }
1402
1671
  }, false), t('agentPromptDesc')),
1672
+ row(t('agentPromptPreset'),
1673
+ React.createElement('select', {
1674
+ className: 'rl-select',
1675
+ value: value.agentPromptPreset || 'technical_expert',
1676
+ disabled: !value.agentPrompt || !ruActive,
1677
+ onChange: (ev) => {
1678
+ try {
1679
+ const val = ev.target.value
1680
+ scope.set('agentPromptPreset', val)
1681
+ } catch (err) { console.warn('dsh-russian-lang: set agentPromptPreset failed', err) }
1682
+ }
1683
+ },
1684
+ React.createElement('option', { value: 'technical_expert' }, t('presetExpert')),
1685
+ React.createElement('option', { value: 'tech_writer' }, t('presetWriter')),
1686
+ React.createElement('option', { value: 'concise' }, t('presetConcise'))
1687
+ ),
1688
+ t('agentPromptDesc')
1689
+ ),
1403
1690
  React.createElement('div', { className: 'rl-note' },
1404
1691
  t('overridesCount') + ': ' + overridesCount),
1405
1692
  React.createElement('div', { className: 'rl-hint' }, t('altL')),
1693
+ React.createElement('div', { className: 'rl-hint' }, t('altT')),
1406
1694
  React.createElement('div', { className: 'rl-actions' },
1407
1695
  React.createElement('a', {
1408
- href: makeIssueUrl({}, '0.2.7'),
1696
+ href: makeIssueUrl({}, '0.2.9'),
1409
1697
  target: '_blank',
1410
1698
  rel: 'noopener noreferrer',
1411
1699
  className: 'rl-link'
@@ -1438,6 +1726,15 @@ const exportSessionToMarkdown = (session, options = {}) => {
1438
1726
  '.rl-lang-chip-active{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2)}',
1439
1727
  '.rl-action-btn{appearance:none;background:0 0;border:1px solid transparent;border-radius:4px;color:var(--dsw-alias-label-secondary);cursor:pointer;font-size:11px;padding:2px 5px;display:inline-flex;align-items:center;transition:all .15s}',
1440
1728
  '.rl-action-btn:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3)}',
1729
+ '.rl-action-btn-active{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-border-l2)}',
1730
+ '.rl-turn-translation{margin:8px 0;padding:10px 12px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);border-radius:8px;font-size:13px;line-height:1.5}',
1731
+ '.rl-trans-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;border-bottom:1px solid var(--dsw-alias-border-l2);padding-bottom:4px}',
1732
+ '.rl-trans-title{font-size:11px;font-weight:600;color:var(--dsw-alias-label-secondary);text-transform:uppercase;letter-spacing:0.5px}',
1733
+ '.rl-trans-tools{display:flex;gap:4px}',
1734
+ '.rl-trans-btn{appearance:none;background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);cursor:pointer;padding:2px 7px;border-radius:4px;font-size:11px}',
1735
+ '.rl-trans-btn:hover{background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary)}',
1736
+ '.rl-trans-body{color:var(--dsw-alias-label-primary);white-space:pre-wrap;word-break:break-word;user-select:text}',
1737
+ '.rl-export-md-btn{font-weight:600}',
1441
1738
  '.rl-select{height:30px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);border-radius:6px;padding:0 8px;font-size:12px;outline:none;margin-top:4px}',
1442
1739
  ].join('\n')
1443
1740
  if (typeof document !== 'undefined' && !document.querySelector('style[data-plugin-css="rl-card"]')) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-russian-lang",
3
- "version": "0.2.8",
3
+ "version": "0.2.9",
4
4
  "description": "Russian localization for DeepSeek Harness (DSH v0.1.5-alpha.1+) web UI: adds Русский as the native language option in Settings - General - Language. 100.0% coverage across 5395 keys (1144 core UI keys + 4251 keys across 40+ ecosystem plugins) with zero machine drafts. Features Russian plural forms (one/few/many), automatic typography pass (guillemets, em dashes, non-breaking spaces, optional ё), wrong-layout input hint (Alt+L), browser spellcheck, and runtime snapshot extension without modifying core files.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",