dsh-context-compression-improved 0.3.1 → 0.4.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.githooks/pre-push +37 -0
- package/package.json +2 -1
- package/packages/selector/cordis.patch.yml +12 -5
- package/packages/selector/lib/client.d.ts +24 -0
- package/packages/selector/lib/client.js +506 -5
- package/packages/selector/lib/config.js +27 -4
- package/packages/selector/lib/index.d.ts +7 -0
- package/packages/selector/lib/index.js +229 -1
- package/packages/selector/lib/pruner.d.ts +254 -0
- package/packages/selector/lib/pruner.js +714 -25
- package/packages/selector/package.json +0 -1
- package/packages/selector/src/client/EstimatorControls.tsx +101 -0
- package/packages/selector/src/client/ReviewOverlay.tsx +320 -0
- package/packages/selector/src/client/index.ts +17 -0
- package/packages/selector/src/client/locales.ts +38 -0
- package/packages/selector/src/client/preset-options.ts +1 -0
- package/packages/selector/src/client/review-scope.ts +16 -0
- package/packages/selector/src/client/settings-section.tsx +17 -8
- package/packages/selector/src/index.ts +308 -0
- package/packages/selector/src/profiles.ts +28 -1
- package/packages/selector/src/pruner/state.ts +27 -0
- package/packages/selector/src/pruner.ts +430 -10
- package/packages/selector/src/runtime/audit.ts +27 -0
- package/packages/selector/src/runtime/config.ts +33 -1
- package/packages/selector/src/runtime/tokenpilot/estimator.ts +60 -13
- package/packages/selector/src/runtime/tokenpilot/proposal.ts +223 -0
- package/packages/selector/src/runtime/tokenpilot/review-queue.ts +231 -0
- package/packages/selector/src/runtime/tokenpilot/review-storage.ts +122 -0
- package/packages/selector/src/runtime/types.ts +17 -0
- package/packages/selector/tests/code-skeleton.client.spec.ts +3 -2
- package/packages/selector/tests/custom-contract.client.spec.ts +3 -2
- package/packages/selector/tests/preset-options-write.client.spec.ts +34 -1
- package/packages/selector/tests/review-overlay.client.spec.tsx +118 -0
- package/packages/selector/tests/review-routes.host.spec.ts +290 -0
- package/packages/selector/tests/runtime/audit.spec.ts +44 -0
- package/packages/selector/tests/runtime/tokenpilot/estimator.spec.ts +23 -0
- package/packages/selector/tests/runtime/tokenpilot/profile-baseline.spec.ts +5 -0
- package/packages/selector/tests/runtime/tokenpilot/proposal.spec.ts +199 -0
- package/packages/selector/tests/runtime/tokenpilot/pruner-review.spec.ts +313 -0
- package/packages/selector/tests/runtime/tokenpilot/review-queue.spec.ts +168 -0
- package/packages/selector/tests/settings-seat.client.spec.ts +5 -4
|
@@ -51,7 +51,6 @@
|
|
|
51
51
|
},
|
|
52
52
|
"scripts": {
|
|
53
53
|
"bundle": "tsdown --config tsdown.config.ts && tsdown --config tsdown.client.config.ts",
|
|
54
|
-
"prepack": "pnpm run bundle",
|
|
55
54
|
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
56
55
|
"test": "vitest run --root ../.. --config vitest.config.ts --project runtime --project selector-host --project selector-client",
|
|
57
56
|
"pack:dry-run": "npm pack --dry-run"
|
|
@@ -275,3 +275,104 @@ export function EstimatorControls({ options, disabled, save, settle, t }: Estima
|
|
|
275
275
|
</section>
|
|
276
276
|
)
|
|
277
277
|
}
|
|
278
|
+
|
|
279
|
+
interface ReviewModeControlsProps {
|
|
280
|
+
options: PresetOptionsSettings
|
|
281
|
+
disabled: boolean
|
|
282
|
+
save: (options: PresetOptionsPatch) => Promise<void>
|
|
283
|
+
settle: (operation: () => Promise<void>) => void
|
|
284
|
+
t: (key: ContextCompressionLocaleKey) => string
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* TokenPilot-inspired review-mode card (beta). When enabled, edge/high-impact
|
|
289
|
+
* reduction candidates queue for manual approval and execute in one merged
|
|
290
|
+
* batch at the next turn boundary; the numeric fields tune the benefit model.
|
|
291
|
+
* Numeric drafts commit on blur and only when they parse to a value the
|
|
292
|
+
* runtime schema accepts, so an invalid keystroke never disables the panel.
|
|
293
|
+
*/
|
|
294
|
+
export function ReviewModeControls({ options, disabled, save, settle, t }: ReviewModeControlsProps) {
|
|
295
|
+
const [turnsDraft, setTurnsDraft] = useState(String(options.reviewTimeoutTurns ?? 6))
|
|
296
|
+
const [alphaDraft, setAlphaDraft] = useState(String(options.cacheHitDiscountAlpha ?? 0.1))
|
|
297
|
+
const [highImpactDraft, setHighImpactDraft] = useState(String(options.reviewHighImpactTokens ?? 4000))
|
|
298
|
+
const reviewMode = options.reviewMode ?? false
|
|
299
|
+
const commit = (patch: PresetOptionsPatch) => {
|
|
300
|
+
settle(() => save(patch))
|
|
301
|
+
}
|
|
302
|
+
const commitTurns = (): void => {
|
|
303
|
+
const next = Number(turnsDraft)
|
|
304
|
+
if (!Number.isSafeInteger(next) || next < 1 || next === (options.reviewTimeoutTurns ?? 6)) return
|
|
305
|
+
commit({ reviewTimeoutTurns: next })
|
|
306
|
+
}
|
|
307
|
+
const commitAlpha = (): void => {
|
|
308
|
+
const next = Number(alphaDraft)
|
|
309
|
+
if (!Number.isFinite(next) || next <= 0 || next >= 1 || next === (options.cacheHitDiscountAlpha ?? 0.1)) return
|
|
310
|
+
commit({ cacheHitDiscountAlpha: next })
|
|
311
|
+
}
|
|
312
|
+
const commitHighImpact = (): void => {
|
|
313
|
+
const next = Number(highImpactDraft)
|
|
314
|
+
if (!Number.isSafeInteger(next) || next < 0 || next === (options.reviewHighImpactTokens ?? 4000)) return
|
|
315
|
+
commit({ reviewHighImpactTokens: next })
|
|
316
|
+
}
|
|
317
|
+
return (
|
|
318
|
+
<section className={css.autoCompact} aria-labelledby="context-compression-review-title">
|
|
319
|
+
<h3 id="context-compression-review-title" className={css.autoCompactTitle}>{t('review.title')}</h3>
|
|
320
|
+
<p className={css.customNote}>{t('review.description')}</p>
|
|
321
|
+
<label className={css.field}>
|
|
322
|
+
<span>{t('review.enabled')}</span>
|
|
323
|
+
<select
|
|
324
|
+
value={reviewMode ? 'on' : 'off'}
|
|
325
|
+
disabled={disabled}
|
|
326
|
+
onChange={(event) => { settle(() => save({ reviewMode: event.currentTarget.value === 'on' })) }}
|
|
327
|
+
>
|
|
328
|
+
<option value="off">{t('review.enabled.off')}</option>
|
|
329
|
+
<option value="on">{t('review.enabled.on')}</option>
|
|
330
|
+
</select>
|
|
331
|
+
</label>
|
|
332
|
+
{reviewMode ? (
|
|
333
|
+
<>
|
|
334
|
+
<label className={css.field}>
|
|
335
|
+
<span>{t('review.timeoutTurns')}</span>
|
|
336
|
+
<input
|
|
337
|
+
type="number"
|
|
338
|
+
min={1}
|
|
339
|
+
step={1}
|
|
340
|
+
value={turnsDraft}
|
|
341
|
+
disabled={disabled}
|
|
342
|
+
onChange={(event) => { setTurnsDraft(event.currentTarget.value) }}
|
|
343
|
+
onBlur={commitTurns}
|
|
344
|
+
onKeyDown={(event) => { if (event.key === 'Enter') event.currentTarget.blur() }}
|
|
345
|
+
/>
|
|
346
|
+
</label>
|
|
347
|
+
<label className={css.field}>
|
|
348
|
+
<span>{t('review.alpha')}</span>
|
|
349
|
+
<input
|
|
350
|
+
type="number"
|
|
351
|
+
min={0.01}
|
|
352
|
+
max={0.99}
|
|
353
|
+
step={0.05}
|
|
354
|
+
value={alphaDraft}
|
|
355
|
+
disabled={disabled}
|
|
356
|
+
onChange={(event) => { setAlphaDraft(event.currentTarget.value) }}
|
|
357
|
+
onBlur={commitAlpha}
|
|
358
|
+
onKeyDown={(event) => { if (event.key === 'Enter') event.currentTarget.blur() }}
|
|
359
|
+
/>
|
|
360
|
+
</label>
|
|
361
|
+
<label className={css.field}>
|
|
362
|
+
<span>{t('review.highImpact')}</span>
|
|
363
|
+
<input
|
|
364
|
+
type="number"
|
|
365
|
+
min={0}
|
|
366
|
+
step={500}
|
|
367
|
+
value={highImpactDraft}
|
|
368
|
+
disabled={disabled}
|
|
369
|
+
onChange={(event) => { setHighImpactDraft(event.currentTarget.value) }}
|
|
370
|
+
onBlur={commitHighImpact}
|
|
371
|
+
onKeyDown={(event) => { if (event.key === 'Enter') event.currentTarget.blur() }}
|
|
372
|
+
/>
|
|
373
|
+
</label>
|
|
374
|
+
</>
|
|
375
|
+
) : null}
|
|
376
|
+
</section>
|
|
377
|
+
)
|
|
378
|
+
}
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TokenPilot-inspired R4: the review floating window.
|
|
3
|
+
*
|
|
4
|
+
* Mounted on the host `shell.overlay` slot (dsh-tidychat precedent: the layer
|
|
5
|
+
* is click-through by default and only the card opts back in), showing a
|
|
6
|
+
* bottom-right badge while any session has pending proposals and a card with
|
|
7
|
+
* the four-state summary row plus one row per proposal. Every 10s it polls the
|
|
8
|
+
* review-queue route; when the queue is empty or review mode is off the
|
|
9
|
+
* component renders null, so it never disturbs the session.
|
|
10
|
+
*
|
|
11
|
+
* Styles carry the `dsh-cc-review-` prefix and ride a one-shot style tag.
|
|
12
|
+
*/
|
|
13
|
+
import { useEffect, useState } from 'react'
|
|
14
|
+
import type { ReactElement } from 'react'
|
|
15
|
+
import type { SettingsScopeLike } from './review-scope.ts'
|
|
16
|
+
|
|
17
|
+
const QUEUE_ROUTES = [
|
|
18
|
+
'/api/dsh-context-compression-improved/review-queue',
|
|
19
|
+
'/endpoint/dsh-context-compression-improved/review-queue',
|
|
20
|
+
] as const
|
|
21
|
+
const DECIDE_ROUTES = [
|
|
22
|
+
'/api/dsh-context-compression-improved/review-decide',
|
|
23
|
+
'/endpoint/dsh-context-compression-improved/review-decide',
|
|
24
|
+
] as const
|
|
25
|
+
|
|
26
|
+
export interface PendingProposal {
|
|
27
|
+
readonly sessionId: string
|
|
28
|
+
readonly id: string
|
|
29
|
+
readonly kind: string
|
|
30
|
+
readonly items: readonly { readonly seq: number, readonly tokensBefore: number, readonly tokensAfter: number }[]
|
|
31
|
+
readonly benefit: {
|
|
32
|
+
readonly recoveredTokens: number
|
|
33
|
+
readonly paybackTurns?: number
|
|
34
|
+
readonly expectedSaving?: number
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ReviewSummary {
|
|
39
|
+
readonly autoApplied: number
|
|
40
|
+
readonly reviewApplied: number
|
|
41
|
+
readonly expired: number
|
|
42
|
+
readonly voided: number
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface ReviewOverlayProps {
|
|
46
|
+
/** Bound settings scope; supplies the reviewMode switch. */
|
|
47
|
+
scope: SettingsScopeLike
|
|
48
|
+
t: (key: string) => string
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const CSS = `
|
|
52
|
+
.dsh-cc-review-badge {
|
|
53
|
+
position: fixed;
|
|
54
|
+
right: 20px;
|
|
55
|
+
bottom: 20px;
|
|
56
|
+
z-index: 70;
|
|
57
|
+
pointer-events: auto;
|
|
58
|
+
box-sizing: border-box;
|
|
59
|
+
min-width: 34px;
|
|
60
|
+
height: 34px;
|
|
61
|
+
padding: 0 10px;
|
|
62
|
+
border-radius: 17px;
|
|
63
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.4));
|
|
64
|
+
background: var(--dsw-alias-bg-layer-3, #fff);
|
|
65
|
+
color: var(--dsw-alias-label-primary, #222);
|
|
66
|
+
font-size: 13px;
|
|
67
|
+
display: flex;
|
|
68
|
+
align-items: center;
|
|
69
|
+
justify-content: center;
|
|
70
|
+
gap: 6px;
|
|
71
|
+
cursor: pointer;
|
|
72
|
+
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.14);
|
|
73
|
+
}
|
|
74
|
+
.dsh-cc-review-card {
|
|
75
|
+
position: fixed;
|
|
76
|
+
right: 20px;
|
|
77
|
+
bottom: 62px;
|
|
78
|
+
z-index: 70;
|
|
79
|
+
pointer-events: auto;
|
|
80
|
+
box-sizing: border-box;
|
|
81
|
+
width: min(420px, calc(100vw - 40px));
|
|
82
|
+
max-height: min(60vh, 520px);
|
|
83
|
+
overflow: auto;
|
|
84
|
+
background: var(--dsw-alias-bg-layer-3, #fff);
|
|
85
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.4));
|
|
86
|
+
border-radius: 12px;
|
|
87
|
+
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.18);
|
|
88
|
+
padding: 12px 14px;
|
|
89
|
+
color: var(--dsw-alias-label-primary, #222);
|
|
90
|
+
font-size: 13px;
|
|
91
|
+
}
|
|
92
|
+
.dsh-cc-review-title {
|
|
93
|
+
font-weight: 600;
|
|
94
|
+
margin: 0 0 6px;
|
|
95
|
+
font-size: 13px;
|
|
96
|
+
}
|
|
97
|
+
.dsh-cc-review-summary {
|
|
98
|
+
display: flex;
|
|
99
|
+
flex-wrap: wrap;
|
|
100
|
+
gap: 4px 12px;
|
|
101
|
+
color: var(--dsw-alias-label-tertiary, #888);
|
|
102
|
+
font-size: 12px;
|
|
103
|
+
margin-bottom: 8px;
|
|
104
|
+
}
|
|
105
|
+
.dsh-cc-review-row {
|
|
106
|
+
border-top: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.25));
|
|
107
|
+
padding: 8px 0;
|
|
108
|
+
}
|
|
109
|
+
.dsh-cc-review-row-meta {
|
|
110
|
+
color: var(--dsw-alias-label-tertiary, #888);
|
|
111
|
+
font-size: 12px;
|
|
112
|
+
margin-bottom: 4px;
|
|
113
|
+
}
|
|
114
|
+
.dsh-cc-review-actions {
|
|
115
|
+
display: flex;
|
|
116
|
+
gap: 8px;
|
|
117
|
+
}
|
|
118
|
+
.dsh-cc-review-btn {
|
|
119
|
+
appearance: none;
|
|
120
|
+
border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.4));
|
|
121
|
+
background: transparent;
|
|
122
|
+
color: inherit;
|
|
123
|
+
border-radius: 6px;
|
|
124
|
+
padding: 3px 10px;
|
|
125
|
+
font-size: 12px;
|
|
126
|
+
cursor: pointer;
|
|
127
|
+
}
|
|
128
|
+
.dsh-cc-review-btn-primary {
|
|
129
|
+
background: var(--dsw-alias-state-business-primary, #3b82f6);
|
|
130
|
+
border-color: transparent;
|
|
131
|
+
color: #fff;
|
|
132
|
+
}
|
|
133
|
+
`
|
|
134
|
+
|
|
135
|
+
function injectOnce(): () => void {
|
|
136
|
+
const tag = document.createElement('style')
|
|
137
|
+
tag.setAttribute('data-plugin-css', 'dsh-context-compression-improved-review')
|
|
138
|
+
tag.textContent = CSS
|
|
139
|
+
document.head.appendChild(tag)
|
|
140
|
+
return () => { tag.remove() }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function fetchJson(route: string, init?: RequestInit): Promise<unknown> {
|
|
144
|
+
const response = await fetch(route, { headers: { 'cache-control': 'no-cache' }, ...init })
|
|
145
|
+
if (!response.ok) return undefined
|
|
146
|
+
return response.json()
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function pollQueue(): Promise<{ pending: PendingProposal[], summary?: ReviewSummary | undefined } | undefined> {
|
|
150
|
+
for (const route of QUEUE_ROUTES) {
|
|
151
|
+
const body = await fetchJson(route) as {
|
|
152
|
+
ok?: boolean
|
|
153
|
+
total?: number
|
|
154
|
+
pending?: PendingProposal[]
|
|
155
|
+
summary?: ReviewSummary
|
|
156
|
+
} | undefined
|
|
157
|
+
if (body?.ok === true) {
|
|
158
|
+
return { pending: body.pending ?? [], summary: body.summary }
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return undefined
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function postDecide(proposal: PendingProposal, decision: string): Promise<boolean> {
|
|
165
|
+
for (const route of DECIDE_ROUTES) {
|
|
166
|
+
try {
|
|
167
|
+
const response = await fetch(route, {
|
|
168
|
+
method: 'POST',
|
|
169
|
+
headers: { 'content-type': 'application/json' },
|
|
170
|
+
body: JSON.stringify({ sessionId: proposal.sessionId, proposalId: proposal.id, decision }),
|
|
171
|
+
})
|
|
172
|
+
if (response.status !== 404) return response.ok
|
|
173
|
+
} catch {
|
|
174
|
+
// Try the next prefix.
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return false
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Slot factory helper: the client entry is a .ts file and cannot carry JSX,
|
|
182
|
+
* so the element construction lives here.
|
|
183
|
+
*/
|
|
184
|
+
export function renderReviewOverlay(
|
|
185
|
+
scope: SettingsScopeLike,
|
|
186
|
+
t: (key: string) => string,
|
|
187
|
+
): ReactElement {
|
|
188
|
+
return <ReviewOverlay scope={scope} t={t} />
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* The floating window itself: renders null (and stays silent) while review
|
|
193
|
+
* mode is off or nothing is pending.
|
|
194
|
+
*/
|
|
195
|
+
export function ReviewOverlay({ scope, t }: ReviewOverlayProps) {
|
|
196
|
+
const [reviewMode, setReviewMode] = useState(false)
|
|
197
|
+
const [pending, setPending] = useState<PendingProposal[]>([])
|
|
198
|
+
const [summary, setSummary] = useState<ReviewSummary | undefined>()
|
|
199
|
+
const [expanded, setExpanded] = useState(false)
|
|
200
|
+
const [busy, setBusy] = useState(false)
|
|
201
|
+
|
|
202
|
+
useEffect(injectOnce, [])
|
|
203
|
+
useEffect(() => {
|
|
204
|
+
const pull = (): void => {
|
|
205
|
+
try {
|
|
206
|
+
const snapshot = scope.getSnapshot()
|
|
207
|
+
setReviewMode(snapshot.status === 'ready' && snapshot.value?.presetOptions?.reviewMode === true)
|
|
208
|
+
} catch {
|
|
209
|
+
setReviewMode(false)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
pull()
|
|
213
|
+
let unsubscribe = (): void => {}
|
|
214
|
+
try {
|
|
215
|
+
unsubscribe = scope.subscribe(pull)
|
|
216
|
+
} catch {
|
|
217
|
+
unsubscribe = (): void => {}
|
|
218
|
+
}
|
|
219
|
+
return unsubscribe
|
|
220
|
+
}, [scope])
|
|
221
|
+
|
|
222
|
+
useEffect(() => {
|
|
223
|
+
if (!reviewMode) return
|
|
224
|
+
let alive = true
|
|
225
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
226
|
+
const tick = (): void => {
|
|
227
|
+
void pollQueue().then((result) => {
|
|
228
|
+
if (!alive) return
|
|
229
|
+
setPending(result?.pending ?? [])
|
|
230
|
+
setSummary(result?.summary)
|
|
231
|
+
timer = setTimeout(tick, 10_000)
|
|
232
|
+
})
|
|
233
|
+
}
|
|
234
|
+
tick()
|
|
235
|
+
return () => {
|
|
236
|
+
alive = false
|
|
237
|
+
if (timer !== undefined) clearTimeout(timer)
|
|
238
|
+
}
|
|
239
|
+
}, [reviewMode])
|
|
240
|
+
|
|
241
|
+
if (!reviewMode || pending.length === 0) return null
|
|
242
|
+
|
|
243
|
+
const decide = (proposal: PendingProposal, decision: string): void => {
|
|
244
|
+
setBusy(true)
|
|
245
|
+
void postDecide(proposal, decision).then(() => {
|
|
246
|
+
return pollQueue().then((result) => {
|
|
247
|
+
setPending(result?.pending ?? [])
|
|
248
|
+
setSummary(result?.summary)
|
|
249
|
+
setBusy(false)
|
|
250
|
+
})
|
|
251
|
+
}).catch(() => { setBusy(false) })
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const seqRange = (proposal: PendingProposal): string => {
|
|
255
|
+
const seqs = proposal.items.map(item => item.seq)
|
|
256
|
+
const min = Math.min(...seqs)
|
|
257
|
+
const max = Math.max(...seqs)
|
|
258
|
+
return min === max ? `#${String(min)}` : `#${String(min)}–#${String(max)}`
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return (
|
|
262
|
+
<>
|
|
263
|
+
{expanded ? (
|
|
264
|
+
<div className="dsh-cc-review-card">
|
|
265
|
+
<p className="dsh-cc-review-title">{t('review.title')}</p>
|
|
266
|
+
{summary !== undefined ? (
|
|
267
|
+
<div className="dsh-cc-review-summary">
|
|
268
|
+
<span>{t('review.summary.autoApplied')}: {String(summary.autoApplied)}</span>
|
|
269
|
+
<span>{t('review.summary.reviewApplied')}: {String(summary.reviewApplied)}</span>
|
|
270
|
+
<span>{t('review.summary.expired')}: {String(summary.expired)}</span>
|
|
271
|
+
<span>{t('review.summary.voided')}: {String(summary.voided)}</span>
|
|
272
|
+
</div>
|
|
273
|
+
) : null}
|
|
274
|
+
{pending.map(proposal => (
|
|
275
|
+
<div className="dsh-cc-review-row" key={proposal.id}>
|
|
276
|
+
<div className="dsh-cc-review-row-meta">
|
|
277
|
+
{proposal.kind} · {seqRange(proposal)} · R ≈ {String(proposal.benefit.recoveredTokens)}
|
|
278
|
+
{proposal.benefit.paybackTurns !== undefined
|
|
279
|
+
? ` · ${t('review.row.payback')}: ${String(Math.round(proposal.benefit.paybackTurns * 100) / 100)}`
|
|
280
|
+
: ''}
|
|
281
|
+
{proposal.benefit.expectedSaving !== undefined
|
|
282
|
+
? ` · ${t('review.row.expectedSaving')}: ${String(Math.round(proposal.benefit.expectedSaving))} (${t('review.row.estimated')})`
|
|
283
|
+
: ''}
|
|
284
|
+
</div>
|
|
285
|
+
<div className="dsh-cc-review-actions">
|
|
286
|
+
<button
|
|
287
|
+
type="button" className="dsh-cc-review-btn dsh-cc-review-btn-primary"
|
|
288
|
+
disabled={busy}
|
|
289
|
+
onClick={() => { decide(proposal, 'approved') }}
|
|
290
|
+
>
|
|
291
|
+
{t('review.action.approve')}
|
|
292
|
+
</button>
|
|
293
|
+
<button
|
|
294
|
+
type="button" className="dsh-cc-review-btn"
|
|
295
|
+
disabled={busy}
|
|
296
|
+
onClick={() => { decide(proposal, 'rejected') }}
|
|
297
|
+
>
|
|
298
|
+
{t('review.action.reject')}
|
|
299
|
+
</button>
|
|
300
|
+
<button
|
|
301
|
+
type="button" className="dsh-cc-review-btn"
|
|
302
|
+
disabled={busy}
|
|
303
|
+
onClick={() => { decide(proposal, 'ignored') }}
|
|
304
|
+
>
|
|
305
|
+
{t('review.action.ignore')}
|
|
306
|
+
</button>
|
|
307
|
+
</div>
|
|
308
|
+
</div>
|
|
309
|
+
))}
|
|
310
|
+
</div>
|
|
311
|
+
) : null}
|
|
312
|
+
<button
|
|
313
|
+
type="button" className="dsh-cc-review-badge"
|
|
314
|
+
onClick={() => { setExpanded(value => !value) }}
|
|
315
|
+
>
|
|
316
|
+
{t('review.badge')} {String(pending.length)}
|
|
317
|
+
</button>
|
|
318
|
+
</>
|
|
319
|
+
)
|
|
320
|
+
}
|
|
@@ -12,6 +12,7 @@ import { DEFAULT_CUSTOM_COMPRESSION_POLICY } from '../profiles.ts'
|
|
|
12
12
|
import { decodeSettings } from './decode.ts'
|
|
13
13
|
import { en, zh } from './locales.ts'
|
|
14
14
|
import { planPresetOptionsOps, presetOptionsOpsAccepted } from './preset-options.ts'
|
|
15
|
+
import { ReviewOverlay, renderReviewOverlay } from './ReviewOverlay.tsx'
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Harness 0.1.5 mounts the web core's `slots` service on the client context
|
|
@@ -141,6 +142,22 @@ export function apply(ctx: ClientContext): void {
|
|
|
141
142
|
} catch (error) {
|
|
142
143
|
console.warn('[dsh-context-compression-improved] settings.section 注册失败(新宿主已收编):', error)
|
|
143
144
|
}
|
|
145
|
+
|
|
146
|
+
// TokenPilot-inspired R4:审查浮窗挂在 shell.overlay(dsh-tidychat 先例:
|
|
147
|
+
// 该层默认点击穿透,卡片自持指针事件)。reviewMode 关闭或无 pending 时组件
|
|
148
|
+
// 渲染 null —— 与 0.1.2 宿主(无此 slot)同构的降级语义:注册失败不影响设置卡。
|
|
149
|
+
// (本文件是 .ts:元素构造在 ReviewOverlay.renderReviewOverlay,不能内联 JSX。)
|
|
150
|
+
try {
|
|
151
|
+
ctx.slots.inject('shell.overlay', () => ctx.slots.register(
|
|
152
|
+
{ name: 'shell.overlay', id: 'context-compression-review' },
|
|
153
|
+
() => {
|
|
154
|
+
const scope = ctx.settingsScope.bind<ContextCompressionSettings>({ namespace: NS, decode: decodeSettings })
|
|
155
|
+
return renderReviewOverlay(scope, ctx.locale.bind(NS) as (key: string) => string)
|
|
156
|
+
},
|
|
157
|
+
))
|
|
158
|
+
} catch (error) {
|
|
159
|
+
console.warn('[dsh-context-compression-improved] shell.overlay 注册失败(宿主无浮层或已收编):', error)
|
|
160
|
+
}
|
|
144
161
|
}
|
|
145
162
|
|
|
146
163
|
export type {
|
|
@@ -35,6 +35,25 @@ export const zh = {
|
|
|
35
35
|
'estimator.apiKey.set': '已设置 · 输入新值覆盖',
|
|
36
36
|
'estimator.apiKey.clear': '清除',
|
|
37
37
|
'estimator.apiKey.overwrite': '已设置保密值,输入新值并失焦即可覆盖。',
|
|
38
|
+
'review.title': '人工审查(beta)',
|
|
39
|
+
'review.description': '开启后,边缘区间与高影响的压缩候选不再自动执行,而是进入待审队列并在你批准后的下一个回合边界批量执行;未处理的提案超过过期轮数后自动作废。仅随「TokenPilot 启发模式」提供,默认关闭。',
|
|
40
|
+
'review.enabled': '审查模式',
|
|
41
|
+
'review.enabled.on': '开(提案等待人工批准)',
|
|
42
|
+
'review.enabled.off': '关(默认,全自动)',
|
|
43
|
+
'review.timeoutTurns': '提案过期轮数',
|
|
44
|
+
'review.alpha': '缓存命中折扣 α',
|
|
45
|
+
'review.highImpact': '高影响门槛(tokens)',
|
|
46
|
+
'review.badge': '待审',
|
|
47
|
+
'review.summary.autoApplied': '自动应用',
|
|
48
|
+
'review.summary.reviewApplied': '审查应用',
|
|
49
|
+
'review.summary.expired': '已过期',
|
|
50
|
+
'review.summary.voided': '已作废',
|
|
51
|
+
'review.row.payback': '回本轮数',
|
|
52
|
+
'review.row.expectedSaving': '预期节省',
|
|
53
|
+
'review.row.estimated': '估计',
|
|
54
|
+
'review.action.approve': '批准',
|
|
55
|
+
'review.action.reject': '驳回',
|
|
56
|
+
'review.action.ignore': '本会话忽略',
|
|
38
57
|
'detail.tokenpilot-inspired': '在平衡模式之上叠加去重指针、恢复豁免、摘要定位块、前缀稳定与读取状态语义;估计器需另行配置端点',
|
|
39
58
|
'profile.custom': 'Custom/实验模式',
|
|
40
59
|
'profile.native': '原生对照',
|
|
@@ -135,6 +154,25 @@ export const en = {
|
|
|
135
154
|
'estimator.apiKey.set': 'Set · type a new value to overwrite',
|
|
136
155
|
'estimator.apiKey.clear': 'Clear',
|
|
137
156
|
'estimator.apiKey.overwrite': 'A secret is stored; type a new value and blur to overwrite it.',
|
|
157
|
+
'review.title': 'Review mode (beta)',
|
|
158
|
+
'review.description': 'When enabled, edge-band and high-impact reduction candidates no longer apply automatically: they queue for manual approval and execute in one merged batch at the next turn boundary after approval. Unhandled proposals expire after the configured number of turns. Ships with the TokenPilot-inspired profile only, off by default.',
|
|
159
|
+
'review.enabled': 'Review mode',
|
|
160
|
+
'review.enabled.on': 'On (proposals wait for manual approval)',
|
|
161
|
+
'review.enabled.off': 'Off (default, fully automatic)',
|
|
162
|
+
'review.timeoutTurns': 'Proposal expiry (turns)',
|
|
163
|
+
'review.alpha': 'Cache-hit discount α',
|
|
164
|
+
'review.highImpact': 'High-impact threshold (tokens)',
|
|
165
|
+
'review.badge': 'Review',
|
|
166
|
+
'review.summary.autoApplied': 'Auto-applied',
|
|
167
|
+
'review.summary.reviewApplied': 'Review-applied',
|
|
168
|
+
'review.summary.expired': 'Expired',
|
|
169
|
+
'review.summary.voided': 'Voided',
|
|
170
|
+
'review.row.payback': 'Payback',
|
|
171
|
+
'review.row.expectedSaving': 'Expected saving',
|
|
172
|
+
'review.row.estimated': 'estimated',
|
|
173
|
+
'review.action.approve': 'Approve',
|
|
174
|
+
'review.action.reject': 'Reject',
|
|
175
|
+
'review.action.ignore': 'Ignore',
|
|
138
176
|
'detail.tokenpilot-inspired': 'Layered on Balanced: dedupe pointers, recovery exemption, summary locators, prefix stabilization, and read-state semantics; the estimator needs an endpoint configured separately',
|
|
139
177
|
'profile.custom': 'Custom / Experimental',
|
|
140
178
|
'profile.native': 'Native baseline',
|
|
@@ -24,6 +24,7 @@ const PRESET_OPTIONS_KEY = 'presetOptions'
|
|
|
24
24
|
const PRESET_OPTION_KEYS = [
|
|
25
25
|
'dedupeToolResults', 'summaryLocator', 'prefixStabilizer', 'readState', 'estimatorMode',
|
|
26
26
|
'estimatorProvider', 'estimatorModel', 'estimatorBaseUrl', 'estimatorApiKey', 'estimatorTimeoutMs',
|
|
27
|
+
'reviewMode', 'reviewTimeoutTurns', 'cacheHitDiscountAlpha', 'reviewHighImpactTokens',
|
|
27
28
|
] as const
|
|
28
29
|
|
|
29
30
|
/** One partial edit of `presetOptions`; `undefined` clears the named field. */
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The minimal face of the bound settings scope the review overlay consumes —
|
|
3
|
+
* structural, so tests can stub it without the settings transport.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface SettingsScopeLike {
|
|
7
|
+
getSnapshot(): {
|
|
8
|
+
status: string
|
|
9
|
+
value?: {
|
|
10
|
+
presetOptions?: {
|
|
11
|
+
reviewMode?: boolean | undefined
|
|
12
|
+
} | undefined
|
|
13
|
+
} | undefined
|
|
14
|
+
}
|
|
15
|
+
subscribe(listener: () => void): () => void
|
|
16
|
+
}
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
} from '../profiles.ts'
|
|
17
17
|
import type { CompressionProfileSelectorProps } from './CompressionProfileSelector.tsx'
|
|
18
18
|
import { AutoCompactThresholdControls, CodeSkeletonControls } from './CompressionProfileControls.tsx'
|
|
19
|
-
import { EstimatorControls, EstimatorInactiveNotice } from './EstimatorControls.tsx'
|
|
19
|
+
import { EstimatorControls, EstimatorInactiveNotice, ReviewModeControls } from './EstimatorControls.tsx'
|
|
20
20
|
import { CustomPolicyEditor, editableCustom } from './CustomPolicyEditor.tsx'
|
|
21
21
|
|
|
22
22
|
/** Full-page Settings surface backed by the same durable selector state. */
|
|
@@ -105,13 +105,22 @@ export function SettingsCompressionProfileControls({
|
|
|
105
105
|
{current !== 'tokenpilot-inspired' ? (
|
|
106
106
|
<EstimatorInactiveNotice profile={t(`profile.${current}`)} t={t} />
|
|
107
107
|
) : (
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
108
|
+
<>
|
|
109
|
+
<EstimatorControls
|
|
110
|
+
options={state.value?.presetOptions ?? {}}
|
|
111
|
+
disabled={busy || !state.writable || !selectorAvailable}
|
|
112
|
+
save={savePresetOptions}
|
|
113
|
+
settle={settle}
|
|
114
|
+
t={t}
|
|
115
|
+
/>
|
|
116
|
+
<ReviewModeControls
|
|
117
|
+
options={state.value?.presetOptions ?? {}}
|
|
118
|
+
disabled={busy || !state.writable || !selectorAvailable}
|
|
119
|
+
save={savePresetOptions}
|
|
120
|
+
settle={settle}
|
|
121
|
+
t={t}
|
|
122
|
+
/>
|
|
123
|
+
</>
|
|
115
124
|
)}
|
|
116
125
|
<div className={css.pricing}>{t('pricing.disclosure')}</div>
|
|
117
126
|
{current !== 'custom' || draft === null || !selectorAvailable ? null : (
|