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.
- package/README.md +1 -0
- package/lib/client.js +53 -15
- 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
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
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
|
|
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
|
-
|
|
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) => {
|