@weibaohui/context-razor 0.3.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 +4 -2
- package/client/bundle.js +120 -27
- package/client/index.js +120 -27
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,9 +10,11 @@
|
|
|
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
|
|
15
|
-
-
|
|
16
|
+
- **精确裁剪**:勾选任意条目一键删除——连续段自动合并处理。删除走宿主 surface replace 协议(与官方 compaction 同款机制),**不经 LLM 总结**,删了什么、删了多少完全由你决定。每行尾部带 ✕ 快捷按钮,**单条直接删**与勾选后**多选批量删**走同一确认弹窗
|
|
17
|
+
- **不重排**:本插件不会改变上下文条目顺序,只会剔除选中条目;「token 高→低」排序仅影响页面显示,模型可见顺序始终由会话 surface 决定
|
|
16
18
|
- **安全护栏**:会话运行中禁止裁剪(等当前回合结束);每次替换附带一条 notice 标记消息,模型知道这段历史被你移除了,需要时会主动向你确认
|
|
17
19
|
- **非破坏**:append-only 日志保留全部痕迹(宿主机制如此)。「删除」= 从模型视野与界面投影中移除
|
|
18
20
|
|
package/client/bundle.js
CHANGED
|
@@ -64,9 +64,10 @@ window.__ModuleLoader__.load({
|
|
|
64
64
|
busyTag: '运行中',
|
|
65
65
|
sortLabel: '排序',
|
|
66
66
|
sortOrder: '上下文顺序',
|
|
67
|
-
sortSeq: '原始序号',
|
|
68
67
|
sortTokens: 'token 高→低',
|
|
69
68
|
orderNote: '本插件不会改变上下文条目顺序,只会剔除选中条目。',
|
|
69
|
+
legendTitle: '按 token 量级筛选(点选高亮某一档)',
|
|
70
|
+
kindFilterLabel: '类型',
|
|
70
71
|
selectVisible: '选中可见',
|
|
71
72
|
selectVisibleHint: '选中当前显示的全部条目(配合色块筛选)',
|
|
72
73
|
clearSelect: '清空选择',
|
|
@@ -88,6 +89,8 @@ window.__ModuleLoader__.load({
|
|
|
88
89
|
deleteTitle: '确认裁剪',
|
|
89
90
|
deleteConfirm: '即将把 {n} 条消息(约 {tokens} token)从模型视野中移除,替换为一条标记消息。日志中保留痕迹,此操作不可恢复。继续?',
|
|
90
91
|
deleteBusyHint: '会话正在运行,等当前回合结束后再裁剪',
|
|
92
|
+
deleteOne: '删除此条',
|
|
93
|
+
deleteOneHint: '删除此条(约 {tokens} token)',
|
|
91
94
|
deleteOk: '执行',
|
|
92
95
|
cancel: '取消',
|
|
93
96
|
deletedToast: '已删除 {n} 条(约 {tokens} token)',
|
|
@@ -103,9 +106,10 @@ window.__ModuleLoader__.load({
|
|
|
103
106
|
busyTag: 'running',
|
|
104
107
|
sortLabel: 'Sort',
|
|
105
108
|
sortOrder: 'Context order',
|
|
106
|
-
sortSeq: 'Original seq',
|
|
107
109
|
sortTokens: 'tokens high→low',
|
|
108
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',
|
|
109
113
|
selectVisible: 'Select shown',
|
|
110
114
|
selectVisibleHint: 'Select every currently shown entry (pairs with tier filters)',
|
|
111
115
|
clearSelect: 'Clear selection',
|
|
@@ -127,6 +131,8 @@ window.__ModuleLoader__.load({
|
|
|
127
131
|
deleteTitle: 'Confirm trim',
|
|
128
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?',
|
|
129
133
|
deleteBusyHint: 'Session is running — trim after the current turn finishes',
|
|
134
|
+
deleteOne: 'Delete this entry',
|
|
135
|
+
deleteOneHint: 'Delete this entry (≈{tokens} tokens)',
|
|
130
136
|
deleteOk: 'Delete',
|
|
131
137
|
cancel: 'Cancel',
|
|
132
138
|
deletedToast: 'Deleted {n} entries (≈{tokens} tokens)',
|
|
@@ -141,9 +147,10 @@ window.__ModuleLoader__.load({
|
|
|
141
147
|
|
|
142
148
|
const kindI18n = (key) => 'kind' + key[0].toUpperCase() + key.slice(1)
|
|
143
149
|
|
|
144
|
-
/** 排序:order = 投影原序(模型可见序);
|
|
150
|
+
/** 排序:order = 投影原序(模型可见序);tokens = 降序(并列按 seq,缺 token 沉底)。
|
|
151
|
+
* 只有两档且都是显示层——裁剪过的会话里 seq 升序 ≠ 模型可见序(replace 标记 seq 大但位置在中间),
|
|
152
|
+
* 与「本页展示模型可见上下文」相悖,故不提供 seq 档。 */
|
|
145
153
|
function sortEntries(entries, sortBy) {
|
|
146
|
-
if (sortBy === 'seq') return [...entries].sort((a, b) => a.seq - b.seq)
|
|
147
154
|
if (sortBy !== 'tokens') return entries
|
|
148
155
|
return [...entries].sort((a, b) => {
|
|
149
156
|
const va = a.tokens, vb = b.tokens
|
|
@@ -175,6 +182,38 @@ window.__ModuleLoader__.load({
|
|
|
175
182
|
return { kind: entry.kind, label: t(kindI18n(entry.kind)) }
|
|
176
183
|
}
|
|
177
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
|
+
|
|
178
217
|
// ── 彩虹分级:颜色越暖 = 占用越多 ────────────────────────────────────────
|
|
179
218
|
// 固定对数档位(相邻约 ×2.5),跨会话语义稳定:绿→黄绿→黄→橙→红→品红。
|
|
180
219
|
// 档位是「这条消息吃掉多少典型上下文预算」的粗标尺,不随会话内最大值缩放。
|
|
@@ -225,6 +264,13 @@ window.__ModuleLoader__.load({
|
|
|
225
264
|
.rz-swatch.tier-3{background:hsl(32,80%,45%)}
|
|
226
265
|
.rz-swatch.tier-4{background:hsl(8,75%,48%)}
|
|
227
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}
|
|
228
274
|
.rz-list{display:flex;flex-direction:column;gap:6px}
|
|
229
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%}
|
|
230
276
|
.rz-row:hover{background:var(--dsw-alias-interactive-bg-hover)}
|
|
@@ -232,6 +278,9 @@ window.__ModuleLoader__.load({
|
|
|
232
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}
|
|
233
279
|
.rz-row-preview:hover{text-decoration:underline;text-underline-offset:3px}
|
|
234
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}
|
|
235
284
|
.rz-badge.tier-0{color:hsl(120,55%,40%);border-color:hsla(120,55%,40%,.45);background:hsla(120,55%,40%,.10)}
|
|
236
285
|
.rz-badge.tier-1{color:hsl(90,60%,38%);border-color:hsla(90,60%,38%,.45);background:hsla(90,60%,38%,.10)}
|
|
237
286
|
.rz-badge.tier-2{color:hsl(60,70%,36%);border-color:hsla(60,70%,36%,.45);background:hsla(60,70%,36%,.12)}
|
|
@@ -264,10 +313,12 @@ window.__ModuleLoader__.load({
|
|
|
264
313
|
|
|
265
314
|
const Chip = ({ kind, label, title }) => h('span', { className: 'rz-chip ' + kind, title }, label)
|
|
266
315
|
|
|
267
|
-
const TokenBadge = ({ entry }) => {
|
|
316
|
+
const TokenBadge = ({ entry, total }) => {
|
|
268
317
|
const tier = tierOf(entry)
|
|
269
|
-
|
|
270
|
-
|
|
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 : ''))
|
|
271
322
|
}
|
|
272
323
|
|
|
273
324
|
/** 分页列表:先渲染 pageSize 行,按需增长(大会话一次挂几千行会卡)。 */
|
|
@@ -282,14 +333,14 @@ window.__ModuleLoader__.load({
|
|
|
282
333
|
}
|
|
283
334
|
|
|
284
335
|
/** 单条全文弹窗(正文经 /entry 异步补全)。 */
|
|
285
|
-
function DetailModal({ detail, t, onClose }) {
|
|
336
|
+
function DetailModal({ detail, t, total, onClose }) {
|
|
286
337
|
if (!detail) return null
|
|
287
338
|
return h('div', { className: 'rz-dlg-backdrop', onClick: onClose },
|
|
288
339
|
h('div', { className: 'rz-dlg', onClick: e => e.stopPropagation() },
|
|
289
340
|
h('h3', null, t('detailTitle') + ' · ' + t('seqLabel', { seq: detail.seq })),
|
|
290
341
|
h('div', { className: 'rz-stats', style: { marginBottom: 10 } },
|
|
291
342
|
h(Chip, { ...entryChip(detail, t) }),
|
|
292
|
-
h(TokenBadge, { entry: detail }),
|
|
343
|
+
h(TokenBadge, { entry: detail, total }),
|
|
293
344
|
h('span', { className: 'rz-label', title: 'seq ' + detail.seq }, formatDateTime(detail.time)),
|
|
294
345
|
h('span', { className: 'rz-label' }, t('detailChars', { chars: formatNum(detail.chars) })),
|
|
295
346
|
detail.usage && h('span', { className: 'rz-label' }, t('detailUsage', {
|
|
@@ -320,10 +371,11 @@ window.__ModuleLoader__.load({
|
|
|
320
371
|
const [ctxLoading, setCtxLoading] = useState(false)
|
|
321
372
|
const [error, setError] = useState(null)
|
|
322
373
|
const [tierFilter, setTierFilter] = useState(() => new Set())
|
|
374
|
+
const [kindFilter, setKindFilter] = useState(() => new Set())
|
|
323
375
|
const [sortBy, setSortBy] = useState('order')
|
|
324
376
|
const [selected, setSelected] = useState(() => new Set())
|
|
325
377
|
const [detail, setDetail] = useState(null)
|
|
326
|
-
const [confirming, setConfirming] = useState(
|
|
378
|
+
const [confirming, setConfirming] = useState(null) // null | number[] 待裁剪 seqs(单条/多选同源)
|
|
327
379
|
const [deleting, setDeleting] = useState(false)
|
|
328
380
|
const [toast, setToast] = useState(null)
|
|
329
381
|
const sessionRef = useRef('')
|
|
@@ -354,16 +406,34 @@ window.__ModuleLoader__.load({
|
|
|
354
406
|
if (!context) return []
|
|
355
407
|
let rows = context.entries
|
|
356
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)))
|
|
357
410
|
return sortEntries(rows, sortBy)
|
|
358
|
-
}, [context, tierFilter, sortBy])
|
|
411
|
+
}, [context, tierFilter, kindFilter, sortBy])
|
|
359
412
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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) => {
|
|
363
431
|
let sum = 0
|
|
364
|
-
for (const seq of
|
|
432
|
+
if (entriesBySeq) for (const seq of seqs) { const e = entriesBySeq.get(seq); if (e) sum += e.tokens || 0 }
|
|
365
433
|
return sum
|
|
366
|
-
}
|
|
434
|
+
}
|
|
435
|
+
const selectedTokens = useMemo(() => sumSeqs(selected), [selected, entriesBySeq])
|
|
436
|
+
const confirmTokens = useMemo(() => confirming ? sumSeqs(confirming) : 0, [confirming, entriesBySeq])
|
|
367
437
|
|
|
368
438
|
const toggleRow = (seq) => setSelected(prev => {
|
|
369
439
|
const next = new Set(prev)
|
|
@@ -377,6 +447,12 @@ window.__ModuleLoader__.load({
|
|
|
377
447
|
else next.add(i)
|
|
378
448
|
return next
|
|
379
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
|
+
})
|
|
380
456
|
const selectVisible = () => setSelected(new Set(visible.map(e => e.seq)))
|
|
381
457
|
|
|
382
458
|
const openDetail = (entry) => {
|
|
@@ -387,17 +463,18 @@ window.__ModuleLoader__.load({
|
|
|
387
463
|
}
|
|
388
464
|
|
|
389
465
|
const doDelete = async () => {
|
|
466
|
+
if (!confirming || confirming.length === 0) return
|
|
390
467
|
setDeleting(true)
|
|
391
468
|
try {
|
|
392
|
-
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 }) })
|
|
393
470
|
const d = await r.json().catch(() => ({}))
|
|
394
471
|
if (!r.ok) throw new Error(d.error || 'HTTP ' + r.status)
|
|
395
|
-
setConfirming(
|
|
472
|
+
setConfirming(null)
|
|
396
473
|
setSelected(new Set())
|
|
397
474
|
showToast(t('deletedToast', { n: d.removed, tokens: formatNum(d.tokensRemoved) }))
|
|
398
475
|
refreshAll()
|
|
399
476
|
} catch (e) {
|
|
400
|
-
setConfirming(
|
|
477
|
+
setConfirming(null)
|
|
401
478
|
setError(e.message)
|
|
402
479
|
} finally { setDeleting(false) }
|
|
403
480
|
}
|
|
@@ -421,9 +498,21 @@ window.__ModuleLoader__.load({
|
|
|
421
498
|
t('selectVisible') + (visible.length ? ` (${visible.length})` : '')),
|
|
422
499
|
h('select', { className: 'rz-select', value: sortBy, onChange: e => setSortBy(e.target.value), title: t('sortLabel'), 'aria-label': t('sortLabel') },
|
|
423
500
|
h('option', { value: 'order' }, t('sortOrder')),
|
|
424
|
-
h('option', { value: 'seq' }, t('sortSeq')),
|
|
425
501
|
h('option', { value: 'tokens' }, t('sortTokens'))),
|
|
426
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))))),
|
|
427
516
|
selected.size > 0 && h('div', { key: 'sel', className: 'rz-stats' },
|
|
428
517
|
h('span', null, t('selectedStats', { n: selected.size, tokens: formatNum(selectedTokens) })),
|
|
429
518
|
h('span', { className: 'rz-spacer' }),
|
|
@@ -431,7 +520,7 @@ window.__ModuleLoader__.load({
|
|
|
431
520
|
h('button', { className: 'rz-btn', onClick: () => setSelected(new Set()) }, t('clearSelect')),
|
|
432
521
|
h('button', { className: 'rz-btn rz-btn-danger', disabled: !canDelete,
|
|
433
522
|
title: busy ? t('deleteBusyHint') : undefined,
|
|
434
|
-
onClick: () => setConfirming(
|
|
523
|
+
onClick: () => setConfirming([...selected]) },
|
|
435
524
|
t('deleteSelected') + ` (${selected.size})`))),
|
|
436
525
|
visible.length === 0
|
|
437
526
|
? h('div', { key: 'empty', className: 'rz-empty' }, t('emptyContext'))
|
|
@@ -442,15 +531,19 @@ window.__ModuleLoader__.load({
|
|
|
442
531
|
role: 'button', tabIndex: 0, onClick: () => toggleRow(entry.seq),
|
|
443
532
|
onKeyDown: e => e.key === 'Enter' && toggleRow(entry.seq) },
|
|
444
533
|
h('input', { type: 'checkbox', checked: selected.has(entry.seq), onClick: e => e.stopPropagation(), onChange: () => toggleRow(entry.seq) }),
|
|
445
|
-
h(TokenBadge, { entry }),
|
|
534
|
+
h(TokenBadge, { entry, total: context.totalTokens }),
|
|
446
535
|
h(Chip, { ...entryChip(entry, t) }),
|
|
447
536
|
h('span', { className: 'rz-row-preview', title: entry.preview,
|
|
448
537
|
onClick: e => { e.stopPropagation(); openDetail(entry) } }, entry.preview || ' '),
|
|
449
|
-
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]) } }, '✕'))
|
|
450
543
|
} })),
|
|
451
|
-
detail && h(DetailModal, { detail, t, onClose: () => setDetail(null) }),
|
|
452
|
-
confirming && h(ConfirmDialog, { n:
|
|
453
|
-
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 }),
|
|
454
547
|
],
|
|
455
548
|
toast && h('div', { className: 'rz-toast' }, toast))
|
|
456
549
|
}
|
|
@@ -462,7 +555,7 @@ window.__ModuleLoader__.load({
|
|
|
462
555
|
module.exports = {
|
|
463
556
|
name: CLIENT_NAME,
|
|
464
557
|
inject: ['slots', 'locale'],
|
|
465
|
-
__internals: { NS, ZH, EN, sortEntries, tierOf, RAZOR_TIERS, formatDateTime, entryChip },
|
|
558
|
+
__internals: { NS, ZH, EN, sortEntries, tierOf, RAZOR_TIERS, formatDateTime, entryChip, entryCategory, categoryLabel, pctOf },
|
|
466
559
|
__boot(container, opts = {}) {
|
|
467
560
|
ensureStyles()
|
|
468
561
|
const t = opts.t || ((key, vars) => {
|
package/client/index.js
CHANGED
|
@@ -54,9 +54,10 @@ const ZH = {
|
|
|
54
54
|
busyTag: '运行中',
|
|
55
55
|
sortLabel: '排序',
|
|
56
56
|
sortOrder: '上下文顺序',
|
|
57
|
-
sortSeq: '原始序号',
|
|
58
57
|
sortTokens: 'token 高→低',
|
|
59
58
|
orderNote: '本插件不会改变上下文条目顺序,只会剔除选中条目。',
|
|
59
|
+
legendTitle: '按 token 量级筛选(点选高亮某一档)',
|
|
60
|
+
kindFilterLabel: '类型',
|
|
60
61
|
selectVisible: '选中可见',
|
|
61
62
|
selectVisibleHint: '选中当前显示的全部条目(配合色块筛选)',
|
|
62
63
|
clearSelect: '清空选择',
|
|
@@ -78,6 +79,8 @@ const ZH = {
|
|
|
78
79
|
deleteTitle: '确认裁剪',
|
|
79
80
|
deleteConfirm: '即将把 {n} 条消息(约 {tokens} token)从模型视野中移除,替换为一条标记消息。日志中保留痕迹,此操作不可恢复。继续?',
|
|
80
81
|
deleteBusyHint: '会话正在运行,等当前回合结束后再裁剪',
|
|
82
|
+
deleteOne: '删除此条',
|
|
83
|
+
deleteOneHint: '删除此条(约 {tokens} token)',
|
|
81
84
|
deleteOk: '执行',
|
|
82
85
|
cancel: '取消',
|
|
83
86
|
deletedToast: '已删除 {n} 条(约 {tokens} token)',
|
|
@@ -93,9 +96,10 @@ const EN = {
|
|
|
93
96
|
busyTag: 'running',
|
|
94
97
|
sortLabel: 'Sort',
|
|
95
98
|
sortOrder: 'Context order',
|
|
96
|
-
sortSeq: 'Original seq',
|
|
97
99
|
sortTokens: 'tokens high→low',
|
|
98
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',
|
|
99
103
|
selectVisible: 'Select shown',
|
|
100
104
|
selectVisibleHint: 'Select every currently shown entry (pairs with tier filters)',
|
|
101
105
|
clearSelect: 'Clear selection',
|
|
@@ -117,6 +121,8 @@ const EN = {
|
|
|
117
121
|
deleteTitle: 'Confirm trim',
|
|
118
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?',
|
|
119
123
|
deleteBusyHint: 'Session is running — trim after the current turn finishes',
|
|
124
|
+
deleteOne: 'Delete this entry',
|
|
125
|
+
deleteOneHint: 'Delete this entry (≈{tokens} tokens)',
|
|
120
126
|
deleteOk: 'Delete',
|
|
121
127
|
cancel: 'Cancel',
|
|
122
128
|
deletedToast: 'Deleted {n} entries (≈{tokens} tokens)',
|
|
@@ -131,9 +137,10 @@ const PAGE_SIZE = 150
|
|
|
131
137
|
|
|
132
138
|
const kindI18n = (key) => 'kind' + key[0].toUpperCase() + key.slice(1)
|
|
133
139
|
|
|
134
|
-
/** 排序:order = 投影原序(模型可见序);
|
|
140
|
+
/** 排序:order = 投影原序(模型可见序);tokens = 降序(并列按 seq,缺 token 沉底)。
|
|
141
|
+
* 只有两档且都是显示层——裁剪过的会话里 seq 升序 ≠ 模型可见序(replace 标记 seq 大但位置在中间),
|
|
142
|
+
* 与「本页展示模型可见上下文」相悖,故不提供 seq 档。 */
|
|
135
143
|
function sortEntries(entries, sortBy) {
|
|
136
|
-
if (sortBy === 'seq') return [...entries].sort((a, b) => a.seq - b.seq)
|
|
137
144
|
if (sortBy !== 'tokens') return entries
|
|
138
145
|
return [...entries].sort((a, b) => {
|
|
139
146
|
const va = a.tokens, vb = b.tokens
|
|
@@ -165,6 +172,38 @@ function entryChip(entry, t) {
|
|
|
165
172
|
return { kind: entry.kind, label: t(kindI18n(entry.kind)) }
|
|
166
173
|
}
|
|
167
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
|
+
|
|
168
207
|
// ── 彩虹分级:颜色越暖 = 占用越多 ────────────────────────────────────────
|
|
169
208
|
// 固定对数档位(相邻约 ×2.5),跨会话语义稳定:绿→黄绿→黄→橙→红→品红。
|
|
170
209
|
// 档位是「这条消息吃掉多少典型上下文预算」的粗标尺,不随会话内最大值缩放。
|
|
@@ -215,6 +254,13 @@ const STYLE = `<style>
|
|
|
215
254
|
.rz-swatch.tier-3{background:hsl(32,80%,45%)}
|
|
216
255
|
.rz-swatch.tier-4{background:hsl(8,75%,48%)}
|
|
217
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}
|
|
218
264
|
.rz-list{display:flex;flex-direction:column;gap:6px}
|
|
219
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%}
|
|
220
266
|
.rz-row:hover{background:var(--dsw-alias-interactive-bg-hover)}
|
|
@@ -222,6 +268,9 @@ const STYLE = `<style>
|
|
|
222
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}
|
|
223
269
|
.rz-row-preview:hover{text-decoration:underline;text-underline-offset:3px}
|
|
224
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}
|
|
225
274
|
.rz-badge.tier-0{color:hsl(120,55%,40%);border-color:hsla(120,55%,40%,.45);background:hsla(120,55%,40%,.10)}
|
|
226
275
|
.rz-badge.tier-1{color:hsl(90,60%,38%);border-color:hsla(90,60%,38%,.45);background:hsla(90,60%,38%,.10)}
|
|
227
276
|
.rz-badge.tier-2{color:hsl(60,70%,36%);border-color:hsla(60,70%,36%,.45);background:hsla(60,70%,36%,.12)}
|
|
@@ -254,10 +303,12 @@ const STYLE = `<style>
|
|
|
254
303
|
|
|
255
304
|
const Chip = ({ kind, label, title }) => h('span', { className: 'rz-chip ' + kind, title }, label)
|
|
256
305
|
|
|
257
|
-
const TokenBadge = ({ entry }) => {
|
|
306
|
+
const TokenBadge = ({ entry, total }) => {
|
|
258
307
|
const tier = tierOf(entry)
|
|
259
|
-
|
|
260
|
-
|
|
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 : ''))
|
|
261
312
|
}
|
|
262
313
|
|
|
263
314
|
/** 分页列表:先渲染 pageSize 行,按需增长(大会话一次挂几千行会卡)。 */
|
|
@@ -272,14 +323,14 @@ function PagedList({ items, render, t }) {
|
|
|
272
323
|
}
|
|
273
324
|
|
|
274
325
|
/** 单条全文弹窗(正文经 /entry 异步补全)。 */
|
|
275
|
-
function DetailModal({ detail, t, onClose }) {
|
|
326
|
+
function DetailModal({ detail, t, total, onClose }) {
|
|
276
327
|
if (!detail) return null
|
|
277
328
|
return h('div', { className: 'rz-dlg-backdrop', onClick: onClose },
|
|
278
329
|
h('div', { className: 'rz-dlg', onClick: e => e.stopPropagation() },
|
|
279
330
|
h('h3', null, t('detailTitle') + ' · ' + t('seqLabel', { seq: detail.seq })),
|
|
280
331
|
h('div', { className: 'rz-stats', style: { marginBottom: 10 } },
|
|
281
332
|
h(Chip, { ...entryChip(detail, t) }),
|
|
282
|
-
h(TokenBadge, { entry: detail }),
|
|
333
|
+
h(TokenBadge, { entry: detail, total }),
|
|
283
334
|
h('span', { className: 'rz-label', title: 'seq ' + detail.seq }, formatDateTime(detail.time)),
|
|
284
335
|
h('span', { className: 'rz-label' }, t('detailChars', { chars: formatNum(detail.chars) })),
|
|
285
336
|
detail.usage && h('span', { className: 'rz-label' }, t('detailUsage', {
|
|
@@ -310,10 +361,11 @@ function RazorPage({ t, fixedSessionId }) {
|
|
|
310
361
|
const [ctxLoading, setCtxLoading] = useState(false)
|
|
311
362
|
const [error, setError] = useState(null)
|
|
312
363
|
const [tierFilter, setTierFilter] = useState(() => new Set())
|
|
364
|
+
const [kindFilter, setKindFilter] = useState(() => new Set())
|
|
313
365
|
const [sortBy, setSortBy] = useState('order')
|
|
314
366
|
const [selected, setSelected] = useState(() => new Set())
|
|
315
367
|
const [detail, setDetail] = useState(null)
|
|
316
|
-
const [confirming, setConfirming] = useState(
|
|
368
|
+
const [confirming, setConfirming] = useState(null) // null | number[] 待裁剪 seqs(单条/多选同源)
|
|
317
369
|
const [deleting, setDeleting] = useState(false)
|
|
318
370
|
const [toast, setToast] = useState(null)
|
|
319
371
|
const sessionRef = useRef('')
|
|
@@ -344,16 +396,34 @@ function RazorPage({ t, fixedSessionId }) {
|
|
|
344
396
|
if (!context) return []
|
|
345
397
|
let rows = context.entries
|
|
346
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)))
|
|
347
400
|
return sortEntries(rows, sortBy)
|
|
348
|
-
}, [context, tierFilter, sortBy])
|
|
401
|
+
}, [context, tierFilter, kindFilter, sortBy])
|
|
349
402
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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) => {
|
|
353
421
|
let sum = 0
|
|
354
|
-
for (const seq of
|
|
422
|
+
if (entriesBySeq) for (const seq of seqs) { const e = entriesBySeq.get(seq); if (e) sum += e.tokens || 0 }
|
|
355
423
|
return sum
|
|
356
|
-
}
|
|
424
|
+
}
|
|
425
|
+
const selectedTokens = useMemo(() => sumSeqs(selected), [selected, entriesBySeq])
|
|
426
|
+
const confirmTokens = useMemo(() => confirming ? sumSeqs(confirming) : 0, [confirming, entriesBySeq])
|
|
357
427
|
|
|
358
428
|
const toggleRow = (seq) => setSelected(prev => {
|
|
359
429
|
const next = new Set(prev)
|
|
@@ -367,6 +437,12 @@ function RazorPage({ t, fixedSessionId }) {
|
|
|
367
437
|
else next.add(i)
|
|
368
438
|
return next
|
|
369
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
|
+
})
|
|
370
446
|
const selectVisible = () => setSelected(new Set(visible.map(e => e.seq)))
|
|
371
447
|
|
|
372
448
|
const openDetail = (entry) => {
|
|
@@ -377,17 +453,18 @@ function RazorPage({ t, fixedSessionId }) {
|
|
|
377
453
|
}
|
|
378
454
|
|
|
379
455
|
const doDelete = async () => {
|
|
456
|
+
if (!confirming || confirming.length === 0) return
|
|
380
457
|
setDeleting(true)
|
|
381
458
|
try {
|
|
382
|
-
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 }) })
|
|
383
460
|
const d = await r.json().catch(() => ({}))
|
|
384
461
|
if (!r.ok) throw new Error(d.error || 'HTTP ' + r.status)
|
|
385
|
-
setConfirming(
|
|
462
|
+
setConfirming(null)
|
|
386
463
|
setSelected(new Set())
|
|
387
464
|
showToast(t('deletedToast', { n: d.removed, tokens: formatNum(d.tokensRemoved) }))
|
|
388
465
|
refreshAll()
|
|
389
466
|
} catch (e) {
|
|
390
|
-
setConfirming(
|
|
467
|
+
setConfirming(null)
|
|
391
468
|
setError(e.message)
|
|
392
469
|
} finally { setDeleting(false) }
|
|
393
470
|
}
|
|
@@ -411,9 +488,21 @@ function RazorPage({ t, fixedSessionId }) {
|
|
|
411
488
|
t('selectVisible') + (visible.length ? ` (${visible.length})` : '')),
|
|
412
489
|
h('select', { className: 'rz-select', value: sortBy, onChange: e => setSortBy(e.target.value), title: t('sortLabel'), 'aria-label': t('sortLabel') },
|
|
413
490
|
h('option', { value: 'order' }, t('sortOrder')),
|
|
414
|
-
h('option', { value: 'seq' }, t('sortSeq')),
|
|
415
491
|
h('option', { value: 'tokens' }, t('sortTokens'))),
|
|
416
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))))),
|
|
417
506
|
selected.size > 0 && h('div', { key: 'sel', className: 'rz-stats' },
|
|
418
507
|
h('span', null, t('selectedStats', { n: selected.size, tokens: formatNum(selectedTokens) })),
|
|
419
508
|
h('span', { className: 'rz-spacer' }),
|
|
@@ -421,7 +510,7 @@ function RazorPage({ t, fixedSessionId }) {
|
|
|
421
510
|
h('button', { className: 'rz-btn', onClick: () => setSelected(new Set()) }, t('clearSelect')),
|
|
422
511
|
h('button', { className: 'rz-btn rz-btn-danger', disabled: !canDelete,
|
|
423
512
|
title: busy ? t('deleteBusyHint') : undefined,
|
|
424
|
-
onClick: () => setConfirming(
|
|
513
|
+
onClick: () => setConfirming([...selected]) },
|
|
425
514
|
t('deleteSelected') + ` (${selected.size})`))),
|
|
426
515
|
visible.length === 0
|
|
427
516
|
? h('div', { key: 'empty', className: 'rz-empty' }, t('emptyContext'))
|
|
@@ -432,15 +521,19 @@ function RazorPage({ t, fixedSessionId }) {
|
|
|
432
521
|
role: 'button', tabIndex: 0, onClick: () => toggleRow(entry.seq),
|
|
433
522
|
onKeyDown: e => e.key === 'Enter' && toggleRow(entry.seq) },
|
|
434
523
|
h('input', { type: 'checkbox', checked: selected.has(entry.seq), onClick: e => e.stopPropagation(), onChange: () => toggleRow(entry.seq) }),
|
|
435
|
-
h(TokenBadge, { entry }),
|
|
524
|
+
h(TokenBadge, { entry, total: context.totalTokens }),
|
|
436
525
|
h(Chip, { ...entryChip(entry, t) }),
|
|
437
526
|
h('span', { className: 'rz-row-preview', title: entry.preview,
|
|
438
527
|
onClick: e => { e.stopPropagation(); openDetail(entry) } }, entry.preview || ' '),
|
|
439
|
-
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]) } }, '✕'))
|
|
440
533
|
} })),
|
|
441
|
-
detail && h(DetailModal, { detail, t, onClose: () => setDetail(null) }),
|
|
442
|
-
confirming && h(ConfirmDialog, { n:
|
|
443
|
-
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 }),
|
|
444
537
|
],
|
|
445
538
|
toast && h('div', { className: 'rz-toast' }, toast))
|
|
446
539
|
}
|
|
@@ -452,7 +545,7 @@ const CLIENT_NAME = '@weibaohui/context-razor'
|
|
|
452
545
|
module.exports = {
|
|
453
546
|
name: CLIENT_NAME,
|
|
454
547
|
inject: ['slots', 'locale'],
|
|
455
|
-
__internals: { NS, ZH, EN, sortEntries, tierOf, RAZOR_TIERS, formatDateTime, entryChip },
|
|
548
|
+
__internals: { NS, ZH, EN, sortEntries, tierOf, RAZOR_TIERS, formatDateTime, entryChip, entryCategory, categoryLabel, pctOf },
|
|
456
549
|
__boot(container, opts = {}) {
|
|
457
550
|
ensureStyles()
|
|
458
551
|
const t = opts.t || ((key, vars) => {
|