@weibaohui/context-razor 0.4.0 → 0.4.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/README.md +3 -1
- package/client/bundle.js +117 -22
- package/client/index.js +117 -22
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,8 +10,10 @@
|
|
|
10
10
|
## 核心功能
|
|
11
11
|
|
|
12
12
|
- **逐条列出**:当前会话模型可见的每条上下文(用户/助手/工具结果)按序排列,带预览、字符数与 ≈token 估算,点 ⋯ 查看全文与真实 usage
|
|
13
|
+
- **占比标注**:每条的 token 徽章同时显示其占全部上下文的百分比(如 `≈5728 · 15%`),一眼看出谁是上下文大头
|
|
14
|
+
- **类型筛选**:按类型一键过滤——用户 / 注入 / 助手,以及各工具名(bash、skill、read、edit…按会话实际出现的动态生成)。多选 toggle,与 token 量级筛选正交叠加
|
|
13
15
|
- **超阈值标红**:可调阈值(默认 500 token,本地记忆),超限条目红色高亮;「只看超阈值」过滤 + 「token 高→低」排序,大头一眼可见
|
|
14
|
-
- **精确裁剪**:勾选任意条目一键删除——连续段自动合并处理。删除走宿主 surface replace 协议(与官方 compaction 同款机制),**不经 LLM
|
|
16
|
+
- **精确裁剪**:勾选任意条目一键删除——连续段自动合并处理。删除走宿主 surface replace 协议(与官方 compaction 同款机制),**不经 LLM 总结**,删了什么、删了多少完全由你决定。每行尾部带 ✕ 快捷按钮,**单条直接删**与勾选后**多选批量删**走同一确认弹窗
|
|
15
17
|
- **不重排**:本插件不会改变上下文条目顺序,只会剔除选中条目;「token 高→低」排序仅影响页面显示,模型可见顺序始终由会话 surface 决定
|
|
16
18
|
- **安全护栏**:会话运行中禁止裁剪(等当前回合结束);每次替换附带一条 notice 标记消息,模型知道这段历史被你移除了,需要时会主动向你确认
|
|
17
19
|
- **非破坏**:append-only 日志保留全部痕迹(宿主机制如此)。「删除」= 从模型视野与界面投影中移除
|
package/client/bundle.js
CHANGED
|
@@ -66,6 +66,8 @@ window.__ModuleLoader__.load({
|
|
|
66
66
|
sortOrder: '上下文顺序',
|
|
67
67
|
sortTokens: 'token 高→低',
|
|
68
68
|
orderNote: '本插件不会改变上下文条目顺序,只会剔除选中条目。',
|
|
69
|
+
legendTitle: '按 token 量级筛选(点选高亮某一档)',
|
|
70
|
+
kindFilterLabel: '类型',
|
|
69
71
|
selectVisible: '选中可见',
|
|
70
72
|
selectVisibleHint: '选中当前显示的全部条目(配合色块筛选)',
|
|
71
73
|
clearSelect: '清空选择',
|
|
@@ -87,6 +89,8 @@ window.__ModuleLoader__.load({
|
|
|
87
89
|
deleteTitle: '确认裁剪',
|
|
88
90
|
deleteConfirm: '即将把 {n} 条消息(约 {tokens} token)从模型视野中移除,替换为一条标记消息。日志中保留痕迹,此操作不可恢复。继续?',
|
|
89
91
|
deleteBusyHint: '会话正在运行,等当前回合结束后再裁剪',
|
|
92
|
+
deleteOne: '删除此条',
|
|
93
|
+
deleteOneHint: '删除此条(约 {tokens} token)',
|
|
90
94
|
deleteOk: '执行',
|
|
91
95
|
cancel: '取消',
|
|
92
96
|
deletedToast: '已删除 {n} 条(约 {tokens} token)',
|
|
@@ -104,6 +108,8 @@ window.__ModuleLoader__.load({
|
|
|
104
108
|
sortOrder: 'Context order',
|
|
105
109
|
sortTokens: 'tokens high→low',
|
|
106
110
|
orderNote: 'This plugin never reorders context entries; it only removes the selected ones.',
|
|
111
|
+
legendTitle: 'Filter by token tier (click to isolate a band)',
|
|
112
|
+
kindFilterLabel: 'Type',
|
|
107
113
|
selectVisible: 'Select shown',
|
|
108
114
|
selectVisibleHint: 'Select every currently shown entry (pairs with tier filters)',
|
|
109
115
|
clearSelect: 'Clear selection',
|
|
@@ -125,6 +131,8 @@ window.__ModuleLoader__.load({
|
|
|
125
131
|
deleteTitle: 'Confirm trim',
|
|
126
132
|
deleteConfirm: 'About to remove {n} messages (≈{tokens} tokens) from the model view, replaced by one marker message. Traces stay in the log; this cannot be undone. Continue?',
|
|
127
133
|
deleteBusyHint: 'Session is running — trim after the current turn finishes',
|
|
134
|
+
deleteOne: 'Delete this entry',
|
|
135
|
+
deleteOneHint: 'Delete this entry (≈{tokens} tokens)',
|
|
128
136
|
deleteOk: 'Delete',
|
|
129
137
|
cancel: 'Cancel',
|
|
130
138
|
deletedToast: 'Deleted {n} entries (≈{tokens} tokens)',
|
|
@@ -174,6 +182,38 @@ window.__ModuleLoader__.load({
|
|
|
174
182
|
return { kind: entry.kind, label: t(kindI18n(entry.kind)) }
|
|
175
183
|
}
|
|
176
184
|
|
|
185
|
+
/**
|
|
186
|
+
* 筛选用稳定分类键,与 entryChip 的可见口径一致:
|
|
187
|
+
* tool → 'tool:<工具名>'(无工具名则 'tool:',按钮文案退回「工具」)
|
|
188
|
+
* user 注入 → 'injected'(sourceKind !== 'user' 的 user 消息,统一一档;
|
|
189
|
+
* 细分来源在 chip 的 title 与详情里已可见,筛选层不再切分,避免按钮爆炸)
|
|
190
|
+
* user / assistant → 自身
|
|
191
|
+
* 「按钮文案」与 chip 同源(categoryLabel),用户在两处看到的是同一个词。
|
|
192
|
+
*/
|
|
193
|
+
function entryCategory(entry) {
|
|
194
|
+
if (entry.kind === 'tool') return 'tool:' + (typeof entry.tool === 'string' ? entry.tool : '')
|
|
195
|
+
if (entry.kind === 'user' && entry.sourceKind && entry.sourceKind !== 'user') return 'injected'
|
|
196
|
+
return entry.kind
|
|
197
|
+
}
|
|
198
|
+
function categoryLabel(cat, t) {
|
|
199
|
+
if (cat === 'user') return t('kindUser')
|
|
200
|
+
if (cat === 'assistant') return t('kindAssistant')
|
|
201
|
+
if (cat === 'injected') return t('kindInjected')
|
|
202
|
+
if (cat.lastIndexOf('tool:', 0) === 0) {
|
|
203
|
+
const name = cat.slice(5)
|
|
204
|
+
return name || t('kindTool')
|
|
205
|
+
}
|
|
206
|
+
return cat
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** 该条 token 占全部上下文的百分比文案;total 非正或 part 为 0 返回 null(不显示)。 */
|
|
210
|
+
function pctOf(part, total) {
|
|
211
|
+
if (!total || !part) return null
|
|
212
|
+
const p = part / total * 100
|
|
213
|
+
if (p < 0.1) return '<0.1%'
|
|
214
|
+
return (p >= 10 ? p.toFixed(0) : p.toFixed(1)) + '%'
|
|
215
|
+
}
|
|
216
|
+
|
|
177
217
|
// ── 彩虹分级:颜色越暖 = 占用越多 ────────────────────────────────────────
|
|
178
218
|
// 固定对数档位(相邻约 ×2.5),跨会话语义稳定:绿→黄绿→黄→橙→红→品红。
|
|
179
219
|
// 档位是「这条消息吃掉多少典型上下文预算」的粗标尺,不随会话内最大值缩放。
|
|
@@ -224,6 +264,13 @@ window.__ModuleLoader__.load({
|
|
|
224
264
|
.rz-swatch.tier-3{background:hsl(32,80%,45%)}
|
|
225
265
|
.rz-swatch.tier-4{background:hsl(8,75%,48%)}
|
|
226
266
|
.rz-swatch.tier-5{background:hsl(320,65%,50%)}
|
|
267
|
+
.rz-kindsbar{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
|
|
268
|
+
.rz-kinds{display:inline-flex;gap:4px;align-items:center;flex-wrap:wrap}
|
|
269
|
+
.rz-kinds.filtering .rz-kindbtn:not(.on){opacity:.45}
|
|
270
|
+
.rz-kindbtn{display:inline-flex;align-items:center;gap:5px;padding:2px 10px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);font-size:11.5px;cursor:pointer;font-family:var(--dsw-font-family);white-space:nowrap}
|
|
271
|
+
.rz-kindbtn:hover{border-color:var(--dsw-alias-border-l3);background:var(--dsw-alias-interactive-bg-hover)}
|
|
272
|
+
.rz-kindbtn.on{color:var(--dsw-alias-state-business-primary);border-color:var(--dsw-alias-state-business-primary);font-weight:600}
|
|
273
|
+
.rz-kindcount{font-size:10px;opacity:.7}
|
|
227
274
|
.rz-list{display:flex;flex-direction:column;gap:6px}
|
|
228
275
|
.rz-row{display:flex;gap:10px;align-items:center;padding:8px 12px;border-radius:10px;border:1px solid var(--dsw-alias-border-l1);border-left:3px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);cursor:pointer;text-align:left;width:100%}
|
|
229
276
|
.rz-row:hover{background:var(--dsw-alias-interactive-bg-hover)}
|
|
@@ -231,6 +278,9 @@ window.__ModuleLoader__.load({
|
|
|
231
278
|
.rz-row-preview{flex:0 1 340px;min-width:100px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-primary);font-size:13px;text-decoration:none}
|
|
232
279
|
.rz-row-preview:hover{text-decoration:underline;text-underline-offset:3px}
|
|
233
280
|
.rz-row-meta{color:var(--dsw-alias-label-tertiary);font-size:11px;flex:none}
|
|
281
|
+
.rz-row-del{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;margin-left:4px;border-radius:6px;border:1px solid transparent;background:transparent;color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1;cursor:pointer;flex:none}
|
|
282
|
+
.rz-row-del:hover:not(:disabled){color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary);background:hsla(0,75%,50%,.08)}
|
|
283
|
+
.rz-row-del:disabled{opacity:.35;cursor:not-allowed}
|
|
234
284
|
.rz-badge.tier-0{color:hsl(120,55%,40%);border-color:hsla(120,55%,40%,.45);background:hsla(120,55%,40%,.10)}
|
|
235
285
|
.rz-badge.tier-1{color:hsl(90,60%,38%);border-color:hsla(90,60%,38%,.45);background:hsla(90,60%,38%,.10)}
|
|
236
286
|
.rz-badge.tier-2{color:hsl(60,70%,36%);border-color:hsla(60,70%,36%,.45);background:hsla(60,70%,36%,.12)}
|
|
@@ -263,10 +313,12 @@ window.__ModuleLoader__.load({
|
|
|
263
313
|
|
|
264
314
|
const Chip = ({ kind, label, title }) => h('span', { className: 'rz-chip ' + kind, title }, label)
|
|
265
315
|
|
|
266
|
-
const TokenBadge = ({ entry }) => {
|
|
316
|
+
const TokenBadge = ({ entry, total }) => {
|
|
267
317
|
const tier = tierOf(entry)
|
|
268
|
-
|
|
269
|
-
|
|
318
|
+
const pct = pctOf(entry.tokens, total)
|
|
319
|
+
const title = RAZOR_TIERS[tier].label + ' token' + (pct ? ' · ' + pct + ' ' + (total ? 'of ≈' + formatNum(total) + ' token' : 'of context') : '')
|
|
320
|
+
return h('span', { className: 'rz-badge tier-' + tier, title },
|
|
321
|
+
'≈' + formatNum(entry.tokens) + (pct ? ' · ' + pct : ''))
|
|
270
322
|
}
|
|
271
323
|
|
|
272
324
|
/** 分页列表:先渲染 pageSize 行,按需增长(大会话一次挂几千行会卡)。 */
|
|
@@ -281,14 +333,14 @@ window.__ModuleLoader__.load({
|
|
|
281
333
|
}
|
|
282
334
|
|
|
283
335
|
/** 单条全文弹窗(正文经 /entry 异步补全)。 */
|
|
284
|
-
function DetailModal({ detail, t, onClose }) {
|
|
336
|
+
function DetailModal({ detail, t, total, onClose }) {
|
|
285
337
|
if (!detail) return null
|
|
286
338
|
return h('div', { className: 'rz-dlg-backdrop', onClick: onClose },
|
|
287
339
|
h('div', { className: 'rz-dlg', onClick: e => e.stopPropagation() },
|
|
288
340
|
h('h3', null, t('detailTitle') + ' · ' + t('seqLabel', { seq: detail.seq })),
|
|
289
341
|
h('div', { className: 'rz-stats', style: { marginBottom: 10 } },
|
|
290
342
|
h(Chip, { ...entryChip(detail, t) }),
|
|
291
|
-
h(TokenBadge, { entry: detail }),
|
|
343
|
+
h(TokenBadge, { entry: detail, total }),
|
|
292
344
|
h('span', { className: 'rz-label', title: 'seq ' + detail.seq }, formatDateTime(detail.time)),
|
|
293
345
|
h('span', { className: 'rz-label' }, t('detailChars', { chars: formatNum(detail.chars) })),
|
|
294
346
|
detail.usage && h('span', { className: 'rz-label' }, t('detailUsage', {
|
|
@@ -319,10 +371,11 @@ window.__ModuleLoader__.load({
|
|
|
319
371
|
const [ctxLoading, setCtxLoading] = useState(false)
|
|
320
372
|
const [error, setError] = useState(null)
|
|
321
373
|
const [tierFilter, setTierFilter] = useState(() => new Set())
|
|
374
|
+
const [kindFilter, setKindFilter] = useState(() => new Set())
|
|
322
375
|
const [sortBy, setSortBy] = useState('order')
|
|
323
376
|
const [selected, setSelected] = useState(() => new Set())
|
|
324
377
|
const [detail, setDetail] = useState(null)
|
|
325
|
-
const [confirming, setConfirming] = useState(
|
|
378
|
+
const [confirming, setConfirming] = useState(null) // null | number[] 待裁剪 seqs(单条/多选同源)
|
|
326
379
|
const [deleting, setDeleting] = useState(false)
|
|
327
380
|
const [toast, setToast] = useState(null)
|
|
328
381
|
const sessionRef = useRef('')
|
|
@@ -353,16 +406,34 @@ window.__ModuleLoader__.load({
|
|
|
353
406
|
if (!context) return []
|
|
354
407
|
let rows = context.entries
|
|
355
408
|
if (tierFilter.size > 0) rows = rows.filter(e => tierFilter.has(tierOf(e)))
|
|
409
|
+
if (kindFilter.size > 0) rows = rows.filter(e => kindFilter.has(entryCategory(e)))
|
|
356
410
|
return sortEntries(rows, sortBy)
|
|
357
|
-
}, [context, tierFilter, sortBy])
|
|
411
|
+
}, [context, tierFilter, kindFilter, sortBy])
|
|
358
412
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
413
|
+
// 当前上下文里实际出现的分类(cat/count/tokens),按条数降序——决定渲染哪些按钮。
|
|
414
|
+
const categories = useMemo(() => {
|
|
415
|
+
if (!context) return []
|
|
416
|
+
const seen = new Map()
|
|
417
|
+
for (const e of context.entries) {
|
|
418
|
+
const c = entryCategory(e)
|
|
419
|
+
const prev = seen.get(c) || { count: 0, tokens: 0 }
|
|
420
|
+
prev.count += 1
|
|
421
|
+
prev.tokens += (e.tokens || 0)
|
|
422
|
+
seen.set(c, prev)
|
|
423
|
+
}
|
|
424
|
+
return [...seen.entries()]
|
|
425
|
+
.map(([cat, v]) => ({ cat, count: v.count, tokens: v.tokens }))
|
|
426
|
+
.sort((a, b) => b.count - a.count || b.tokens - a.tokens)
|
|
427
|
+
}, [context])
|
|
428
|
+
|
|
429
|
+
const entriesBySeq = useMemo(() => context ? new Map(context.entries.map(e => [e.seq, e])) : null, [context])
|
|
430
|
+
const sumSeqs = (seqs) => {
|
|
362
431
|
let sum = 0
|
|
363
|
-
for (const seq of
|
|
432
|
+
if (entriesBySeq) for (const seq of seqs) { const e = entriesBySeq.get(seq); if (e) sum += e.tokens || 0 }
|
|
364
433
|
return sum
|
|
365
|
-
}
|
|
434
|
+
}
|
|
435
|
+
const selectedTokens = useMemo(() => sumSeqs(selected), [selected, entriesBySeq])
|
|
436
|
+
const confirmTokens = useMemo(() => confirming ? sumSeqs(confirming) : 0, [confirming, entriesBySeq])
|
|
366
437
|
|
|
367
438
|
const toggleRow = (seq) => setSelected(prev => {
|
|
368
439
|
const next = new Set(prev)
|
|
@@ -376,6 +447,12 @@ window.__ModuleLoader__.load({
|
|
|
376
447
|
else next.add(i)
|
|
377
448
|
return next
|
|
378
449
|
})
|
|
450
|
+
const toggleKind = (cat) => setKindFilter(prev => {
|
|
451
|
+
const next = new Set(prev)
|
|
452
|
+
if (next.has(cat)) next.delete(cat)
|
|
453
|
+
else next.add(cat)
|
|
454
|
+
return next
|
|
455
|
+
})
|
|
379
456
|
const selectVisible = () => setSelected(new Set(visible.map(e => e.seq)))
|
|
380
457
|
|
|
381
458
|
const openDetail = (entry) => {
|
|
@@ -386,17 +463,18 @@ window.__ModuleLoader__.load({
|
|
|
386
463
|
}
|
|
387
464
|
|
|
388
465
|
const doDelete = async () => {
|
|
466
|
+
if (!confirming || confirming.length === 0) return
|
|
389
467
|
setDeleting(true)
|
|
390
468
|
try {
|
|
391
|
-
const r = await fetch(API + '/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ session: sessionId, seqs:
|
|
469
|
+
const r = await fetch(API + '/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ session: sessionId, seqs: confirming }) })
|
|
392
470
|
const d = await r.json().catch(() => ({}))
|
|
393
471
|
if (!r.ok) throw new Error(d.error || 'HTTP ' + r.status)
|
|
394
|
-
setConfirming(
|
|
472
|
+
setConfirming(null)
|
|
395
473
|
setSelected(new Set())
|
|
396
474
|
showToast(t('deletedToast', { n: d.removed, tokens: formatNum(d.tokensRemoved) }))
|
|
397
475
|
refreshAll()
|
|
398
476
|
} catch (e) {
|
|
399
|
-
setConfirming(
|
|
477
|
+
setConfirming(null)
|
|
400
478
|
setError(e.message)
|
|
401
479
|
} finally { setDeleting(false) }
|
|
402
480
|
}
|
|
@@ -422,6 +500,19 @@ window.__ModuleLoader__.load({
|
|
|
422
500
|
h('option', { value: 'order' }, t('sortOrder')),
|
|
423
501
|
h('option', { value: 'tokens' }, t('sortTokens'))),
|
|
424
502
|
h('button', { className: 'rz-btn', onClick: refreshAll, title: t('refresh') }, t('refresh'))),
|
|
503
|
+
// 类型筛选:与 chip 同口径的可点选按钮(user/injected/assistant/tool:<名>),
|
|
504
|
+
// 多选 toggle,与 tier 量级筛选正交叠加。按钮带条数;hover 看 token 占比。
|
|
505
|
+
categories.length > 0 && h('div', { key: 'kinds', className: 'rz-kindsbar' },
|
|
506
|
+
h('span', { className: 'rz-label' }, t('kindFilterLabel')),
|
|
507
|
+
h('span', { className: 'rz-kinds' + (kindFilter.size > 0 ? ' filtering' : '') },
|
|
508
|
+
categories.map(({ cat, count, tokens }) =>
|
|
509
|
+
h('button', { key: cat,
|
|
510
|
+
className: 'rz-kindbtn' + (kindFilter.has(cat) ? ' on' : ''),
|
|
511
|
+
title: categoryLabel(cat, t) + ' · ' + count + ' 条 · ≈' + formatNum(tokens) + ' token'
|
|
512
|
+
+ (context.totalTokens ? '(' + (pctOf(tokens, context.totalTokens) || '0%') + ')' : ''),
|
|
513
|
+
onClick: () => toggleKind(cat) },
|
|
514
|
+
categoryLabel(cat, t),
|
|
515
|
+
h('span', { className: 'rz-kindcount' }, count))))),
|
|
425
516
|
selected.size > 0 && h('div', { key: 'sel', className: 'rz-stats' },
|
|
426
517
|
h('span', null, t('selectedStats', { n: selected.size, tokens: formatNum(selectedTokens) })),
|
|
427
518
|
h('span', { className: 'rz-spacer' }),
|
|
@@ -429,7 +520,7 @@ window.__ModuleLoader__.load({
|
|
|
429
520
|
h('button', { className: 'rz-btn', onClick: () => setSelected(new Set()) }, t('clearSelect')),
|
|
430
521
|
h('button', { className: 'rz-btn rz-btn-danger', disabled: !canDelete,
|
|
431
522
|
title: busy ? t('deleteBusyHint') : undefined,
|
|
432
|
-
onClick: () => setConfirming(
|
|
523
|
+
onClick: () => setConfirming([...selected]) },
|
|
433
524
|
t('deleteSelected') + ` (${selected.size})`))),
|
|
434
525
|
visible.length === 0
|
|
435
526
|
? h('div', { key: 'empty', className: 'rz-empty' }, t('emptyContext'))
|
|
@@ -440,15 +531,19 @@ window.__ModuleLoader__.load({
|
|
|
440
531
|
role: 'button', tabIndex: 0, onClick: () => toggleRow(entry.seq),
|
|
441
532
|
onKeyDown: e => e.key === 'Enter' && toggleRow(entry.seq) },
|
|
442
533
|
h('input', { type: 'checkbox', checked: selected.has(entry.seq), onClick: e => e.stopPropagation(), onChange: () => toggleRow(entry.seq) }),
|
|
443
|
-
h(TokenBadge, { entry }),
|
|
534
|
+
h(TokenBadge, { entry, total: context.totalTokens }),
|
|
444
535
|
h(Chip, { ...entryChip(entry, t) }),
|
|
445
536
|
h('span', { className: 'rz-row-preview', title: entry.preview,
|
|
446
537
|
onClick: e => { e.stopPropagation(); openDetail(entry) } }, entry.preview || ' '),
|
|
447
|
-
h('span', { className: 'rz-row-meta', title: 'seq ' + entry.seq }, formatDateTime(entry.time))
|
|
538
|
+
h('span', { className: 'rz-row-meta', title: 'seq ' + entry.seq }, formatDateTime(entry.time)),
|
|
539
|
+
h('button', { className: 'rz-row-del', type: 'button', disabled: busy,
|
|
540
|
+
title: busy ? t('deleteBusyHint') : t('deleteOneHint', { tokens: formatNum(entry.tokens) }),
|
|
541
|
+
'aria-label': t('deleteOne'),
|
|
542
|
+
onClick: e => { e.stopPropagation(); if (!busy) setConfirming([entry.seq]) } }, '✕'))
|
|
448
543
|
} })),
|
|
449
|
-
detail && h(DetailModal, { detail, t, onClose: () => setDetail(null) }),
|
|
450
|
-
confirming && h(ConfirmDialog, { n:
|
|
451
|
-
onCancel: () => setConfirming(
|
|
544
|
+
detail && h(DetailModal, { detail, t, total: context.totalTokens, onClose: () => setDetail(null) }),
|
|
545
|
+
confirming && confirming.length > 0 && h(ConfirmDialog, { n: confirming.length, tokens: formatNum(confirmTokens), deleting, t,
|
|
546
|
+
onCancel: () => setConfirming(null), onOk: doDelete }),
|
|
452
547
|
],
|
|
453
548
|
toast && h('div', { className: 'rz-toast' }, toast))
|
|
454
549
|
}
|
|
@@ -460,7 +555,7 @@ window.__ModuleLoader__.load({
|
|
|
460
555
|
module.exports = {
|
|
461
556
|
name: CLIENT_NAME,
|
|
462
557
|
inject: ['slots', 'locale'],
|
|
463
|
-
__internals: { NS, ZH, EN, sortEntries, tierOf, RAZOR_TIERS, formatDateTime, entryChip },
|
|
558
|
+
__internals: { NS, ZH, EN, sortEntries, tierOf, RAZOR_TIERS, formatDateTime, entryChip, entryCategory, categoryLabel, pctOf },
|
|
464
559
|
__boot(container, opts = {}) {
|
|
465
560
|
ensureStyles()
|
|
466
561
|
const t = opts.t || ((key, vars) => {
|
package/client/index.js
CHANGED
|
@@ -56,6 +56,8 @@ const ZH = {
|
|
|
56
56
|
sortOrder: '上下文顺序',
|
|
57
57
|
sortTokens: 'token 高→低',
|
|
58
58
|
orderNote: '本插件不会改变上下文条目顺序,只会剔除选中条目。',
|
|
59
|
+
legendTitle: '按 token 量级筛选(点选高亮某一档)',
|
|
60
|
+
kindFilterLabel: '类型',
|
|
59
61
|
selectVisible: '选中可见',
|
|
60
62
|
selectVisibleHint: '选中当前显示的全部条目(配合色块筛选)',
|
|
61
63
|
clearSelect: '清空选择',
|
|
@@ -77,6 +79,8 @@ const ZH = {
|
|
|
77
79
|
deleteTitle: '确认裁剪',
|
|
78
80
|
deleteConfirm: '即将把 {n} 条消息(约 {tokens} token)从模型视野中移除,替换为一条标记消息。日志中保留痕迹,此操作不可恢复。继续?',
|
|
79
81
|
deleteBusyHint: '会话正在运行,等当前回合结束后再裁剪',
|
|
82
|
+
deleteOne: '删除此条',
|
|
83
|
+
deleteOneHint: '删除此条(约 {tokens} token)',
|
|
80
84
|
deleteOk: '执行',
|
|
81
85
|
cancel: '取消',
|
|
82
86
|
deletedToast: '已删除 {n} 条(约 {tokens} token)',
|
|
@@ -94,6 +98,8 @@ const EN = {
|
|
|
94
98
|
sortOrder: 'Context order',
|
|
95
99
|
sortTokens: 'tokens high→low',
|
|
96
100
|
orderNote: 'This plugin never reorders context entries; it only removes the selected ones.',
|
|
101
|
+
legendTitle: 'Filter by token tier (click to isolate a band)',
|
|
102
|
+
kindFilterLabel: 'Type',
|
|
97
103
|
selectVisible: 'Select shown',
|
|
98
104
|
selectVisibleHint: 'Select every currently shown entry (pairs with tier filters)',
|
|
99
105
|
clearSelect: 'Clear selection',
|
|
@@ -115,6 +121,8 @@ const EN = {
|
|
|
115
121
|
deleteTitle: 'Confirm trim',
|
|
116
122
|
deleteConfirm: 'About to remove {n} messages (≈{tokens} tokens) from the model view, replaced by one marker message. Traces stay in the log; this cannot be undone. Continue?',
|
|
117
123
|
deleteBusyHint: 'Session is running — trim after the current turn finishes',
|
|
124
|
+
deleteOne: 'Delete this entry',
|
|
125
|
+
deleteOneHint: 'Delete this entry (≈{tokens} tokens)',
|
|
118
126
|
deleteOk: 'Delete',
|
|
119
127
|
cancel: 'Cancel',
|
|
120
128
|
deletedToast: 'Deleted {n} entries (≈{tokens} tokens)',
|
|
@@ -164,6 +172,38 @@ function entryChip(entry, t) {
|
|
|
164
172
|
return { kind: entry.kind, label: t(kindI18n(entry.kind)) }
|
|
165
173
|
}
|
|
166
174
|
|
|
175
|
+
/**
|
|
176
|
+
* 筛选用稳定分类键,与 entryChip 的可见口径一致:
|
|
177
|
+
* tool → 'tool:<工具名>'(无工具名则 'tool:',按钮文案退回「工具」)
|
|
178
|
+
* user 注入 → 'injected'(sourceKind !== 'user' 的 user 消息,统一一档;
|
|
179
|
+
* 细分来源在 chip 的 title 与详情里已可见,筛选层不再切分,避免按钮爆炸)
|
|
180
|
+
* user / assistant → 自身
|
|
181
|
+
* 「按钮文案」与 chip 同源(categoryLabel),用户在两处看到的是同一个词。
|
|
182
|
+
*/
|
|
183
|
+
function entryCategory(entry) {
|
|
184
|
+
if (entry.kind === 'tool') return 'tool:' + (typeof entry.tool === 'string' ? entry.tool : '')
|
|
185
|
+
if (entry.kind === 'user' && entry.sourceKind && entry.sourceKind !== 'user') return 'injected'
|
|
186
|
+
return entry.kind
|
|
187
|
+
}
|
|
188
|
+
function categoryLabel(cat, t) {
|
|
189
|
+
if (cat === 'user') return t('kindUser')
|
|
190
|
+
if (cat === 'assistant') return t('kindAssistant')
|
|
191
|
+
if (cat === 'injected') return t('kindInjected')
|
|
192
|
+
if (cat.lastIndexOf('tool:', 0) === 0) {
|
|
193
|
+
const name = cat.slice(5)
|
|
194
|
+
return name || t('kindTool')
|
|
195
|
+
}
|
|
196
|
+
return cat
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** 该条 token 占全部上下文的百分比文案;total 非正或 part 为 0 返回 null(不显示)。 */
|
|
200
|
+
function pctOf(part, total) {
|
|
201
|
+
if (!total || !part) return null
|
|
202
|
+
const p = part / total * 100
|
|
203
|
+
if (p < 0.1) return '<0.1%'
|
|
204
|
+
return (p >= 10 ? p.toFixed(0) : p.toFixed(1)) + '%'
|
|
205
|
+
}
|
|
206
|
+
|
|
167
207
|
// ── 彩虹分级:颜色越暖 = 占用越多 ────────────────────────────────────────
|
|
168
208
|
// 固定对数档位(相邻约 ×2.5),跨会话语义稳定:绿→黄绿→黄→橙→红→品红。
|
|
169
209
|
// 档位是「这条消息吃掉多少典型上下文预算」的粗标尺,不随会话内最大值缩放。
|
|
@@ -214,6 +254,13 @@ const STYLE = `<style>
|
|
|
214
254
|
.rz-swatch.tier-3{background:hsl(32,80%,45%)}
|
|
215
255
|
.rz-swatch.tier-4{background:hsl(8,75%,48%)}
|
|
216
256
|
.rz-swatch.tier-5{background:hsl(320,65%,50%)}
|
|
257
|
+
.rz-kindsbar{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
|
|
258
|
+
.rz-kinds{display:inline-flex;gap:4px;align-items:center;flex-wrap:wrap}
|
|
259
|
+
.rz-kinds.filtering .rz-kindbtn:not(.on){opacity:.45}
|
|
260
|
+
.rz-kindbtn{display:inline-flex;align-items:center;gap:5px;padding:2px 10px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);font-size:11.5px;cursor:pointer;font-family:var(--dsw-font-family);white-space:nowrap}
|
|
261
|
+
.rz-kindbtn:hover{border-color:var(--dsw-alias-border-l3);background:var(--dsw-alias-interactive-bg-hover)}
|
|
262
|
+
.rz-kindbtn.on{color:var(--dsw-alias-state-business-primary);border-color:var(--dsw-alias-state-business-primary);font-weight:600}
|
|
263
|
+
.rz-kindcount{font-size:10px;opacity:.7}
|
|
217
264
|
.rz-list{display:flex;flex-direction:column;gap:6px}
|
|
218
265
|
.rz-row{display:flex;gap:10px;align-items:center;padding:8px 12px;border-radius:10px;border:1px solid var(--dsw-alias-border-l1);border-left:3px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);cursor:pointer;text-align:left;width:100%}
|
|
219
266
|
.rz-row:hover{background:var(--dsw-alias-interactive-bg-hover)}
|
|
@@ -221,6 +268,9 @@ const STYLE = `<style>
|
|
|
221
268
|
.rz-row-preview{flex:0 1 340px;min-width:100px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-primary);font-size:13px;text-decoration:none}
|
|
222
269
|
.rz-row-preview:hover{text-decoration:underline;text-underline-offset:3px}
|
|
223
270
|
.rz-row-meta{color:var(--dsw-alias-label-tertiary);font-size:11px;flex:none}
|
|
271
|
+
.rz-row-del{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;margin-left:4px;border-radius:6px;border:1px solid transparent;background:transparent;color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1;cursor:pointer;flex:none}
|
|
272
|
+
.rz-row-del:hover:not(:disabled){color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary);background:hsla(0,75%,50%,.08)}
|
|
273
|
+
.rz-row-del:disabled{opacity:.35;cursor:not-allowed}
|
|
224
274
|
.rz-badge.tier-0{color:hsl(120,55%,40%);border-color:hsla(120,55%,40%,.45);background:hsla(120,55%,40%,.10)}
|
|
225
275
|
.rz-badge.tier-1{color:hsl(90,60%,38%);border-color:hsla(90,60%,38%,.45);background:hsla(90,60%,38%,.10)}
|
|
226
276
|
.rz-badge.tier-2{color:hsl(60,70%,36%);border-color:hsla(60,70%,36%,.45);background:hsla(60,70%,36%,.12)}
|
|
@@ -253,10 +303,12 @@ const STYLE = `<style>
|
|
|
253
303
|
|
|
254
304
|
const Chip = ({ kind, label, title }) => h('span', { className: 'rz-chip ' + kind, title }, label)
|
|
255
305
|
|
|
256
|
-
const TokenBadge = ({ entry }) => {
|
|
306
|
+
const TokenBadge = ({ entry, total }) => {
|
|
257
307
|
const tier = tierOf(entry)
|
|
258
|
-
|
|
259
|
-
|
|
308
|
+
const pct = pctOf(entry.tokens, total)
|
|
309
|
+
const title = RAZOR_TIERS[tier].label + ' token' + (pct ? ' · ' + pct + ' ' + (total ? 'of ≈' + formatNum(total) + ' token' : 'of context') : '')
|
|
310
|
+
return h('span', { className: 'rz-badge tier-' + tier, title },
|
|
311
|
+
'≈' + formatNum(entry.tokens) + (pct ? ' · ' + pct : ''))
|
|
260
312
|
}
|
|
261
313
|
|
|
262
314
|
/** 分页列表:先渲染 pageSize 行,按需增长(大会话一次挂几千行会卡)。 */
|
|
@@ -271,14 +323,14 @@ function PagedList({ items, render, t }) {
|
|
|
271
323
|
}
|
|
272
324
|
|
|
273
325
|
/** 单条全文弹窗(正文经 /entry 异步补全)。 */
|
|
274
|
-
function DetailModal({ detail, t, onClose }) {
|
|
326
|
+
function DetailModal({ detail, t, total, onClose }) {
|
|
275
327
|
if (!detail) return null
|
|
276
328
|
return h('div', { className: 'rz-dlg-backdrop', onClick: onClose },
|
|
277
329
|
h('div', { className: 'rz-dlg', onClick: e => e.stopPropagation() },
|
|
278
330
|
h('h3', null, t('detailTitle') + ' · ' + t('seqLabel', { seq: detail.seq })),
|
|
279
331
|
h('div', { className: 'rz-stats', style: { marginBottom: 10 } },
|
|
280
332
|
h(Chip, { ...entryChip(detail, t) }),
|
|
281
|
-
h(TokenBadge, { entry: detail }),
|
|
333
|
+
h(TokenBadge, { entry: detail, total }),
|
|
282
334
|
h('span', { className: 'rz-label', title: 'seq ' + detail.seq }, formatDateTime(detail.time)),
|
|
283
335
|
h('span', { className: 'rz-label' }, t('detailChars', { chars: formatNum(detail.chars) })),
|
|
284
336
|
detail.usage && h('span', { className: 'rz-label' }, t('detailUsage', {
|
|
@@ -309,10 +361,11 @@ function RazorPage({ t, fixedSessionId }) {
|
|
|
309
361
|
const [ctxLoading, setCtxLoading] = useState(false)
|
|
310
362
|
const [error, setError] = useState(null)
|
|
311
363
|
const [tierFilter, setTierFilter] = useState(() => new Set())
|
|
364
|
+
const [kindFilter, setKindFilter] = useState(() => new Set())
|
|
312
365
|
const [sortBy, setSortBy] = useState('order')
|
|
313
366
|
const [selected, setSelected] = useState(() => new Set())
|
|
314
367
|
const [detail, setDetail] = useState(null)
|
|
315
|
-
const [confirming, setConfirming] = useState(
|
|
368
|
+
const [confirming, setConfirming] = useState(null) // null | number[] 待裁剪 seqs(单条/多选同源)
|
|
316
369
|
const [deleting, setDeleting] = useState(false)
|
|
317
370
|
const [toast, setToast] = useState(null)
|
|
318
371
|
const sessionRef = useRef('')
|
|
@@ -343,16 +396,34 @@ function RazorPage({ t, fixedSessionId }) {
|
|
|
343
396
|
if (!context) return []
|
|
344
397
|
let rows = context.entries
|
|
345
398
|
if (tierFilter.size > 0) rows = rows.filter(e => tierFilter.has(tierOf(e)))
|
|
399
|
+
if (kindFilter.size > 0) rows = rows.filter(e => kindFilter.has(entryCategory(e)))
|
|
346
400
|
return sortEntries(rows, sortBy)
|
|
347
|
-
}, [context, tierFilter, sortBy])
|
|
401
|
+
}, [context, tierFilter, kindFilter, sortBy])
|
|
348
402
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
403
|
+
// 当前上下文里实际出现的分类(cat/count/tokens),按条数降序——决定渲染哪些按钮。
|
|
404
|
+
const categories = useMemo(() => {
|
|
405
|
+
if (!context) return []
|
|
406
|
+
const seen = new Map()
|
|
407
|
+
for (const e of context.entries) {
|
|
408
|
+
const c = entryCategory(e)
|
|
409
|
+
const prev = seen.get(c) || { count: 0, tokens: 0 }
|
|
410
|
+
prev.count += 1
|
|
411
|
+
prev.tokens += (e.tokens || 0)
|
|
412
|
+
seen.set(c, prev)
|
|
413
|
+
}
|
|
414
|
+
return [...seen.entries()]
|
|
415
|
+
.map(([cat, v]) => ({ cat, count: v.count, tokens: v.tokens }))
|
|
416
|
+
.sort((a, b) => b.count - a.count || b.tokens - a.tokens)
|
|
417
|
+
}, [context])
|
|
418
|
+
|
|
419
|
+
const entriesBySeq = useMemo(() => context ? new Map(context.entries.map(e => [e.seq, e])) : null, [context])
|
|
420
|
+
const sumSeqs = (seqs) => {
|
|
352
421
|
let sum = 0
|
|
353
|
-
for (const seq of
|
|
422
|
+
if (entriesBySeq) for (const seq of seqs) { const e = entriesBySeq.get(seq); if (e) sum += e.tokens || 0 }
|
|
354
423
|
return sum
|
|
355
|
-
}
|
|
424
|
+
}
|
|
425
|
+
const selectedTokens = useMemo(() => sumSeqs(selected), [selected, entriesBySeq])
|
|
426
|
+
const confirmTokens = useMemo(() => confirming ? sumSeqs(confirming) : 0, [confirming, entriesBySeq])
|
|
356
427
|
|
|
357
428
|
const toggleRow = (seq) => setSelected(prev => {
|
|
358
429
|
const next = new Set(prev)
|
|
@@ -366,6 +437,12 @@ function RazorPage({ t, fixedSessionId }) {
|
|
|
366
437
|
else next.add(i)
|
|
367
438
|
return next
|
|
368
439
|
})
|
|
440
|
+
const toggleKind = (cat) => setKindFilter(prev => {
|
|
441
|
+
const next = new Set(prev)
|
|
442
|
+
if (next.has(cat)) next.delete(cat)
|
|
443
|
+
else next.add(cat)
|
|
444
|
+
return next
|
|
445
|
+
})
|
|
369
446
|
const selectVisible = () => setSelected(new Set(visible.map(e => e.seq)))
|
|
370
447
|
|
|
371
448
|
const openDetail = (entry) => {
|
|
@@ -376,17 +453,18 @@ function RazorPage({ t, fixedSessionId }) {
|
|
|
376
453
|
}
|
|
377
454
|
|
|
378
455
|
const doDelete = async () => {
|
|
456
|
+
if (!confirming || confirming.length === 0) return
|
|
379
457
|
setDeleting(true)
|
|
380
458
|
try {
|
|
381
|
-
const r = await fetch(API + '/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ session: sessionId, seqs:
|
|
459
|
+
const r = await fetch(API + '/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ session: sessionId, seqs: confirming }) })
|
|
382
460
|
const d = await r.json().catch(() => ({}))
|
|
383
461
|
if (!r.ok) throw new Error(d.error || 'HTTP ' + r.status)
|
|
384
|
-
setConfirming(
|
|
462
|
+
setConfirming(null)
|
|
385
463
|
setSelected(new Set())
|
|
386
464
|
showToast(t('deletedToast', { n: d.removed, tokens: formatNum(d.tokensRemoved) }))
|
|
387
465
|
refreshAll()
|
|
388
466
|
} catch (e) {
|
|
389
|
-
setConfirming(
|
|
467
|
+
setConfirming(null)
|
|
390
468
|
setError(e.message)
|
|
391
469
|
} finally { setDeleting(false) }
|
|
392
470
|
}
|
|
@@ -412,6 +490,19 @@ function RazorPage({ t, fixedSessionId }) {
|
|
|
412
490
|
h('option', { value: 'order' }, t('sortOrder')),
|
|
413
491
|
h('option', { value: 'tokens' }, t('sortTokens'))),
|
|
414
492
|
h('button', { className: 'rz-btn', onClick: refreshAll, title: t('refresh') }, t('refresh'))),
|
|
493
|
+
// 类型筛选:与 chip 同口径的可点选按钮(user/injected/assistant/tool:<名>),
|
|
494
|
+
// 多选 toggle,与 tier 量级筛选正交叠加。按钮带条数;hover 看 token 占比。
|
|
495
|
+
categories.length > 0 && h('div', { key: 'kinds', className: 'rz-kindsbar' },
|
|
496
|
+
h('span', { className: 'rz-label' }, t('kindFilterLabel')),
|
|
497
|
+
h('span', { className: 'rz-kinds' + (kindFilter.size > 0 ? ' filtering' : '') },
|
|
498
|
+
categories.map(({ cat, count, tokens }) =>
|
|
499
|
+
h('button', { key: cat,
|
|
500
|
+
className: 'rz-kindbtn' + (kindFilter.has(cat) ? ' on' : ''),
|
|
501
|
+
title: categoryLabel(cat, t) + ' · ' + count + ' 条 · ≈' + formatNum(tokens) + ' token'
|
|
502
|
+
+ (context.totalTokens ? '(' + (pctOf(tokens, context.totalTokens) || '0%') + ')' : ''),
|
|
503
|
+
onClick: () => toggleKind(cat) },
|
|
504
|
+
categoryLabel(cat, t),
|
|
505
|
+
h('span', { className: 'rz-kindcount' }, count))))),
|
|
415
506
|
selected.size > 0 && h('div', { key: 'sel', className: 'rz-stats' },
|
|
416
507
|
h('span', null, t('selectedStats', { n: selected.size, tokens: formatNum(selectedTokens) })),
|
|
417
508
|
h('span', { className: 'rz-spacer' }),
|
|
@@ -419,7 +510,7 @@ function RazorPage({ t, fixedSessionId }) {
|
|
|
419
510
|
h('button', { className: 'rz-btn', onClick: () => setSelected(new Set()) }, t('clearSelect')),
|
|
420
511
|
h('button', { className: 'rz-btn rz-btn-danger', disabled: !canDelete,
|
|
421
512
|
title: busy ? t('deleteBusyHint') : undefined,
|
|
422
|
-
onClick: () => setConfirming(
|
|
513
|
+
onClick: () => setConfirming([...selected]) },
|
|
423
514
|
t('deleteSelected') + ` (${selected.size})`))),
|
|
424
515
|
visible.length === 0
|
|
425
516
|
? h('div', { key: 'empty', className: 'rz-empty' }, t('emptyContext'))
|
|
@@ -430,15 +521,19 @@ function RazorPage({ t, fixedSessionId }) {
|
|
|
430
521
|
role: 'button', tabIndex: 0, onClick: () => toggleRow(entry.seq),
|
|
431
522
|
onKeyDown: e => e.key === 'Enter' && toggleRow(entry.seq) },
|
|
432
523
|
h('input', { type: 'checkbox', checked: selected.has(entry.seq), onClick: e => e.stopPropagation(), onChange: () => toggleRow(entry.seq) }),
|
|
433
|
-
h(TokenBadge, { entry }),
|
|
524
|
+
h(TokenBadge, { entry, total: context.totalTokens }),
|
|
434
525
|
h(Chip, { ...entryChip(entry, t) }),
|
|
435
526
|
h('span', { className: 'rz-row-preview', title: entry.preview,
|
|
436
527
|
onClick: e => { e.stopPropagation(); openDetail(entry) } }, entry.preview || ' '),
|
|
437
|
-
h('span', { className: 'rz-row-meta', title: 'seq ' + entry.seq }, formatDateTime(entry.time))
|
|
528
|
+
h('span', { className: 'rz-row-meta', title: 'seq ' + entry.seq }, formatDateTime(entry.time)),
|
|
529
|
+
h('button', { className: 'rz-row-del', type: 'button', disabled: busy,
|
|
530
|
+
title: busy ? t('deleteBusyHint') : t('deleteOneHint', { tokens: formatNum(entry.tokens) }),
|
|
531
|
+
'aria-label': t('deleteOne'),
|
|
532
|
+
onClick: e => { e.stopPropagation(); if (!busy) setConfirming([entry.seq]) } }, '✕'))
|
|
438
533
|
} })),
|
|
439
|
-
detail && h(DetailModal, { detail, t, onClose: () => setDetail(null) }),
|
|
440
|
-
confirming && h(ConfirmDialog, { n:
|
|
441
|
-
onCancel: () => setConfirming(
|
|
534
|
+
detail && h(DetailModal, { detail, t, total: context.totalTokens, onClose: () => setDetail(null) }),
|
|
535
|
+
confirming && confirming.length > 0 && h(ConfirmDialog, { n: confirming.length, tokens: formatNum(confirmTokens), deleting, t,
|
|
536
|
+
onCancel: () => setConfirming(null), onOk: doDelete }),
|
|
442
537
|
],
|
|
443
538
|
toast && h('div', { className: 'rz-toast' }, toast))
|
|
444
539
|
}
|
|
@@ -450,7 +545,7 @@ const CLIENT_NAME = '@weibaohui/context-razor'
|
|
|
450
545
|
module.exports = {
|
|
451
546
|
name: CLIENT_NAME,
|
|
452
547
|
inject: ['slots', 'locale'],
|
|
453
|
-
__internals: { NS, ZH, EN, sortEntries, tierOf, RAZOR_TIERS, formatDateTime, entryChip },
|
|
548
|
+
__internals: { NS, ZH, EN, sortEntries, tierOf, RAZOR_TIERS, formatDateTime, entryChip, entryCategory, categoryLabel, pctOf },
|
|
454
549
|
__boot(container, opts = {}) {
|
|
455
550
|
ensureStyles()
|
|
456
551
|
const t = opts.t || ((key, vars) => {
|