@weibaohui/context-razor 0.1.0 → 0.2.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 +1 -1
- package/client/bundle.js +133 -111
- package/client/index.js +133 -111
- package/docs/demo.gif +0 -0
- package/package.json +1 -1
- package/src/index.js +30 -4
package/README.md
CHANGED
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,28 @@ 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: '上下文顺序',
|
|
72
67
|
sortTokens: 'token 高→低',
|
|
73
|
-
|
|
74
|
-
|
|
68
|
+
selectVisible: '选中可见',
|
|
69
|
+
selectVisibleHint: '选中当前显示的全部条目(配合色块筛选)',
|
|
75
70
|
clearSelect: '清空选择',
|
|
76
71
|
deleteSelected: '删除选中',
|
|
77
72
|
selectedStats: '已选 {n} 条 / ≈{tokens} token',
|
|
78
73
|
stats: '{nodes} 条 · 合计 ≈{tokens} token{mode}',
|
|
79
74
|
modeHeuristic: '(启发式估算:词表未加载)',
|
|
80
75
|
kindUser: '用户',
|
|
76
|
+
kindInjected: '注入',
|
|
81
77
|
kindAssistant: '助手',
|
|
82
78
|
kindTool: '工具',
|
|
83
79
|
emptyContext: '该会话上下文为空',
|
|
84
80
|
showMore: '显示更多(剩余 {n})',
|
|
85
81
|
detailTitle: '条目详情',
|
|
86
|
-
|
|
82
|
+
detailChars: '{chars} 字符',
|
|
87
83
|
detailUsage: '真实用量:输入 {input} / 输出 {output}{cache}',
|
|
88
84
|
detailUsageCache: ' / 缓存读 {cache}',
|
|
89
85
|
close: '关闭',
|
|
@@ -99,32 +95,28 @@ window.__ModuleLoader__.load({
|
|
|
99
95
|
|
|
100
96
|
const EN = {
|
|
101
97
|
title: 'Context Razor',
|
|
102
|
-
|
|
103
|
-
sessionLabel: 'Session',
|
|
104
|
-
pickSession: 'Pick a session…',
|
|
98
|
+
pickSession: 'Open a conversation tab to inspect its context',
|
|
105
99
|
loading: 'Loading…',
|
|
106
100
|
refresh: 'Refresh',
|
|
107
101
|
busyTag: 'running',
|
|
108
|
-
idleTag: 'idle',
|
|
109
|
-
thresholdLabel: 'Red threshold',
|
|
110
|
-
thresholdUnit: 'tokens',
|
|
111
102
|
sortLabel: 'Sort',
|
|
112
103
|
sortOrder: 'Context order',
|
|
113
104
|
sortTokens: 'tokens high→low',
|
|
114
|
-
|
|
115
|
-
|
|
105
|
+
selectVisible: 'Select shown',
|
|
106
|
+
selectVisibleHint: 'Select every currently shown entry (pairs with tier filters)',
|
|
116
107
|
clearSelect: 'Clear selection',
|
|
117
108
|
deleteSelected: 'Delete selected',
|
|
118
109
|
selectedStats: '{n} selected / ≈{tokens} tokens',
|
|
119
110
|
stats: '{nodes} entries · total ≈{tokens} tokens{mode}',
|
|
120
111
|
modeHeuristic: ' (heuristic: ranks unavailable)',
|
|
121
112
|
kindUser: 'User',
|
|
113
|
+
kindInjected: 'injected',
|
|
122
114
|
kindAssistant: 'Assistant',
|
|
123
115
|
kindTool: 'Tool',
|
|
124
116
|
emptyContext: 'This session has no context entries',
|
|
125
117
|
showMore: 'Show more ({n} left)',
|
|
126
118
|
detailTitle: 'Entry detail',
|
|
127
|
-
|
|
119
|
+
detailChars: '{chars} chars',
|
|
128
120
|
detailUsage: 'Actual usage: in {input} / out {output}{cache}',
|
|
129
121
|
detailUsageCache: ' / cache-read {cache}',
|
|
130
122
|
close: 'Close',
|
|
@@ -142,16 +134,9 @@ window.__ModuleLoader__.load({
|
|
|
142
134
|
|
|
143
135
|
const API = '/context-razor/api'
|
|
144
136
|
const PAGE_SIZE = 150
|
|
145
|
-
const DEFAULT_THRESHOLD = 500
|
|
146
|
-
const THRESHOLD_KEY = 'context-razor-threshold'
|
|
147
137
|
|
|
148
138
|
const kindI18n = (key) => 'kind' + key[0].toUpperCase() + key.slice(1)
|
|
149
139
|
|
|
150
|
-
/** 行是否标红(仅 user/assistant/tool 有意义,阈值 <= 0 视为关闭)。 */
|
|
151
|
-
function overThreshold(entry, threshold) {
|
|
152
|
-
return threshold > 0 && typeof entry.tokens === 'number' && entry.tokens > threshold
|
|
153
|
-
}
|
|
154
|
-
|
|
155
140
|
/** 排序:order = 投影原序;tokens = 降序(并列按 seq,缺 token 沉底)。 */
|
|
156
141
|
function sortEntries(entries, sortBy) {
|
|
157
142
|
if (sortBy !== 'tokens') return entries
|
|
@@ -166,6 +151,41 @@ window.__ModuleLoader__.load({
|
|
|
166
151
|
|
|
167
152
|
const formatNum = (n) => Number.isFinite(n) ? n.toLocaleString('en-US') : '-'
|
|
168
153
|
|
|
154
|
+
/** 完整日期时间(短格式:2026/9/2 11:03;ts 兼容 epoch 毫秒与 ISO 串)。 */
|
|
155
|
+
function formatDateTime(ts) {
|
|
156
|
+
if (!ts) return ''
|
|
157
|
+
const t = typeof ts === 'number' ? ts : Date.parse(ts)
|
|
158
|
+
if (!Number.isFinite(t)) return ''
|
|
159
|
+
try { return new Date(t).toLocaleString([], { dateStyle: 'short', timeStyle: 'short' }) } catch { return new Date(t).toLocaleString() }
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** 行首 chip 的文案与提示:工具结果显示工具名,注入类 user 消息标「注入」。 */
|
|
163
|
+
function entryChip(entry, t) {
|
|
164
|
+
if (entry.kind === 'tool') {
|
|
165
|
+
return { kind: 'tool', label: entry.tool || t('kindTool'), title: entry.tool }
|
|
166
|
+
}
|
|
167
|
+
if (entry.kind === 'user' && entry.sourceKind && entry.sourceKind !== 'user') {
|
|
168
|
+
return { kind: 'user', label: t('kindInjected'), title: [entry.sourceKind, entry.sourceForm, entry.sourcePlugin].filter(Boolean).join(' · ') }
|
|
169
|
+
}
|
|
170
|
+
return { kind: entry.kind, label: t(kindI18n(entry.kind)) }
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ── 彩虹分级:颜色越暖 = 占用越多 ────────────────────────────────────────
|
|
174
|
+
// 固定对数档位(相邻约 ×2.5),跨会话语义稳定:绿→黄绿→黄→橙→红→品红。
|
|
175
|
+
// 档位是「这条消息吃掉多少典型上下文预算」的粗标尺,不随会话内最大值缩放。
|
|
176
|
+
const RAZOR_TIERS = [
|
|
177
|
+
{ max: 100, hue: 120, label: '0-100' },
|
|
178
|
+
{ max: 300, hue: 90, label: '100-300' },
|
|
179
|
+
{ max: 800, hue: 60, label: '300-800' },
|
|
180
|
+
{ max: 2000, hue: 32, label: '800-2k' },
|
|
181
|
+
{ max: 5000, hue: 8, label: '2k-5k' },
|
|
182
|
+
{ max: Infinity, hue: 320, label: '>5k' },
|
|
183
|
+
]
|
|
184
|
+
function tierOf(entry) {
|
|
185
|
+
const tokens = typeof entry.tokens === 'number' ? entry.tokens : 0
|
|
186
|
+
return RAZOR_TIERS.findIndex(t => tokens <= t.max)
|
|
187
|
+
}
|
|
188
|
+
|
|
169
189
|
async function getJson(url) {
|
|
170
190
|
const r = await fetch(url)
|
|
171
191
|
if (!r.ok) {
|
|
@@ -178,28 +198,47 @@ window.__ModuleLoader__.load({
|
|
|
178
198
|
// ── Token-based stylesheet (light/dark adaptive by construction) ────────
|
|
179
199
|
|
|
180
200
|
const STYLE = `<style>
|
|
181
|
-
.rz-page{
|
|
201
|
+
.rz-page,.rz-page *{box-sizing:border-box}
|
|
202
|
+
.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
203
|
.rz-hint{color:var(--dsw-alias-label-secondary);font-size:12.5px;line-height:1.5}
|
|
183
204
|
.rz-toolbar{display:flex;gap:10px;align-items:center;flex-wrap:wrap}
|
|
184
205
|
.rz-spacer{flex:1}
|
|
185
206
|
.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
207
|
.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
208
|
.rz-stats{display:flex;gap:12px;align-items:center;flex-wrap:wrap;color:var(--dsw-alias-label-secondary);font-size:12.5px}
|
|
190
209
|
.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
210
|
.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
211
|
.rz-chip.user{color:var(--dsw-alias-state-business-primary);border-color:var(--dsw-alias-state-business-primary)}
|
|
194
212
|
.rz-chip.assistant{color:var(--dsw-alias-state-success-primary);border-color:var(--dsw-alias-state-success-primary)}
|
|
213
|
+
.rz-legend{display:inline-flex;gap:4px;align-items:center}
|
|
214
|
+
.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)}
|
|
215
|
+
.rz-legend.filtering .rz-swatch:not(.on){opacity:.4}
|
|
216
|
+
.rz-swatch.on{box-shadow:0 0 0 2px var(--dsw-alias-border-l3,rgba(128,128,128,.65))}
|
|
217
|
+
.rz-swatch.tier-0{background:hsl(120,55%,40%)}
|
|
218
|
+
.rz-swatch.tier-1{background:hsl(90,60%,38%)}
|
|
219
|
+
.rz-swatch.tier-2{background:hsl(60,70%,36%)}
|
|
220
|
+
.rz-swatch.tier-3{background:hsl(32,80%,45%)}
|
|
221
|
+
.rz-swatch.tier-4{background:hsl(8,75%,48%)}
|
|
222
|
+
.rz-swatch.tier-5{background:hsl(320,65%,50%)}
|
|
195
223
|
.rz-list{display:flex;flex-direction:column;gap:6px}
|
|
196
224
|
.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
225
|
.rz-row:hover{background:var(--dsw-alias-interactive-bg-hover)}
|
|
198
|
-
.rz-row.over{border-left-color:var(--dsw-alias-state-error-primary)}
|
|
199
226
|
.rz-row.checked{border-color:var(--dsw-alias-state-business-primary)}
|
|
200
|
-
.rz-row-preview{flex:1;min-width:
|
|
201
|
-
.rz-row
|
|
227
|
+
.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}
|
|
228
|
+
.rz-row-preview:hover{text-decoration:underline;text-underline-offset:3px}
|
|
202
229
|
.rz-row-meta{color:var(--dsw-alias-label-tertiary);font-size:11px;flex:none}
|
|
230
|
+
.rz-badge.tier-0{color:hsl(120,55%,40%);border-color:hsla(120,55%,40%,.45);background:hsla(120,55%,40%,.10)}
|
|
231
|
+
.rz-badge.tier-1{color:hsl(90,60%,38%);border-color:hsla(90,60%,38%,.45);background:hsla(90,60%,38%,.10)}
|
|
232
|
+
.rz-badge.tier-2{color:hsl(60,70%,36%);border-color:hsla(60,70%,36%,.45);background:hsla(60,70%,36%,.12)}
|
|
233
|
+
.rz-badge.tier-3{color:hsl(32,80%,45%);border-color:hsla(32,80%,45%,.5);background:hsla(32,80%,45%,.12)}
|
|
234
|
+
.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}
|
|
235
|
+
.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}
|
|
236
|
+
.rz-row.tier-0{border-left-color:hsl(120,55%,40%)}
|
|
237
|
+
.rz-row.tier-1{border-left-color:hsl(90,60%,38%)}
|
|
238
|
+
.rz-row.tier-2{border-left-color:hsl(60,70%,36%)}
|
|
239
|
+
.rz-row.tier-3{border-left-color:hsl(32,80%,45%)}
|
|
240
|
+
.rz-row.tier-4{border-left-color:hsl(8,75%,48%)}
|
|
241
|
+
.rz-row.tier-5{border-left-color:hsl(320,65%,50%)}
|
|
203
242
|
.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
243
|
.rz-loading{padding:36px;text-align:center;color:var(--dsw-alias-label-secondary)}
|
|
205
244
|
.rz-footbtns{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
|
|
@@ -218,11 +257,11 @@ window.__ModuleLoader__.load({
|
|
|
218
257
|
|
|
219
258
|
// ── Small building blocks ────────────────────────────────────────────────
|
|
220
259
|
|
|
221
|
-
const Chip = ({ kind, label }) => h('span', { className: 'rz-chip ' + kind }, label)
|
|
260
|
+
const Chip = ({ kind, label, title }) => h('span', { className: 'rz-chip ' + kind, title }, label)
|
|
222
261
|
|
|
223
|
-
const TokenBadge = ({ entry
|
|
224
|
-
const
|
|
225
|
-
return h('span', { className: 'rz-badge' +
|
|
262
|
+
const TokenBadge = ({ entry }) => {
|
|
263
|
+
const tier = tierOf(entry)
|
|
264
|
+
return h('span', { className: 'rz-badge tier-' + tier, title: RAZOR_TIERS[tier].label + ' token' },
|
|
226
265
|
'≈' + formatNum(entry.tokens))
|
|
227
266
|
}
|
|
228
267
|
|
|
@@ -238,15 +277,16 @@ window.__ModuleLoader__.load({
|
|
|
238
277
|
}
|
|
239
278
|
|
|
240
279
|
/** 单条全文弹窗(正文经 /entry 异步补全)。 */
|
|
241
|
-
function DetailModal({ detail,
|
|
280
|
+
function DetailModal({ detail, t, onClose }) {
|
|
242
281
|
if (!detail) return null
|
|
243
282
|
return h('div', { className: 'rz-dlg-backdrop', onClick: onClose },
|
|
244
283
|
h('div', { className: 'rz-dlg', onClick: e => e.stopPropagation() },
|
|
245
284
|
h('h3', null, t('detailTitle') + ' · ' + t('seqLabel', { seq: detail.seq })),
|
|
246
285
|
h('div', { className: 'rz-stats', style: { marginBottom: 10 } },
|
|
247
|
-
h(Chip, {
|
|
248
|
-
h(TokenBadge, { entry: detail
|
|
249
|
-
h('span', { className: 'rz-label'
|
|
286
|
+
h(Chip, { ...entryChip(detail, t) }),
|
|
287
|
+
h(TokenBadge, { entry: detail }),
|
|
288
|
+
h('span', { className: 'rz-label', title: 'seq ' + detail.seq }, formatDateTime(detail.time)),
|
|
289
|
+
h('span', { className: 'rz-label' }, t('detailChars', { chars: formatNum(detail.chars) })),
|
|
250
290
|
detail.usage && h('span', { className: 'rz-label' }, t('detailUsage', {
|
|
251
291
|
input: formatNum(detail.usage.input), output: formatNum(detail.usage.output),
|
|
252
292
|
cache: detail.usage.cacheRead ? t('detailUsageCache', { cache: formatNum(detail.usage.cacheRead) }) : '' }))),
|
|
@@ -268,17 +308,14 @@ window.__ModuleLoader__.load({
|
|
|
268
308
|
|
|
269
309
|
// ── Page ────────────────────────────────────────────────────────────────
|
|
270
310
|
|
|
271
|
-
function RazorPage({ t }) {
|
|
272
|
-
|
|
311
|
+
function RazorPage({ t, fixedSessionId }) {
|
|
312
|
+
useEffect(ensureStyles, [])
|
|
273
313
|
const [sessionId, setSessionId] = useState('')
|
|
274
314
|
const [context, setContext] = useState(null)
|
|
275
315
|
const [ctxLoading, setCtxLoading] = useState(false)
|
|
276
316
|
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
|
-
})
|
|
317
|
+
const [tierFilter, setTierFilter] = useState(() => new Set())
|
|
280
318
|
const [sortBy, setSortBy] = useState('order')
|
|
281
|
-
const [overOnly, setOverOnly] = useState(false)
|
|
282
319
|
const [selected, setSelected] = useState(() => new Set())
|
|
283
320
|
const [detail, setDetail] = useState(null)
|
|
284
321
|
const [confirming, setConfirming] = useState(false)
|
|
@@ -288,8 +325,10 @@ window.__ModuleLoader__.load({
|
|
|
288
325
|
|
|
289
326
|
const showToast = (text) => { setToast(text); setTimeout(() => setToast(null), 3000) }
|
|
290
327
|
|
|
291
|
-
|
|
292
|
-
useEffect(() => {
|
|
328
|
+
// 会话视图挂载:只管当前会话(fixedSessionId 来自 slot props)
|
|
329
|
+
useEffect(() => {
|
|
330
|
+
if (fixedSessionId) { sessionRef.current = fixedSessionId; setSessionId(fixedSessionId) }
|
|
331
|
+
}, [fixedSessionId])
|
|
293
332
|
|
|
294
333
|
const loadContext = (id) => {
|
|
295
334
|
if (!id) { setContext(null); return }
|
|
@@ -303,25 +342,16 @@ window.__ModuleLoader__.load({
|
|
|
303
342
|
useEffect(() => { if (sessionId) loadContext(sessionId) }, [sessionId])
|
|
304
343
|
|
|
305
344
|
const refreshAll = () => {
|
|
306
|
-
loadSessions()
|
|
307
345
|
if (sessionRef.current) loadContext(sessionRef.current)
|
|
308
346
|
}
|
|
309
347
|
|
|
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
348
|
const visible = useMemo(() => {
|
|
318
349
|
if (!context) return []
|
|
319
350
|
let rows = context.entries
|
|
320
|
-
if (
|
|
351
|
+
if (tierFilter.size > 0) rows = rows.filter(e => tierFilter.has(tierOf(e)))
|
|
321
352
|
return sortEntries(rows, sortBy)
|
|
322
|
-
}, [context,
|
|
353
|
+
}, [context, tierFilter, sortBy])
|
|
323
354
|
|
|
324
|
-
const overList = useMemo(() => (context ? context.entries.filter(e => overThreshold(e, threshold)) : []), [context, threshold])
|
|
325
355
|
const selectedTokens = useMemo(() => {
|
|
326
356
|
if (!context) return 0
|
|
327
357
|
const bySeq = new Map(context.entries.map(e => [e.seq, e]))
|
|
@@ -336,12 +366,18 @@ window.__ModuleLoader__.load({
|
|
|
336
366
|
else next.add(seq)
|
|
337
367
|
return next
|
|
338
368
|
})
|
|
339
|
-
const
|
|
369
|
+
const toggleTier = (i) => setTierFilter(prev => {
|
|
370
|
+
const next = new Set(prev)
|
|
371
|
+
if (next.has(i)) next.delete(i)
|
|
372
|
+
else next.add(i)
|
|
373
|
+
return next
|
|
374
|
+
})
|
|
375
|
+
const selectVisible = () => setSelected(new Set(visible.map(e => e.seq)))
|
|
340
376
|
|
|
341
377
|
const openDetail = (entry) => {
|
|
342
378
|
setDetail(entry)
|
|
343
379
|
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))
|
|
380
|
+
.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
381
|
.catch(() => {})
|
|
346
382
|
}
|
|
347
383
|
|
|
@@ -365,60 +401,47 @@ window.__ModuleLoader__.load({
|
|
|
365
401
|
const canDelete = selected.size > 0 && !busy && !deleting
|
|
366
402
|
|
|
367
403
|
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
404
|
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'))),
|
|
405
|
+
!sessionId && h('div', { className: 'rz-empty' }, t('pickSession')),
|
|
381
406
|
sessionId && ctxLoading && h('div', { className: 'rz-loading' }, t('loading')),
|
|
382
407
|
sessionId && !ctxLoading && context && [
|
|
383
|
-
h('div', { key: '
|
|
408
|
+
h('div', { key: 'bar', className: 'rz-stats' },
|
|
384
409
|
h('span', null, t('stats', { nodes: context.nodes, tokens: formatNum(context.totalTokens), mode: context.encoder === 'heuristic' ? t('modeHeuristic') : '' })),
|
|
410
|
+
h('span', { className: 'rz-legend' + (tierFilter.size > 0 ? ' filtering' : ''), title: t('legendTitle') },
|
|
411
|
+
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
412
|
h('span', { className: 'rz-spacer' }),
|
|
386
|
-
h('
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
t('thresholdUnit')),
|
|
413
|
+
busy && h('span', { className: 'rz-badge' }, t('busyTag')),
|
|
414
|
+
h('button', { className: 'rz-btn', onClick: selectVisible, disabled: visible.length === 0 || busy, title: t('selectVisibleHint') },
|
|
415
|
+
t('selectVisible') + (visible.length ? ` (${visible.length})` : '')),
|
|
390
416
|
h('select', { className: 'rz-select', value: sortBy, onChange: e => setSortBy(e.target.value), title: t('sortLabel'), 'aria-label': t('sortLabel') },
|
|
391
417
|
h('option', { value: 'order' }, t('sortOrder')),
|
|
392
418
|
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) }) : ' '),
|
|
419
|
+
h('button', { className: 'rz-btn', onClick: refreshAll, title: t('refresh') }, t('refresh'))),
|
|
420
|
+
selected.size > 0 && h('div', { key: 'sel', className: 'rz-stats' },
|
|
421
|
+
h('span', null, t('selectedStats', { n: selected.size, tokens: formatNum(selectedTokens) })),
|
|
397
422
|
h('span', { className: 'rz-spacer' }),
|
|
398
423
|
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')),
|
|
424
|
+
h('button', { className: 'rz-btn', onClick: () => setSelected(new Set()) }, t('clearSelect')),
|
|
401
425
|
h('button', { className: 'rz-btn rz-btn-danger', disabled: !canDelete,
|
|
402
426
|
title: busy ? t('deleteBusyHint') : undefined,
|
|
403
427
|
onClick: () => setConfirming(true) },
|
|
404
|
-
t('deleteSelected') +
|
|
428
|
+
t('deleteSelected') + ` (${selected.size})`))),
|
|
405
429
|
visible.length === 0
|
|
406
|
-
? h('div', { key: 'empty', className: 'rz-empty' },
|
|
430
|
+
? h('div', { key: 'empty', className: 'rz-empty' }, t('emptyContext'))
|
|
407
431
|
: h('div', { key: 'list', className: 'rz-list' },
|
|
408
432
|
h(PagedList, { items: visible, t, render: entry => {
|
|
409
|
-
const
|
|
410
|
-
return h('div', { key: entry.seq, className: 'rz-row' +
|
|
433
|
+
const tier = tierOf(entry)
|
|
434
|
+
return h('div', { key: entry.seq, className: 'rz-row tier-' + tier + (selected.has(entry.seq) ? ' checked' : ''),
|
|
411
435
|
role: 'button', tabIndex: 0, onClick: () => toggleRow(entry.seq),
|
|
412
436
|
onKeyDown: e => e.key === 'Enter' && toggleRow(entry.seq) },
|
|
413
437
|
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))
|
|
438
|
+
h(TokenBadge, { entry }),
|
|
439
|
+
h(Chip, { ...entryChip(entry, t) }),
|
|
440
|
+
h('span', { className: 'rz-row-preview', title: entry.preview,
|
|
441
|
+
onClick: e => { e.stopPropagation(); openDetail(entry) } }, entry.preview || ' '),
|
|
442
|
+
h('span', { className: 'rz-row-meta', title: 'seq ' + entry.seq }, formatDateTime(entry.time)))
|
|
420
443
|
} })),
|
|
421
|
-
detail && h(DetailModal, { detail,
|
|
444
|
+
detail && h(DetailModal, { detail, t, onClose: () => setDetail(null) }),
|
|
422
445
|
confirming && h(ConfirmDialog, { n: selected.size, tokens: formatNum(selectedTokens), deleting, t,
|
|
423
446
|
onCancel: () => setConfirming(false), onOk: doDelete }),
|
|
424
447
|
],
|
|
@@ -432,7 +455,7 @@ window.__ModuleLoader__.load({
|
|
|
432
455
|
module.exports = {
|
|
433
456
|
name: CLIENT_NAME,
|
|
434
457
|
inject: ['slots', 'locale'],
|
|
435
|
-
__internals: { NS, ZH, EN,
|
|
458
|
+
__internals: { NS, ZH, EN, sortEntries, tierOf, RAZOR_TIERS, formatDateTime, entryChip },
|
|
436
459
|
__boot(container, opts = {}) {
|
|
437
460
|
ensureStyles()
|
|
438
461
|
const t = opts.t || ((key, vars) => {
|
|
@@ -462,21 +485,20 @@ window.__ModuleLoader__.load({
|
|
|
462
485
|
}
|
|
463
486
|
}
|
|
464
487
|
} catch (e) { try { console.error('[context-razor] locale init:', e) } catch {} }
|
|
488
|
+
// 会话视图标签:order 30 排在 Chat(0)/Trajectory(10)/Context(20)/Hermes Loop(25) 之后
|
|
465
489
|
ctx.effect(() => {
|
|
466
490
|
try {
|
|
467
|
-
ctx.slots.inject('
|
|
468
|
-
name: '
|
|
491
|
+
ctx.slots.inject('conversation.view', () => ctx.slots.register({
|
|
492
|
+
name: 'conversation.view',
|
|
469
493
|
id: CLIENT_NAME,
|
|
470
|
-
order:
|
|
494
|
+
order: 30,
|
|
471
495
|
locale: NS,
|
|
472
496
|
label: () => t('title'),
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
useEffect(ensureStyles, [])
|
|
476
|
-
return h(RazorPage, { t })
|
|
497
|
+
}, function RazorViewSlot(props) {
|
|
498
|
+
return h(RazorPage, { t, fixedSessionId: props && props.sessionId })
|
|
477
499
|
}))
|
|
478
|
-
} catch (e) { (globalThis.__rzErrors = globalThis.__rzErrors || []).push('
|
|
479
|
-
}, 'context-razor:
|
|
500
|
+
} catch (e) { (globalThis.__rzErrors = globalThis.__rzErrors || []).push('conversation.view:' + (e && e.message)); throw e }
|
|
501
|
+
}, 'context-razor: conversation view tab')
|
|
480
502
|
},
|
|
481
503
|
}
|
|
482
504
|
|
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,28 @@ 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: '上下文顺序',
|
|
62
57
|
sortTokens: 'token 高→低',
|
|
63
|
-
|
|
64
|
-
|
|
58
|
+
selectVisible: '选中可见',
|
|
59
|
+
selectVisibleHint: '选中当前显示的全部条目(配合色块筛选)',
|
|
65
60
|
clearSelect: '清空选择',
|
|
66
61
|
deleteSelected: '删除选中',
|
|
67
62
|
selectedStats: '已选 {n} 条 / ≈{tokens} token',
|
|
68
63
|
stats: '{nodes} 条 · 合计 ≈{tokens} token{mode}',
|
|
69
64
|
modeHeuristic: '(启发式估算:词表未加载)',
|
|
70
65
|
kindUser: '用户',
|
|
66
|
+
kindInjected: '注入',
|
|
71
67
|
kindAssistant: '助手',
|
|
72
68
|
kindTool: '工具',
|
|
73
69
|
emptyContext: '该会话上下文为空',
|
|
74
70
|
showMore: '显示更多(剩余 {n})',
|
|
75
71
|
detailTitle: '条目详情',
|
|
76
|
-
|
|
72
|
+
detailChars: '{chars} 字符',
|
|
77
73
|
detailUsage: '真实用量:输入 {input} / 输出 {output}{cache}',
|
|
78
74
|
detailUsageCache: ' / 缓存读 {cache}',
|
|
79
75
|
close: '关闭',
|
|
@@ -89,32 +85,28 @@ const ZH = {
|
|
|
89
85
|
|
|
90
86
|
const EN = {
|
|
91
87
|
title: 'Context Razor',
|
|
92
|
-
|
|
93
|
-
sessionLabel: 'Session',
|
|
94
|
-
pickSession: 'Pick a session…',
|
|
88
|
+
pickSession: 'Open a conversation tab to inspect its context',
|
|
95
89
|
loading: 'Loading…',
|
|
96
90
|
refresh: 'Refresh',
|
|
97
91
|
busyTag: 'running',
|
|
98
|
-
idleTag: 'idle',
|
|
99
|
-
thresholdLabel: 'Red threshold',
|
|
100
|
-
thresholdUnit: 'tokens',
|
|
101
92
|
sortLabel: 'Sort',
|
|
102
93
|
sortOrder: 'Context order',
|
|
103
94
|
sortTokens: 'tokens high→low',
|
|
104
|
-
|
|
105
|
-
|
|
95
|
+
selectVisible: 'Select shown',
|
|
96
|
+
selectVisibleHint: 'Select every currently shown entry (pairs with tier filters)',
|
|
106
97
|
clearSelect: 'Clear selection',
|
|
107
98
|
deleteSelected: 'Delete selected',
|
|
108
99
|
selectedStats: '{n} selected / ≈{tokens} tokens',
|
|
109
100
|
stats: '{nodes} entries · total ≈{tokens} tokens{mode}',
|
|
110
101
|
modeHeuristic: ' (heuristic: ranks unavailable)',
|
|
111
102
|
kindUser: 'User',
|
|
103
|
+
kindInjected: 'injected',
|
|
112
104
|
kindAssistant: 'Assistant',
|
|
113
105
|
kindTool: 'Tool',
|
|
114
106
|
emptyContext: 'This session has no context entries',
|
|
115
107
|
showMore: 'Show more ({n} left)',
|
|
116
108
|
detailTitle: 'Entry detail',
|
|
117
|
-
|
|
109
|
+
detailChars: '{chars} chars',
|
|
118
110
|
detailUsage: 'Actual usage: in {input} / out {output}{cache}',
|
|
119
111
|
detailUsageCache: ' / cache-read {cache}',
|
|
120
112
|
close: 'Close',
|
|
@@ -132,16 +124,9 @@ const EN = {
|
|
|
132
124
|
|
|
133
125
|
const API = '/context-razor/api'
|
|
134
126
|
const PAGE_SIZE = 150
|
|
135
|
-
const DEFAULT_THRESHOLD = 500
|
|
136
|
-
const THRESHOLD_KEY = 'context-razor-threshold'
|
|
137
127
|
|
|
138
128
|
const kindI18n = (key) => 'kind' + key[0].toUpperCase() + key.slice(1)
|
|
139
129
|
|
|
140
|
-
/** 行是否标红(仅 user/assistant/tool 有意义,阈值 <= 0 视为关闭)。 */
|
|
141
|
-
function overThreshold(entry, threshold) {
|
|
142
|
-
return threshold > 0 && typeof entry.tokens === 'number' && entry.tokens > threshold
|
|
143
|
-
}
|
|
144
|
-
|
|
145
130
|
/** 排序:order = 投影原序;tokens = 降序(并列按 seq,缺 token 沉底)。 */
|
|
146
131
|
function sortEntries(entries, sortBy) {
|
|
147
132
|
if (sortBy !== 'tokens') return entries
|
|
@@ -156,6 +141,41 @@ function sortEntries(entries, sortBy) {
|
|
|
156
141
|
|
|
157
142
|
const formatNum = (n) => Number.isFinite(n) ? n.toLocaleString('en-US') : '-'
|
|
158
143
|
|
|
144
|
+
/** 完整日期时间(短格式:2026/9/2 11:03;ts 兼容 epoch 毫秒与 ISO 串)。 */
|
|
145
|
+
function formatDateTime(ts) {
|
|
146
|
+
if (!ts) return ''
|
|
147
|
+
const t = typeof ts === 'number' ? ts : Date.parse(ts)
|
|
148
|
+
if (!Number.isFinite(t)) return ''
|
|
149
|
+
try { return new Date(t).toLocaleString([], { dateStyle: 'short', timeStyle: 'short' }) } catch { return new Date(t).toLocaleString() }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** 行首 chip 的文案与提示:工具结果显示工具名,注入类 user 消息标「注入」。 */
|
|
153
|
+
function entryChip(entry, t) {
|
|
154
|
+
if (entry.kind === 'tool') {
|
|
155
|
+
return { kind: 'tool', label: entry.tool || t('kindTool'), title: entry.tool }
|
|
156
|
+
}
|
|
157
|
+
if (entry.kind === 'user' && entry.sourceKind && entry.sourceKind !== 'user') {
|
|
158
|
+
return { kind: 'user', label: t('kindInjected'), title: [entry.sourceKind, entry.sourceForm, entry.sourcePlugin].filter(Boolean).join(' · ') }
|
|
159
|
+
}
|
|
160
|
+
return { kind: entry.kind, label: t(kindI18n(entry.kind)) }
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ── 彩虹分级:颜色越暖 = 占用越多 ────────────────────────────────────────
|
|
164
|
+
// 固定对数档位(相邻约 ×2.5),跨会话语义稳定:绿→黄绿→黄→橙→红→品红。
|
|
165
|
+
// 档位是「这条消息吃掉多少典型上下文预算」的粗标尺,不随会话内最大值缩放。
|
|
166
|
+
const RAZOR_TIERS = [
|
|
167
|
+
{ max: 100, hue: 120, label: '0-100' },
|
|
168
|
+
{ max: 300, hue: 90, label: '100-300' },
|
|
169
|
+
{ max: 800, hue: 60, label: '300-800' },
|
|
170
|
+
{ max: 2000, hue: 32, label: '800-2k' },
|
|
171
|
+
{ max: 5000, hue: 8, label: '2k-5k' },
|
|
172
|
+
{ max: Infinity, hue: 320, label: '>5k' },
|
|
173
|
+
]
|
|
174
|
+
function tierOf(entry) {
|
|
175
|
+
const tokens = typeof entry.tokens === 'number' ? entry.tokens : 0
|
|
176
|
+
return RAZOR_TIERS.findIndex(t => tokens <= t.max)
|
|
177
|
+
}
|
|
178
|
+
|
|
159
179
|
async function getJson(url) {
|
|
160
180
|
const r = await fetch(url)
|
|
161
181
|
if (!r.ok) {
|
|
@@ -168,28 +188,47 @@ async function getJson(url) {
|
|
|
168
188
|
// ── Token-based stylesheet (light/dark adaptive by construction) ────────
|
|
169
189
|
|
|
170
190
|
const STYLE = `<style>
|
|
171
|
-
.rz-page{
|
|
191
|
+
.rz-page,.rz-page *{box-sizing:border-box}
|
|
192
|
+
.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
193
|
.rz-hint{color:var(--dsw-alias-label-secondary);font-size:12.5px;line-height:1.5}
|
|
173
194
|
.rz-toolbar{display:flex;gap:10px;align-items:center;flex-wrap:wrap}
|
|
174
195
|
.rz-spacer{flex:1}
|
|
175
196
|
.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
197
|
.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
198
|
.rz-stats{display:flex;gap:12px;align-items:center;flex-wrap:wrap;color:var(--dsw-alias-label-secondary);font-size:12.5px}
|
|
180
199
|
.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
200
|
.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
201
|
.rz-chip.user{color:var(--dsw-alias-state-business-primary);border-color:var(--dsw-alias-state-business-primary)}
|
|
184
202
|
.rz-chip.assistant{color:var(--dsw-alias-state-success-primary);border-color:var(--dsw-alias-state-success-primary)}
|
|
203
|
+
.rz-legend{display:inline-flex;gap:4px;align-items:center}
|
|
204
|
+
.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)}
|
|
205
|
+
.rz-legend.filtering .rz-swatch:not(.on){opacity:.4}
|
|
206
|
+
.rz-swatch.on{box-shadow:0 0 0 2px var(--dsw-alias-border-l3,rgba(128,128,128,.65))}
|
|
207
|
+
.rz-swatch.tier-0{background:hsl(120,55%,40%)}
|
|
208
|
+
.rz-swatch.tier-1{background:hsl(90,60%,38%)}
|
|
209
|
+
.rz-swatch.tier-2{background:hsl(60,70%,36%)}
|
|
210
|
+
.rz-swatch.tier-3{background:hsl(32,80%,45%)}
|
|
211
|
+
.rz-swatch.tier-4{background:hsl(8,75%,48%)}
|
|
212
|
+
.rz-swatch.tier-5{background:hsl(320,65%,50%)}
|
|
185
213
|
.rz-list{display:flex;flex-direction:column;gap:6px}
|
|
186
214
|
.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
215
|
.rz-row:hover{background:var(--dsw-alias-interactive-bg-hover)}
|
|
188
|
-
.rz-row.over{border-left-color:var(--dsw-alias-state-error-primary)}
|
|
189
216
|
.rz-row.checked{border-color:var(--dsw-alias-state-business-primary)}
|
|
190
|
-
.rz-row-preview{flex:1;min-width:
|
|
191
|
-
.rz-row
|
|
217
|
+
.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}
|
|
218
|
+
.rz-row-preview:hover{text-decoration:underline;text-underline-offset:3px}
|
|
192
219
|
.rz-row-meta{color:var(--dsw-alias-label-tertiary);font-size:11px;flex:none}
|
|
220
|
+
.rz-badge.tier-0{color:hsl(120,55%,40%);border-color:hsla(120,55%,40%,.45);background:hsla(120,55%,40%,.10)}
|
|
221
|
+
.rz-badge.tier-1{color:hsl(90,60%,38%);border-color:hsla(90,60%,38%,.45);background:hsla(90,60%,38%,.10)}
|
|
222
|
+
.rz-badge.tier-2{color:hsl(60,70%,36%);border-color:hsla(60,70%,36%,.45);background:hsla(60,70%,36%,.12)}
|
|
223
|
+
.rz-badge.tier-3{color:hsl(32,80%,45%);border-color:hsla(32,80%,45%,.5);background:hsla(32,80%,45%,.12)}
|
|
224
|
+
.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}
|
|
225
|
+
.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}
|
|
226
|
+
.rz-row.tier-0{border-left-color:hsl(120,55%,40%)}
|
|
227
|
+
.rz-row.tier-1{border-left-color:hsl(90,60%,38%)}
|
|
228
|
+
.rz-row.tier-2{border-left-color:hsl(60,70%,36%)}
|
|
229
|
+
.rz-row.tier-3{border-left-color:hsl(32,80%,45%)}
|
|
230
|
+
.rz-row.tier-4{border-left-color:hsl(8,75%,48%)}
|
|
231
|
+
.rz-row.tier-5{border-left-color:hsl(320,65%,50%)}
|
|
193
232
|
.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
233
|
.rz-loading{padding:36px;text-align:center;color:var(--dsw-alias-label-secondary)}
|
|
195
234
|
.rz-footbtns{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
|
|
@@ -208,11 +247,11 @@ const STYLE = `<style>
|
|
|
208
247
|
|
|
209
248
|
// ── Small building blocks ────────────────────────────────────────────────
|
|
210
249
|
|
|
211
|
-
const Chip = ({ kind, label }) => h('span', { className: 'rz-chip ' + kind }, label)
|
|
250
|
+
const Chip = ({ kind, label, title }) => h('span', { className: 'rz-chip ' + kind, title }, label)
|
|
212
251
|
|
|
213
|
-
const TokenBadge = ({ entry
|
|
214
|
-
const
|
|
215
|
-
return h('span', { className: 'rz-badge' +
|
|
252
|
+
const TokenBadge = ({ entry }) => {
|
|
253
|
+
const tier = tierOf(entry)
|
|
254
|
+
return h('span', { className: 'rz-badge tier-' + tier, title: RAZOR_TIERS[tier].label + ' token' },
|
|
216
255
|
'≈' + formatNum(entry.tokens))
|
|
217
256
|
}
|
|
218
257
|
|
|
@@ -228,15 +267,16 @@ function PagedList({ items, render, t }) {
|
|
|
228
267
|
}
|
|
229
268
|
|
|
230
269
|
/** 单条全文弹窗(正文经 /entry 异步补全)。 */
|
|
231
|
-
function DetailModal({ detail,
|
|
270
|
+
function DetailModal({ detail, t, onClose }) {
|
|
232
271
|
if (!detail) return null
|
|
233
272
|
return h('div', { className: 'rz-dlg-backdrop', onClick: onClose },
|
|
234
273
|
h('div', { className: 'rz-dlg', onClick: e => e.stopPropagation() },
|
|
235
274
|
h('h3', null, t('detailTitle') + ' · ' + t('seqLabel', { seq: detail.seq })),
|
|
236
275
|
h('div', { className: 'rz-stats', style: { marginBottom: 10 } },
|
|
237
|
-
h(Chip, {
|
|
238
|
-
h(TokenBadge, { entry: detail
|
|
239
|
-
h('span', { className: 'rz-label'
|
|
276
|
+
h(Chip, { ...entryChip(detail, t) }),
|
|
277
|
+
h(TokenBadge, { entry: detail }),
|
|
278
|
+
h('span', { className: 'rz-label', title: 'seq ' + detail.seq }, formatDateTime(detail.time)),
|
|
279
|
+
h('span', { className: 'rz-label' }, t('detailChars', { chars: formatNum(detail.chars) })),
|
|
240
280
|
detail.usage && h('span', { className: 'rz-label' }, t('detailUsage', {
|
|
241
281
|
input: formatNum(detail.usage.input), output: formatNum(detail.usage.output),
|
|
242
282
|
cache: detail.usage.cacheRead ? t('detailUsageCache', { cache: formatNum(detail.usage.cacheRead) }) : '' }))),
|
|
@@ -258,17 +298,14 @@ function ConfirmDialog({ n, tokens, deleting, t, onCancel, onOk }) {
|
|
|
258
298
|
|
|
259
299
|
// ── Page ────────────────────────────────────────────────────────────────
|
|
260
300
|
|
|
261
|
-
function RazorPage({ t }) {
|
|
262
|
-
|
|
301
|
+
function RazorPage({ t, fixedSessionId }) {
|
|
302
|
+
useEffect(ensureStyles, [])
|
|
263
303
|
const [sessionId, setSessionId] = useState('')
|
|
264
304
|
const [context, setContext] = useState(null)
|
|
265
305
|
const [ctxLoading, setCtxLoading] = useState(false)
|
|
266
306
|
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
|
-
})
|
|
307
|
+
const [tierFilter, setTierFilter] = useState(() => new Set())
|
|
270
308
|
const [sortBy, setSortBy] = useState('order')
|
|
271
|
-
const [overOnly, setOverOnly] = useState(false)
|
|
272
309
|
const [selected, setSelected] = useState(() => new Set())
|
|
273
310
|
const [detail, setDetail] = useState(null)
|
|
274
311
|
const [confirming, setConfirming] = useState(false)
|
|
@@ -278,8 +315,10 @@ function RazorPage({ t }) {
|
|
|
278
315
|
|
|
279
316
|
const showToast = (text) => { setToast(text); setTimeout(() => setToast(null), 3000) }
|
|
280
317
|
|
|
281
|
-
|
|
282
|
-
useEffect(() => {
|
|
318
|
+
// 会话视图挂载:只管当前会话(fixedSessionId 来自 slot props)
|
|
319
|
+
useEffect(() => {
|
|
320
|
+
if (fixedSessionId) { sessionRef.current = fixedSessionId; setSessionId(fixedSessionId) }
|
|
321
|
+
}, [fixedSessionId])
|
|
283
322
|
|
|
284
323
|
const loadContext = (id) => {
|
|
285
324
|
if (!id) { setContext(null); return }
|
|
@@ -293,25 +332,16 @@ function RazorPage({ t }) {
|
|
|
293
332
|
useEffect(() => { if (sessionId) loadContext(sessionId) }, [sessionId])
|
|
294
333
|
|
|
295
334
|
const refreshAll = () => {
|
|
296
|
-
loadSessions()
|
|
297
335
|
if (sessionRef.current) loadContext(sessionRef.current)
|
|
298
336
|
}
|
|
299
337
|
|
|
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
338
|
const visible = useMemo(() => {
|
|
308
339
|
if (!context) return []
|
|
309
340
|
let rows = context.entries
|
|
310
|
-
if (
|
|
341
|
+
if (tierFilter.size > 0) rows = rows.filter(e => tierFilter.has(tierOf(e)))
|
|
311
342
|
return sortEntries(rows, sortBy)
|
|
312
|
-
}, [context,
|
|
343
|
+
}, [context, tierFilter, sortBy])
|
|
313
344
|
|
|
314
|
-
const overList = useMemo(() => (context ? context.entries.filter(e => overThreshold(e, threshold)) : []), [context, threshold])
|
|
315
345
|
const selectedTokens = useMemo(() => {
|
|
316
346
|
if (!context) return 0
|
|
317
347
|
const bySeq = new Map(context.entries.map(e => [e.seq, e]))
|
|
@@ -326,12 +356,18 @@ function RazorPage({ t }) {
|
|
|
326
356
|
else next.add(seq)
|
|
327
357
|
return next
|
|
328
358
|
})
|
|
329
|
-
const
|
|
359
|
+
const toggleTier = (i) => setTierFilter(prev => {
|
|
360
|
+
const next = new Set(prev)
|
|
361
|
+
if (next.has(i)) next.delete(i)
|
|
362
|
+
else next.add(i)
|
|
363
|
+
return next
|
|
364
|
+
})
|
|
365
|
+
const selectVisible = () => setSelected(new Set(visible.map(e => e.seq)))
|
|
330
366
|
|
|
331
367
|
const openDetail = (entry) => {
|
|
332
368
|
setDetail(entry)
|
|
333
369
|
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))
|
|
370
|
+
.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
371
|
.catch(() => {})
|
|
336
372
|
}
|
|
337
373
|
|
|
@@ -355,60 +391,47 @@ function RazorPage({ t }) {
|
|
|
355
391
|
const canDelete = selected.size > 0 && !busy && !deleting
|
|
356
392
|
|
|
357
393
|
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
394
|
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'))),
|
|
395
|
+
!sessionId && h('div', { className: 'rz-empty' }, t('pickSession')),
|
|
371
396
|
sessionId && ctxLoading && h('div', { className: 'rz-loading' }, t('loading')),
|
|
372
397
|
sessionId && !ctxLoading && context && [
|
|
373
|
-
h('div', { key: '
|
|
398
|
+
h('div', { key: 'bar', className: 'rz-stats' },
|
|
374
399
|
h('span', null, t('stats', { nodes: context.nodes, tokens: formatNum(context.totalTokens), mode: context.encoder === 'heuristic' ? t('modeHeuristic') : '' })),
|
|
400
|
+
h('span', { className: 'rz-legend' + (tierFilter.size > 0 ? ' filtering' : ''), title: t('legendTitle') },
|
|
401
|
+
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
402
|
h('span', { className: 'rz-spacer' }),
|
|
376
|
-
h('
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
t('thresholdUnit')),
|
|
403
|
+
busy && h('span', { className: 'rz-badge' }, t('busyTag')),
|
|
404
|
+
h('button', { className: 'rz-btn', onClick: selectVisible, disabled: visible.length === 0 || busy, title: t('selectVisibleHint') },
|
|
405
|
+
t('selectVisible') + (visible.length ? ` (${visible.length})` : '')),
|
|
380
406
|
h('select', { className: 'rz-select', value: sortBy, onChange: e => setSortBy(e.target.value), title: t('sortLabel'), 'aria-label': t('sortLabel') },
|
|
381
407
|
h('option', { value: 'order' }, t('sortOrder')),
|
|
382
408
|
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) }) : ' '),
|
|
409
|
+
h('button', { className: 'rz-btn', onClick: refreshAll, title: t('refresh') }, t('refresh'))),
|
|
410
|
+
selected.size > 0 && h('div', { key: 'sel', className: 'rz-stats' },
|
|
411
|
+
h('span', null, t('selectedStats', { n: selected.size, tokens: formatNum(selectedTokens) })),
|
|
387
412
|
h('span', { className: 'rz-spacer' }),
|
|
388
413
|
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')),
|
|
414
|
+
h('button', { className: 'rz-btn', onClick: () => setSelected(new Set()) }, t('clearSelect')),
|
|
391
415
|
h('button', { className: 'rz-btn rz-btn-danger', disabled: !canDelete,
|
|
392
416
|
title: busy ? t('deleteBusyHint') : undefined,
|
|
393
417
|
onClick: () => setConfirming(true) },
|
|
394
|
-
t('deleteSelected') +
|
|
418
|
+
t('deleteSelected') + ` (${selected.size})`))),
|
|
395
419
|
visible.length === 0
|
|
396
|
-
? h('div', { key: 'empty', className: 'rz-empty' },
|
|
420
|
+
? h('div', { key: 'empty', className: 'rz-empty' }, t('emptyContext'))
|
|
397
421
|
: h('div', { key: 'list', className: 'rz-list' },
|
|
398
422
|
h(PagedList, { items: visible, t, render: entry => {
|
|
399
|
-
const
|
|
400
|
-
return h('div', { key: entry.seq, className: 'rz-row' +
|
|
423
|
+
const tier = tierOf(entry)
|
|
424
|
+
return h('div', { key: entry.seq, className: 'rz-row tier-' + tier + (selected.has(entry.seq) ? ' checked' : ''),
|
|
401
425
|
role: 'button', tabIndex: 0, onClick: () => toggleRow(entry.seq),
|
|
402
426
|
onKeyDown: e => e.key === 'Enter' && toggleRow(entry.seq) },
|
|
403
427
|
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))
|
|
428
|
+
h(TokenBadge, { entry }),
|
|
429
|
+
h(Chip, { ...entryChip(entry, t) }),
|
|
430
|
+
h('span', { className: 'rz-row-preview', title: entry.preview,
|
|
431
|
+
onClick: e => { e.stopPropagation(); openDetail(entry) } }, entry.preview || ' '),
|
|
432
|
+
h('span', { className: 'rz-row-meta', title: 'seq ' + entry.seq }, formatDateTime(entry.time)))
|
|
410
433
|
} })),
|
|
411
|
-
detail && h(DetailModal, { detail,
|
|
434
|
+
detail && h(DetailModal, { detail, t, onClose: () => setDetail(null) }),
|
|
412
435
|
confirming && h(ConfirmDialog, { n: selected.size, tokens: formatNum(selectedTokens), deleting, t,
|
|
413
436
|
onCancel: () => setConfirming(false), onOk: doDelete }),
|
|
414
437
|
],
|
|
@@ -422,7 +445,7 @@ const CLIENT_NAME = '@weibaohui/context-razor'
|
|
|
422
445
|
module.exports = {
|
|
423
446
|
name: CLIENT_NAME,
|
|
424
447
|
inject: ['slots', 'locale'],
|
|
425
|
-
__internals: { NS, ZH, EN,
|
|
448
|
+
__internals: { NS, ZH, EN, sortEntries, tierOf, RAZOR_TIERS, formatDateTime, entryChip },
|
|
426
449
|
__boot(container, opts = {}) {
|
|
427
450
|
ensureStyles()
|
|
428
451
|
const t = opts.t || ((key, vars) => {
|
|
@@ -452,20 +475,19 @@ module.exports = {
|
|
|
452
475
|
}
|
|
453
476
|
}
|
|
454
477
|
} catch (e) { try { console.error('[context-razor] locale init:', e) } catch {} }
|
|
478
|
+
// 会话视图标签:order 30 排在 Chat(0)/Trajectory(10)/Context(20)/Hermes Loop(25) 之后
|
|
455
479
|
ctx.effect(() => {
|
|
456
480
|
try {
|
|
457
|
-
ctx.slots.inject('
|
|
458
|
-
name: '
|
|
481
|
+
ctx.slots.inject('conversation.view', () => ctx.slots.register({
|
|
482
|
+
name: 'conversation.view',
|
|
459
483
|
id: CLIENT_NAME,
|
|
460
|
-
order:
|
|
484
|
+
order: 30,
|
|
461
485
|
locale: NS,
|
|
462
486
|
label: () => t('title'),
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
useEffect(ensureStyles, [])
|
|
466
|
-
return h(RazorPage, { t })
|
|
487
|
+
}, function RazorViewSlot(props) {
|
|
488
|
+
return h(RazorPage, { t, fixedSessionId: props && props.sessionId })
|
|
467
489
|
}))
|
|
468
|
-
} catch (e) { (globalThis.__rzErrors = globalThis.__rzErrors || []).push('
|
|
469
|
-
}, 'context-razor:
|
|
490
|
+
} catch (e) { (globalThis.__rzErrors = globalThis.__rzErrors || []).push('conversation.view:' + (e && e.message)); throw e }
|
|
491
|
+
}, 'context-razor: conversation view tab')
|
|
470
492
|
},
|
|
471
493
|
}
|
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
|
|