@weibaohui/context-razor 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/client/bundle.js +141 -112
- package/client/index.js +141 -112
- package/docs/demo.gif +0 -0
- package/package.json +1 -1
- package/src/index.js +30 -4
package/README.md
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
- **逐条列出**:当前会话模型可见的每条上下文(用户/助手/工具结果)按序排列,带预览、字符数与 ≈token 估算,点 ⋯ 查看全文与真实 usage
|
|
13
13
|
- **超阈值标红**:可调阈值(默认 500 token,本地记忆),超限条目红色高亮;「只看超阈值」过滤 + 「token 高→低」排序,大头一眼可见
|
|
14
14
|
- **精确裁剪**:勾选任意条目一键删除——连续段自动合并处理。删除走宿主 surface replace 协议(与官方 compaction 同款机制),**不经 LLM 总结**,删了什么、删了多少完全由你决定
|
|
15
|
+
- **不重排**:本插件不会改变上下文条目顺序,只会剔除选中条目;排序下拉仅影响页面显示(上下文顺序 / 原始序号 / token 高→低),模型可见顺序始终由会话 surface 决定
|
|
15
16
|
- **安全护栏**:会话运行中禁止裁剪(等当前回合结束);每次替换附带一条 notice 标记消息,模型知道这段历史被你移除了,需要时会主动向你确认
|
|
16
17
|
- **非破坏**:append-only 日志保留全部痕迹(宿主机制如此)。「删除」= 从模型视野与界面投影中移除
|
|
17
18
|
|
|
@@ -21,7 +22,7 @@
|
|
|
21
22
|
dsh plugin --profile web add @weibaohui/context-razor -w
|
|
22
23
|
```
|
|
23
24
|
|
|
24
|
-
装完重启 `dsh web`
|
|
25
|
+
装完重启 `dsh web` 即生效。入口:打开会话 → 顶部「上下文剃刀」标签(Hermes Loop 右侧),自动锁定当前会话。
|
|
25
26
|
|
|
26
27
|
## 删除的实现方式
|
|
27
28
|
|
package/client/bundle.js
CHANGED
|
@@ -11,12 +11,12 @@ window.__ModuleLoader__.load({
|
|
|
11
11
|
/**
|
|
12
12
|
* dsh-plugin-razor — Browser half.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
* session,
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
14
|
+
* Conversation-view tab surface: the model-visible context entries of the
|
|
15
|
+
* CURRENT session, each with a rainbow tier (≈token, log-scale buckets —
|
|
16
|
+
* warmer = heavier) doubling as filter buttons; select and razor away.
|
|
17
|
+
* All copy comes from the locale registry (`zh`/`en`); tier hues are data
|
|
18
|
+
* (hsl computed per bucket). No class components — render errors land in
|
|
19
|
+
* globalThis.__rzErrors.
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
22
|
let __React = null
|
|
@@ -58,32 +58,30 @@ window.__ModuleLoader__.load({
|
|
|
58
58
|
|
|
59
59
|
const ZH = {
|
|
60
60
|
title: '上下文剃刀',
|
|
61
|
-
|
|
62
|
-
sessionLabel: '会话',
|
|
63
|
-
pickSession: '选择会话…',
|
|
61
|
+
pickSession: '在会话顶部标签打开以查看上下文',
|
|
64
62
|
loading: '正在加载…',
|
|
65
63
|
refresh: '刷新',
|
|
66
64
|
busyTag: '运行中',
|
|
67
|
-
idleTag: '空闲',
|
|
68
|
-
thresholdLabel: '标红阈值',
|
|
69
|
-
thresholdUnit: 'token',
|
|
70
65
|
sortLabel: '排序',
|
|
71
66
|
sortOrder: '上下文顺序',
|
|
67
|
+
sortSeq: '原始序号',
|
|
72
68
|
sortTokens: 'token 高→低',
|
|
73
|
-
|
|
74
|
-
|
|
69
|
+
orderNote: '本插件不会改变上下文条目顺序,只会剔除选中条目。',
|
|
70
|
+
selectVisible: '选中可见',
|
|
71
|
+
selectVisibleHint: '选中当前显示的全部条目(配合色块筛选)',
|
|
75
72
|
clearSelect: '清空选择',
|
|
76
73
|
deleteSelected: '删除选中',
|
|
77
74
|
selectedStats: '已选 {n} 条 / ≈{tokens} token',
|
|
78
75
|
stats: '{nodes} 条 · 合计 ≈{tokens} token{mode}',
|
|
79
76
|
modeHeuristic: '(启发式估算:词表未加载)',
|
|
80
77
|
kindUser: '用户',
|
|
78
|
+
kindInjected: '注入',
|
|
81
79
|
kindAssistant: '助手',
|
|
82
80
|
kindTool: '工具',
|
|
83
81
|
emptyContext: '该会话上下文为空',
|
|
84
82
|
showMore: '显示更多(剩余 {n})',
|
|
85
83
|
detailTitle: '条目详情',
|
|
86
|
-
|
|
84
|
+
detailChars: '{chars} 字符',
|
|
87
85
|
detailUsage: '真实用量:输入 {input} / 输出 {output}{cache}',
|
|
88
86
|
detailUsageCache: ' / 缓存读 {cache}',
|
|
89
87
|
close: '关闭',
|
|
@@ -99,32 +97,30 @@ window.__ModuleLoader__.load({
|
|
|
99
97
|
|
|
100
98
|
const EN = {
|
|
101
99
|
title: 'Context Razor',
|
|
102
|
-
|
|
103
|
-
sessionLabel: 'Session',
|
|
104
|
-
pickSession: 'Pick a session…',
|
|
100
|
+
pickSession: 'Open a conversation tab to inspect its context',
|
|
105
101
|
loading: 'Loading…',
|
|
106
102
|
refresh: 'Refresh',
|
|
107
103
|
busyTag: 'running',
|
|
108
|
-
idleTag: 'idle',
|
|
109
|
-
thresholdLabel: 'Red threshold',
|
|
110
|
-
thresholdUnit: 'tokens',
|
|
111
104
|
sortLabel: 'Sort',
|
|
112
105
|
sortOrder: 'Context order',
|
|
106
|
+
sortSeq: 'Original seq',
|
|
113
107
|
sortTokens: 'tokens high→low',
|
|
114
|
-
|
|
115
|
-
|
|
108
|
+
orderNote: 'This plugin never reorders context entries; it only removes the selected ones.',
|
|
109
|
+
selectVisible: 'Select shown',
|
|
110
|
+
selectVisibleHint: 'Select every currently shown entry (pairs with tier filters)',
|
|
116
111
|
clearSelect: 'Clear selection',
|
|
117
112
|
deleteSelected: 'Delete selected',
|
|
118
113
|
selectedStats: '{n} selected / ≈{tokens} tokens',
|
|
119
114
|
stats: '{nodes} entries · total ≈{tokens} tokens{mode}',
|
|
120
115
|
modeHeuristic: ' (heuristic: ranks unavailable)',
|
|
121
116
|
kindUser: 'User',
|
|
117
|
+
kindInjected: 'injected',
|
|
122
118
|
kindAssistant: 'Assistant',
|
|
123
119
|
kindTool: 'Tool',
|
|
124
120
|
emptyContext: 'This session has no context entries',
|
|
125
121
|
showMore: 'Show more ({n} left)',
|
|
126
122
|
detailTitle: 'Entry detail',
|
|
127
|
-
|
|
123
|
+
detailChars: '{chars} chars',
|
|
128
124
|
detailUsage: 'Actual usage: in {input} / out {output}{cache}',
|
|
129
125
|
detailUsageCache: ' / cache-read {cache}',
|
|
130
126
|
close: 'Close',
|
|
@@ -142,18 +138,12 @@ window.__ModuleLoader__.load({
|
|
|
142
138
|
|
|
143
139
|
const API = '/context-razor/api'
|
|
144
140
|
const PAGE_SIZE = 150
|
|
145
|
-
const DEFAULT_THRESHOLD = 500
|
|
146
|
-
const THRESHOLD_KEY = 'context-razor-threshold'
|
|
147
141
|
|
|
148
142
|
const kindI18n = (key) => 'kind' + key[0].toUpperCase() + key.slice(1)
|
|
149
143
|
|
|
150
|
-
/**
|
|
151
|
-
function overThreshold(entry, threshold) {
|
|
152
|
-
return threshold > 0 && typeof entry.tokens === 'number' && entry.tokens > threshold
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/** 排序:order = 投影原序;tokens = 降序(并列按 seq,缺 token 沉底)。 */
|
|
144
|
+
/** 排序:order = 投影原序(模型可见序);seq = 原始序号升序(裁剪过的会话里与 order 不同:标记消息 seq 大但在中间);tokens = 降序(并列按 seq,缺 token 沉底)。 */
|
|
156
145
|
function sortEntries(entries, sortBy) {
|
|
146
|
+
if (sortBy === 'seq') return [...entries].sort((a, b) => a.seq - b.seq)
|
|
157
147
|
if (sortBy !== 'tokens') return entries
|
|
158
148
|
return [...entries].sort((a, b) => {
|
|
159
149
|
const va = a.tokens, vb = b.tokens
|
|
@@ -166,6 +156,41 @@ window.__ModuleLoader__.load({
|
|
|
166
156
|
|
|
167
157
|
const formatNum = (n) => Number.isFinite(n) ? n.toLocaleString('en-US') : '-'
|
|
168
158
|
|
|
159
|
+
/** 完整日期时间(短格式:2026/9/2 11:03;ts 兼容 epoch 毫秒与 ISO 串)。 */
|
|
160
|
+
function formatDateTime(ts) {
|
|
161
|
+
if (!ts) return ''
|
|
162
|
+
const t = typeof ts === 'number' ? ts : Date.parse(ts)
|
|
163
|
+
if (!Number.isFinite(t)) return ''
|
|
164
|
+
try { return new Date(t).toLocaleString([], { dateStyle: 'short', timeStyle: 'short' }) } catch { return new Date(t).toLocaleString() }
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** 行首 chip 的文案与提示:工具结果显示工具名,注入类 user 消息标「注入」。 */
|
|
168
|
+
function entryChip(entry, t) {
|
|
169
|
+
if (entry.kind === 'tool') {
|
|
170
|
+
return { kind: 'tool', label: entry.tool || t('kindTool'), title: entry.tool }
|
|
171
|
+
}
|
|
172
|
+
if (entry.kind === 'user' && entry.sourceKind && entry.sourceKind !== 'user') {
|
|
173
|
+
return { kind: 'user', label: t('kindInjected'), title: [entry.sourceKind, entry.sourceForm, entry.sourcePlugin].filter(Boolean).join(' · ') }
|
|
174
|
+
}
|
|
175
|
+
return { kind: entry.kind, label: t(kindI18n(entry.kind)) }
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ── 彩虹分级:颜色越暖 = 占用越多 ────────────────────────────────────────
|
|
179
|
+
// 固定对数档位(相邻约 ×2.5),跨会话语义稳定:绿→黄绿→黄→橙→红→品红。
|
|
180
|
+
// 档位是「这条消息吃掉多少典型上下文预算」的粗标尺,不随会话内最大值缩放。
|
|
181
|
+
const RAZOR_TIERS = [
|
|
182
|
+
{ max: 100, hue: 120, label: '0-100' },
|
|
183
|
+
{ max: 300, hue: 90, label: '100-300' },
|
|
184
|
+
{ max: 800, hue: 60, label: '300-800' },
|
|
185
|
+
{ max: 2000, hue: 32, label: '800-2k' },
|
|
186
|
+
{ max: 5000, hue: 8, label: '2k-5k' },
|
|
187
|
+
{ max: Infinity, hue: 320, label: '>5k' },
|
|
188
|
+
]
|
|
189
|
+
function tierOf(entry) {
|
|
190
|
+
const tokens = typeof entry.tokens === 'number' ? entry.tokens : 0
|
|
191
|
+
return RAZOR_TIERS.findIndex(t => tokens <= t.max)
|
|
192
|
+
}
|
|
193
|
+
|
|
169
194
|
async function getJson(url) {
|
|
170
195
|
const r = await fetch(url)
|
|
171
196
|
if (!r.ok) {
|
|
@@ -178,28 +203,47 @@ window.__ModuleLoader__.load({
|
|
|
178
203
|
// ── Token-based stylesheet (light/dark adaptive by construction) ────────
|
|
179
204
|
|
|
180
205
|
const STYLE = `<style>
|
|
181
|
-
.rz-page{
|
|
206
|
+
.rz-page,.rz-page *{box-sizing:border-box}
|
|
207
|
+
.rz-page{position:relative;display:flex;flex-direction:column;gap:12px;padding:16px 20px;min-width:0;color:var(--dsw-alias-label-primary);font-family:var(--dsw-font-family);font-size:var(--dsw-font-sm-14,14px)}
|
|
182
208
|
.rz-hint{color:var(--dsw-alias-label-secondary);font-size:12.5px;line-height:1.5}
|
|
183
209
|
.rz-toolbar{display:flex;gap:10px;align-items:center;flex-wrap:wrap}
|
|
184
210
|
.rz-spacer{flex:1}
|
|
185
211
|
.rz-label{color:var(--dsw-alias-label-secondary);font-size:12.5px;white-space:nowrap}
|
|
186
|
-
.rz-input{min-height:30px;padding:4px 10px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-specific-input-major,var(--dsw-alias-bg-layer-1));color:var(--dsw-alias-label-primary);font-size:13px;font-family:var(--dsw-font-family);outline:none}
|
|
187
|
-
.rz-input:focus{border-color:var(--dsw-alias-state-business-primary)}
|
|
188
212
|
.rz-select{min-height:30px;padding:4px 10px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-specific-input-major,var(--dsw-alias-bg-layer-1));color:var(--dsw-alias-label-primary);font-size:13px;font-family:var(--dsw-font-family);outline:none}
|
|
189
213
|
.rz-stats{display:flex;gap:12px;align-items:center;flex-wrap:wrap;color:var(--dsw-alias-label-secondary);font-size:12.5px}
|
|
190
214
|
.rz-badge{display:inline-flex;align-items:center;padding:1px 8px;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;white-space:nowrap}
|
|
191
|
-
.rz-badge.over{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary);font-weight:600}
|
|
192
215
|
.rz-chip{display:inline-flex;align-items:center;padding:1px 8px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);font-size:11.5px;white-space:nowrap;flex:none}
|
|
193
216
|
.rz-chip.user{color:var(--dsw-alias-state-business-primary);border-color:var(--dsw-alias-state-business-primary)}
|
|
194
217
|
.rz-chip.assistant{color:var(--dsw-alias-state-success-primary);border-color:var(--dsw-alias-state-success-primary)}
|
|
218
|
+
.rz-legend{display:inline-flex;gap:4px;align-items:center}
|
|
219
|
+
.rz-swatch{border:none;cursor:pointer;padding:2px 8px;border-radius:999px;font-size:10.5px;color:var(--dsw-alias-label-primary-inverted,#fff);white-space:nowrap;font-family:var(--dsw-font-family)}
|
|
220
|
+
.rz-legend.filtering .rz-swatch:not(.on){opacity:.4}
|
|
221
|
+
.rz-swatch.on{box-shadow:0 0 0 2px var(--dsw-alias-border-l3,rgba(128,128,128,.65))}
|
|
222
|
+
.rz-swatch.tier-0{background:hsl(120,55%,40%)}
|
|
223
|
+
.rz-swatch.tier-1{background:hsl(90,60%,38%)}
|
|
224
|
+
.rz-swatch.tier-2{background:hsl(60,70%,36%)}
|
|
225
|
+
.rz-swatch.tier-3{background:hsl(32,80%,45%)}
|
|
226
|
+
.rz-swatch.tier-4{background:hsl(8,75%,48%)}
|
|
227
|
+
.rz-swatch.tier-5{background:hsl(320,65%,50%)}
|
|
195
228
|
.rz-list{display:flex;flex-direction:column;gap:6px}
|
|
196
229
|
.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%}
|
|
197
230
|
.rz-row:hover{background:var(--dsw-alias-interactive-bg-hover)}
|
|
198
|
-
.rz-row.over{border-left-color:var(--dsw-alias-state-error-primary)}
|
|
199
231
|
.rz-row.checked{border-color:var(--dsw-alias-state-business-primary)}
|
|
200
|
-
.rz-row-preview{flex:1;min-width:
|
|
201
|
-
.rz-row
|
|
232
|
+
.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
|
+
.rz-row-preview:hover{text-decoration:underline;text-underline-offset:3px}
|
|
202
234
|
.rz-row-meta{color:var(--dsw-alias-label-tertiary);font-size:11px;flex:none}
|
|
235
|
+
.rz-badge.tier-0{color:hsl(120,55%,40%);border-color:hsla(120,55%,40%,.45);background:hsla(120,55%,40%,.10)}
|
|
236
|
+
.rz-badge.tier-1{color:hsl(90,60%,38%);border-color:hsla(90,60%,38%,.45);background:hsla(90,60%,38%,.10)}
|
|
237
|
+
.rz-badge.tier-2{color:hsl(60,70%,36%);border-color:hsla(60,70%,36%,.45);background:hsla(60,70%,36%,.12)}
|
|
238
|
+
.rz-badge.tier-3{color:hsl(32,80%,45%);border-color:hsla(32,80%,45%,.5);background:hsla(32,80%,45%,.12)}
|
|
239
|
+
.rz-badge.tier-4{color:hsl(8,75%,48%);border-color:hsla(8,75%,48%,.5);background:hsla(8,75%,48%,.12);font-weight:600}
|
|
240
|
+
.rz-badge.tier-5{color:hsl(320,65%,50%);border-color:hsla(320,65%,50%,.5);background:hsla(320,65%,50%,.12);font-weight:600}
|
|
241
|
+
.rz-row.tier-0{border-left-color:hsl(120,55%,40%)}
|
|
242
|
+
.rz-row.tier-1{border-left-color:hsl(90,60%,38%)}
|
|
243
|
+
.rz-row.tier-2{border-left-color:hsl(60,70%,36%)}
|
|
244
|
+
.rz-row.tier-3{border-left-color:hsl(32,80%,45%)}
|
|
245
|
+
.rz-row.tier-4{border-left-color:hsl(8,75%,48%)}
|
|
246
|
+
.rz-row.tier-5{border-left-color:hsl(320,65%,50%)}
|
|
203
247
|
.rz-empty{border:1px dashed var(--dsw-alias-border-l2);border-radius:12px;padding:36px 20px;text-align:center;color:var(--dsw-alias-label-secondary)}
|
|
204
248
|
.rz-loading{padding:36px;text-align:center;color:var(--dsw-alias-label-secondary)}
|
|
205
249
|
.rz-footbtns{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
|
|
@@ -218,11 +262,11 @@ window.__ModuleLoader__.load({
|
|
|
218
262
|
|
|
219
263
|
// ── Small building blocks ────────────────────────────────────────────────
|
|
220
264
|
|
|
221
|
-
const Chip = ({ kind, label }) => h('span', { className: 'rz-chip ' + kind }, label)
|
|
265
|
+
const Chip = ({ kind, label, title }) => h('span', { className: 'rz-chip ' + kind, title }, label)
|
|
222
266
|
|
|
223
|
-
const TokenBadge = ({ entry
|
|
224
|
-
const
|
|
225
|
-
return h('span', { className: 'rz-badge' +
|
|
267
|
+
const TokenBadge = ({ entry }) => {
|
|
268
|
+
const tier = tierOf(entry)
|
|
269
|
+
return h('span', { className: 'rz-badge tier-' + tier, title: RAZOR_TIERS[tier].label + ' token' },
|
|
226
270
|
'≈' + formatNum(entry.tokens))
|
|
227
271
|
}
|
|
228
272
|
|
|
@@ -238,15 +282,16 @@ window.__ModuleLoader__.load({
|
|
|
238
282
|
}
|
|
239
283
|
|
|
240
284
|
/** 单条全文弹窗(正文经 /entry 异步补全)。 */
|
|
241
|
-
function DetailModal({ detail,
|
|
285
|
+
function DetailModal({ detail, t, onClose }) {
|
|
242
286
|
if (!detail) return null
|
|
243
287
|
return h('div', { className: 'rz-dlg-backdrop', onClick: onClose },
|
|
244
288
|
h('div', { className: 'rz-dlg', onClick: e => e.stopPropagation() },
|
|
245
289
|
h('h3', null, t('detailTitle') + ' · ' + t('seqLabel', { seq: detail.seq })),
|
|
246
290
|
h('div', { className: 'rz-stats', style: { marginBottom: 10 } },
|
|
247
|
-
h(Chip, {
|
|
248
|
-
h(TokenBadge, { entry: detail
|
|
249
|
-
h('span', { className: 'rz-label'
|
|
291
|
+
h(Chip, { ...entryChip(detail, t) }),
|
|
292
|
+
h(TokenBadge, { entry: detail }),
|
|
293
|
+
h('span', { className: 'rz-label', title: 'seq ' + detail.seq }, formatDateTime(detail.time)),
|
|
294
|
+
h('span', { className: 'rz-label' }, t('detailChars', { chars: formatNum(detail.chars) })),
|
|
250
295
|
detail.usage && h('span', { className: 'rz-label' }, t('detailUsage', {
|
|
251
296
|
input: formatNum(detail.usage.input), output: formatNum(detail.usage.output),
|
|
252
297
|
cache: detail.usage.cacheRead ? t('detailUsageCache', { cache: formatNum(detail.usage.cacheRead) }) : '' }))),
|
|
@@ -268,17 +313,14 @@ window.__ModuleLoader__.load({
|
|
|
268
313
|
|
|
269
314
|
// ── Page ────────────────────────────────────────────────────────────────
|
|
270
315
|
|
|
271
|
-
function RazorPage({ t }) {
|
|
272
|
-
|
|
316
|
+
function RazorPage({ t, fixedSessionId }) {
|
|
317
|
+
useEffect(ensureStyles, [])
|
|
273
318
|
const [sessionId, setSessionId] = useState('')
|
|
274
319
|
const [context, setContext] = useState(null)
|
|
275
320
|
const [ctxLoading, setCtxLoading] = useState(false)
|
|
276
321
|
const [error, setError] = useState(null)
|
|
277
|
-
const [
|
|
278
|
-
try { const v = Number(localStorage.getItem(THRESHOLD_KEY)); return Number.isFinite(v) && v > 0 ? v : DEFAULT_THRESHOLD } catch { return DEFAULT_THRESHOLD }
|
|
279
|
-
})
|
|
322
|
+
const [tierFilter, setTierFilter] = useState(() => new Set())
|
|
280
323
|
const [sortBy, setSortBy] = useState('order')
|
|
281
|
-
const [overOnly, setOverOnly] = useState(false)
|
|
282
324
|
const [selected, setSelected] = useState(() => new Set())
|
|
283
325
|
const [detail, setDetail] = useState(null)
|
|
284
326
|
const [confirming, setConfirming] = useState(false)
|
|
@@ -288,8 +330,10 @@ window.__ModuleLoader__.load({
|
|
|
288
330
|
|
|
289
331
|
const showToast = (text) => { setToast(text); setTimeout(() => setToast(null), 3000) }
|
|
290
332
|
|
|
291
|
-
|
|
292
|
-
useEffect(() => {
|
|
333
|
+
// 会话视图挂载:只管当前会话(fixedSessionId 来自 slot props)
|
|
334
|
+
useEffect(() => {
|
|
335
|
+
if (fixedSessionId) { sessionRef.current = fixedSessionId; setSessionId(fixedSessionId) }
|
|
336
|
+
}, [fixedSessionId])
|
|
293
337
|
|
|
294
338
|
const loadContext = (id) => {
|
|
295
339
|
if (!id) { setContext(null); return }
|
|
@@ -303,25 +347,16 @@ window.__ModuleLoader__.load({
|
|
|
303
347
|
useEffect(() => { if (sessionId) loadContext(sessionId) }, [sessionId])
|
|
304
348
|
|
|
305
349
|
const refreshAll = () => {
|
|
306
|
-
loadSessions()
|
|
307
350
|
if (sessionRef.current) loadContext(sessionRef.current)
|
|
308
351
|
}
|
|
309
352
|
|
|
310
|
-
const pickSession = (id) => { sessionRef.current = id; setSessionId(id) }
|
|
311
|
-
const changeThreshold = (v) => {
|
|
312
|
-
const n = Number(v)
|
|
313
|
-
setThreshold(Number.isFinite(n) && n > 0 ? n : 0)
|
|
314
|
-
try { localStorage.setItem(THRESHOLD_KEY, String(n)) } catch {}
|
|
315
|
-
}
|
|
316
|
-
|
|
317
353
|
const visible = useMemo(() => {
|
|
318
354
|
if (!context) return []
|
|
319
355
|
let rows = context.entries
|
|
320
|
-
if (
|
|
356
|
+
if (tierFilter.size > 0) rows = rows.filter(e => tierFilter.has(tierOf(e)))
|
|
321
357
|
return sortEntries(rows, sortBy)
|
|
322
|
-
}, [context,
|
|
358
|
+
}, [context, tierFilter, sortBy])
|
|
323
359
|
|
|
324
|
-
const overList = useMemo(() => (context ? context.entries.filter(e => overThreshold(e, threshold)) : []), [context, threshold])
|
|
325
360
|
const selectedTokens = useMemo(() => {
|
|
326
361
|
if (!context) return 0
|
|
327
362
|
const bySeq = new Map(context.entries.map(e => [e.seq, e]))
|
|
@@ -336,12 +371,18 @@ window.__ModuleLoader__.load({
|
|
|
336
371
|
else next.add(seq)
|
|
337
372
|
return next
|
|
338
373
|
})
|
|
339
|
-
const
|
|
374
|
+
const toggleTier = (i) => setTierFilter(prev => {
|
|
375
|
+
const next = new Set(prev)
|
|
376
|
+
if (next.has(i)) next.delete(i)
|
|
377
|
+
else next.add(i)
|
|
378
|
+
return next
|
|
379
|
+
})
|
|
380
|
+
const selectVisible = () => setSelected(new Set(visible.map(e => e.seq)))
|
|
340
381
|
|
|
341
382
|
const openDetail = (entry) => {
|
|
342
383
|
setDetail(entry)
|
|
343
384
|
getJson(`${API}/entry?session=${encodeURIComponent(sessionId)}&seq=${entry.seq}`)
|
|
344
|
-
.then(d => setDetail(cur => (cur && cur.seq === entry.seq) ? { ...cur, text: d.text, tokens: d.tokens } : cur))
|
|
385
|
+
.then(d => setDetail(cur => (cur && cur.seq === entry.seq) ? { ...cur, text: d.text, tokens: d.tokens, tool: d.tool, sourceKind: d.sourceKind, sourceForm: d.sourceForm, sourcePlugin: d.sourcePlugin } : cur))
|
|
345
386
|
.catch(() => {})
|
|
346
387
|
}
|
|
347
388
|
|
|
@@ -365,60 +406,49 @@ window.__ModuleLoader__.load({
|
|
|
365
406
|
const canDelete = selected.size > 0 && !busy && !deleting
|
|
366
407
|
|
|
367
408
|
return h('div', { className: 'rz-page' },
|
|
368
|
-
h('div', { className: 'rz-hint' }, t('hint')),
|
|
369
|
-
h('div', { className: 'rz-toolbar' },
|
|
370
|
-
h('span', { className: 'rz-label' }, t('sessionLabel')),
|
|
371
|
-
h('select', { className: 'rz-select', value: sessionId, onChange: e => pickSession(e.target.value), style: { maxWidth: 380 } },
|
|
372
|
-
h('option', { value: '' }, t('pickSession')),
|
|
373
|
-
(sessions || []).map(s => h('option', { key: s.id, value: s.id },
|
|
374
|
-
(s.cwd ? s.cwd.split('/').pop() + ' · ' : '') + s.id.slice(0, 8) + ' · ' + s.nodes))),
|
|
375
|
-
h('button', { className: 'rz-btn', onClick: refreshAll, title: t('refresh') }, t('refresh')),
|
|
376
|
-
context && h('span', { className: 'rz-badge' }, context.busy ? t('busyTag') : t('idleTag'))),
|
|
377
409
|
error && h('div', { className: 'rz-hint', style: { color: 'var(--dsw-alias-state-error-primary)' } }, t('operationFailed') + ': ' + error),
|
|
378
|
-
!sessionId && (
|
|
379
|
-
? h('div', { className: 'rz-loading' }, t('loading'))
|
|
380
|
-
: h('div', { className: 'rz-empty' }, t('pickSession'))),
|
|
410
|
+
!sessionId && h('div', { className: 'rz-empty' }, t('pickSession')),
|
|
381
411
|
sessionId && ctxLoading && h('div', { className: 'rz-loading' }, t('loading')),
|
|
382
412
|
sessionId && !ctxLoading && context && [
|
|
383
|
-
h('div', { key: '
|
|
413
|
+
h('div', { key: 'note', className: 'rz-hint' }, t('orderNote')),
|
|
414
|
+
h('div', { key: 'bar', className: 'rz-stats' },
|
|
384
415
|
h('span', null, t('stats', { nodes: context.nodes, tokens: formatNum(context.totalTokens), mode: context.encoder === 'heuristic' ? t('modeHeuristic') : '' })),
|
|
416
|
+
h('span', { className: 'rz-legend' + (tierFilter.size > 0 ? ' filtering' : ''), title: t('legendTitle') },
|
|
417
|
+
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))),
|
|
385
418
|
h('span', { className: 'rz-spacer' }),
|
|
386
|
-
h('
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
t('thresholdUnit')),
|
|
419
|
+
busy && h('span', { className: 'rz-badge' }, t('busyTag')),
|
|
420
|
+
h('button', { className: 'rz-btn', onClick: selectVisible, disabled: visible.length === 0 || busy, title: t('selectVisibleHint') },
|
|
421
|
+
t('selectVisible') + (visible.length ? ` (${visible.length})` : '')),
|
|
390
422
|
h('select', { className: 'rz-select', value: sortBy, onChange: e => setSortBy(e.target.value), title: t('sortLabel'), 'aria-label': t('sortLabel') },
|
|
391
423
|
h('option', { value: 'order' }, t('sortOrder')),
|
|
424
|
+
h('option', { value: 'seq' }, t('sortSeq')),
|
|
392
425
|
h('option', { value: 'tokens' }, t('sortTokens'))),
|
|
393
|
-
h('
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
h('span', null, selected.size > 0 ? t('selectedStats', { n: selected.size, tokens: formatNum(selectedTokens) }) : ' '),
|
|
426
|
+
h('button', { className: 'rz-btn', onClick: refreshAll, title: t('refresh') }, t('refresh'))),
|
|
427
|
+
selected.size > 0 && h('div', { key: 'sel', className: 'rz-stats' },
|
|
428
|
+
h('span', null, t('selectedStats', { n: selected.size, tokens: formatNum(selectedTokens) })),
|
|
397
429
|
h('span', { className: 'rz-spacer' }),
|
|
398
430
|
h('div', { className: 'rz-footbtns' },
|
|
399
|
-
h('button', { className: 'rz-btn', onClick:
|
|
400
|
-
h('button', { className: 'rz-btn', onClick: () => setSelected(new Set()), disabled: selected.size === 0 }, t('clearSelect')),
|
|
431
|
+
h('button', { className: 'rz-btn', onClick: () => setSelected(new Set()) }, t('clearSelect')),
|
|
401
432
|
h('button', { className: 'rz-btn rz-btn-danger', disabled: !canDelete,
|
|
402
433
|
title: busy ? t('deleteBusyHint') : undefined,
|
|
403
434
|
onClick: () => setConfirming(true) },
|
|
404
|
-
t('deleteSelected') +
|
|
435
|
+
t('deleteSelected') + ` (${selected.size})`))),
|
|
405
436
|
visible.length === 0
|
|
406
|
-
? h('div', { key: 'empty', className: 'rz-empty' },
|
|
437
|
+
? h('div', { key: 'empty', className: 'rz-empty' }, t('emptyContext'))
|
|
407
438
|
: h('div', { key: 'list', className: 'rz-list' },
|
|
408
439
|
h(PagedList, { items: visible, t, render: entry => {
|
|
409
|
-
const
|
|
410
|
-
return h('div', { key: entry.seq, className: 'rz-row' +
|
|
440
|
+
const tier = tierOf(entry)
|
|
441
|
+
return h('div', { key: entry.seq, className: 'rz-row tier-' + tier + (selected.has(entry.seq) ? ' checked' : ''),
|
|
411
442
|
role: 'button', tabIndex: 0, onClick: () => toggleRow(entry.seq),
|
|
412
443
|
onKeyDown: e => e.key === 'Enter' && toggleRow(entry.seq) },
|
|
413
444
|
h('input', { type: 'checkbox', checked: selected.has(entry.seq), onClick: e => e.stopPropagation(), onChange: () => toggleRow(entry.seq) }),
|
|
414
|
-
h(
|
|
415
|
-
h(
|
|
416
|
-
h(
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
h('span', { className: 'rz-row-meta' }, 'seq ' + entry.seq))
|
|
445
|
+
h(TokenBadge, { entry }),
|
|
446
|
+
h(Chip, { ...entryChip(entry, t) }),
|
|
447
|
+
h('span', { className: 'rz-row-preview', title: entry.preview,
|
|
448
|
+
onClick: e => { e.stopPropagation(); openDetail(entry) } }, entry.preview || ' '),
|
|
449
|
+
h('span', { className: 'rz-row-meta', title: 'seq ' + entry.seq }, formatDateTime(entry.time)))
|
|
420
450
|
} })),
|
|
421
|
-
detail && h(DetailModal, { detail,
|
|
451
|
+
detail && h(DetailModal, { detail, t, onClose: () => setDetail(null) }),
|
|
422
452
|
confirming && h(ConfirmDialog, { n: selected.size, tokens: formatNum(selectedTokens), deleting, t,
|
|
423
453
|
onCancel: () => setConfirming(false), onOk: doDelete }),
|
|
424
454
|
],
|
|
@@ -432,7 +462,7 @@ window.__ModuleLoader__.load({
|
|
|
432
462
|
module.exports = {
|
|
433
463
|
name: CLIENT_NAME,
|
|
434
464
|
inject: ['slots', 'locale'],
|
|
435
|
-
__internals: { NS, ZH, EN,
|
|
465
|
+
__internals: { NS, ZH, EN, sortEntries, tierOf, RAZOR_TIERS, formatDateTime, entryChip },
|
|
436
466
|
__boot(container, opts = {}) {
|
|
437
467
|
ensureStyles()
|
|
438
468
|
const t = opts.t || ((key, vars) => {
|
|
@@ -462,21 +492,20 @@ window.__ModuleLoader__.load({
|
|
|
462
492
|
}
|
|
463
493
|
}
|
|
464
494
|
} catch (e) { try { console.error('[context-razor] locale init:', e) } catch {} }
|
|
495
|
+
// 会话视图标签:order 30 排在 Chat(0)/Trajectory(10)/Context(20)/Hermes Loop(25) 之后
|
|
465
496
|
ctx.effect(() => {
|
|
466
497
|
try {
|
|
467
|
-
ctx.slots.inject('
|
|
468
|
-
name: '
|
|
498
|
+
ctx.slots.inject('conversation.view', () => ctx.slots.register({
|
|
499
|
+
name: 'conversation.view',
|
|
469
500
|
id: CLIENT_NAME,
|
|
470
|
-
order:
|
|
501
|
+
order: 30,
|
|
471
502
|
locale: NS,
|
|
472
503
|
label: () => t('title'),
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
useEffect(ensureStyles, [])
|
|
476
|
-
return h(RazorPage, { t })
|
|
504
|
+
}, function RazorViewSlot(props) {
|
|
505
|
+
return h(RazorPage, { t, fixedSessionId: props && props.sessionId })
|
|
477
506
|
}))
|
|
478
|
-
} catch (e) { (globalThis.__rzErrors = globalThis.__rzErrors || []).push('
|
|
479
|
-
}, 'context-razor:
|
|
507
|
+
} catch (e) { (globalThis.__rzErrors = globalThis.__rzErrors || []).push('conversation.view:' + (e && e.message)); throw e }
|
|
508
|
+
}, 'context-razor: conversation view tab')
|
|
480
509
|
},
|
|
481
510
|
}
|
|
482
511
|
|
package/client/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dsh-plugin-razor — Browser half.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* session,
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* Conversation-view tab surface: the model-visible context entries of the
|
|
5
|
+
* CURRENT session, each with a rainbow tier (≈token, log-scale buckets —
|
|
6
|
+
* warmer = heavier) doubling as filter buttons; select and razor away.
|
|
7
|
+
* All copy comes from the locale registry (`zh`/`en`); tier hues are data
|
|
8
|
+
* (hsl computed per bucket). No class components — render errors land in
|
|
9
|
+
* globalThis.__rzErrors.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
let __React = null
|
|
@@ -48,32 +48,30 @@ const NS = 'contextRazor'
|
|
|
48
48
|
|
|
49
49
|
const ZH = {
|
|
50
50
|
title: '上下文剃刀',
|
|
51
|
-
|
|
52
|
-
sessionLabel: '会话',
|
|
53
|
-
pickSession: '选择会话…',
|
|
51
|
+
pickSession: '在会话顶部标签打开以查看上下文',
|
|
54
52
|
loading: '正在加载…',
|
|
55
53
|
refresh: '刷新',
|
|
56
54
|
busyTag: '运行中',
|
|
57
|
-
idleTag: '空闲',
|
|
58
|
-
thresholdLabel: '标红阈值',
|
|
59
|
-
thresholdUnit: 'token',
|
|
60
55
|
sortLabel: '排序',
|
|
61
56
|
sortOrder: '上下文顺序',
|
|
57
|
+
sortSeq: '原始序号',
|
|
62
58
|
sortTokens: 'token 高→低',
|
|
63
|
-
|
|
64
|
-
|
|
59
|
+
orderNote: '本插件不会改变上下文条目顺序,只会剔除选中条目。',
|
|
60
|
+
selectVisible: '选中可见',
|
|
61
|
+
selectVisibleHint: '选中当前显示的全部条目(配合色块筛选)',
|
|
65
62
|
clearSelect: '清空选择',
|
|
66
63
|
deleteSelected: '删除选中',
|
|
67
64
|
selectedStats: '已选 {n} 条 / ≈{tokens} token',
|
|
68
65
|
stats: '{nodes} 条 · 合计 ≈{tokens} token{mode}',
|
|
69
66
|
modeHeuristic: '(启发式估算:词表未加载)',
|
|
70
67
|
kindUser: '用户',
|
|
68
|
+
kindInjected: '注入',
|
|
71
69
|
kindAssistant: '助手',
|
|
72
70
|
kindTool: '工具',
|
|
73
71
|
emptyContext: '该会话上下文为空',
|
|
74
72
|
showMore: '显示更多(剩余 {n})',
|
|
75
73
|
detailTitle: '条目详情',
|
|
76
|
-
|
|
74
|
+
detailChars: '{chars} 字符',
|
|
77
75
|
detailUsage: '真实用量:输入 {input} / 输出 {output}{cache}',
|
|
78
76
|
detailUsageCache: ' / 缓存读 {cache}',
|
|
79
77
|
close: '关闭',
|
|
@@ -89,32 +87,30 @@ const ZH = {
|
|
|
89
87
|
|
|
90
88
|
const EN = {
|
|
91
89
|
title: 'Context Razor',
|
|
92
|
-
|
|
93
|
-
sessionLabel: 'Session',
|
|
94
|
-
pickSession: 'Pick a session…',
|
|
90
|
+
pickSession: 'Open a conversation tab to inspect its context',
|
|
95
91
|
loading: 'Loading…',
|
|
96
92
|
refresh: 'Refresh',
|
|
97
93
|
busyTag: 'running',
|
|
98
|
-
idleTag: 'idle',
|
|
99
|
-
thresholdLabel: 'Red threshold',
|
|
100
|
-
thresholdUnit: 'tokens',
|
|
101
94
|
sortLabel: 'Sort',
|
|
102
95
|
sortOrder: 'Context order',
|
|
96
|
+
sortSeq: 'Original seq',
|
|
103
97
|
sortTokens: 'tokens high→low',
|
|
104
|
-
|
|
105
|
-
|
|
98
|
+
orderNote: 'This plugin never reorders context entries; it only removes the selected ones.',
|
|
99
|
+
selectVisible: 'Select shown',
|
|
100
|
+
selectVisibleHint: 'Select every currently shown entry (pairs with tier filters)',
|
|
106
101
|
clearSelect: 'Clear selection',
|
|
107
102
|
deleteSelected: 'Delete selected',
|
|
108
103
|
selectedStats: '{n} selected / ≈{tokens} tokens',
|
|
109
104
|
stats: '{nodes} entries · total ≈{tokens} tokens{mode}',
|
|
110
105
|
modeHeuristic: ' (heuristic: ranks unavailable)',
|
|
111
106
|
kindUser: 'User',
|
|
107
|
+
kindInjected: 'injected',
|
|
112
108
|
kindAssistant: 'Assistant',
|
|
113
109
|
kindTool: 'Tool',
|
|
114
110
|
emptyContext: 'This session has no context entries',
|
|
115
111
|
showMore: 'Show more ({n} left)',
|
|
116
112
|
detailTitle: 'Entry detail',
|
|
117
|
-
|
|
113
|
+
detailChars: '{chars} chars',
|
|
118
114
|
detailUsage: 'Actual usage: in {input} / out {output}{cache}',
|
|
119
115
|
detailUsageCache: ' / cache-read {cache}',
|
|
120
116
|
close: 'Close',
|
|
@@ -132,18 +128,12 @@ const EN = {
|
|
|
132
128
|
|
|
133
129
|
const API = '/context-razor/api'
|
|
134
130
|
const PAGE_SIZE = 150
|
|
135
|
-
const DEFAULT_THRESHOLD = 500
|
|
136
|
-
const THRESHOLD_KEY = 'context-razor-threshold'
|
|
137
131
|
|
|
138
132
|
const kindI18n = (key) => 'kind' + key[0].toUpperCase() + key.slice(1)
|
|
139
133
|
|
|
140
|
-
/**
|
|
141
|
-
function overThreshold(entry, threshold) {
|
|
142
|
-
return threshold > 0 && typeof entry.tokens === 'number' && entry.tokens > threshold
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/** 排序:order = 投影原序;tokens = 降序(并列按 seq,缺 token 沉底)。 */
|
|
134
|
+
/** 排序:order = 投影原序(模型可见序);seq = 原始序号升序(裁剪过的会话里与 order 不同:标记消息 seq 大但在中间);tokens = 降序(并列按 seq,缺 token 沉底)。 */
|
|
146
135
|
function sortEntries(entries, sortBy) {
|
|
136
|
+
if (sortBy === 'seq') return [...entries].sort((a, b) => a.seq - b.seq)
|
|
147
137
|
if (sortBy !== 'tokens') return entries
|
|
148
138
|
return [...entries].sort((a, b) => {
|
|
149
139
|
const va = a.tokens, vb = b.tokens
|
|
@@ -156,6 +146,41 @@ function sortEntries(entries, sortBy) {
|
|
|
156
146
|
|
|
157
147
|
const formatNum = (n) => Number.isFinite(n) ? n.toLocaleString('en-US') : '-'
|
|
158
148
|
|
|
149
|
+
/** 完整日期时间(短格式:2026/9/2 11:03;ts 兼容 epoch 毫秒与 ISO 串)。 */
|
|
150
|
+
function formatDateTime(ts) {
|
|
151
|
+
if (!ts) return ''
|
|
152
|
+
const t = typeof ts === 'number' ? ts : Date.parse(ts)
|
|
153
|
+
if (!Number.isFinite(t)) return ''
|
|
154
|
+
try { return new Date(t).toLocaleString([], { dateStyle: 'short', timeStyle: 'short' }) } catch { return new Date(t).toLocaleString() }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** 行首 chip 的文案与提示:工具结果显示工具名,注入类 user 消息标「注入」。 */
|
|
158
|
+
function entryChip(entry, t) {
|
|
159
|
+
if (entry.kind === 'tool') {
|
|
160
|
+
return { kind: 'tool', label: entry.tool || t('kindTool'), title: entry.tool }
|
|
161
|
+
}
|
|
162
|
+
if (entry.kind === 'user' && entry.sourceKind && entry.sourceKind !== 'user') {
|
|
163
|
+
return { kind: 'user', label: t('kindInjected'), title: [entry.sourceKind, entry.sourceForm, entry.sourcePlugin].filter(Boolean).join(' · ') }
|
|
164
|
+
}
|
|
165
|
+
return { kind: entry.kind, label: t(kindI18n(entry.kind)) }
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ── 彩虹分级:颜色越暖 = 占用越多 ────────────────────────────────────────
|
|
169
|
+
// 固定对数档位(相邻约 ×2.5),跨会话语义稳定:绿→黄绿→黄→橙→红→品红。
|
|
170
|
+
// 档位是「这条消息吃掉多少典型上下文预算」的粗标尺,不随会话内最大值缩放。
|
|
171
|
+
const RAZOR_TIERS = [
|
|
172
|
+
{ max: 100, hue: 120, label: '0-100' },
|
|
173
|
+
{ max: 300, hue: 90, label: '100-300' },
|
|
174
|
+
{ max: 800, hue: 60, label: '300-800' },
|
|
175
|
+
{ max: 2000, hue: 32, label: '800-2k' },
|
|
176
|
+
{ max: 5000, hue: 8, label: '2k-5k' },
|
|
177
|
+
{ max: Infinity, hue: 320, label: '>5k' },
|
|
178
|
+
]
|
|
179
|
+
function tierOf(entry) {
|
|
180
|
+
const tokens = typeof entry.tokens === 'number' ? entry.tokens : 0
|
|
181
|
+
return RAZOR_TIERS.findIndex(t => tokens <= t.max)
|
|
182
|
+
}
|
|
183
|
+
|
|
159
184
|
async function getJson(url) {
|
|
160
185
|
const r = await fetch(url)
|
|
161
186
|
if (!r.ok) {
|
|
@@ -168,28 +193,47 @@ async function getJson(url) {
|
|
|
168
193
|
// ── Token-based stylesheet (light/dark adaptive by construction) ────────
|
|
169
194
|
|
|
170
195
|
const STYLE = `<style>
|
|
171
|
-
.rz-page{
|
|
196
|
+
.rz-page,.rz-page *{box-sizing:border-box}
|
|
197
|
+
.rz-page{position:relative;display:flex;flex-direction:column;gap:12px;padding:16px 20px;min-width:0;color:var(--dsw-alias-label-primary);font-family:var(--dsw-font-family);font-size:var(--dsw-font-sm-14,14px)}
|
|
172
198
|
.rz-hint{color:var(--dsw-alias-label-secondary);font-size:12.5px;line-height:1.5}
|
|
173
199
|
.rz-toolbar{display:flex;gap:10px;align-items:center;flex-wrap:wrap}
|
|
174
200
|
.rz-spacer{flex:1}
|
|
175
201
|
.rz-label{color:var(--dsw-alias-label-secondary);font-size:12.5px;white-space:nowrap}
|
|
176
|
-
.rz-input{min-height:30px;padding:4px 10px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-specific-input-major,var(--dsw-alias-bg-layer-1));color:var(--dsw-alias-label-primary);font-size:13px;font-family:var(--dsw-font-family);outline:none}
|
|
177
|
-
.rz-input:focus{border-color:var(--dsw-alias-state-business-primary)}
|
|
178
202
|
.rz-select{min-height:30px;padding:4px 10px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-specific-input-major,var(--dsw-alias-bg-layer-1));color:var(--dsw-alias-label-primary);font-size:13px;font-family:var(--dsw-font-family);outline:none}
|
|
179
203
|
.rz-stats{display:flex;gap:12px;align-items:center;flex-wrap:wrap;color:var(--dsw-alias-label-secondary);font-size:12.5px}
|
|
180
204
|
.rz-badge{display:inline-flex;align-items:center;padding:1px 8px;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;white-space:nowrap}
|
|
181
|
-
.rz-badge.over{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary);font-weight:600}
|
|
182
205
|
.rz-chip{display:inline-flex;align-items:center;padding:1px 8px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);font-size:11.5px;white-space:nowrap;flex:none}
|
|
183
206
|
.rz-chip.user{color:var(--dsw-alias-state-business-primary);border-color:var(--dsw-alias-state-business-primary)}
|
|
184
207
|
.rz-chip.assistant{color:var(--dsw-alias-state-success-primary);border-color:var(--dsw-alias-state-success-primary)}
|
|
208
|
+
.rz-legend{display:inline-flex;gap:4px;align-items:center}
|
|
209
|
+
.rz-swatch{border:none;cursor:pointer;padding:2px 8px;border-radius:999px;font-size:10.5px;color:var(--dsw-alias-label-primary-inverted,#fff);white-space:nowrap;font-family:var(--dsw-font-family)}
|
|
210
|
+
.rz-legend.filtering .rz-swatch:not(.on){opacity:.4}
|
|
211
|
+
.rz-swatch.on{box-shadow:0 0 0 2px var(--dsw-alias-border-l3,rgba(128,128,128,.65))}
|
|
212
|
+
.rz-swatch.tier-0{background:hsl(120,55%,40%)}
|
|
213
|
+
.rz-swatch.tier-1{background:hsl(90,60%,38%)}
|
|
214
|
+
.rz-swatch.tier-2{background:hsl(60,70%,36%)}
|
|
215
|
+
.rz-swatch.tier-3{background:hsl(32,80%,45%)}
|
|
216
|
+
.rz-swatch.tier-4{background:hsl(8,75%,48%)}
|
|
217
|
+
.rz-swatch.tier-5{background:hsl(320,65%,50%)}
|
|
185
218
|
.rz-list{display:flex;flex-direction:column;gap:6px}
|
|
186
219
|
.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%}
|
|
187
220
|
.rz-row:hover{background:var(--dsw-alias-interactive-bg-hover)}
|
|
188
|
-
.rz-row.over{border-left-color:var(--dsw-alias-state-error-primary)}
|
|
189
221
|
.rz-row.checked{border-color:var(--dsw-alias-state-business-primary)}
|
|
190
|
-
.rz-row-preview{flex:1;min-width:
|
|
191
|
-
.rz-row
|
|
222
|
+
.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
|
+
.rz-row-preview:hover{text-decoration:underline;text-underline-offset:3px}
|
|
192
224
|
.rz-row-meta{color:var(--dsw-alias-label-tertiary);font-size:11px;flex:none}
|
|
225
|
+
.rz-badge.tier-0{color:hsl(120,55%,40%);border-color:hsla(120,55%,40%,.45);background:hsla(120,55%,40%,.10)}
|
|
226
|
+
.rz-badge.tier-1{color:hsl(90,60%,38%);border-color:hsla(90,60%,38%,.45);background:hsla(90,60%,38%,.10)}
|
|
227
|
+
.rz-badge.tier-2{color:hsl(60,70%,36%);border-color:hsla(60,70%,36%,.45);background:hsla(60,70%,36%,.12)}
|
|
228
|
+
.rz-badge.tier-3{color:hsl(32,80%,45%);border-color:hsla(32,80%,45%,.5);background:hsla(32,80%,45%,.12)}
|
|
229
|
+
.rz-badge.tier-4{color:hsl(8,75%,48%);border-color:hsla(8,75%,48%,.5);background:hsla(8,75%,48%,.12);font-weight:600}
|
|
230
|
+
.rz-badge.tier-5{color:hsl(320,65%,50%);border-color:hsla(320,65%,50%,.5);background:hsla(320,65%,50%,.12);font-weight:600}
|
|
231
|
+
.rz-row.tier-0{border-left-color:hsl(120,55%,40%)}
|
|
232
|
+
.rz-row.tier-1{border-left-color:hsl(90,60%,38%)}
|
|
233
|
+
.rz-row.tier-2{border-left-color:hsl(60,70%,36%)}
|
|
234
|
+
.rz-row.tier-3{border-left-color:hsl(32,80%,45%)}
|
|
235
|
+
.rz-row.tier-4{border-left-color:hsl(8,75%,48%)}
|
|
236
|
+
.rz-row.tier-5{border-left-color:hsl(320,65%,50%)}
|
|
193
237
|
.rz-empty{border:1px dashed var(--dsw-alias-border-l2);border-radius:12px;padding:36px 20px;text-align:center;color:var(--dsw-alias-label-secondary)}
|
|
194
238
|
.rz-loading{padding:36px;text-align:center;color:var(--dsw-alias-label-secondary)}
|
|
195
239
|
.rz-footbtns{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
|
|
@@ -208,11 +252,11 @@ const STYLE = `<style>
|
|
|
208
252
|
|
|
209
253
|
// ── Small building blocks ────────────────────────────────────────────────
|
|
210
254
|
|
|
211
|
-
const Chip = ({ kind, label }) => h('span', { className: 'rz-chip ' + kind }, label)
|
|
255
|
+
const Chip = ({ kind, label, title }) => h('span', { className: 'rz-chip ' + kind, title }, label)
|
|
212
256
|
|
|
213
|
-
const TokenBadge = ({ entry
|
|
214
|
-
const
|
|
215
|
-
return h('span', { className: 'rz-badge' +
|
|
257
|
+
const TokenBadge = ({ entry }) => {
|
|
258
|
+
const tier = tierOf(entry)
|
|
259
|
+
return h('span', { className: 'rz-badge tier-' + tier, title: RAZOR_TIERS[tier].label + ' token' },
|
|
216
260
|
'≈' + formatNum(entry.tokens))
|
|
217
261
|
}
|
|
218
262
|
|
|
@@ -228,15 +272,16 @@ function PagedList({ items, render, t }) {
|
|
|
228
272
|
}
|
|
229
273
|
|
|
230
274
|
/** 单条全文弹窗(正文经 /entry 异步补全)。 */
|
|
231
|
-
function DetailModal({ detail,
|
|
275
|
+
function DetailModal({ detail, t, onClose }) {
|
|
232
276
|
if (!detail) return null
|
|
233
277
|
return h('div', { className: 'rz-dlg-backdrop', onClick: onClose },
|
|
234
278
|
h('div', { className: 'rz-dlg', onClick: e => e.stopPropagation() },
|
|
235
279
|
h('h3', null, t('detailTitle') + ' · ' + t('seqLabel', { seq: detail.seq })),
|
|
236
280
|
h('div', { className: 'rz-stats', style: { marginBottom: 10 } },
|
|
237
|
-
h(Chip, {
|
|
238
|
-
h(TokenBadge, { entry: detail
|
|
239
|
-
h('span', { className: 'rz-label'
|
|
281
|
+
h(Chip, { ...entryChip(detail, t) }),
|
|
282
|
+
h(TokenBadge, { entry: detail }),
|
|
283
|
+
h('span', { className: 'rz-label', title: 'seq ' + detail.seq }, formatDateTime(detail.time)),
|
|
284
|
+
h('span', { className: 'rz-label' }, t('detailChars', { chars: formatNum(detail.chars) })),
|
|
240
285
|
detail.usage && h('span', { className: 'rz-label' }, t('detailUsage', {
|
|
241
286
|
input: formatNum(detail.usage.input), output: formatNum(detail.usage.output),
|
|
242
287
|
cache: detail.usage.cacheRead ? t('detailUsageCache', { cache: formatNum(detail.usage.cacheRead) }) : '' }))),
|
|
@@ -258,17 +303,14 @@ function ConfirmDialog({ n, tokens, deleting, t, onCancel, onOk }) {
|
|
|
258
303
|
|
|
259
304
|
// ── Page ────────────────────────────────────────────────────────────────
|
|
260
305
|
|
|
261
|
-
function RazorPage({ t }) {
|
|
262
|
-
|
|
306
|
+
function RazorPage({ t, fixedSessionId }) {
|
|
307
|
+
useEffect(ensureStyles, [])
|
|
263
308
|
const [sessionId, setSessionId] = useState('')
|
|
264
309
|
const [context, setContext] = useState(null)
|
|
265
310
|
const [ctxLoading, setCtxLoading] = useState(false)
|
|
266
311
|
const [error, setError] = useState(null)
|
|
267
|
-
const [
|
|
268
|
-
try { const v = Number(localStorage.getItem(THRESHOLD_KEY)); return Number.isFinite(v) && v > 0 ? v : DEFAULT_THRESHOLD } catch { return DEFAULT_THRESHOLD }
|
|
269
|
-
})
|
|
312
|
+
const [tierFilter, setTierFilter] = useState(() => new Set())
|
|
270
313
|
const [sortBy, setSortBy] = useState('order')
|
|
271
|
-
const [overOnly, setOverOnly] = useState(false)
|
|
272
314
|
const [selected, setSelected] = useState(() => new Set())
|
|
273
315
|
const [detail, setDetail] = useState(null)
|
|
274
316
|
const [confirming, setConfirming] = useState(false)
|
|
@@ -278,8 +320,10 @@ function RazorPage({ t }) {
|
|
|
278
320
|
|
|
279
321
|
const showToast = (text) => { setToast(text); setTimeout(() => setToast(null), 3000) }
|
|
280
322
|
|
|
281
|
-
|
|
282
|
-
useEffect(() => {
|
|
323
|
+
// 会话视图挂载:只管当前会话(fixedSessionId 来自 slot props)
|
|
324
|
+
useEffect(() => {
|
|
325
|
+
if (fixedSessionId) { sessionRef.current = fixedSessionId; setSessionId(fixedSessionId) }
|
|
326
|
+
}, [fixedSessionId])
|
|
283
327
|
|
|
284
328
|
const loadContext = (id) => {
|
|
285
329
|
if (!id) { setContext(null); return }
|
|
@@ -293,25 +337,16 @@ function RazorPage({ t }) {
|
|
|
293
337
|
useEffect(() => { if (sessionId) loadContext(sessionId) }, [sessionId])
|
|
294
338
|
|
|
295
339
|
const refreshAll = () => {
|
|
296
|
-
loadSessions()
|
|
297
340
|
if (sessionRef.current) loadContext(sessionRef.current)
|
|
298
341
|
}
|
|
299
342
|
|
|
300
|
-
const pickSession = (id) => { sessionRef.current = id; setSessionId(id) }
|
|
301
|
-
const changeThreshold = (v) => {
|
|
302
|
-
const n = Number(v)
|
|
303
|
-
setThreshold(Number.isFinite(n) && n > 0 ? n : 0)
|
|
304
|
-
try { localStorage.setItem(THRESHOLD_KEY, String(n)) } catch {}
|
|
305
|
-
}
|
|
306
|
-
|
|
307
343
|
const visible = useMemo(() => {
|
|
308
344
|
if (!context) return []
|
|
309
345
|
let rows = context.entries
|
|
310
|
-
if (
|
|
346
|
+
if (tierFilter.size > 0) rows = rows.filter(e => tierFilter.has(tierOf(e)))
|
|
311
347
|
return sortEntries(rows, sortBy)
|
|
312
|
-
}, [context,
|
|
348
|
+
}, [context, tierFilter, sortBy])
|
|
313
349
|
|
|
314
|
-
const overList = useMemo(() => (context ? context.entries.filter(e => overThreshold(e, threshold)) : []), [context, threshold])
|
|
315
350
|
const selectedTokens = useMemo(() => {
|
|
316
351
|
if (!context) return 0
|
|
317
352
|
const bySeq = new Map(context.entries.map(e => [e.seq, e]))
|
|
@@ -326,12 +361,18 @@ function RazorPage({ t }) {
|
|
|
326
361
|
else next.add(seq)
|
|
327
362
|
return next
|
|
328
363
|
})
|
|
329
|
-
const
|
|
364
|
+
const toggleTier = (i) => setTierFilter(prev => {
|
|
365
|
+
const next = new Set(prev)
|
|
366
|
+
if (next.has(i)) next.delete(i)
|
|
367
|
+
else next.add(i)
|
|
368
|
+
return next
|
|
369
|
+
})
|
|
370
|
+
const selectVisible = () => setSelected(new Set(visible.map(e => e.seq)))
|
|
330
371
|
|
|
331
372
|
const openDetail = (entry) => {
|
|
332
373
|
setDetail(entry)
|
|
333
374
|
getJson(`${API}/entry?session=${encodeURIComponent(sessionId)}&seq=${entry.seq}`)
|
|
334
|
-
.then(d => setDetail(cur => (cur && cur.seq === entry.seq) ? { ...cur, text: d.text, tokens: d.tokens } : cur))
|
|
375
|
+
.then(d => setDetail(cur => (cur && cur.seq === entry.seq) ? { ...cur, text: d.text, tokens: d.tokens, tool: d.tool, sourceKind: d.sourceKind, sourceForm: d.sourceForm, sourcePlugin: d.sourcePlugin } : cur))
|
|
335
376
|
.catch(() => {})
|
|
336
377
|
}
|
|
337
378
|
|
|
@@ -355,60 +396,49 @@ function RazorPage({ t }) {
|
|
|
355
396
|
const canDelete = selected.size > 0 && !busy && !deleting
|
|
356
397
|
|
|
357
398
|
return h('div', { className: 'rz-page' },
|
|
358
|
-
h('div', { className: 'rz-hint' }, t('hint')),
|
|
359
|
-
h('div', { className: 'rz-toolbar' },
|
|
360
|
-
h('span', { className: 'rz-label' }, t('sessionLabel')),
|
|
361
|
-
h('select', { className: 'rz-select', value: sessionId, onChange: e => pickSession(e.target.value), style: { maxWidth: 380 } },
|
|
362
|
-
h('option', { value: '' }, t('pickSession')),
|
|
363
|
-
(sessions || []).map(s => h('option', { key: s.id, value: s.id },
|
|
364
|
-
(s.cwd ? s.cwd.split('/').pop() + ' · ' : '') + s.id.slice(0, 8) + ' · ' + s.nodes))),
|
|
365
|
-
h('button', { className: 'rz-btn', onClick: refreshAll, title: t('refresh') }, t('refresh')),
|
|
366
|
-
context && h('span', { className: 'rz-badge' }, context.busy ? t('busyTag') : t('idleTag'))),
|
|
367
399
|
error && h('div', { className: 'rz-hint', style: { color: 'var(--dsw-alias-state-error-primary)' } }, t('operationFailed') + ': ' + error),
|
|
368
|
-
!sessionId && (
|
|
369
|
-
? h('div', { className: 'rz-loading' }, t('loading'))
|
|
370
|
-
: h('div', { className: 'rz-empty' }, t('pickSession'))),
|
|
400
|
+
!sessionId && h('div', { className: 'rz-empty' }, t('pickSession')),
|
|
371
401
|
sessionId && ctxLoading && h('div', { className: 'rz-loading' }, t('loading')),
|
|
372
402
|
sessionId && !ctxLoading && context && [
|
|
373
|
-
h('div', { key: '
|
|
403
|
+
h('div', { key: 'note', className: 'rz-hint' }, t('orderNote')),
|
|
404
|
+
h('div', { key: 'bar', className: 'rz-stats' },
|
|
374
405
|
h('span', null, t('stats', { nodes: context.nodes, tokens: formatNum(context.totalTokens), mode: context.encoder === 'heuristic' ? t('modeHeuristic') : '' })),
|
|
406
|
+
h('span', { className: 'rz-legend' + (tierFilter.size > 0 ? ' filtering' : ''), title: t('legendTitle') },
|
|
407
|
+
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))),
|
|
375
408
|
h('span', { className: 'rz-spacer' }),
|
|
376
|
-
h('
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
t('thresholdUnit')),
|
|
409
|
+
busy && h('span', { className: 'rz-badge' }, t('busyTag')),
|
|
410
|
+
h('button', { className: 'rz-btn', onClick: selectVisible, disabled: visible.length === 0 || busy, title: t('selectVisibleHint') },
|
|
411
|
+
t('selectVisible') + (visible.length ? ` (${visible.length})` : '')),
|
|
380
412
|
h('select', { className: 'rz-select', value: sortBy, onChange: e => setSortBy(e.target.value), title: t('sortLabel'), 'aria-label': t('sortLabel') },
|
|
381
413
|
h('option', { value: 'order' }, t('sortOrder')),
|
|
414
|
+
h('option', { value: 'seq' }, t('sortSeq')),
|
|
382
415
|
h('option', { value: 'tokens' }, t('sortTokens'))),
|
|
383
|
-
h('
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
h('span', null, selected.size > 0 ? t('selectedStats', { n: selected.size, tokens: formatNum(selectedTokens) }) : ' '),
|
|
416
|
+
h('button', { className: 'rz-btn', onClick: refreshAll, title: t('refresh') }, t('refresh'))),
|
|
417
|
+
selected.size > 0 && h('div', { key: 'sel', className: 'rz-stats' },
|
|
418
|
+
h('span', null, t('selectedStats', { n: selected.size, tokens: formatNum(selectedTokens) })),
|
|
387
419
|
h('span', { className: 'rz-spacer' }),
|
|
388
420
|
h('div', { className: 'rz-footbtns' },
|
|
389
|
-
h('button', { className: 'rz-btn', onClick:
|
|
390
|
-
h('button', { className: 'rz-btn', onClick: () => setSelected(new Set()), disabled: selected.size === 0 }, t('clearSelect')),
|
|
421
|
+
h('button', { className: 'rz-btn', onClick: () => setSelected(new Set()) }, t('clearSelect')),
|
|
391
422
|
h('button', { className: 'rz-btn rz-btn-danger', disabled: !canDelete,
|
|
392
423
|
title: busy ? t('deleteBusyHint') : undefined,
|
|
393
424
|
onClick: () => setConfirming(true) },
|
|
394
|
-
t('deleteSelected') +
|
|
425
|
+
t('deleteSelected') + ` (${selected.size})`))),
|
|
395
426
|
visible.length === 0
|
|
396
|
-
? h('div', { key: 'empty', className: 'rz-empty' },
|
|
427
|
+
? h('div', { key: 'empty', className: 'rz-empty' }, t('emptyContext'))
|
|
397
428
|
: h('div', { key: 'list', className: 'rz-list' },
|
|
398
429
|
h(PagedList, { items: visible, t, render: entry => {
|
|
399
|
-
const
|
|
400
|
-
return h('div', { key: entry.seq, className: 'rz-row' +
|
|
430
|
+
const tier = tierOf(entry)
|
|
431
|
+
return h('div', { key: entry.seq, className: 'rz-row tier-' + tier + (selected.has(entry.seq) ? ' checked' : ''),
|
|
401
432
|
role: 'button', tabIndex: 0, onClick: () => toggleRow(entry.seq),
|
|
402
433
|
onKeyDown: e => e.key === 'Enter' && toggleRow(entry.seq) },
|
|
403
434
|
h('input', { type: 'checkbox', checked: selected.has(entry.seq), onClick: e => e.stopPropagation(), onChange: () => toggleRow(entry.seq) }),
|
|
404
|
-
h(
|
|
405
|
-
h(
|
|
406
|
-
h(
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
h('span', { className: 'rz-row-meta' }, 'seq ' + entry.seq))
|
|
435
|
+
h(TokenBadge, { entry }),
|
|
436
|
+
h(Chip, { ...entryChip(entry, t) }),
|
|
437
|
+
h('span', { className: 'rz-row-preview', title: entry.preview,
|
|
438
|
+
onClick: e => { e.stopPropagation(); openDetail(entry) } }, entry.preview || ' '),
|
|
439
|
+
h('span', { className: 'rz-row-meta', title: 'seq ' + entry.seq }, formatDateTime(entry.time)))
|
|
410
440
|
} })),
|
|
411
|
-
detail && h(DetailModal, { detail,
|
|
441
|
+
detail && h(DetailModal, { detail, t, onClose: () => setDetail(null) }),
|
|
412
442
|
confirming && h(ConfirmDialog, { n: selected.size, tokens: formatNum(selectedTokens), deleting, t,
|
|
413
443
|
onCancel: () => setConfirming(false), onOk: doDelete }),
|
|
414
444
|
],
|
|
@@ -422,7 +452,7 @@ const CLIENT_NAME = '@weibaohui/context-razor'
|
|
|
422
452
|
module.exports = {
|
|
423
453
|
name: CLIENT_NAME,
|
|
424
454
|
inject: ['slots', 'locale'],
|
|
425
|
-
__internals: { NS, ZH, EN,
|
|
455
|
+
__internals: { NS, ZH, EN, sortEntries, tierOf, RAZOR_TIERS, formatDateTime, entryChip },
|
|
426
456
|
__boot(container, opts = {}) {
|
|
427
457
|
ensureStyles()
|
|
428
458
|
const t = opts.t || ((key, vars) => {
|
|
@@ -452,20 +482,19 @@ module.exports = {
|
|
|
452
482
|
}
|
|
453
483
|
}
|
|
454
484
|
} catch (e) { try { console.error('[context-razor] locale init:', e) } catch {} }
|
|
485
|
+
// 会话视图标签:order 30 排在 Chat(0)/Trajectory(10)/Context(20)/Hermes Loop(25) 之后
|
|
455
486
|
ctx.effect(() => {
|
|
456
487
|
try {
|
|
457
|
-
ctx.slots.inject('
|
|
458
|
-
name: '
|
|
488
|
+
ctx.slots.inject('conversation.view', () => ctx.slots.register({
|
|
489
|
+
name: 'conversation.view',
|
|
459
490
|
id: CLIENT_NAME,
|
|
460
|
-
order:
|
|
491
|
+
order: 30,
|
|
461
492
|
locale: NS,
|
|
462
493
|
label: () => t('title'),
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
useEffect(ensureStyles, [])
|
|
466
|
-
return h(RazorPage, { t })
|
|
494
|
+
}, function RazorViewSlot(props) {
|
|
495
|
+
return h(RazorPage, { t, fixedSessionId: props && props.sessionId })
|
|
467
496
|
}))
|
|
468
|
-
} catch (e) { (globalThis.__rzErrors = globalThis.__rzErrors || []).push('
|
|
469
|
-
}, 'context-razor:
|
|
497
|
+
} catch (e) { (globalThis.__rzErrors = globalThis.__rzErrors || []).push('conversation.view:' + (e && e.message)); throw e }
|
|
498
|
+
}, 'context-razor: conversation view tab')
|
|
470
499
|
},
|
|
471
500
|
}
|
package/docs/demo.gif
CHANGED
|
Binary file
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -87,12 +87,19 @@ function sessionBusy(session) {
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
/**
|
|
90
|
-
* 当前 surface 投影成条目列表:seq 有序(模型可见顺序)、逐条文本、
|
|
91
|
-
*
|
|
92
|
-
*
|
|
90
|
+
* 当前 surface 投影成条目列表:seq 有序(模型可见顺序)、逐条文本、token 估算、
|
|
91
|
+
* 时间、工具名(tool/result 按 callId 关联日志里的 tool/call)与注入来源。
|
|
92
|
+
* 被此前 compaction/prune 影子化的节点本就不在 surface.nodes 里——列表即模型
|
|
93
|
+
* 此刻真实可见的上下文。
|
|
93
94
|
*/
|
|
94
95
|
function projectContext(session) {
|
|
95
96
|
const bySeq = new Map(session.events.map((event) => [event.seq, event]))
|
|
97
|
+
const toolNames = new Map()
|
|
98
|
+
for (const event of session.events) {
|
|
99
|
+
if (event.type === 'tool/call' && event.data && typeof event.data.callId === 'string') {
|
|
100
|
+
toolNames.set(event.data.callId, typeof event.data.name === 'string' ? event.data.name : undefined)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
96
103
|
const entries = []
|
|
97
104
|
let totalTokens = 0
|
|
98
105
|
let mode = 'cl100k'
|
|
@@ -103,11 +110,16 @@ function projectContext(session) {
|
|
|
103
110
|
const { tokens, mode: m } = countTokens(text)
|
|
104
111
|
mode = m
|
|
105
112
|
const usage = event.type === 'assistant/message' ? event.data.usage : undefined
|
|
113
|
+
const source = event.type === 'user/message' && event.data.source && typeof event.data.source === 'object' ? event.data.source : undefined
|
|
106
114
|
entries.push({
|
|
107
115
|
seq,
|
|
108
116
|
kind: entryKindOf(event.type),
|
|
109
117
|
time: event.time,
|
|
110
118
|
turn: typeof event.data.turn === 'number' ? event.data.turn : undefined,
|
|
119
|
+
tool: event.type === 'tool/result' ? toolNames.get(event.data.message && event.data.message.source && event.data.message.source.callId) : undefined,
|
|
120
|
+
sourceKind: source ? source.kind : undefined,
|
|
121
|
+
sourceForm: source ? source.form : undefined,
|
|
122
|
+
sourcePlugin: source ? source.plugin : undefined,
|
|
111
123
|
chars: text.length,
|
|
112
124
|
tokens,
|
|
113
125
|
preview: text.length > 220 ? text.slice(0, 220) + '…' : text,
|
|
@@ -270,7 +282,21 @@ module.exports = {
|
|
|
270
282
|
const event = session.events.find((candidate) => candidate.seq === seq)
|
|
271
283
|
if (event === undefined) { fail(404, 'entry not found'); return }
|
|
272
284
|
const text = entryText(event)
|
|
273
|
-
|
|
285
|
+
const toolName = event.type === 'tool/result' && event.data.message
|
|
286
|
+
? session.events.find((candidate) => candidate.type === 'tool/call' && candidate.data.callId === event.data.message.source.callId)
|
|
287
|
+
: undefined
|
|
288
|
+
const source = event.type === 'user/message' && event.data.source && typeof event.data.source === 'object' ? event.data.source : undefined
|
|
289
|
+
sendJson(200, {
|
|
290
|
+
seq,
|
|
291
|
+
kind: entryKindOf(event.type),
|
|
292
|
+
time: event.time,
|
|
293
|
+
tokens: countTokens(text).tokens,
|
|
294
|
+
text,
|
|
295
|
+
tool: toolName && typeof toolName.data.name === 'string' ? toolName.data.name : undefined,
|
|
296
|
+
sourceKind: source ? source.kind : undefined,
|
|
297
|
+
sourceForm: source ? source.form : undefined,
|
|
298
|
+
sourcePlugin: source ? source.plugin : undefined,
|
|
299
|
+
})
|
|
274
300
|
return
|
|
275
301
|
}
|
|
276
302
|
|