dsh-code-ui 1.6.5 → 1.6.6

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 +1 -0
  2. package/lib/client.js +53 -15
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -114,6 +114,7 @@ dsh-code-ui/
114
114
 
115
115
  | 时间 | 版本 | 修复 / 新增 |
116
116
  | --- | --- | --- |
117
+ | 2026-10-13 | v1.6.6 | 修复:**选中 zip 等二进制文件页面卡死**——双重根因:① 编辑器对任何文件都按文本渲染,zip 经截断读取的 4MB 乱码进 tokenizer/highlight 层(数万 span)瞬间撑爆页面;现对已知二进制扩展名(zip/gz/tar/7z/jar/exe/dll/png/jpg/pdf/woff/ttf/mp3/mp4 等 40+ 种)直接跳过文本预览,打开只显示占位提示与只读备注。② v1.6.5 引入的已发消息着色观察器在大 DOM 上每次变动都全量遍历(文本节点级祖先匹配,无法剪枝),编辑器载入大文件后与聊天区流式输出叠加把主线程拖死;现改元素级剪枝(TreeWalker 对 textarea/pre/code/.cdex-workbench/.cdex-dock 等命中即整棵子树跳过)+ 单轮扫描超 80ms 自动进入 1.5s 冷却让出主线程 |
117
118
  | 2026-10-13 | v1.6.5 | 优化:**已发消息的引用标签着色**——chip 序列化进正文的 `N:文件名:起-止` / `🌐N:文件名` 标签在发出后以蓝色高亮展示(`--dsw-alias-state-business-primary`,与宿主输入框 chip 同色加粗),用户回看消息时能一眼看到引用了什么。实现:MutationObserver + TreeWalker 纯文本节点 overlay(只 split/wrap,不碰 React 管理结构;React 原位改文本时自愈 unwrap 重扫);整文件引用强制要求扩展名(不误伤 `12:30` 时间文本),匹配点黏在更长单词内(`a2:app.ts:1-2`)跳过;跳过输入框/代码块/链接/宿主 chip 及本插件自有 UI 区域 |
118
119
  | 2026-10-13 | v1.6.4 | 优化:发送后消息**保留引用标签文本**——chip 序列化从「空串隐身」改为输出编号标签(`2:package.json:2-3` / `🌐4:package.json:26-32`),编号取发送时刻冻结的 seq;消息里看得见引用了什么,AI 侧仍经宿主 systemPrompt 动态 section(`code-ui-references`)获知全部引用并调 `get_referenced_snippets` 读取 |
119
120
  | 2026-10-13 | v1.6.3 | 优化:**引用指针不再出现在消息正文**——消息末尾的 `[code UI] 已标记 N 段代码引用…` 附加块整体移除(用户反馈:希望不展示出来但还让 AI 知道);引用信息改由宿主 `systemPrompt` 动态 section(order 2950,按 session 头取 `lastRefBatches` 批次渲染)注入模型上下文,AI 对用户的观感不可见;客户端 chip serialize 返回空串、consumePendingRefs 每次发送带 `{ids, sessionId}` 调 `use-references`(空=清批次) |
