@weibaohui/context-razor 0.4.0 → 0.4.2
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 +139 -37
- package/client/index.js +139 -37
- 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,9 +66,11 @@ window.__ModuleLoader__.load({
|
|
|
66
66
|
sortOrder: '上下文顺序',
|
|
67
67
|
sortTokens: 'token 高→低',
|
|
68
68
|
orderNote: '本插件不会改变上下文条目顺序,只会剔除选中条目。',
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
legendTitle: '按 token 量级筛选(点选高亮某一档)',
|
|
70
|
+
kindFilterLabel: '类型',
|
|
71
|
+
selectVisible: '全选',
|
|
72
|
+
selectVisibleHint: '全选当前可见条目(配合类型/量级筛选)',
|
|
73
|
+
clearSelect: '取消全选',
|
|
72
74
|
deleteSelected: '删除选中',
|
|
73
75
|
selectedStats: '已选 {n} 条 / ≈{tokens} token',
|
|
74
76
|
stats: '{nodes} 条 · 合计 ≈{tokens} token{mode}',
|
|
@@ -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,9 +108,11 @@ 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.',
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
111
|
+
legendTitle: 'Filter by token tier (click to isolate a band)',
|
|
112
|
+
kindFilterLabel: 'Type',
|
|
113
|
+
selectVisible: 'All',
|
|
114
|
+
selectVisibleHint: 'Select all currently visible entries (pairs with type/tier filters)',
|
|
115
|
+
clearSelect: 'Clear all',
|
|
110
116
|
deleteSelected: 'Delete selected',
|
|
111
117
|
selectedStats: '{n} selected / ≈{tokens} tokens',
|
|
112
118
|
stats: '{nodes} entries · total ≈{tokens} tokens{mode}',
|
|
@@ -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,18 @@ 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}
|
|
274
|
+
.rz-thead{display:flex;gap:8px;align-items:center;flex-wrap:wrap;padding:6px 12px;border:1px solid var(--dsw-alias-border-l1);border-radius:10px;background:var(--dsw-alias-bg-layer-1)}
|
|
275
|
+
.rz-thead-l{display:inline-flex;gap:8px;align-items:center;flex-wrap:wrap}
|
|
276
|
+
.rz-sel-stats{color:var(--dsw-alias-label-secondary);font-size:12px;white-space:nowrap}
|
|
277
|
+
.rz-col-tok{flex:0 0 62px;display:flex;justify-content:flex-end}
|
|
278
|
+
.rz-col-pct{flex:0 0 44px;text-align:right;color:var(--dsw-alias-label-tertiary);font-size:11px;white-space:nowrap}
|
|
227
279
|
.rz-list{display:flex;flex-direction:column;gap:6px}
|
|
228
280
|
.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
281
|
.rz-row:hover{background:var(--dsw-alias-interactive-bg-hover)}
|
|
@@ -231,6 +283,9 @@ window.__ModuleLoader__.load({
|
|
|
231
283
|
.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
284
|
.rz-row-preview:hover{text-decoration:underline;text-underline-offset:3px}
|
|
233
285
|
.rz-row-meta{color:var(--dsw-alias-label-tertiary);font-size:11px;flex:none}
|
|
286
|
+
.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}
|
|
287
|
+
.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)}
|
|
288
|
+
.rz-row-del:disabled{opacity:.35;cursor:not-allowed}
|
|
234
289
|
.rz-badge.tier-0{color:hsl(120,55%,40%);border-color:hsla(120,55%,40%,.45);background:hsla(120,55%,40%,.10)}
|
|
235
290
|
.rz-badge.tier-1{color:hsl(90,60%,38%);border-color:hsla(90,60%,38%,.45);background:hsla(90,60%,38%,.10)}
|
|
236
291
|
.rz-badge.tier-2{color:hsl(60,70%,36%);border-color:hsla(60,70%,36%,.45);background:hsla(60,70%,36%,.12)}
|
|
@@ -281,14 +336,16 @@ window.__ModuleLoader__.load({
|
|
|
281
336
|
}
|
|
282
337
|
|
|
283
338
|
/** 单条全文弹窗(正文经 /entry 异步补全)。 */
|
|
284
|
-
function DetailModal({ detail, t, onClose }) {
|
|
339
|
+
function DetailModal({ detail, t, total, onClose }) {
|
|
285
340
|
if (!detail) return null
|
|
341
|
+
const pct = pctOf(detail.tokens, total)
|
|
286
342
|
return h('div', { className: 'rz-dlg-backdrop', onClick: onClose },
|
|
287
343
|
h('div', { className: 'rz-dlg', onClick: e => e.stopPropagation() },
|
|
288
344
|
h('h3', null, t('detailTitle') + ' · ' + t('seqLabel', { seq: detail.seq })),
|
|
289
345
|
h('div', { className: 'rz-stats', style: { marginBottom: 10 } },
|
|
290
346
|
h(Chip, { ...entryChip(detail, t) }),
|
|
291
347
|
h(TokenBadge, { entry: detail }),
|
|
348
|
+
pct && h('span', { className: 'rz-label', title: pct + ' of ≈' + formatNum(total) + ' token' }, pct),
|
|
292
349
|
h('span', { className: 'rz-label', title: 'seq ' + detail.seq }, formatDateTime(detail.time)),
|
|
293
350
|
h('span', { className: 'rz-label' }, t('detailChars', { chars: formatNum(detail.chars) })),
|
|
294
351
|
detail.usage && h('span', { className: 'rz-label' }, t('detailUsage', {
|
|
@@ -319,10 +376,11 @@ window.__ModuleLoader__.load({
|
|
|
319
376
|
const [ctxLoading, setCtxLoading] = useState(false)
|
|
320
377
|
const [error, setError] = useState(null)
|
|
321
378
|
const [tierFilter, setTierFilter] = useState(() => new Set())
|
|
379
|
+
const [kindFilter, setKindFilter] = useState(() => new Set())
|
|
322
380
|
const [sortBy, setSortBy] = useState('order')
|
|
323
381
|
const [selected, setSelected] = useState(() => new Set())
|
|
324
382
|
const [detail, setDetail] = useState(null)
|
|
325
|
-
const [confirming, setConfirming] = useState(
|
|
383
|
+
const [confirming, setConfirming] = useState(null) // null | number[] 待裁剪 seqs(单条/多选同源)
|
|
326
384
|
const [deleting, setDeleting] = useState(false)
|
|
327
385
|
const [toast, setToast] = useState(null)
|
|
328
386
|
const sessionRef = useRef('')
|
|
@@ -353,16 +411,34 @@ window.__ModuleLoader__.load({
|
|
|
353
411
|
if (!context) return []
|
|
354
412
|
let rows = context.entries
|
|
355
413
|
if (tierFilter.size > 0) rows = rows.filter(e => tierFilter.has(tierOf(e)))
|
|
414
|
+
if (kindFilter.size > 0) rows = rows.filter(e => kindFilter.has(entryCategory(e)))
|
|
356
415
|
return sortEntries(rows, sortBy)
|
|
357
|
-
}, [context, tierFilter, sortBy])
|
|
416
|
+
}, [context, tierFilter, kindFilter, sortBy])
|
|
358
417
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
418
|
+
// 当前上下文里实际出现的分类(cat/count/tokens),按条数降序——决定渲染哪些按钮。
|
|
419
|
+
const categories = useMemo(() => {
|
|
420
|
+
if (!context) return []
|
|
421
|
+
const seen = new Map()
|
|
422
|
+
for (const e of context.entries) {
|
|
423
|
+
const c = entryCategory(e)
|
|
424
|
+
const prev = seen.get(c) || { count: 0, tokens: 0 }
|
|
425
|
+
prev.count += 1
|
|
426
|
+
prev.tokens += (e.tokens || 0)
|
|
427
|
+
seen.set(c, prev)
|
|
428
|
+
}
|
|
429
|
+
return [...seen.entries()]
|
|
430
|
+
.map(([cat, v]) => ({ cat, count: v.count, tokens: v.tokens }))
|
|
431
|
+
.sort((a, b) => b.count - a.count || b.tokens - a.tokens)
|
|
432
|
+
}, [context])
|
|
433
|
+
|
|
434
|
+
const entriesBySeq = useMemo(() => context ? new Map(context.entries.map(e => [e.seq, e])) : null, [context])
|
|
435
|
+
const sumSeqs = (seqs) => {
|
|
362
436
|
let sum = 0
|
|
363
|
-
for (const seq of
|
|
437
|
+
if (entriesBySeq) for (const seq of seqs) { const e = entriesBySeq.get(seq); if (e) sum += e.tokens || 0 }
|
|
364
438
|
return sum
|
|
365
|
-
}
|
|
439
|
+
}
|
|
440
|
+
const selectedTokens = useMemo(() => sumSeqs(selected), [selected, entriesBySeq])
|
|
441
|
+
const confirmTokens = useMemo(() => confirming ? sumSeqs(confirming) : 0, [confirming, entriesBySeq])
|
|
366
442
|
|
|
367
443
|
const toggleRow = (seq) => setSelected(prev => {
|
|
368
444
|
const next = new Set(prev)
|
|
@@ -376,6 +452,12 @@ window.__ModuleLoader__.load({
|
|
|
376
452
|
else next.add(i)
|
|
377
453
|
return next
|
|
378
454
|
})
|
|
455
|
+
const toggleKind = (cat) => setKindFilter(prev => {
|
|
456
|
+
const next = new Set(prev)
|
|
457
|
+
if (next.has(cat)) next.delete(cat)
|
|
458
|
+
else next.add(cat)
|
|
459
|
+
return next
|
|
460
|
+
})
|
|
379
461
|
const selectVisible = () => setSelected(new Set(visible.map(e => e.seq)))
|
|
380
462
|
|
|
381
463
|
const openDetail = (entry) => {
|
|
@@ -386,17 +468,18 @@ window.__ModuleLoader__.load({
|
|
|
386
468
|
}
|
|
387
469
|
|
|
388
470
|
const doDelete = async () => {
|
|
471
|
+
if (!confirming || confirming.length === 0) return
|
|
389
472
|
setDeleting(true)
|
|
390
473
|
try {
|
|
391
|
-
const r = await fetch(API + '/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ session: sessionId, seqs:
|
|
474
|
+
const r = await fetch(API + '/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ session: sessionId, seqs: confirming }) })
|
|
392
475
|
const d = await r.json().catch(() => ({}))
|
|
393
476
|
if (!r.ok) throw new Error(d.error || 'HTTP ' + r.status)
|
|
394
|
-
setConfirming(
|
|
477
|
+
setConfirming(null)
|
|
395
478
|
setSelected(new Set())
|
|
396
479
|
showToast(t('deletedToast', { n: d.removed, tokens: formatNum(d.tokensRemoved) }))
|
|
397
480
|
refreshAll()
|
|
398
481
|
} catch (e) {
|
|
399
|
-
setConfirming(
|
|
482
|
+
setConfirming(null)
|
|
400
483
|
setError(e.message)
|
|
401
484
|
} finally { setDeleting(false) }
|
|
402
485
|
}
|
|
@@ -415,40 +498,59 @@ window.__ModuleLoader__.load({
|
|
|
415
498
|
h('span', { className: 'rz-legend' + (tierFilter.size > 0 ? ' filtering' : ''), title: t('legendTitle') },
|
|
416
499
|
RAZOR_TIERS.map((tr, i) => h('button', { key: i, className: 'rz-swatch tier-' + i + (tierFilter.has(i) ? ' on' : ''), title: tr.label + ' token', onClick: () => toggleTier(i) }, tr.label))),
|
|
417
500
|
h('span', { className: 'rz-spacer' }),
|
|
418
|
-
busy && h('span', { className: 'rz-badge' }, t('busyTag')),
|
|
419
|
-
|
|
420
|
-
|
|
501
|
+
busy && h('span', { className: 'rz-badge' }, t('busyTag'))),
|
|
502
|
+
// 类型筛选:与 chip 同口径的可点选按钮(user/injected/assistant/tool:<名>),
|
|
503
|
+
// 多选 toggle,与 tier 量级筛选正交叠加。按钮带条数;hover 看 token 占比。
|
|
504
|
+
categories.length > 0 && h('div', { key: 'kinds', className: 'rz-kindsbar' },
|
|
505
|
+
h('span', { className: 'rz-label' }, t('kindFilterLabel')),
|
|
506
|
+
h('span', { className: 'rz-kinds' + (kindFilter.size > 0 ? ' filtering' : '') },
|
|
507
|
+
categories.map(({ cat, count, tokens }) =>
|
|
508
|
+
h('button', { key: cat,
|
|
509
|
+
className: 'rz-kindbtn' + (kindFilter.has(cat) ? ' on' : ''),
|
|
510
|
+
title: categoryLabel(cat, t) + ' · ' + count + ' 条 · ≈' + formatNum(tokens) + ' token'
|
|
511
|
+
+ (context.totalTokens ? '(' + (pctOf(tokens, context.totalTokens) || '0%') + ')' : ''),
|
|
512
|
+
onClick: () => toggleKind(cat) },
|
|
513
|
+
categoryLabel(cat, t),
|
|
514
|
+
h('span', { className: 'rz-kindcount' }, count))))),
|
|
515
|
+
// 表头(在类型筛选下方):左 刷新/全选,选中后追加 删除·取消全选;右 排序。
|
|
516
|
+
h('div', { key: 'thead', className: 'rz-thead' },
|
|
517
|
+
h('div', { className: 'rz-thead-l' },
|
|
518
|
+
h('button', { className: 'rz-btn', onClick: refreshAll, title: t('refresh') }, t('refresh')),
|
|
519
|
+
h('button', { className: 'rz-btn', onClick: selectVisible, disabled: visible.length === 0 || busy, title: t('selectVisibleHint') },
|
|
520
|
+
t('selectVisible') + (visible.length ? ` (${visible.length})` : '')),
|
|
521
|
+
selected.size > 0 && h('button', { className: 'rz-btn rz-btn-danger', disabled: !canDelete,
|
|
522
|
+
title: busy ? t('deleteBusyHint') : undefined,
|
|
523
|
+
onClick: () => setConfirming([...selected]) },
|
|
524
|
+
t('deleteSelected') + ` (${selected.size})`),
|
|
525
|
+
selected.size > 0 && h('button', { className: 'rz-btn', onClick: () => setSelected(new Set()) }, t('clearSelect'))),
|
|
526
|
+
h('span', { className: 'rz-spacer' }),
|
|
421
527
|
h('select', { className: 'rz-select', value: sortBy, onChange: e => setSortBy(e.target.value), title: t('sortLabel'), 'aria-label': t('sortLabel') },
|
|
422
528
|
h('option', { value: 'order' }, t('sortOrder')),
|
|
423
|
-
h('option', { value: 'tokens' }, t('sortTokens'))),
|
|
424
|
-
h('button', { className: 'rz-btn', onClick: refreshAll, title: t('refresh') }, t('refresh'))),
|
|
425
|
-
selected.size > 0 && h('div', { key: 'sel', className: 'rz-stats' },
|
|
426
|
-
h('span', null, t('selectedStats', { n: selected.size, tokens: formatNum(selectedTokens) })),
|
|
427
|
-
h('span', { className: 'rz-spacer' }),
|
|
428
|
-
h('div', { className: 'rz-footbtns' },
|
|
429
|
-
h('button', { className: 'rz-btn', onClick: () => setSelected(new Set()) }, t('clearSelect')),
|
|
430
|
-
h('button', { className: 'rz-btn rz-btn-danger', disabled: !canDelete,
|
|
431
|
-
title: busy ? t('deleteBusyHint') : undefined,
|
|
432
|
-
onClick: () => setConfirming(true) },
|
|
433
|
-
t('deleteSelected') + ` (${selected.size})`))),
|
|
529
|
+
h('option', { value: 'tokens' }, t('sortTokens')))),
|
|
434
530
|
visible.length === 0
|
|
435
531
|
? h('div', { key: 'empty', className: 'rz-empty' }, t('emptyContext'))
|
|
436
532
|
: h('div', { key: 'list', className: 'rz-list' },
|
|
437
533
|
h(PagedList, { items: visible, t, render: entry => {
|
|
438
534
|
const tier = tierOf(entry)
|
|
535
|
+
const pct = pctOf(entry.tokens, context.totalTokens)
|
|
439
536
|
return h('div', { key: entry.seq, className: 'rz-row tier-' + tier + (selected.has(entry.seq) ? ' checked' : ''),
|
|
440
537
|
role: 'button', tabIndex: 0, onClick: () => toggleRow(entry.seq),
|
|
441
538
|
onKeyDown: e => e.key === 'Enter' && toggleRow(entry.seq) },
|
|
442
539
|
h('input', { type: 'checkbox', checked: selected.has(entry.seq), onClick: e => e.stopPropagation(), onChange: () => toggleRow(entry.seq) }),
|
|
443
|
-
h(TokenBadge, { entry }),
|
|
540
|
+
h('span', { className: 'rz-col-tok' }, h(TokenBadge, { entry })),
|
|
541
|
+
h('span', { className: 'rz-col-pct', title: pct ? pct + ' of ≈' + formatNum(context.totalTokens) + ' token' : '' }, pct || ''),
|
|
444
542
|
h(Chip, { ...entryChip(entry, t) }),
|
|
445
543
|
h('span', { className: 'rz-row-preview', title: entry.preview,
|
|
446
544
|
onClick: e => { e.stopPropagation(); openDetail(entry) } }, entry.preview || ' '),
|
|
447
|
-
h('span', { className: 'rz-row-meta', title: 'seq ' + entry.seq }, formatDateTime(entry.time))
|
|
545
|
+
h('span', { className: 'rz-row-meta', title: 'seq ' + entry.seq }, formatDateTime(entry.time)),
|
|
546
|
+
h('button', { className: 'rz-row-del', type: 'button', disabled: busy,
|
|
547
|
+
title: busy ? t('deleteBusyHint') : t('deleteOneHint', { tokens: formatNum(entry.tokens) }),
|
|
548
|
+
'aria-label': t('deleteOne'),
|
|
549
|
+
onClick: e => { e.stopPropagation(); if (!busy) setConfirming([entry.seq]) } }, '✕'))
|
|
448
550
|
} })),
|
|
449
|
-
detail && h(DetailModal, { detail, t, onClose: () => setDetail(null) }),
|
|
450
|
-
confirming && h(ConfirmDialog, { n:
|
|
451
|
-
onCancel: () => setConfirming(
|
|
551
|
+
detail && h(DetailModal, { detail, t, total: context.totalTokens, onClose: () => setDetail(null) }),
|
|
552
|
+
confirming && confirming.length > 0 && h(ConfirmDialog, { n: confirming.length, tokens: formatNum(confirmTokens), deleting, t,
|
|
553
|
+
onCancel: () => setConfirming(null), onOk: doDelete }),
|
|
452
554
|
],
|
|
453
555
|
toast && h('div', { className: 'rz-toast' }, toast))
|
|
454
556
|
}
|
|
@@ -460,7 +562,7 @@ window.__ModuleLoader__.load({
|
|
|
460
562
|
module.exports = {
|
|
461
563
|
name: CLIENT_NAME,
|
|
462
564
|
inject: ['slots', 'locale'],
|
|
463
|
-
__internals: { NS, ZH, EN, sortEntries, tierOf, RAZOR_TIERS, formatDateTime, entryChip },
|
|
565
|
+
__internals: { NS, ZH, EN, sortEntries, tierOf, RAZOR_TIERS, formatDateTime, entryChip, entryCategory, categoryLabel, pctOf },
|
|
464
566
|
__boot(container, opts = {}) {
|
|
465
567
|
ensureStyles()
|
|
466
568
|
const t = opts.t || ((key, vars) => {
|
package/client/index.js
CHANGED
|
@@ -56,9 +56,11 @@ const ZH = {
|
|
|
56
56
|
sortOrder: '上下文顺序',
|
|
57
57
|
sortTokens: 'token 高→低',
|
|
58
58
|
orderNote: '本插件不会改变上下文条目顺序,只会剔除选中条目。',
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
legendTitle: '按 token 量级筛选(点选高亮某一档)',
|
|
60
|
+
kindFilterLabel: '类型',
|
|
61
|
+
selectVisible: '全选',
|
|
62
|
+
selectVisibleHint: '全选当前可见条目(配合类型/量级筛选)',
|
|
63
|
+
clearSelect: '取消全选',
|
|
62
64
|
deleteSelected: '删除选中',
|
|
63
65
|
selectedStats: '已选 {n} 条 / ≈{tokens} token',
|
|
64
66
|
stats: '{nodes} 条 · 合计 ≈{tokens} token{mode}',
|
|
@@ -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,9 +98,11 @@ 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.',
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
101
|
+
legendTitle: 'Filter by token tier (click to isolate a band)',
|
|
102
|
+
kindFilterLabel: 'Type',
|
|
103
|
+
selectVisible: 'All',
|
|
104
|
+
selectVisibleHint: 'Select all currently visible entries (pairs with type/tier filters)',
|
|
105
|
+
clearSelect: 'Clear all',
|
|
100
106
|
deleteSelected: 'Delete selected',
|
|
101
107
|
selectedStats: '{n} selected / ≈{tokens} tokens',
|
|
102
108
|
stats: '{nodes} entries · total ≈{tokens} tokens{mode}',
|
|
@@ -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,18 @@ 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}
|
|
264
|
+
.rz-thead{display:flex;gap:8px;align-items:center;flex-wrap:wrap;padding:6px 12px;border:1px solid var(--dsw-alias-border-l1);border-radius:10px;background:var(--dsw-alias-bg-layer-1)}
|
|
265
|
+
.rz-thead-l{display:inline-flex;gap:8px;align-items:center;flex-wrap:wrap}
|
|
266
|
+
.rz-sel-stats{color:var(--dsw-alias-label-secondary);font-size:12px;white-space:nowrap}
|
|
267
|
+
.rz-col-tok{flex:0 0 62px;display:flex;justify-content:flex-end}
|
|
268
|
+
.rz-col-pct{flex:0 0 44px;text-align:right;color:var(--dsw-alias-label-tertiary);font-size:11px;white-space:nowrap}
|
|
217
269
|
.rz-list{display:flex;flex-direction:column;gap:6px}
|
|
218
270
|
.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
271
|
.rz-row:hover{background:var(--dsw-alias-interactive-bg-hover)}
|
|
@@ -221,6 +273,9 @@ const STYLE = `<style>
|
|
|
221
273
|
.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
274
|
.rz-row-preview:hover{text-decoration:underline;text-underline-offset:3px}
|
|
223
275
|
.rz-row-meta{color:var(--dsw-alias-label-tertiary);font-size:11px;flex:none}
|
|
276
|
+
.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}
|
|
277
|
+
.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)}
|
|
278
|
+
.rz-row-del:disabled{opacity:.35;cursor:not-allowed}
|
|
224
279
|
.rz-badge.tier-0{color:hsl(120,55%,40%);border-color:hsla(120,55%,40%,.45);background:hsla(120,55%,40%,.10)}
|
|
225
280
|
.rz-badge.tier-1{color:hsl(90,60%,38%);border-color:hsla(90,60%,38%,.45);background:hsla(90,60%,38%,.10)}
|
|
226
281
|
.rz-badge.tier-2{color:hsl(60,70%,36%);border-color:hsla(60,70%,36%,.45);background:hsla(60,70%,36%,.12)}
|
|
@@ -271,14 +326,16 @@ function PagedList({ items, render, t }) {
|
|
|
271
326
|
}
|
|
272
327
|
|
|
273
328
|
/** 单条全文弹窗(正文经 /entry 异步补全)。 */
|
|
274
|
-
function DetailModal({ detail, t, onClose }) {
|
|
329
|
+
function DetailModal({ detail, t, total, onClose }) {
|
|
275
330
|
if (!detail) return null
|
|
331
|
+
const pct = pctOf(detail.tokens, total)
|
|
276
332
|
return h('div', { className: 'rz-dlg-backdrop', onClick: onClose },
|
|
277
333
|
h('div', { className: 'rz-dlg', onClick: e => e.stopPropagation() },
|
|
278
334
|
h('h3', null, t('detailTitle') + ' · ' + t('seqLabel', { seq: detail.seq })),
|
|
279
335
|
h('div', { className: 'rz-stats', style: { marginBottom: 10 } },
|
|
280
336
|
h(Chip, { ...entryChip(detail, t) }),
|
|
281
337
|
h(TokenBadge, { entry: detail }),
|
|
338
|
+
pct && h('span', { className: 'rz-label', title: pct + ' of ≈' + formatNum(total) + ' token' }, pct),
|
|
282
339
|
h('span', { className: 'rz-label', title: 'seq ' + detail.seq }, formatDateTime(detail.time)),
|
|
283
340
|
h('span', { className: 'rz-label' }, t('detailChars', { chars: formatNum(detail.chars) })),
|
|
284
341
|
detail.usage && h('span', { className: 'rz-label' }, t('detailUsage', {
|
|
@@ -309,10 +366,11 @@ function RazorPage({ t, fixedSessionId }) {
|
|
|
309
366
|
const [ctxLoading, setCtxLoading] = useState(false)
|
|
310
367
|
const [error, setError] = useState(null)
|
|
311
368
|
const [tierFilter, setTierFilter] = useState(() => new Set())
|
|
369
|
+
const [kindFilter, setKindFilter] = useState(() => new Set())
|
|
312
370
|
const [sortBy, setSortBy] = useState('order')
|
|
313
371
|
const [selected, setSelected] = useState(() => new Set())
|
|
314
372
|
const [detail, setDetail] = useState(null)
|
|
315
|
-
const [confirming, setConfirming] = useState(
|
|
373
|
+
const [confirming, setConfirming] = useState(null) // null | number[] 待裁剪 seqs(单条/多选同源)
|
|
316
374
|
const [deleting, setDeleting] = useState(false)
|
|
317
375
|
const [toast, setToast] = useState(null)
|
|
318
376
|
const sessionRef = useRef('')
|
|
@@ -343,16 +401,34 @@ function RazorPage({ t, fixedSessionId }) {
|
|
|
343
401
|
if (!context) return []
|
|
344
402
|
let rows = context.entries
|
|
345
403
|
if (tierFilter.size > 0) rows = rows.filter(e => tierFilter.has(tierOf(e)))
|
|
404
|
+
if (kindFilter.size > 0) rows = rows.filter(e => kindFilter.has(entryCategory(e)))
|
|
346
405
|
return sortEntries(rows, sortBy)
|
|
347
|
-
}, [context, tierFilter, sortBy])
|
|
406
|
+
}, [context, tierFilter, kindFilter, sortBy])
|
|
348
407
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
408
|
+
// 当前上下文里实际出现的分类(cat/count/tokens),按条数降序——决定渲染哪些按钮。
|
|
409
|
+
const categories = useMemo(() => {
|
|
410
|
+
if (!context) return []
|
|
411
|
+
const seen = new Map()
|
|
412
|
+
for (const e of context.entries) {
|
|
413
|
+
const c = entryCategory(e)
|
|
414
|
+
const prev = seen.get(c) || { count: 0, tokens: 0 }
|
|
415
|
+
prev.count += 1
|
|
416
|
+
prev.tokens += (e.tokens || 0)
|
|
417
|
+
seen.set(c, prev)
|
|
418
|
+
}
|
|
419
|
+
return [...seen.entries()]
|
|
420
|
+
.map(([cat, v]) => ({ cat, count: v.count, tokens: v.tokens }))
|
|
421
|
+
.sort((a, b) => b.count - a.count || b.tokens - a.tokens)
|
|
422
|
+
}, [context])
|
|
423
|
+
|
|
424
|
+
const entriesBySeq = useMemo(() => context ? new Map(context.entries.map(e => [e.seq, e])) : null, [context])
|
|
425
|
+
const sumSeqs = (seqs) => {
|
|
352
426
|
let sum = 0
|
|
353
|
-
for (const seq of
|
|
427
|
+
if (entriesBySeq) for (const seq of seqs) { const e = entriesBySeq.get(seq); if (e) sum += e.tokens || 0 }
|
|
354
428
|
return sum
|
|
355
|
-
}
|
|
429
|
+
}
|
|
430
|
+
const selectedTokens = useMemo(() => sumSeqs(selected), [selected, entriesBySeq])
|
|
431
|
+
const confirmTokens = useMemo(() => confirming ? sumSeqs(confirming) : 0, [confirming, entriesBySeq])
|
|
356
432
|
|
|
357
433
|
const toggleRow = (seq) => setSelected(prev => {
|
|
358
434
|
const next = new Set(prev)
|
|
@@ -366,6 +442,12 @@ function RazorPage({ t, fixedSessionId }) {
|
|
|
366
442
|
else next.add(i)
|
|
367
443
|
return next
|
|
368
444
|
})
|
|
445
|
+
const toggleKind = (cat) => setKindFilter(prev => {
|
|
446
|
+
const next = new Set(prev)
|
|
447
|
+
if (next.has(cat)) next.delete(cat)
|
|
448
|
+
else next.add(cat)
|
|
449
|
+
return next
|
|
450
|
+
})
|
|
369
451
|
const selectVisible = () => setSelected(new Set(visible.map(e => e.seq)))
|
|
370
452
|
|
|
371
453
|
const openDetail = (entry) => {
|
|
@@ -376,17 +458,18 @@ function RazorPage({ t, fixedSessionId }) {
|
|
|
376
458
|
}
|
|
377
459
|
|
|
378
460
|
const doDelete = async () => {
|
|
461
|
+
if (!confirming || confirming.length === 0) return
|
|
379
462
|
setDeleting(true)
|
|
380
463
|
try {
|
|
381
|
-
const r = await fetch(API + '/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ session: sessionId, seqs:
|
|
464
|
+
const r = await fetch(API + '/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ session: sessionId, seqs: confirming }) })
|
|
382
465
|
const d = await r.json().catch(() => ({}))
|
|
383
466
|
if (!r.ok) throw new Error(d.error || 'HTTP ' + r.status)
|
|
384
|
-
setConfirming(
|
|
467
|
+
setConfirming(null)
|
|
385
468
|
setSelected(new Set())
|
|
386
469
|
showToast(t('deletedToast', { n: d.removed, tokens: formatNum(d.tokensRemoved) }))
|
|
387
470
|
refreshAll()
|
|
388
471
|
} catch (e) {
|
|
389
|
-
setConfirming(
|
|
472
|
+
setConfirming(null)
|
|
390
473
|
setError(e.message)
|
|
391
474
|
} finally { setDeleting(false) }
|
|
392
475
|
}
|
|
@@ -405,40 +488,59 @@ function RazorPage({ t, fixedSessionId }) {
|
|
|
405
488
|
h('span', { className: 'rz-legend' + (tierFilter.size > 0 ? ' filtering' : ''), title: t('legendTitle') },
|
|
406
489
|
RAZOR_TIERS.map((tr, i) => h('button', { key: i, className: 'rz-swatch tier-' + i + (tierFilter.has(i) ? ' on' : ''), title: tr.label + ' token', onClick: () => toggleTier(i) }, tr.label))),
|
|
407
490
|
h('span', { className: 'rz-spacer' }),
|
|
408
|
-
busy && h('span', { className: 'rz-badge' }, t('busyTag')),
|
|
409
|
-
|
|
410
|
-
|
|
491
|
+
busy && h('span', { className: 'rz-badge' }, t('busyTag'))),
|
|
492
|
+
// 类型筛选:与 chip 同口径的可点选按钮(user/injected/assistant/tool:<名>),
|
|
493
|
+
// 多选 toggle,与 tier 量级筛选正交叠加。按钮带条数;hover 看 token 占比。
|
|
494
|
+
categories.length > 0 && h('div', { key: 'kinds', className: 'rz-kindsbar' },
|
|
495
|
+
h('span', { className: 'rz-label' }, t('kindFilterLabel')),
|
|
496
|
+
h('span', { className: 'rz-kinds' + (kindFilter.size > 0 ? ' filtering' : '') },
|
|
497
|
+
categories.map(({ cat, count, tokens }) =>
|
|
498
|
+
h('button', { key: cat,
|
|
499
|
+
className: 'rz-kindbtn' + (kindFilter.has(cat) ? ' on' : ''),
|
|
500
|
+
title: categoryLabel(cat, t) + ' · ' + count + ' 条 · ≈' + formatNum(tokens) + ' token'
|
|
501
|
+
+ (context.totalTokens ? '(' + (pctOf(tokens, context.totalTokens) || '0%') + ')' : ''),
|
|
502
|
+
onClick: () => toggleKind(cat) },
|
|
503
|
+
categoryLabel(cat, t),
|
|
504
|
+
h('span', { className: 'rz-kindcount' }, count))))),
|
|
505
|
+
// 表头(在类型筛选下方):左 刷新/全选,选中后追加 删除·取消全选;右 排序。
|
|
506
|
+
h('div', { key: 'thead', className: 'rz-thead' },
|
|
507
|
+
h('div', { className: 'rz-thead-l' },
|
|
508
|
+
h('button', { className: 'rz-btn', onClick: refreshAll, title: t('refresh') }, t('refresh')),
|
|
509
|
+
h('button', { className: 'rz-btn', onClick: selectVisible, disabled: visible.length === 0 || busy, title: t('selectVisibleHint') },
|
|
510
|
+
t('selectVisible') + (visible.length ? ` (${visible.length})` : '')),
|
|
511
|
+
selected.size > 0 && h('button', { className: 'rz-btn rz-btn-danger', disabled: !canDelete,
|
|
512
|
+
title: busy ? t('deleteBusyHint') : undefined,
|
|
513
|
+
onClick: () => setConfirming([...selected]) },
|
|
514
|
+
t('deleteSelected') + ` (${selected.size})`),
|
|
515
|
+
selected.size > 0 && h('button', { className: 'rz-btn', onClick: () => setSelected(new Set()) }, t('clearSelect'))),
|
|
516
|
+
h('span', { className: 'rz-spacer' }),
|
|
411
517
|
h('select', { className: 'rz-select', value: sortBy, onChange: e => setSortBy(e.target.value), title: t('sortLabel'), 'aria-label': t('sortLabel') },
|
|
412
518
|
h('option', { value: 'order' }, t('sortOrder')),
|
|
413
|
-
h('option', { value: 'tokens' }, t('sortTokens'))),
|
|
414
|
-
h('button', { className: 'rz-btn', onClick: refreshAll, title: t('refresh') }, t('refresh'))),
|
|
415
|
-
selected.size > 0 && h('div', { key: 'sel', className: 'rz-stats' },
|
|
416
|
-
h('span', null, t('selectedStats', { n: selected.size, tokens: formatNum(selectedTokens) })),
|
|
417
|
-
h('span', { className: 'rz-spacer' }),
|
|
418
|
-
h('div', { className: 'rz-footbtns' },
|
|
419
|
-
h('button', { className: 'rz-btn', onClick: () => setSelected(new Set()) }, t('clearSelect')),
|
|
420
|
-
h('button', { className: 'rz-btn rz-btn-danger', disabled: !canDelete,
|
|
421
|
-
title: busy ? t('deleteBusyHint') : undefined,
|
|
422
|
-
onClick: () => setConfirming(true) },
|
|
423
|
-
t('deleteSelected') + ` (${selected.size})`))),
|
|
519
|
+
h('option', { value: 'tokens' }, t('sortTokens')))),
|
|
424
520
|
visible.length === 0
|
|
425
521
|
? h('div', { key: 'empty', className: 'rz-empty' }, t('emptyContext'))
|
|
426
522
|
: h('div', { key: 'list', className: 'rz-list' },
|
|
427
523
|
h(PagedList, { items: visible, t, render: entry => {
|
|
428
524
|
const tier = tierOf(entry)
|
|
525
|
+
const pct = pctOf(entry.tokens, context.totalTokens)
|
|
429
526
|
return h('div', { key: entry.seq, className: 'rz-row tier-' + tier + (selected.has(entry.seq) ? ' checked' : ''),
|
|
430
527
|
role: 'button', tabIndex: 0, onClick: () => toggleRow(entry.seq),
|
|
431
528
|
onKeyDown: e => e.key === 'Enter' && toggleRow(entry.seq) },
|
|
432
529
|
h('input', { type: 'checkbox', checked: selected.has(entry.seq), onClick: e => e.stopPropagation(), onChange: () => toggleRow(entry.seq) }),
|
|
433
|
-
h(TokenBadge, { entry }),
|
|
530
|
+
h('span', { className: 'rz-col-tok' }, h(TokenBadge, { entry })),
|
|
531
|
+
h('span', { className: 'rz-col-pct', title: pct ? pct + ' of ≈' + formatNum(context.totalTokens) + ' token' : '' }, pct || ''),
|
|
434
532
|
h(Chip, { ...entryChip(entry, t) }),
|
|
435
533
|
h('span', { className: 'rz-row-preview', title: entry.preview,
|
|
436
534
|
onClick: e => { e.stopPropagation(); openDetail(entry) } }, entry.preview || ' '),
|
|
437
|
-
h('span', { className: 'rz-row-meta', title: 'seq ' + entry.seq }, formatDateTime(entry.time))
|
|
535
|
+
h('span', { className: 'rz-row-meta', title: 'seq ' + entry.seq }, formatDateTime(entry.time)),
|
|
536
|
+
h('button', { className: 'rz-row-del', type: 'button', disabled: busy,
|
|
537
|
+
title: busy ? t('deleteBusyHint') : t('deleteOneHint', { tokens: formatNum(entry.tokens) }),
|
|
538
|
+
'aria-label': t('deleteOne'),
|
|
539
|
+
onClick: e => { e.stopPropagation(); if (!busy) setConfirming([entry.seq]) } }, '✕'))
|
|
438
540
|
} })),
|
|
439
|
-
detail && h(DetailModal, { detail, t, onClose: () => setDetail(null) }),
|
|
440
|
-
confirming && h(ConfirmDialog, { n:
|
|
441
|
-
onCancel: () => setConfirming(
|
|
541
|
+
detail && h(DetailModal, { detail, t, total: context.totalTokens, onClose: () => setDetail(null) }),
|
|
542
|
+
confirming && confirming.length > 0 && h(ConfirmDialog, { n: confirming.length, tokens: formatNum(confirmTokens), deleting, t,
|
|
543
|
+
onCancel: () => setConfirming(null), onOk: doDelete }),
|
|
442
544
|
],
|
|
443
545
|
toast && h('div', { className: 'rz-toast' }, toast))
|
|
444
546
|
}
|
|
@@ -450,7 +552,7 @@ const CLIENT_NAME = '@weibaohui/context-razor'
|
|
|
450
552
|
module.exports = {
|
|
451
553
|
name: CLIENT_NAME,
|
|
452
554
|
inject: ['slots', 'locale'],
|
|
453
|
-
__internals: { NS, ZH, EN, sortEntries, tierOf, RAZOR_TIERS, formatDateTime, entryChip },
|
|
555
|
+
__internals: { NS, ZH, EN, sortEntries, tierOf, RAZOR_TIERS, formatDateTime, entryChip, entryCategory, categoryLabel, pctOf },
|
|
454
556
|
__boot(container, opts = {}) {
|
|
455
557
|
ensureStyles()
|
|
456
558
|
const t = opts.t || ((key, vars) => {
|