dsh-vt-uiux-enhancer 1.0.1 → 1.1.0
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/DESIGN.md +8 -3
- package/README.md +9 -5
- package/client.js +514 -3
- package/docs/VERIFY.md +36 -6
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +1 -1
package/DESIGN.md
CHANGED
|
@@ -8,9 +8,13 @@
|
|
|
8
8
|
|
|
9
9
|
- 让 Harness 读起来像一份**干净的白纸界面**:纯白画布、暖调中性文字、极浅的边界。
|
|
10
10
|
- **默认只做外观**:颜色、圆角形状、阴影、间距、对齐、字重;不改布局结构、不改交互逻辑、不加功能。
|
|
11
|
-
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
- 越界只有**两处可逆的密度处理**,共同前提是三条同时成立 —— 内容没有消失(双向可回)、
|
|
12
|
+
没有新增持久状态、关掉插件后原样回来:
|
|
13
|
+
1. **用入口换空间**:某个低频功能占着常驻空间时,把它的常驻入口收进已有的信息行、把空间还给内容。
|
|
14
|
+
目前只有「视图切换入口」走这条路。
|
|
15
|
+
2. **用披露换密度**:内容本身没有变,只是默认不一次性铺满。超长用户消息折叠成 20 行,
|
|
16
|
+
保留「展开」把全文原位放回来。这一条不是"美化",是"读得下去":一条 60 行的粘贴内容会把
|
|
17
|
+
后面所有轮次推出屏幕,滚动本身就成了任务。
|
|
14
18
|
- **必须可完全撤销**:插件关闭后 Harness 精确回到出厂外观。因此不写死任何 Harness 文件,也不留下
|
|
15
19
|
常驻的定时器、监听器或网络请求。
|
|
16
20
|
|
|
@@ -56,6 +60,7 @@
|
|
|
56
60
|
| 输入框「+」 | 去掉常驻灰底,默认裸字形,hover 才出底(与麦克风同一个 token);字形缩到与整行同档;再向审查 chip 拉近 8px(视觉间距 4px) | 一行控件里只有它常驻底色,等于把「可点」画成了「已选」;裸字形之后,原先被底板衬住的 11px 加号成了整行最重的图标;12px 盒间距在「裸字形 + 带内距的 chip」之间会读成约 27px,左簇因此被拉散;拉近到视觉 4px 才读成一簇 |
|
|
57
61
|
| 视图切换入口 | 顶部标签行整条移除,header 从 76px 收到 51px;「轨迹」改为状态行第一项(列表图标 + 视图名),在轨迹页原地变成「对话」 | 只有两个视图时,常驻 36px 的标签行 + 右侧「…」是在为低频功能付固定成本;状态行本来就是本会话读数所在,宽度成本为零。入口做成双向的,是因为移除标签行后不能再有第二个「对话」入口 —— 但也不该为回程新开一条 40px 的 header 行 |
|
|
58
62
|
| 队列消息卡片 | 浅灰 `#f5f5f5`(暗色沿用浮层材质) | 它借的是浮层材质上色,浮层改成近白后它和白画布分不开;但它不是浮层 —— 它在画布上叠一层,该用「比画布深一档」而不是「和浮层一样白」 |
|
|
63
|
+
| 长用户消息折叠 | 20 行(19 行正文 + 独占一行的「…」)+ 气泡内底部「展开/收起」;阈值按渲染行数;展开原位 | 参照本机 Codex:省略号单独占一行、按钮留在气泡填充里,气泡才仍读作**一个对象**(按钮放到气泡外会读成第二个对象、第二条消息)。按行数而不是字数,是因为窄窗口下 600 字只排 9 行、宽窗口下能排 30 行 —— 字数阈值在响应式布局里等于没有阈值。按钮文字用 `label-secondary` 而不是应用 disclosure 行的 `label-tertiary`:它坐在气泡浅蓝填充上,13px 下 tertiary 的对比度不足 4:1 |
|
|
59
64
|
|
|
60
65
|
---
|
|
61
66
|
|
package/README.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# dsh-vt-uiux-enhancer · VT UI/UX 增强
|
|
2
2
|
|
|
3
|
-
DeepSeek Harness
|
|
4
|
-
|
|
3
|
+
DeepSeek Harness 的界面改善插件:把外观调成**一张干净的白纸**。
|
|
4
|
+
默认只改看得见的样式;唯一越界的是两处**可逆的密度处理**(低频入口收进状态行、超长用户消息折叠成 20 行),
|
|
5
|
+
都不新增持久状态,关闭插件后 Harness 精确回到出厂外观。
|
|
5
6
|
|
|
6
7
|
## 它改了什么
|
|
7
8
|
|
|
@@ -12,6 +13,9 @@ DeepSeek Harness 的界面改善插件:把外观调成**一张干净的白纸*
|
|
|
12
13
|
- **动效**:动画只表达"状态正在变";轮次跳转条静止为短刻度、靠近指针才长出来。
|
|
13
14
|
- **输入框**:原生 `title` tooltip 换成自绘气泡,沿用应用自己的 tooltip token。
|
|
14
15
|
- **空间回收**:视图切换标签行收进状态行,header 从 76px 收到 51px;入口双向保留。
|
|
16
|
+
- **长消息折叠**:用户自己的消息超过 20 行时,显示 19 行正文 + 一行「…」,气泡内底部出现
|
|
17
|
+
「展开/收起」+ 原生 chevron;展开后原位显示全文。阈值是**渲染行数**,窗口变窄或字号变大
|
|
18
|
+
都不会改变折叠点。
|
|
15
19
|
- **细节**:会话边缘上下 24px 渐隐、队列消息卡片用"比画布深一档"的底色。
|
|
16
20
|
|
|
17
21
|
每条补丁的取值理由写在 `client.js` 的注释里;跨补丁的取舍记在 `DESIGN.md`。
|
|
@@ -32,16 +36,16 @@ https://vontean.github.io/vt-dsh-plugin/index.json
|
|
|
32
36
|
**npm**
|
|
33
37
|
|
|
34
38
|
```bash
|
|
35
|
-
dsh plugin --profile <profile> add dsh-vt-uiux-enhancer@1.0
|
|
39
|
+
dsh plugin --profile <profile> add dsh-vt-uiux-enhancer@1.1.0
|
|
36
40
|
```
|
|
37
41
|
|
|
38
42
|
**Release tarball**
|
|
39
43
|
|
|
40
44
|
到 [Releases](https://github.com/Vontean/vt-dsh-plugin/releases) 下载
|
|
41
|
-
`dsh-vt-uiux-enhancer-1.0.
|
|
45
|
+
`dsh-vt-uiux-enhancer-1.1.0.tgz`,然后:
|
|
42
46
|
|
|
43
47
|
```bash
|
|
44
|
-
dsh plugin --profile <profile> add /绝对路径/dsh-vt-uiux-enhancer-1.0.
|
|
48
|
+
dsh plugin --profile <profile> add /绝对路径/dsh-vt-uiux-enhancer-1.1.0.tgz
|
|
45
49
|
```
|
|
46
50
|
|
|
47
51
|
## 支持的 DSH 版本
|
package/client.js
CHANGED
|
@@ -43,7 +43,7 @@ window.__ModuleLoader__.load({
|
|
|
43
43
|
'use strict';
|
|
44
44
|
|
|
45
45
|
const PLUGIN_ID = 'dsh-vt-uiux-enhancer';
|
|
46
|
-
const VERSION = '1.0
|
|
46
|
+
const VERSION = '1.1.0';
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
49
|
* DSH ships `:root { --dsw-corner-shape: superellipse(1.5) }` and applies it
|
|
@@ -292,6 +292,53 @@ window.__ModuleLoader__.load({
|
|
|
292
292
|
toConversation: ['M10 4L6.70711 7.29289C6.31658 7.68342 6.31658 8.31658 6.70711 8.70711L10 12'],
|
|
293
293
|
};
|
|
294
294
|
|
|
295
|
+
/**
|
|
296
|
+
* Long user messages in the transcript.
|
|
297
|
+
*
|
|
298
|
+
* A pasted stack trace turns one turn into four screenfuls and pushes every
|
|
299
|
+
* later exchange out of reach. Codex clamps the message and offers a
|
|
300
|
+
* disclosure; this copies that, with one difference that matters for a
|
|
301
|
+
* responsive layout: the threshold is RENDERED ROWS, never characters.
|
|
302
|
+
* Twenty rows means twenty rows at any column width and any content font
|
|
303
|
+
* size, because the count is measured from the bubble the browser laid out;
|
|
304
|
+
* a character budget would show 9 lines in a narrow window and 40 in a wide
|
|
305
|
+
* one, which is exactly the scrolling problem this is meant to solve.
|
|
306
|
+
*
|
|
307
|
+
* The reference this was modelled on (Codex) spends all twenty rows: the
|
|
308
|
+
* nineteenth is the last line of text, the twentieth is a lone "…" that says
|
|
309
|
+
* "there is more", and the disclosure row sits under it inside the same
|
|
310
|
+
* fill. Expanded, the text runs to the bottom and only the disclosure row is
|
|
311
|
+
* left.
|
|
312
|
+
*
|
|
313
|
+
* Geometry is expressed against the bubble's own line box
|
|
314
|
+
* (`line-height: calc(22px + var(--dsh-content-font-delta))` — the second
|
|
315
|
+
* term is the Appearance content-size preference), so the clamp follows the
|
|
316
|
+
* font setting through CSS `calc()` and the line count never has to be
|
|
317
|
+
* recomputed. `padding` mirrors the shipped bubble (`10px 16px`) and
|
|
318
|
+
* `stackGap` the shipped `.userStack` gap; the two extra rows are a SIBLING
|
|
319
|
+
* of the bubble, because `overflow: hidden` clips at the padding box
|
|
320
|
+
* (see trap 10 in DEVELOPMENT.md).
|
|
321
|
+
*/
|
|
322
|
+
const MESSAGE_CLAMP = {
|
|
323
|
+
/**
|
|
324
|
+
* Rows shown while clamped, the "…" marker included. Twenty is where a
|
|
325
|
+
* message stops reading as a message and starts reading as a document;
|
|
326
|
+
* below it nothing changes at all.
|
|
327
|
+
*/
|
|
328
|
+
rows: 20,
|
|
329
|
+
/** Space between the "…" row and the disclosure row. */
|
|
330
|
+
rowGap: 8,
|
|
331
|
+
/** The shipped bubble's bottom/side padding, and the stack gap it sits in. */
|
|
332
|
+
padding: { bottom: 10, x: 16 },
|
|
333
|
+
stackGap: 8,
|
|
334
|
+
/** Gap between the last text line and the disclosure block when expanded. */
|
|
335
|
+
textGap: 4,
|
|
336
|
+
/** Chevron: the icon set's own default box; regular stroke is 1 at 16. */
|
|
337
|
+
glyph: { box: 14, stroke: 1 },
|
|
338
|
+
/** Icon-to-label gap, the value the app's disclosure titles use. */
|
|
339
|
+
iconGap: 6,
|
|
340
|
+
};
|
|
341
|
+
|
|
295
342
|
/** Codex's spring: linear() with a 0.8% overshoot instead of a bezier. */
|
|
296
343
|
const RAIL_SPRING = 'linear(0, .398 10%, .682 20%, .843 30%, .925 40%, .972 50%, 1.004 60%, 1.008 70%, 1.003 80%, 1)';
|
|
297
344
|
|
|
@@ -1029,6 +1076,106 @@ ${responseLine} {
|
|
|
1029
1076
|
fallback: '._6DJyyG_panel',
|
|
1030
1077
|
rules: (selector) => `[data-queue-dock] ${selector}::before { background: var(--vt-queue-fill) !important; }`,
|
|
1031
1078
|
},
|
|
1079
|
+
{
|
|
1080
|
+
kind: 'component',
|
|
1081
|
+
id: 'user-bubble',
|
|
1082
|
+
/**
|
|
1083
|
+
* The bubble the user's own message is painted on — the only surface in
|
|
1084
|
+
* the app filled with `--dsw-specific-bubble`, which makes the signature
|
|
1085
|
+
* unambiguous without naming a hashed class.
|
|
1086
|
+
*
|
|
1087
|
+
* This patch owns the clamp, the "…" row and the disclosure block,
|
|
1088
|
+
* because all three are placed against the bubble's own line box.
|
|
1089
|
+
* `data-vt-clamp` is written by the `message-clamp` behaviour.
|
|
1090
|
+
*
|
|
1091
|
+
* The two extra rows are a SIBLING of the bubble, not rows inside it:
|
|
1092
|
+
* `overflow: hidden` clips at the PADDING box, so a reserved
|
|
1093
|
+
* `padding-bottom` would still have text painted over it (trap 10). The
|
|
1094
|
+
* clamp therefore zeroes the bottom padding, which puts the clip edge
|
|
1095
|
+
* exactly on the 19th line's bottom edge, and the block below carries the
|
|
1096
|
+
* same fill plus squared top corners, so both boxes read as one bubble.
|
|
1097
|
+
*/
|
|
1098
|
+
signature: ['background:var(--dsw-specific-bubble)'],
|
|
1099
|
+
selectorFilter: '_bubble$',
|
|
1100
|
+
fallback: '.AkCqbG_bubble',
|
|
1101
|
+
rules: (selector) => {
|
|
1102
|
+
const { rows, rowGap, padding, stackGap, textGap, glyph, iconGap } = MESSAGE_CLAMP;
|
|
1103
|
+
return `
|
|
1104
|
+
${selector}[data-vt-clamp="clamped"] {
|
|
1105
|
+
max-height: calc(${rows - 1} * (22px + var(--dsh-content-font-delta, 0px))) !important;
|
|
1106
|
+
overflow: hidden !important;
|
|
1107
|
+
padding-bottom: 0 !important;
|
|
1108
|
+
border-bottom-left-radius: 0 !important;
|
|
1109
|
+
border-bottom-right-radius: 0 !important;
|
|
1110
|
+
}
|
|
1111
|
+
${selector}[data-vt-clamp="expanded"] {
|
|
1112
|
+
border-bottom-left-radius: 0 !important;
|
|
1113
|
+
border-bottom-right-radius: 0 !important;
|
|
1114
|
+
}
|
|
1115
|
+
.vt-msg-clamp-tail {
|
|
1116
|
+
align-self: stretch;
|
|
1117
|
+
display: flex;
|
|
1118
|
+
flex-direction: column;
|
|
1119
|
+
gap: ${rowGap}px;
|
|
1120
|
+
/* Sibling inside .userStack, a flex column with an 8px gap: cancel it so the
|
|
1121
|
+
two filled boxes touch, and restore the bottom corners here. */
|
|
1122
|
+
margin-top: -${stackGap}px;
|
|
1123
|
+
padding: 0 ${padding.x}px ${padding.bottom}px;
|
|
1124
|
+
background: var(--dsw-specific-bubble);
|
|
1125
|
+
border-radius: 0 0 var(--dsw-radius-xl) var(--dsw-radius-xl);
|
|
1126
|
+
}
|
|
1127
|
+
${selector}[data-vt-clamp="expanded"] + .vt-msg-clamp-tail {
|
|
1128
|
+
padding-top: ${textGap}px;
|
|
1129
|
+
}
|
|
1130
|
+
.vt-msg-clamp-more {
|
|
1131
|
+
height: calc(22px + var(--dsh-content-font-delta, 0px));
|
|
1132
|
+
line-height: calc(22px + var(--dsh-content-font-delta, 0px));
|
|
1133
|
+
color: var(--dsw-alias-label-primary);
|
|
1134
|
+
user-select: none;
|
|
1135
|
+
}
|
|
1136
|
+
${selector}[data-vt-clamp="expanded"] + .vt-msg-clamp-tail .vt-msg-clamp-more {
|
|
1137
|
+
display: none !important;
|
|
1138
|
+
}
|
|
1139
|
+
.vt-msg-clamp-toggle {
|
|
1140
|
+
display: flex;
|
|
1141
|
+
align-items: center;
|
|
1142
|
+
gap: ${iconGap}px;
|
|
1143
|
+
height: calc(22px + var(--dsh-content-font-delta, 0px));
|
|
1144
|
+
margin: 0;
|
|
1145
|
+
padding: 0;
|
|
1146
|
+
border: 0;
|
|
1147
|
+
background: none;
|
|
1148
|
+
font: inherit;
|
|
1149
|
+
font-size: var(--dsh-content-font-size-secondary, 13px);
|
|
1150
|
+
line-height: 1;
|
|
1151
|
+
/* label-secondary, not the disclosure rows' label-tertiary: those sit on the
|
|
1152
|
+
white canvas, this one on the tinted fill where tertiary drops under 4:1. */
|
|
1153
|
+
color: var(--dsw-alias-label-secondary);
|
|
1154
|
+
cursor: pointer;
|
|
1155
|
+
transition: color 0.1s;
|
|
1156
|
+
}
|
|
1157
|
+
.vt-msg-clamp-toggle:hover {
|
|
1158
|
+
color: var(--dsw-alias-label-primary);
|
|
1159
|
+
}
|
|
1160
|
+
.vt-msg-clamp-toggle:focus-visible {
|
|
1161
|
+
outline: 1.5px solid var(--dsw-alias-state-business-primary);
|
|
1162
|
+
outline-offset: 2px;
|
|
1163
|
+
border-radius: var(--dsw-radius-sm);
|
|
1164
|
+
}
|
|
1165
|
+
.vt-msg-clamp-icon {
|
|
1166
|
+
display: inline-flex;
|
|
1167
|
+
flex: none;
|
|
1168
|
+
}
|
|
1169
|
+
.vt-msg-clamp-icon svg {
|
|
1170
|
+
display: block;
|
|
1171
|
+
width: ${glyph.box}px;
|
|
1172
|
+
height: ${glyph.box}px;
|
|
1173
|
+
}
|
|
1174
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1175
|
+
.vt-msg-clamp-toggle { transition: none; }
|
|
1176
|
+
}`;
|
|
1177
|
+
},
|
|
1178
|
+
},
|
|
1032
1179
|
{
|
|
1033
1180
|
kind: 'behavior',
|
|
1034
1181
|
id: 'view-switch',
|
|
@@ -1049,6 +1196,23 @@ ${responseLine} {
|
|
|
1049
1196
|
behavior: 'view-switch',
|
|
1050
1197
|
css: () => '',
|
|
1051
1198
|
},
|
|
1199
|
+
{
|
|
1200
|
+
kind: 'behavior',
|
|
1201
|
+
id: 'message-clamp',
|
|
1202
|
+
/**
|
|
1203
|
+
* A long user message is clamped to 20 rendered rows, with a disclosure
|
|
1204
|
+
* row inside its own bubble. A behaviour, not a component patch: the
|
|
1205
|
+
* decision ("is this message longer than 20 rows?") needs a layout
|
|
1206
|
+
* measurement, and the toggle has to remember one bit of state per
|
|
1207
|
+
* message. CSS can do neither.
|
|
1208
|
+
*
|
|
1209
|
+
* The style half lives in `user-bubble`, because the geometry has to key
|
|
1210
|
+
* off the discovered bubble class. This entry owns the DOM half: the
|
|
1211
|
+
* state attribute, the sibling disclosure block, the ellipsis row.
|
|
1212
|
+
*/
|
|
1213
|
+
behavior: 'message-clamp',
|
|
1214
|
+
css: () => '',
|
|
1215
|
+
},
|
|
1052
1216
|
];
|
|
1053
1217
|
|
|
1054
1218
|
// ═══════════════════════════════════════════════════════════════════════
|
|
@@ -1122,6 +1286,13 @@ ${responseLine} {
|
|
|
1122
1286
|
const disposers = [];
|
|
1123
1287
|
/** Why a behaviour failed to install; surfaced through diagnostics. */
|
|
1124
1288
|
const behaviorErrors = [];
|
|
1289
|
+
/**
|
|
1290
|
+
* Measurements a behaviour publishes into the report channel. Behaviours do
|
|
1291
|
+
* not produce `--vt-report-<patch>` tokens (they resolve no selectors), so
|
|
1292
|
+
* without this a behaviour could install successfully and still be
|
|
1293
|
+
* invisible to the one verification channel that works without DevTools.
|
|
1294
|
+
*/
|
|
1295
|
+
const behaviorMetrics = {};
|
|
1125
1296
|
/** Created on first sync, so a document-less host never touches the DOM. */
|
|
1126
1297
|
let sheet = null;
|
|
1127
1298
|
const ensureSheet = () => sheet || (sheet = createStyleHost(ctx));
|
|
@@ -1273,7 +1444,13 @@ ${responseLine} {
|
|
|
1273
1444
|
report[`${id}-radius`] = style.borderRadius;
|
|
1274
1445
|
report[`${id}-size`] = `${Math.round(rect.width)}x${Math.round(rect.height)}`;
|
|
1275
1446
|
if (id.includes('row')) report[`${id}-pad`] = style.padding;
|
|
1447
|
+
// The two numbers the long-message clamp derives from: the line box it
|
|
1448
|
+
// counts in, and the box model its max-height is measured against.
|
|
1449
|
+
if (id === 'user-bubble') report[`${id}-box`] = `${style.boxSizing}-${style.lineHeight}`;
|
|
1276
1450
|
}
|
|
1451
|
+
// Behaviour readings: the only way to see a behaviour work without
|
|
1452
|
+
// DevTools, since behaviours publish no selector report of their own.
|
|
1453
|
+
for (const [key, value] of Object.entries(behaviorMetrics)) report[key] = value;
|
|
1277
1454
|
|
|
1278
1455
|
const tokens = {};
|
|
1279
1456
|
for (const [key, value] of Object.entries(report)) {
|
|
@@ -1759,6 +1936,319 @@ ${responseLine} {
|
|
|
1759
1936
|
}, ViewSwitch));
|
|
1760
1937
|
}
|
|
1761
1938
|
|
|
1939
|
+
/**
|
|
1940
|
+
* Clamp a long user message to 20 rendered rows with a disclosure block.
|
|
1941
|
+
*
|
|
1942
|
+
* Three constraints shape this more than the feature itself:
|
|
1943
|
+
*
|
|
1944
|
+
* 1. React owns the bubble's children. Appending leaf nodes is safe as long
|
|
1945
|
+
* as a re-render that drops them is noticed and they are re-added — that
|
|
1946
|
+
* is what the sweep does on every structural mutation. Reparenting or
|
|
1947
|
+
* wrapping React's content would not be safe, so the text is never
|
|
1948
|
+
* moved: the clamp is `max-height` on the bubble, and the disclosure is
|
|
1949
|
+
* a SIBLING of it (trap 10 is why it cannot be inside the clipped box).
|
|
1950
|
+
* 2. The decision needs layout, so it is measured, never guessed. An
|
|
1951
|
+
* unclamped bubble's `scrollHeight` is `paddingTop + rows × lineHeight +
|
|
1952
|
+
* paddingBottom`, which answers "longer than 20 rows?" in one read. Then
|
|
1953
|
+
* the clamped bubble is checked against its own overflow: if it does not
|
|
1954
|
+
* actually hide anything (an inline chip inflated a line, or the estimate
|
|
1955
|
+
* rounded up), the clamp is released. A measured post-condition beats a
|
|
1956
|
+
* clever formula.
|
|
1957
|
+
* 3. The transcript re-mounts rows on session switches and compaction, so
|
|
1958
|
+
* expansion is remembered per flow key, not per node, otherwise the
|
|
1959
|
+
* message the reader just opened would close again.
|
|
1960
|
+
*
|
|
1961
|
+
* Failure surface: a missing chevron icon module or a missing
|
|
1962
|
+
* `react-dom/client` throws into `installBehaviors`, which reports it as a
|
|
1963
|
+
* failed behaviour instead of leaving half a feature wired.
|
|
1964
|
+
*/
|
|
1965
|
+
function installMessageClamp(readSelectors) {
|
|
1966
|
+
const ROW_SELECTOR = '[data-chat-flow-kind="user"],[data-chat-flow-kind="steering"]';
|
|
1967
|
+
const STATE = 'data-vt-clamp';
|
|
1968
|
+
const TOGGLE = 'vt-msg-clamp-toggle';
|
|
1969
|
+
const MORE = 'vt-msg-clamp-more';
|
|
1970
|
+
const TAIL = 'vt-msg-clamp-tail';
|
|
1971
|
+
/** Settle time: the transcript mutates continuously while a turn streams. */
|
|
1972
|
+
const SETTLE_MS = 120;
|
|
1973
|
+
/** Expansion memory is bounded so switching sessions cannot grow it forever. */
|
|
1974
|
+
const MEMORY = 400;
|
|
1975
|
+
|
|
1976
|
+
const React = require('react');
|
|
1977
|
+
const reactDom = require('react-dom/client');
|
|
1978
|
+
const icons = require('@deepseek-ai/dsh-client-ui-primitives');
|
|
1979
|
+
const ChevronDown = icons.IconChevronDownOutlineRegular;
|
|
1980
|
+
const ChevronUp = icons.IconChevronUpOutlineRegular;
|
|
1981
|
+
if (typeof ChevronDown !== 'function' || typeof ChevronUp !== 'function') {
|
|
1982
|
+
throw new Error('chevron icons unavailable in @deepseek-ai/dsh-client-ui-primitives');
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
/** Labels follow the app language instead of shipping an English-only row. */
|
|
1986
|
+
const locale = ctx.get('locale');
|
|
1987
|
+
const translate = (zh, en) => (
|
|
1988
|
+
locale && typeof locale.resolveText === 'function' ? locale.resolveText({ zh, en }) : en
|
|
1989
|
+
);
|
|
1990
|
+
const label = { open: translate('展开', 'Show more'), close: translate('收起', 'Show less') };
|
|
1991
|
+
|
|
1992
|
+
/** Expanded flow keys, so the memory survives the re-mounts. */
|
|
1993
|
+
const open = new Set();
|
|
1994
|
+
/** Live disclosure entries, so dispose removes exactly what we added. */
|
|
1995
|
+
const entries = new Set();
|
|
1996
|
+
/** bubble -> entry; a WeakMap keeps dropped rows collectable. */
|
|
1997
|
+
const wired = new WeakMap();
|
|
1998
|
+
let timer = 0;
|
|
1999
|
+
let published = '';
|
|
2000
|
+
let revoke = null;
|
|
2001
|
+
|
|
2002
|
+
const narrowed = () => {
|
|
2003
|
+
while (open.size > MEMORY) open.delete(open.values().next().value);
|
|
2004
|
+
};
|
|
2005
|
+
|
|
2006
|
+
/** Flow key of the row a bubble sits in — the identity that outlives the node. */
|
|
2007
|
+
const flowKey = (bubble) => {
|
|
2008
|
+
const row = bubble.closest('[data-chat-flow-key]');
|
|
2009
|
+
return row ? (row.getAttribute('data-chat-flow-key') || '') : '';
|
|
2010
|
+
};
|
|
2011
|
+
|
|
2012
|
+
/** The bubble class resolved by the `user-bubble` patch, else its own fallback. */
|
|
2013
|
+
const bubblePatch = patches.components.find((patch) => patch.id === 'user-bubble');
|
|
2014
|
+
const bubbleSelector = () => {
|
|
2015
|
+
const map = (typeof readSelectors === 'function' && readSelectors()) || {};
|
|
2016
|
+
const list = map['user-bubble'];
|
|
2017
|
+
if (list && list.length) return list.join(',');
|
|
2018
|
+
return (bubblePatch && bubblePatch.fallback) || '';
|
|
2019
|
+
};
|
|
2020
|
+
|
|
2021
|
+
/**
|
|
2022
|
+
* Rendered row count of an *unclamped* bubble. `scrollHeight` includes
|
|
2023
|
+
* the padding, so the padding is subtracted before dividing by the line
|
|
2024
|
+
* box — the result is the number of line boxes the browser actually laid
|
|
2025
|
+
* out, which is the unit the 20-row rule is defined in.
|
|
2026
|
+
*/
|
|
2027
|
+
const lineCount = (bubble) => {
|
|
2028
|
+
const style = getComputedStyle(bubble);
|
|
2029
|
+
const lineHeight = parseFloat(style.lineHeight);
|
|
2030
|
+
if (!isFinite(lineHeight) || lineHeight <= 0) return 0;
|
|
2031
|
+
const padding = (parseFloat(style.paddingTop) || 0) + (parseFloat(style.paddingBottom) || 0);
|
|
2032
|
+
return Math.round((bubble.scrollHeight - padding) / lineHeight);
|
|
2033
|
+
};
|
|
2034
|
+
|
|
2035
|
+
/** Does the applied clamp actually hide text? */
|
|
2036
|
+
const overflows = (bubble) => bubble.clientHeight > 0 && bubble.scrollHeight > bubble.clientHeight + 1;
|
|
2037
|
+
|
|
2038
|
+
/** One chevron, rendered from the app's own icon component. */
|
|
2039
|
+
const chevron = (expanded) => {
|
|
2040
|
+
const host = document.createElement('span');
|
|
2041
|
+
host.className = 'vt-msg-clamp-icon';
|
|
2042
|
+
const root = reactDom.createRoot(host);
|
|
2043
|
+
const paint = (isOpen) => root.render(React.createElement(isOpen ? ChevronUp : ChevronDown, {
|
|
2044
|
+
size: MESSAGE_CLAMP.glyph.box,
|
|
2045
|
+
strokeWidth: MESSAGE_CLAMP.glyph.stroke,
|
|
2046
|
+
}));
|
|
2047
|
+
paint(expanded);
|
|
2048
|
+
return { host, paint, dispose: () => root.unmount() };
|
|
2049
|
+
};
|
|
2050
|
+
|
|
2051
|
+
/**
|
|
2052
|
+
* The clamp's verification channel. Published here rather than only
|
|
2053
|
+
* through the engine's report: the engine republishes on body mutations
|
|
2054
|
+
* alone, so on an idle page a reading taken right after install would
|
|
2055
|
+
* never reach the token list.
|
|
2056
|
+
*/
|
|
2057
|
+
const publish = (rows, bubbles, clamped, expandedCount, first, scan) => {
|
|
2058
|
+
const fields = {
|
|
2059
|
+
messages: `rows-${rows} bubbles-${bubbles} clamped-${clamped} expanded-${expandedCount}`,
|
|
2060
|
+
};
|
|
2061
|
+
if (scan && scan.length) fields['messages-scan'] = scan.join(' ');
|
|
2062
|
+
if (first) {
|
|
2063
|
+
const style = getComputedStyle(first.bubble);
|
|
2064
|
+
fields['messages-first'] = [
|
|
2065
|
+
`lines-${first.lines}`,
|
|
2066
|
+
`max-${style.maxHeight || 'none'}`,
|
|
2067
|
+
`h-${first.bubble.clientHeight}`,
|
|
2068
|
+
`scroll-${first.bubble.scrollHeight}`,
|
|
2069
|
+
`pb-${style.paddingBottom}`,
|
|
2070
|
+
`lh-${style.lineHeight}`,
|
|
2071
|
+
`box-${style.boxSizing}`,
|
|
2072
|
+
].join('-');
|
|
2073
|
+
}
|
|
2074
|
+
const signature = JSON.stringify(fields);
|
|
2075
|
+
if (signature === published) return;
|
|
2076
|
+
published = signature;
|
|
2077
|
+
const tokens = {};
|
|
2078
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
2079
|
+
tokens[`--vt-report-${sanitize(key)}-${sanitize(value)}`] = { light: '', dark: '' };
|
|
2080
|
+
}
|
|
2081
|
+
try {
|
|
2082
|
+
revoke = ctx.theme.overrideTokens('vt-interface:messages', tokens);
|
|
2083
|
+
} catch (err) { /* diagnostics must never break the visual layer */ }
|
|
2084
|
+
};
|
|
2085
|
+
|
|
2086
|
+
const unwire = (bubble, entry) => {
|
|
2087
|
+
entries.delete(entry);
|
|
2088
|
+
wired.delete(bubble);
|
|
2089
|
+
try { entry.icon.dispose(); } catch (err) { /* already gone */ }
|
|
2090
|
+
entry.tail.remove();
|
|
2091
|
+
bubble.removeAttribute(STATE);
|
|
2092
|
+
bubble.removeAttribute('data-vt-clamp-lines');
|
|
2093
|
+
};
|
|
2094
|
+
|
|
2095
|
+
const wire = (bubble, lines, expanded) => {
|
|
2096
|
+
const key = flowKey(bubble);
|
|
2097
|
+
|
|
2098
|
+
const more = document.createElement('span');
|
|
2099
|
+
more.className = MORE;
|
|
2100
|
+
more.setAttribute('aria-hidden', 'true');
|
|
2101
|
+
more.textContent = '…';
|
|
2102
|
+
|
|
2103
|
+
const toggle = document.createElement('button');
|
|
2104
|
+
toggle.type = 'button';
|
|
2105
|
+
toggle.className = TOGGLE;
|
|
2106
|
+
const caption = document.createElement('span');
|
|
2107
|
+
caption.className = 'vt-msg-clamp-label';
|
|
2108
|
+
const icon = chevron(expanded);
|
|
2109
|
+
toggle.appendChild(caption);
|
|
2110
|
+
toggle.appendChild(icon.host);
|
|
2111
|
+
|
|
2112
|
+
/*
|
|
2113
|
+
* The disclosure is a SIBLING of the bubble, never a child: the clamp
|
|
2114
|
+
* uses `overflow: hidden`, which clips at the padding box, so any row
|
|
2115
|
+
* reserved inside the bubble would still be painted over by the text
|
|
2116
|
+
* that overflows the content box.
|
|
2117
|
+
*/
|
|
2118
|
+
const tail = document.createElement('div');
|
|
2119
|
+
tail.className = TAIL;
|
|
2120
|
+
tail.appendChild(more);
|
|
2121
|
+
tail.appendChild(toggle);
|
|
2122
|
+
|
|
2123
|
+
const entry = { bubble, tail, toggle, more, icon, caption, key, lines, expanded };
|
|
2124
|
+
|
|
2125
|
+
const paint = () => {
|
|
2126
|
+
bubble.setAttribute(STATE, entry.expanded ? 'expanded' : 'clamped');
|
|
2127
|
+
bubble.setAttribute('data-vt-clamp-lines', String(entry.lines));
|
|
2128
|
+
toggle.setAttribute('aria-expanded', String(entry.expanded));
|
|
2129
|
+
caption.textContent = entry.expanded ? label.close : label.open;
|
|
2130
|
+
icon.paint(entry.expanded);
|
|
2131
|
+
};
|
|
2132
|
+
|
|
2133
|
+
toggle.addEventListener('click', (event) => {
|
|
2134
|
+
// The transcript row has its own click affordances; a disclosure is
|
|
2135
|
+
// not one of them.
|
|
2136
|
+
event.preventDefault();
|
|
2137
|
+
event.stopPropagation();
|
|
2138
|
+
entry.expanded = !entry.expanded;
|
|
2139
|
+
if (entry.key) {
|
|
2140
|
+
if (entry.expanded) open.add(entry.key);
|
|
2141
|
+
else open.delete(entry.key);
|
|
2142
|
+
narrowed();
|
|
2143
|
+
}
|
|
2144
|
+
paint();
|
|
2145
|
+
schedule();
|
|
2146
|
+
});
|
|
2147
|
+
|
|
2148
|
+
bubble.after(tail);
|
|
2149
|
+
entries.add(entry);
|
|
2150
|
+
wired.set(bubble, entry);
|
|
2151
|
+
paint();
|
|
2152
|
+
return entry;
|
|
2153
|
+
};
|
|
2154
|
+
|
|
2155
|
+
const sweep = () => {
|
|
2156
|
+
timer = 0;
|
|
2157
|
+
// A row that left the document must release its nodes and its React
|
|
2158
|
+
// root instead of waiting for dispose.
|
|
2159
|
+
for (const entry of [...entries]) {
|
|
2160
|
+
if (entry.bubble.isConnected) continue;
|
|
2161
|
+
entries.delete(entry);
|
|
2162
|
+
try { entry.icon.dispose(); } catch (err) { /* already gone */ }
|
|
2163
|
+
entry.tail.remove();
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
const rows = document.querySelectorAll(ROW_SELECTOR);
|
|
2167
|
+
const bubbles = [];
|
|
2168
|
+
for (const row of rows) {
|
|
2169
|
+
let found;
|
|
2170
|
+
try { found = row.querySelectorAll(bubbleSelector()); } catch (err) { found = []; }
|
|
2171
|
+
for (const bubble of found) bubbles.push(bubble);
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2174
|
+
let clamped = 0;
|
|
2175
|
+
let expandedCount = 0;
|
|
2176
|
+
let first = null;
|
|
2177
|
+
const scan = [];
|
|
2178
|
+
|
|
2179
|
+
for (const bubble of bubbles) {
|
|
2180
|
+
let entry = wired.get(bubble) || null;
|
|
2181
|
+
// A re-render can drop our nodes while keeping the bubble: rebuild.
|
|
2182
|
+
if (entry && !entry.tail.isConnected) {
|
|
2183
|
+
unwire(bubble, entry);
|
|
2184
|
+
entry = null;
|
|
2185
|
+
}
|
|
2186
|
+
if (!entry) {
|
|
2187
|
+
if (bubble.getAttribute(STATE) === 'none') continue;
|
|
2188
|
+
const lines = lineCount(bubble);
|
|
2189
|
+
if (scan.length < 4) {
|
|
2190
|
+
scan.push(`${lines}L/${bubble.clientHeight}h/${bubble.scrollHeight}s/${bubble.getAttribute(STATE) || '-'}`);
|
|
2191
|
+
}
|
|
2192
|
+
if (lines <= 0) continue;
|
|
2193
|
+
if (lines <= MESSAGE_CLAMP.rows) {
|
|
2194
|
+
bubble.setAttribute(STATE, 'none');
|
|
2195
|
+
continue;
|
|
2196
|
+
}
|
|
2197
|
+
entry = wire(bubble, lines, open.has(flowKey(bubble)));
|
|
2198
|
+
// Measured post-condition: keep the clamp only if it hides text.
|
|
2199
|
+
if (!entry.expanded && !overflows(bubble)) {
|
|
2200
|
+
unwire(bubble, entry);
|
|
2201
|
+
bubble.setAttribute(STATE, 'none');
|
|
2202
|
+
continue;
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
if (entry.expanded) expandedCount += 1;
|
|
2206
|
+
else {
|
|
2207
|
+
clamped += 1;
|
|
2208
|
+
if (!first) first = entry;
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
publish(rows.length, bubbles.length, clamped, expandedCount, first, scan);
|
|
2213
|
+
};
|
|
2214
|
+
|
|
2215
|
+
const schedule = () => {
|
|
2216
|
+
if (timer) return;
|
|
2217
|
+
timer = setTimeout(sweep, SETTLE_MS);
|
|
2218
|
+
};
|
|
2219
|
+
|
|
2220
|
+
// A language switch relabels every visible disclosure row.
|
|
2221
|
+
const relabel = () => {
|
|
2222
|
+
label.open = translate('展开', 'Show more');
|
|
2223
|
+
label.close = translate('收起', 'Show less');
|
|
2224
|
+
for (const entry of entries) entry.caption.textContent = entry.expanded ? label.close : label.open;
|
|
2225
|
+
};
|
|
2226
|
+
const unsubscribe = locale && typeof locale.subscribe === 'function' ? locale.subscribe(relabel) : null;
|
|
2227
|
+
|
|
2228
|
+
const observer = typeof MutationObserver === 'undefined'
|
|
2229
|
+
? null
|
|
2230
|
+
: new MutationObserver(schedule);
|
|
2231
|
+
if (observer) observer.observe(document.body, { childList: true, subtree: true });
|
|
2232
|
+
|
|
2233
|
+
sweep();
|
|
2234
|
+
|
|
2235
|
+
return () => {
|
|
2236
|
+
clearTimeout(timer);
|
|
2237
|
+
if (observer) observer.disconnect();
|
|
2238
|
+
if (typeof unsubscribe === 'function') unsubscribe();
|
|
2239
|
+
for (const entry of [...entries]) {
|
|
2240
|
+
entries.delete(entry);
|
|
2241
|
+
try { entry.icon.dispose(); } catch (err) { /* already gone */ }
|
|
2242
|
+
entry.tail.remove();
|
|
2243
|
+
}
|
|
2244
|
+
for (const bubble of document.querySelectorAll(`[${STATE}]`)) bubble.removeAttribute(STATE);
|
|
2245
|
+
for (const bubble of document.querySelectorAll('[data-vt-clamp-lines]')) {
|
|
2246
|
+
bubble.removeAttribute('data-vt-clamp-lines');
|
|
2247
|
+
}
|
|
2248
|
+
if (typeof revoke === 'function') safeCall(revoke);
|
|
2249
|
+
};
|
|
2250
|
+
}
|
|
2251
|
+
|
|
1762
2252
|
/**
|
|
1763
2253
|
* Behaviour implementations, keyed by the name a patch declares.
|
|
1764
2254
|
*
|
|
@@ -1771,6 +2261,7 @@ ${responseLine} {
|
|
|
1771
2261
|
'native-tooltip': (readSelectors) => installNativeTitleTakeover(readSelectors),
|
|
1772
2262
|
'scroll-edges': () => installScrollEdgeFade(),
|
|
1773
2263
|
'view-switch': () => installViewSwitch(ctx),
|
|
2264
|
+
'message-clamp': (readSelectors) => installMessageClamp(readSelectors),
|
|
1774
2265
|
};
|
|
1775
2266
|
|
|
1776
2267
|
return {
|
|
@@ -1809,7 +2300,7 @@ ${responseLine} {
|
|
|
1809
2300
|
try {
|
|
1810
2301
|
const factory = BEHAVIORS[patch.behavior];
|
|
1811
2302
|
if (typeof factory !== 'function') throw new Error(`unknown behaviour "${patch.behavior}"`);
|
|
1812
|
-
disposers.push(factory(readSelectors));
|
|
2303
|
+
disposers.push(factory(readSelectors, behaviorMetrics));
|
|
1813
2304
|
installed += 1;
|
|
1814
2305
|
} catch (err) {
|
|
1815
2306
|
// A behaviour that throws must not take the visual layer down, but
|
|
@@ -1817,6 +2308,10 @@ ${responseLine} {
|
|
|
1817
2308
|
behaviorErrors.push(`${patch.id}: ${err && err.message ? err.message : String(err)}`);
|
|
1818
2309
|
}
|
|
1819
2310
|
}
|
|
2311
|
+
// Encoded into report tokens, so a failed install is also readable
|
|
2312
|
+
// without DevTools (see the `behaviors` field in docs/VERIFY.md).
|
|
2313
|
+
behaviorMetrics.behaviors = `${installed}-of-${patches.behaviors.length}`;
|
|
2314
|
+
if (behaviorErrors.length > 0) behaviorMetrics['behaviour-errors'] = behaviorErrors.join(' | ');
|
|
1820
2315
|
return { installed, total: patches.behaviors.length, errors: behaviorErrors };
|
|
1821
2316
|
},
|
|
1822
2317
|
|
|
@@ -1825,6 +2320,11 @@ ${responseLine} {
|
|
|
1825
2320
|
return [...behaviorErrors];
|
|
1826
2321
|
},
|
|
1827
2322
|
|
|
2323
|
+
/** Live readings a behaviour publishes for the report channel. */
|
|
2324
|
+
behaviorMetrics() {
|
|
2325
|
+
return { ...behaviorMetrics };
|
|
2326
|
+
},
|
|
2327
|
+
|
|
1828
2328
|
dispose() {
|
|
1829
2329
|
if (sheet) sheet.dispose();
|
|
1830
2330
|
for (const d of disposers.reverse()) safeCall(d);
|
|
@@ -2218,7 +2718,7 @@ ${responseLine} {
|
|
|
2218
2718
|
.filter((node) => String(node.dataset.pluginCss || '').startsWith(`${PLUGIN_ID}/`))
|
|
2219
2719
|
.map((node) => node.textContent || '')
|
|
2220
2720
|
.join('\n')
|
|
2221
|
-
.slice(0,
|
|
2721
|
+
.slice(0, 16000);
|
|
2222
2722
|
}
|
|
2223
2723
|
// Resolved selectors and patch states, handed in by the engine: the ground
|
|
2224
2724
|
// truth for "which element did this patch actually land on".
|
|
@@ -2487,6 +2987,9 @@ ${responseLine} {
|
|
|
2487
2987
|
|
|
2488
2988
|
sync();
|
|
2489
2989
|
const behaviors = ui.installBehaviors(() => lastSelectors);
|
|
2990
|
+
// The install count is recorded while behaviours install; one more publish
|
|
2991
|
+
// makes it visible without waiting for the next body mutation.
|
|
2992
|
+
sync();
|
|
2490
2993
|
|
|
2491
2994
|
// Plugin stylesheets are injected asynchronously during boot, so the first
|
|
2492
2995
|
// discovery pass may legitimately find nothing. Re-resolve when style
|
|
@@ -2556,6 +3059,14 @@ ${responseLine} {
|
|
|
2556
3059
|
behaviors() {
|
|
2557
3060
|
return { ...behaviors, errors: ui.behaviorErrors() };
|
|
2558
3061
|
},
|
|
3062
|
+
/**
|
|
3063
|
+
* Live readings published by behaviours: how many user rows the clamp
|
|
3064
|
+
* found, how many are clamped or expanded, and the first clamped
|
|
3065
|
+
* bubble's measured geometry. Same values as its report tokens.
|
|
3066
|
+
*/
|
|
3067
|
+
messages() {
|
|
3068
|
+
return ui.behaviorMetrics();
|
|
3069
|
+
},
|
|
2559
3070
|
/** Resolved selectors per patch, for auditing what each patch targets. */
|
|
2560
3071
|
selectors() {
|
|
2561
3072
|
return { ...lastSelectors };
|
package/docs/VERIFY.md
CHANGED
|
@@ -41,8 +41,16 @@ cordis_inspect_query(platform="client", provider="Theme", method="listTokens")
|
|
|
41
41
|
扫 `--vt-report-` 前缀即可。**完成判据**:每个 `component` 补丁都有 `discovered-N`(`fallback` 表示仍在
|
|
42
42
|
兜底工作,可接受但要记录原因),且 `build` 与代码一致。
|
|
43
43
|
|
|
44
|
-
`behavior`
|
|
45
|
-
|
|
44
|
+
`behavior` 补丁不解析选择器,所以没有 `discovered` 这类 token。它自己额外发两组:
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
--vt-report-behaviors-<installed>-of-<total> 四个行为是否都装上了;失败时会多一条
|
|
48
|
+
--vt-report-behaviour-errors-<原因> 同 `__vtInterface.behaviors().errors`
|
|
49
|
+
--vt-report-messages-rows-<n>-bubbles-<n>-clamped-<n>-expanded-<n> 长消息折叠的读数
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`messages-*` 由 `message-clamp` 自己写入独立主题层(`vt-interface:messages`),不等引擎的重发周期 ——
|
|
53
|
+
页面静止时引擎不会再上报,那个读数就永远不出现(这条踩过)。
|
|
46
54
|
|
|
47
55
|
这个通道每次 body 变动后 600ms 重发一次,只测「解析到没有 + 首个元素的尺寸/圆角」,
|
|
48
56
|
刻意保持廉价。
|
|
@@ -52,6 +60,27 @@ cordis_inspect_query(platform="client", provider="Theme", method="listTokens")
|
|
|
52
60
|
|
|
53
61
|
---
|
|
54
62
|
|
|
63
|
+
## 长消息折叠:怎么判断生效
|
|
64
|
+
|
|
65
|
+
折叠只在**超过 20 行的用户消息出现在屏幕上**时才工作,所以先让那条消息进入会话视口,再读 token。
|
|
66
|
+
|
|
67
|
+
| token | 期望 | 说明 |
|
|
68
|
+
|---|---|---|
|
|
69
|
+
| `--vt-report-user-bubble-discovered-1` | 有 | 气泡类名解析到了(签名是 `--dsw-specific-bubble` 填充,全仓唯一) |
|
|
70
|
+
| `--vt-report-user-bubble-box-content-box-22px` | 有 | 两个前提:`content-box`(Harness 没有全局 `border-box`)+ 行高 22px |
|
|
71
|
+
| `--vt-report-messages-rows-<n>-…-clamped-1` | `clamped` ≥ 1 | 长消息已折叠;`bubbles` 应与 `rows` 同量级 |
|
|
72
|
+
| `--vt-report-messages-first-lines-<n>-max-418px-h-428-scroll-<n>-pb-0px-lh-22px-box-content-box` | 见左 | 14px 字号下:19 行 ≈ 418px 内容盒,气泡实测 428px(+10 上内距),`pb-0px` 是折叠态(内距归零,裁切边缘才对得上第 19 行的底边) |
|
|
73
|
+
|
|
74
|
+
`clamped-0` 而屏幕上确实有一条长消息时,先看 `--vt-report-messages-scan-<n>L/<高>h/<滚动高>s/<状态>`:
|
|
75
|
+
它是前四个气泡的「行数/高度/滚动高度/状态」快照。
|
|
76
|
+
|
|
77
|
+
- 行数是 0:气泡还没布局(在隐藏的容器里),等它可见后一次 sweep 会补上。
|
|
78
|
+
- 行数 ≤ 20:判定为「不用折叠」,气泡被标记成 `none` 且不会再量第二次。
|
|
79
|
+
- 行数 > 20 但 `clamped-0`:`overflows` 后置检查把它退了(估算法失手时守法,例如某一行被行内 chip 撑高)。
|
|
80
|
+
|
|
81
|
+
展开后 token 变成 `clamped-0 expanded-1`,再点一次回来。这两个数值变化只证明**状态**变了;
|
|
82
|
+
「看得见的变化」仍然要肉眼确认一次,因为 `max-height` 与兄弟块的拼接没有计算值可读。
|
|
83
|
+
|
|
55
84
|
## 通道 B:探针(按需开启)
|
|
56
85
|
|
|
57
86
|
回答「这个元素**现在**的计算样式是什么」。默认关闭 —— 发布态不该带定时器和网络请求。
|
|
@@ -136,8 +165,9 @@ tail -f /tmp/vt-dump.jsonl | jq -c 'select(.kind=="hover") | .hovered'
|
|
|
136
165
|
|
|
137
166
|
## 复核作用面
|
|
138
167
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
168
|
+
`myCss` 打印的是本插件自己那张 `<style>` 的**全文**(按 `data-plugin-css="<插件 id>/…"` 过滤)。
|
|
169
|
+
Harness 上游不写 `!important`,所以带 `!important` 的那部分就是会覆盖上游的作用面;不带
|
|
170
|
+
`!important` 的规则全部落在本插件自己的 `vt-` 类名上(`vt-interface-tip`、`vt-edge-fade`、
|
|
171
|
+
`vt-msg-clamp-*`),作用面同样一眼可数。新增补丁后跑一次 `static` dump,确认规则数与选择器都在预期内。
|
|
142
172
|
|
|
143
|
-
`myCss` 截断在
|
|
173
|
+
`myCss` 截断在 16000 字符(当前约 10.5k)。余量过半时先抬高截断值,否则「全文」这个前提就不成立了。
|
package/locale/en.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
3
|
"title": "VT UI/UX Enhancer",
|
|
4
|
-
"description": "UI/UX refinements for DeepSeek Harness: palette, elevation, control shapes and motion unified, plus space reclaimed from low-frequency features. Restores the original interface completely when disabled."
|
|
4
|
+
"description": "UI/UX refinements for DeepSeek Harness: palette, elevation, control shapes and motion unified, long user messages folded to 20 lines, plus space reclaimed from low-frequency features. Restores the original interface completely when disabled."
|
|
5
5
|
}
|
|
6
6
|
}
|
package/locale/zh.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vt-uiux-enhancer",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "VT UI/UX Enhancer: reversible UI/UX refinements for DeepSeek Harness — palette, elevation, control shapes and motion shipped as patches over DSH's own tokens, slots and styles. Disabling the plugin restores the shipped interface exactly.",
|
|
6
6
|
"license": "MIT",
|