package/lib/client.js CHANGED
@@ -485,6 +485,13 @@ window.__ModuleLoader__.load({
485
485
  png: '#dd6eb4', jpg: '#dd6eb4', jpeg: '#dd6eb4', gif: '#dd6eb4', webp: '#dd6eb4', svg: '#dd6eb4', ico: '#dd6eb4',
486
486
  zip: '#c7a252', gz: '#c7a252', tgz: '#c7a252', lock: '#8a919f',
487
487
  }
488
+ // —— 已知二进制扩展名:不做文本读取/渲染(防 4MB 乱码进高亮层把页面卡死),打开只给占位提示 ——
489
+ const BINARY_EXT = new Set(['zip', 'gz', 'tgz', 'bz2', 'xz', 'tar', 'rar', '7z', 'jar', 'war', 'apk', 'exe', 'dll', 'so', 'dylib', 'bin', 'dat', 'class', 'pyc', 'pyd', 'db', 'sqlite', 'sqlite3', 'png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp', 'ico', 'icns', 'avif', 'pdf', 'woff', 'woff2', 'ttf', 'otf', 'eot', 'mp3', 'wav', 'ogg', 'flac', 'm4a', 'mp4', 'webm', 'mov', 'avi', 'mkv', 'psd', 'ai', 'sketch'])
490
+ function isBinaryName(name) {
491
+ const dot = String(name || '').lastIndexOf('.')
492
+ if (dot <= 0) return false
493
+ return BINARY_EXT.has(String(name).slice(dot + 1).toLowerCase())
494
+ }
488
495
  function fileIconSvg(path) {
489
496
  const name = baseName(path)
490
497
  const dot = name.lastIndexOf('.')
@@ -980,6 +987,24 @@ window.__ModuleLoader__.load({
980
987
  store.activePath = path
981
988
  store.error = null
982
989
  if (!(path in store.buffers)) {
990
+ // 二进制文件(zip/图片/音视频/字体等):不读盘不渲染,占位提示即可——
991
+ // 4MB 乱码进 tokenizer/highlight 层是「选中 zip 页面卡死」的根因之一
992
+ if (isBinaryName(name)) {
993
+ const hint = '【二进制文件:' + name + ',不支持文本预览】'
994
+ store.buffers[path] = hint
995
+ store.savedContent[path] = hint
996
+ store.dirty[path] = false
997
+ store.readOnly = store.readOnly || {}
998
+ store.readOnly[path] = true
999
+ store.readOnlyReason = store.readOnlyReason || {}
1000
+ store.readOnlyReason[path] = '二进制文件,仅可备注/引用文件名'
1001
+ store.loadSeq[path] = (store.loadSeq[path] || 0) + 1
1002
+ store.externalChanged[path] = false
1003
+ emit()
1004
+ showToast('二进制文件(' + name + '),已跳过文本预览')
1005
+ loadNotes(path)
1006
+ return
1007
+ }
983
1008
  store.buffers[path] = ''
984
1009
  store.savedContent[path] = ''
985
1010
  store.dirty[path] = false
@@ -1482,15 +1507,9 @@ window.__ModuleLoader__.load({
1482
1507
  // 带行号:N:文件名:起-止(文件名段不含空白/冒号,Windows basename 本无冒号)
1483
1508
  // 整文件:N:文件名.扩展名(强制要求扩展名,避免误伤 "12:30" 这类时间文本)
1484
1509
  const SENT_CHIP_RE = /(🌐)?\d+:([^\s::,,;;()()「」]+?:\d+-\d+|[^\s::,,;;()()「」]+?\.[A-Za-z0-9]{1,8})(?![\w.:-])/gu
1485
- const SENT_CHIP_SKIP_SEL = 'textarea,input,script,style,pre,code,a,[contenteditable="true"],[data-ref-chip],.cdex-aiinput-tags,.cdex-refcard,.cdex-workbench,.cdex-dock,.cdex-sent-chip'
1486
- function sentChipAncestorSkip(node) {
1487
- let el = node.parentElement
1488
- while (el) {
1489
- if (el.nodeType === 1 && typeof el.matches === 'function' && el.matches(SENT_CHIP_SKIP_SEL)) return true
1490
- el = el.parentElement
1491
- }
1492
- return false
1493
- }
1510
+ // 元素级剪枝:命中即整棵子树跳过(TreeWalker 对元素 FILTER_REJECT 不进入其子节点),
1511
+ // 编辑器大文件(数万文本节点)因此不会被反复全量遍历——这是 zip 卡死修复的关键之一
1512
+ const SENT_CHIP_PRUNE_SEL = 'textarea,input,script,style,pre,code,a,[contenteditable="true"],[data-ref-chip],.cdex-aiinput-tags,.cdex-refcard,.cdex-workbench,.cdex-dock,.cdex-sent-chip'
1494
1513
  function wrapSentChipsInTextNode(node) {
1495
1514
  const text = node.nodeValue
1496
1515
  if (!text || text.indexOf(':') < 0) return
@@ -1525,9 +1544,7 @@ window.__ModuleLoader__.load({
1525
1544
  root.querySelectorAll('.cdex-sent-chip').forEach((chip) => {
1526
1545
  const t = chip.textContent || ''
1527
1546
  SENT_CHIP_RE.lastIndex = 0
1528
- const full = SENT_CHIP_RE.test(t)
1529
- SENT_CHIP_RE.lastIndex = 0
1530
- const m = full ? SENT_CHIP_RE.exec(t) : null
1547
+ const m = SENT_CHIP_RE.exec(t)
1531
1548
  if (!m || m.index !== 0 || m[0].length !== t.length) {
1532
1549
  const p = chip.parentNode
1533
1550
  if (!p) return
@@ -1537,11 +1554,16 @@ window.__ModuleLoader__.load({
1537
1554
  })
1538
1555
  } catch (e) {}
1539
1556
  if (typeof document.createTreeWalker !== 'function') return
1540
- const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
1557
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT, {
1541
1558
  acceptNode(node) {
1559
+ if (node.nodeType === 1) {
1560
+ // 元素:剪枝(REJECT=连同子树跳过)或下探(SKIP=自身不收、继续子节点)
1561
+ return (typeof node.matches === 'function' && node.matches(SENT_CHIP_PRUNE_SEL))
1562
+ ? NodeFilter.FILTER_REJECT
1563
+ : NodeFilter.FILTER_SKIP
1564
+ }
1542
1565
  const v = node.nodeValue
1543
1566
  if (!v || v.indexOf(':') < 0) return NodeFilter.FILTER_REJECT
1544
- if (sentChipAncestorSkip(node)) return NodeFilter.FILTER_REJECT
1545
1567
  SENT_CHIP_RE.lastIndex = 0
1546
1568
  return SENT_CHIP_RE.test(v) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT
1547
1569
  },
@@ -1554,10 +1576,25 @@ window.__ModuleLoader__.load({
1554
1576
  function observeSentChips() {
1555
1577
  if (typeof MutationObserver === 'undefined' || typeof document === 'undefined' || !document.body) return () => {}
1556
1578
  let scheduled = false
1557
- const scan = () => { scheduled = false; try { scanSentChips(document.body) } catch (e) {} }
1579
+ let cooldownUntil = 0 // 扫描超时后的冷却期:大 DOM 场景让出主线程,防持续冻结
1580
+ let cooldownTimer = null
1581
+ const scan = () => {
1582
+ scheduled = false
1583
+ const t0 = Date.now()
1584
+ try { scanSentChips(document.body) } catch (e) {}
1585
+ if (Date.now() - t0 > 80) cooldownUntil = Date.now() + 1500
1586
+ }
1558
1587
  const schedule = () => {
1559
1588
  if (scheduled) return
1560
1589
  scheduled = true
1590
+ const delay = cooldownUntil - Date.now()
1591
+ if (delay > 0) {
1592
+ if (!cooldownTimer) cooldownTimer = setTimeout(() => { cooldownTimer = null; kick() }, delay)
1593
+ return
1594
+ }
1595
+ kick()
1596
+ }
1597
+ const kick = () => {
1561
1598
  if (typeof window !== 'undefined' && window.requestAnimationFrame) window.requestAnimationFrame(scan)
1562
1599
  else setTimeout(scan, 60)
1563
1600
  }
@@ -1566,6 +1603,7 @@ window.__ModuleLoader__.load({
1566
1603
  schedule()
1567
1604
  return () => {
1568
1605
  try { mo.disconnect() } catch (e) {}
1606
+ if (cooldownTimer) { clearTimeout(cooldownTimer); cooldownTimer = null }
1569
1607
  // unwrap 还原文本,不留痕迹
1570
1608
  try {
1571
1609
  document.querySelectorAll('.cdex-sent-chip').forEach((chip) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-code-ui",
3
- "version": "1.6.5",
3
+ "version": "1.6.6",
4
4
  "description": "Cursor 风格的 AI 代码编辑器工作台插件(DSH):文件树、多标签页编辑、语法高亮、引用/翻译行内标签给 AI、右侧停靠/悬浮窗口双布局",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",