dsh-context-compression-improved 0.5.0 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.ja.md +51 -0
- package/CHANGELOG.ko.md +51 -0
- package/CHANGELOG.md +55 -0
- package/CHANGELOG.zh.md +45 -0
- package/package.json +1 -1
- package/packages/selector/lib/advisor-state.js +4 -231
- package/packages/selector/lib/client.d.ts +0 -24
- package/packages/selector/lib/client.js +6 -501
- package/packages/selector/lib/index.d.ts +4 -10
- package/packages/selector/lib/index.js +65 -235
- package/packages/selector/lib/pruner.d.ts +13 -248
- package/packages/selector/lib/pruner.js +148 -552
- package/packages/selector/src/client/EstimatorControls.tsx +277 -378
- package/packages/selector/src/client/index.ts +0 -17
- package/packages/selector/src/client/locales.ts +196 -234
- package/packages/selector/src/client/preset-options.ts +3 -2
- package/packages/selector/src/client/settings-section.tsx +8 -17
- package/packages/selector/src/index.ts +463 -710
- package/packages/selector/src/preset-overlay.ts +60 -1
- package/packages/selector/src/profiles.ts +4 -27
- package/packages/selector/src/pruner/state.ts +50 -73
- package/packages/selector/src/pruner.ts +2402 -2730
- package/packages/selector/src/runtime/audit.ts +27 -21
- package/packages/selector/src/runtime/config.ts +6 -32
- package/packages/selector/src/runtime/tokenpilot/advisor-state.ts +16 -0
- package/packages/selector/src/runtime/tokenpilot/benefit.ts +200 -0
- package/packages/selector/src/runtime/types.ts +0 -17
- package/packages/selector/tests/built/client-artifact.spec.ts +9 -5
- package/packages/selector/tests/preset-options-write.client.spec.ts +7 -23
- package/packages/selector/tests/runtime/advice-never-withholds.host.spec.ts +232 -0
- package/packages/selector/tests/runtime/audit.spec.ts +35 -21
- package/packages/selector/tests/runtime/deprecated-preset-options.spec.ts +96 -0
- package/packages/selector/tests/runtime/tokenpilot/benefit.spec.ts +217 -0
- package/packages/selector/tests/runtime/tokenpilot/profile-baseline.spec.ts +4 -5
- package/packages/selector/tests/settings-seat.client.spec.ts +16 -10
- package/packages/selector/tests/standing-generation.host.spec.ts +54 -5
- package/scripts/packed-components-smoke.mjs +30 -8
- package/scripts/packed-install-e2e.mjs +69 -15
- package/packages/selector/src/client/ReviewOverlay.tsx +0 -320
- package/packages/selector/src/client/review-scope.ts +0 -16
- package/packages/selector/src/runtime/tokenpilot/proposal.ts +0 -267
- package/packages/selector/src/runtime/tokenpilot/review-queue.ts +0 -231
- package/packages/selector/src/runtime/tokenpilot/review-registry.ts +0 -117
- package/packages/selector/src/runtime/tokenpilot/review-storage.ts +0 -122
- package/packages/selector/tests/review-overlay.client.spec.tsx +0 -118
- package/packages/selector/tests/review-routes-registry.host.spec.ts +0 -142
- package/packages/selector/tests/review-routes.host.spec.ts +0 -290
- package/packages/selector/tests/runtime/tokenpilot/proposal.spec.ts +0 -393
- package/packages/selector/tests/runtime/tokenpilot/pruner-review.spec.ts +0 -382
- package/packages/selector/tests/runtime/tokenpilot/review-queue.spec.ts +0 -168
|
@@ -3,12 +3,14 @@
|
|
|
3
3
|
*
|
|
4
4
|
* This spec locks WHERE the compression settings panel mounts: exactly ONE
|
|
5
5
|
* seat — the standalone `settings.section` entry (设置 → 上下文压缩) — and
|
|
6
|
-
* explicitly NOT the Plugins-section surfaces
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
6
|
+
* explicitly NOT the Plugins-section surfaces, and NOT a `shell.overlay` float
|
|
7
|
+
* (the retired review panel's seat was removed with the gate). History this
|
|
8
|
+
* pins against: the 0.1.5 line first lost every entry (a lazy `ctx.get` of
|
|
9
|
+
* locale/settingsScope raced the settings client and apply bailed), then showed
|
|
10
|
+
* the panel twice (item card + tab on top of the standalone section). Both were
|
|
11
|
+
* fixed by the declarative-inject + single-section shape below. When a future
|
|
12
|
+
* DSH line moves the seat again, migrate src/client/index.ts AND this file
|
|
13
|
+
* together.
|
|
12
14
|
*/
|
|
13
15
|
import { describe, expect, it, vi } from 'vitest'
|
|
14
16
|
import type { Context as ClientContext } from '@deepseek-ai/cordis'
|
|
@@ -58,12 +60,16 @@ describe('settings-seat contract (standalone settings.section only)', () => {
|
|
|
58
60
|
expect(inject).toEqual(['slots', 'locale', 'settingsScope'])
|
|
59
61
|
})
|
|
60
62
|
|
|
61
|
-
it('injects exactly the settings.section seat
|
|
63
|
+
it('injects exactly the settings.section seat and nothing else', () => {
|
|
62
64
|
const { declared, registrations } = collectRegistrations()
|
|
63
|
-
expect(declared).toEqual(['settings.section'
|
|
64
|
-
expect(registrations).toHaveLength(
|
|
65
|
+
expect(declared).toEqual(['settings.section'])
|
|
66
|
+
expect(registrations).toHaveLength(1)
|
|
65
67
|
expect(registrations[0]!.slot).toBe('settings.section')
|
|
66
|
-
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('no longer claims a shell.overlay float (the review panel it served is gone)', () => {
|
|
71
|
+
const { declared } = collectRegistrations()
|
|
72
|
+
expect(declared).not.toContain('shell.overlay')
|
|
67
73
|
})
|
|
68
74
|
|
|
69
75
|
it('pins the section identity (id/order/label/locale) and the panel component', () => {
|
|
@@ -101,6 +101,37 @@ async function overlayFiles(sourceMarker: string): Promise<{ path: string, rende
|
|
|
101
101
|
return files
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
/**
|
|
105
|
+
* The mtime this platform actually persists for one given stamp.
|
|
106
|
+
*
|
|
107
|
+
* `node:fs` `utimes` writes the seconds component through a 32-bit field on
|
|
108
|
+
* Windows: on node v22 win32, `utimes(file, 4677832452 s)` reads back as
|
|
109
|
+
* `382865156 s` — exactly `seconds >>> 0` — while sub-second precision
|
|
110
|
+
* survives. The standing stamp deliberately parks identities around year 2162
|
|
111
|
+
* (`STANDING_MTIME_EPOCH_SECONDS + 0xffffffff`, src/preset-overlay.ts), so its
|
|
112
|
+
* seconds component sits past that 2^32-second horizon (max year 2106) and the
|
|
113
|
+
* persisted literal differs on that platform. Deterministically so: the wrap is
|
|
114
|
+
* a pure function of the identity stamp, which is why identity → mtime stays
|
|
115
|
+
* stable and the collision escalation still absorbs any merge.
|
|
116
|
+
*
|
|
117
|
+
* Instead of hardcoding the platform or weakening the assertion, calibrate:
|
|
118
|
+
* ask the filesystem what it stores for exactly this stamp, then require the
|
|
119
|
+
* overlay to persist that same value. On a platform with a full-width seconds
|
|
120
|
+
* field this collapses to the stamp itself, so the assertion stays strict.
|
|
121
|
+
*/
|
|
122
|
+
async function platformPersistedMs(stampMs: number): Promise<number> {
|
|
123
|
+
const directory = await mkdtemp(join(tmpdir(), 'dsh-selector-mtime-probe-'))
|
|
124
|
+
const probe = join(directory, 'probe')
|
|
125
|
+
try {
|
|
126
|
+
await writeFile(probe, 'probe')
|
|
127
|
+
const stamp = new Date(stampMs)
|
|
128
|
+
await utimes(probe, stamp, stamp)
|
|
129
|
+
return (await stat(probe)).mtimeMs
|
|
130
|
+
} finally {
|
|
131
|
+
await rm(directory, { recursive: true, force: true })
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
104
135
|
let sourceRoot: string | undefined
|
|
105
136
|
|
|
106
137
|
/** Rewrite the source preset to an equal-length marker module. */
|
|
@@ -329,8 +360,17 @@ describe('real AgentPresets standing generations with the overlay threshold', ()
|
|
|
329
360
|
expect(firstStat.size).toBe(secondStat.size)
|
|
330
361
|
expect(firstStat.mtimeMs % 1000).toBe(0)
|
|
331
362
|
expect(secondStat.mtimeMs % 1000).toBe(0)
|
|
332
|
-
|
|
333
|
-
|
|
363
|
+
// The coarse metadata surface floors both stamps to a whole second, and the
|
|
364
|
+
// platform may additionally wrap the seconds field (platformPersistedMs),
|
|
365
|
+
// so require exactly what the filesystem stores for those identity stamps.
|
|
366
|
+
const firstStamp = await platformPersistedMs(
|
|
367
|
+
Math.floor(standingStampMsAtWindow(collision.firstIdentity, 0) / 1000) * 1000,
|
|
368
|
+
)
|
|
369
|
+
const secondStamp = await platformPersistedMs(
|
|
370
|
+
Math.floor(standingStampMsAtWindow(collision.secondIdentity, 1) / 1000) * 1000,
|
|
371
|
+
)
|
|
372
|
+
expect(Math.floor(firstStat.mtimeMs / 1000)).toBe(Math.floor(firstStamp / 1000))
|
|
373
|
+
expect(Math.floor(secondStat.mtimeMs / 1000)).toBe(Math.floor(secondStamp / 1000))
|
|
334
374
|
|
|
335
375
|
await installation.dispose()
|
|
336
376
|
})
|
|
@@ -382,11 +422,19 @@ describe('real AgentPresets standing generations with the overlay threshold', ()
|
|
|
382
422
|
expect(files.length).toBeGreaterThanOrEqual(3)
|
|
383
423
|
const buckets = await Promise.all(files.map(async file => Math.floor((await stat(file.path)).mtimeMs / 1000)))
|
|
384
424
|
expect(new Set(buckets).size).toBe(buckets.length)
|
|
385
|
-
// No staging leftovers survive a publish.
|
|
425
|
+
// No staging leftovers survive a publish. A spec file running in parallel
|
|
426
|
+
// disposes its own store while this scan runs, so a vanished directory means
|
|
427
|
+
// "no leftovers to find" rather than a failure (mirrors overlayFiles).
|
|
386
428
|
const storeDirs = await Promise.all(
|
|
387
429
|
(await readdir(tmpdir(), { withFileTypes: true }))
|
|
388
430
|
.filter(entry => entry.isDirectory() && entry.name.startsWith('dsh-context-compression-presets-'))
|
|
389
|
-
.map(async entry =>
|
|
431
|
+
.map(async entry => {
|
|
432
|
+
try {
|
|
433
|
+
return await readdir(join(tmpdir(), entry.name))
|
|
434
|
+
} catch {
|
|
435
|
+
return []
|
|
436
|
+
}
|
|
437
|
+
}),
|
|
390
438
|
)
|
|
391
439
|
expect(storeDirs.flat().some(name => name.endsWith('.tmp'))).toBe(false)
|
|
392
440
|
|
|
@@ -451,8 +499,9 @@ describe('real AgentPresets standing generations with the overlay threshold', ()
|
|
|
451
499
|
? /standard-([0-9a-f]+)\.agent\.cordis\.yml$/u.exec(files[0]!.path)?.[1]
|
|
452
500
|
: undefined
|
|
453
501
|
expect(identity).toBeDefined()
|
|
502
|
+
const persistedStamp = await platformPersistedMs(standingStampMs(identity as string))
|
|
454
503
|
expect(Math.floor(details.mtimeMs / 1000))
|
|
455
|
-
.toBe(Math.floor(
|
|
504
|
+
.toBe(Math.floor(persistedStamp / 1000))
|
|
456
505
|
const observedSubSecond = details.mtimeMs % 1000
|
|
457
506
|
const expectedSubSecond = standingStampMs(identity as string) % 1000
|
|
458
507
|
expect(observedSubSecond === 0 || Math.abs(observedSubSecond - expectedSubSecond) < 1).toBe(true)
|
|
@@ -561,13 +561,25 @@ try {
|
|
|
561
561
|
&& JSON.stringify(originalImage).includes('packed-image-800x600')
|
|
562
562
|
&& JSON.stringify(originalImage).includes('"type":"image"'),
|
|
563
563
|
'packed vision image-bearing result is no longer intact on the surface')
|
|
564
|
+
// The character basis retired the `exact-tokenizer-unavailable` skip: a
|
|
565
|
+
// History planning skip can no longer refuse for a missing exact count (see
|
|
566
|
+
// the HistoryPlanOutcome note in src/pruner/types.ts). A vision session
|
|
567
|
+
// without the exact tokenizer is therefore judged on characters and its
|
|
568
|
+
// skip carries a real reason (observed: fresh/aggregate `at-or-below-
|
|
569
|
+
// trigger`, history `below-profile-trigger`) — assert that observable
|
|
570
|
+
// contract instead of the removed literal.
|
|
564
571
|
for (const component of ['fresh', 'history']) {
|
|
565
|
-
|
|
566
|
-
&&
|
|
567
|
-
&&
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
572
|
+
const record = visionAudit.find(entry => entry.kind === 'component-evaluation'
|
|
573
|
+
&& entry.sessionId === String(visionImage.id)
|
|
574
|
+
&& entry.component === component)
|
|
575
|
+
assert(record !== undefined,
|
|
576
|
+
`packed vision image session lacks the ${component} component-evaluation audit`)
|
|
577
|
+
assert(record.status === 'skipped',
|
|
578
|
+
`packed vision ${component} status is ${String(record.status)}, expected skipped`)
|
|
579
|
+
assert(record.measurementKind === 'characters',
|
|
580
|
+
`packed vision ${component} was decided on ${String(record.measurementKind)}, expected characters`)
|
|
581
|
+
assert(typeof record.reason === 'string' && record.reason.length > 0,
|
|
582
|
+
`packed vision ${component} skip carries no diagnostic reason`)
|
|
571
583
|
}
|
|
572
584
|
|
|
573
585
|
console.info(`PACKED_VISION_E2E ${JSON.stringify({
|
|
@@ -651,9 +663,19 @@ try {
|
|
|
651
663
|
&& firstIndex('history') < firstIndex('tail-trim'),
|
|
652
664
|
'installed component rewrite order is wrong')
|
|
653
665
|
const pipelineRewrites = rewrites.filter(record => record.sessionId === String(session.id))
|
|
666
|
+
// The character basis decides on Unicode code points, so a committed rewrite
|
|
667
|
+
// may report an unchanged token figure while still reclaiming characters
|
|
668
|
+
// (observed once here: history `historical-tool-result-aging` landed at
|
|
669
|
+
// 129 -> 129 with tokensRemoved 0). The rewrite audit carries no character
|
|
670
|
+
// field yet, so assert what the basis can actually guarantee: integer
|
|
671
|
+
// accounting, a consistent removal figure, and never an increase. A strict
|
|
672
|
+
// token decrease would re-encode the retired exact-token authority — see the
|
|
673
|
+
// measurementBasis note in src/runtime/audit.ts.
|
|
654
674
|
assert(pipelineRewrites.every(record => Number.isSafeInteger(record.tokensBefore)
|
|
655
|
-
&& Number.isSafeInteger(record.tokensAfter)
|
|
656
|
-
|
|
675
|
+
&& Number.isSafeInteger(record.tokensAfter)
|
|
676
|
+
&& record.tokensBefore >= record.tokensAfter
|
|
677
|
+
&& record.tokensRemoved === record.tokensBefore - record.tokensAfter),
|
|
678
|
+
'installed rewrite token accounting is inconsistent or increasing')
|
|
657
679
|
assert(audit.some(record => record.kind === 'native-auto-compact'
|
|
658
680
|
&& record.sessionId === String(session.id)),
|
|
659
681
|
'installed Runtime did not audit official Native summary')
|
|
@@ -2,7 +2,7 @@ import { createHash } from 'node:crypto'
|
|
|
2
2
|
import { spawn } from 'node:child_process'
|
|
3
3
|
import { createReadStream } from 'node:fs'
|
|
4
4
|
import { copyFile, mkdir, mkdtemp, readFile, readdir, realpath, rm, stat, writeFile } from 'node:fs/promises'
|
|
5
|
-
import { readFileSync } from 'node:fs'
|
|
5
|
+
import { readFileSync, readdirSync } from 'node:fs'
|
|
6
6
|
import { createServer } from 'node:http'
|
|
7
7
|
import { createRequire } from 'node:module'
|
|
8
8
|
import { tmpdir } from 'node:os'
|
|
@@ -27,8 +27,17 @@ const officialHostPackages = Object.entries(rootManifest.devDependencies)
|
|
|
27
27
|
.filter(([name]) => name.startsWith('@deepseek-ai/'))
|
|
28
28
|
.map(([name, version]) => `${name}@${version}`)
|
|
29
29
|
|
|
30
|
+
// Only the package-manager shims need a shell on Windows; every real executable
|
|
31
|
+
// must be spawned directly, because cmd.exe re-parses arguments: `^` is its
|
|
32
|
+
// escape character, so `HEAD^{tree}` reached git as `HEAD{tree}`, and a
|
|
33
|
+
// multi-line `node -e` settings script was cut at its first newline into
|
|
34
|
+
// `[eval]:1 const` / `SyntaxError: Unexpected end of input`. Both killed the
|
|
35
|
+
// official-clone leg for reasons unrelated to what it asserts.
|
|
36
|
+
const SHELL_COMMANDS = new Set(['npm', 'npm.cmd', 'pnpm', 'pnpm.cmd'])
|
|
37
|
+
const usesShell = (command) => process.platform === 'win32' && SHELL_COMMANDS.has(command)
|
|
38
|
+
|
|
30
39
|
const run = (command, args, options = {}) => new Promise((resolve, reject) => {
|
|
31
|
-
const child = spawn(command, args, { stdio: 'inherit', shell:
|
|
40
|
+
const child = spawn(command, args, { stdio: 'inherit', shell: usesShell(command), ...options })
|
|
32
41
|
child.once('error', reject)
|
|
33
42
|
child.once('exit', (code, signal) => {
|
|
34
43
|
if (code === 0) resolve()
|
|
@@ -37,7 +46,7 @@ const run = (command, args, options = {}) => new Promise((resolve, reject) => {
|
|
|
37
46
|
})
|
|
38
47
|
|
|
39
48
|
const capture = (command, args, options = {}) => new Promise((resolve, reject) => {
|
|
40
|
-
const child = spawn(command, args, { shell:
|
|
49
|
+
const child = spawn(command, args, { shell: usesShell(command), ...options, stdio: ['ignore', 'pipe', 'pipe'] })
|
|
41
50
|
let stdout = ''
|
|
42
51
|
let stderr = ''
|
|
43
52
|
child.stdout.setEncoding('utf8')
|
|
@@ -56,7 +65,7 @@ const capture = (command, args, options = {}) => new Promise((resolve, reject) =
|
|
|
56
65
|
})
|
|
57
66
|
|
|
58
67
|
const captureOutcome = (command, args, options = {}) => new Promise((resolve, reject) => {
|
|
59
|
-
const child = spawn(command, args, { shell:
|
|
68
|
+
const child = spawn(command, args, { shell: usesShell(command), ...options, stdio: ['ignore', 'pipe', 'pipe'] })
|
|
60
69
|
let stdout = ''
|
|
61
70
|
let stderr = ''
|
|
62
71
|
child.stdout.setEncoding('utf8')
|
|
@@ -306,7 +315,23 @@ async function runOfficialCloneCliSmoke(referenceRoot, registry, upgradeFrom, ca
|
|
|
306
315
|
const dshHome = join(temporaryRoot, 'dsh-home')
|
|
307
316
|
let added = false
|
|
308
317
|
const environment = { ...process.env, DSH_HOME: dshHome }
|
|
309
|
-
|
|
318
|
+
let lastLifecycle = { command: '(none)', code: 0, stdout: '', stderr: '' }
|
|
319
|
+
const dsh = async (...args) => {
|
|
320
|
+
const outcome = await captureOutcome('pnpm', ['dsh', ...args], { cwd: worktree, env: environment })
|
|
321
|
+
lastLifecycle = {
|
|
322
|
+
command: `pnpm dsh ${args.join(' ')}`,
|
|
323
|
+
code: outcome.code ?? -1,
|
|
324
|
+
stdout: outcome.stdout,
|
|
325
|
+
stderr: outcome.stderr,
|
|
326
|
+
}
|
|
327
|
+
if (outcome.code !== 0) {
|
|
328
|
+
throw new Error([
|
|
329
|
+
`${lastLifecycle.command} exited ${String(lastLifecycle.code)}`,
|
|
330
|
+
outcome.stdout.trim(),
|
|
331
|
+
outcome.stderr.trim(),
|
|
332
|
+
].filter(Boolean).join('\n'))
|
|
333
|
+
}
|
|
334
|
+
}
|
|
310
335
|
const dumpConfig = () => capture('pnpm', ['dsh', '--profile', 'web', '--dump-config'], {
|
|
311
336
|
cwd: worktree,
|
|
312
337
|
env: environment,
|
|
@@ -316,7 +341,36 @@ async function runOfficialCloneCliSmoke(referenceRoot, registry, upgradeFrom, ca
|
|
|
316
341
|
const profilePackages = () => {
|
|
317
342
|
const profileRoot = join(dshHome, 'profiles/web')
|
|
318
343
|
const profileRequire = createRequire(join(profileRoot, 'package.json'))
|
|
319
|
-
|
|
344
|
+
let selectorPath
|
|
345
|
+
try {
|
|
346
|
+
selectorPath = profileRequire.resolve('dsh-context-compression-improved/package.json')
|
|
347
|
+
} catch (error) {
|
|
348
|
+
// A silent no-op lifecycle command is the difference between "the package
|
|
349
|
+
// manager refused" and "the CLI exited 0 without installing anything";
|
|
350
|
+
// carry that evidence instead of a bare MODULE_NOT_FOUND.
|
|
351
|
+
const declared = (() => {
|
|
352
|
+
try {
|
|
353
|
+
return JSON.stringify(JSON.parse(readFileSync(join(profileRoot, 'package.json'), 'utf8')).dependencies ?? {})
|
|
354
|
+
} catch {
|
|
355
|
+
return '(profile package.json unreadable)'
|
|
356
|
+
}
|
|
357
|
+
})()
|
|
358
|
+
const installed = (() => {
|
|
359
|
+
try {
|
|
360
|
+
return JSON.stringify(readdirSync(join(profileRoot, 'node_modules')).slice(0, 40))
|
|
361
|
+
} catch {
|
|
362
|
+
return '(profiles/web/node_modules unreadable)'
|
|
363
|
+
}
|
|
364
|
+
})()
|
|
365
|
+
throw new Error([
|
|
366
|
+
`official profile ${profileRoot} does not resolve dsh-context-compression-improved`,
|
|
367
|
+
`last lifecycle: ${lastLifecycle.command} (exit ${String(lastLifecycle.code)})`,
|
|
368
|
+
`lifecycle stdout: ${lastLifecycle.stdout.trim()}`,
|
|
369
|
+
`lifecycle stderr: ${lastLifecycle.stderr.trim()}`,
|
|
370
|
+
`profile dependencies: ${declared}`,
|
|
371
|
+
`profiles/web/node_modules: ${installed}`,
|
|
372
|
+
].join('\n'), { cause: error })
|
|
373
|
+
}
|
|
320
374
|
return {
|
|
321
375
|
profileRoot,
|
|
322
376
|
selectorPath,
|
|
@@ -623,8 +677,8 @@ async function runOfficialCloneCliSmoke(referenceRoot, registry, upgradeFrom, ca
|
|
|
623
677
|
// $DSH_HOME/profiles/node_modules — the shared-module fallback the
|
|
624
678
|
// plugin's harness peers resolve through in every later raw-node proof.
|
|
625
679
|
const addedDump = await dumpConfig()
|
|
626
|
-
assert(addedDump.includes('context-compression-
|
|
627
|
-
'official post-add dump lacks the
|
|
680
|
+
assert(addedDump.includes('context-compression-improved-bundle'),
|
|
681
|
+
'official post-add dump lacks the context-compression Bundle layer')
|
|
628
682
|
|
|
629
683
|
// Real profile start on the previous release, BEFORE seeding: the
|
|
630
684
|
// previous-release schema predates the autoCompact section, so this probe
|
|
@@ -646,11 +700,11 @@ async function runOfficialCloneCliSmoke(referenceRoot, registry, upgradeFrom, ca
|
|
|
646
700
|
const loadProof = await provePluginLoads()
|
|
647
701
|
const postUpBoot = await runProfileBootProbe('post-up', true)
|
|
648
702
|
const upDump = await dumpConfig()
|
|
649
|
-
for (const marker of ['context-compression-
|
|
703
|
+
for (const marker of ['context-compression-improved-bundle', 'presetOverlay: true']) {
|
|
650
704
|
assert(upDump.includes(marker), `official post-up dump lacks ${marker}`)
|
|
651
705
|
}
|
|
652
|
-
assert(upDump.match(/context-compression-
|
|
653
|
-
'official post-up dump contains more than one
|
|
706
|
+
assert(upDump.match(/context-compression-improved-bundle/gu)?.length === 1,
|
|
707
|
+
'official post-up dump contains more than one context-compression Bundle layer')
|
|
654
708
|
|
|
655
709
|
// pnpm's peers check is informational: plugin peers resolve through the
|
|
656
710
|
// healed profiles/node_modules fallback, which pnpm cannot see by design.
|
|
@@ -661,14 +715,14 @@ async function runOfficialCloneCliSmoke(referenceRoot, registry, upgradeFrom, ca
|
|
|
661
715
|
|
|
662
716
|
await dsh('plugin', '--profile', 'web', 'remove', 'dsh-context-compression-improved')
|
|
663
717
|
const removedDump = await dumpConfig()
|
|
664
|
-
assert(!removedDump.includes('context-compression-
|
|
665
|
-
'official CLI remove left the
|
|
718
|
+
assert(!removedDump.includes('context-compression-improved-bundle'),
|
|
719
|
+
'official CLI remove left the context-compression Bundle layer active')
|
|
666
720
|
|
|
667
721
|
await dsh('plugin', '--profile', 'web', 'add',
|
|
668
722
|
'dsh-context-compression-improved@latest', '--registry', registry)
|
|
669
723
|
const secondDump = await dumpConfig()
|
|
670
|
-
assert(secondDump.includes('context-compression-
|
|
671
|
-
'official CLI reinstall did not restore the
|
|
724
|
+
assert(secondDump.includes('context-compression-improved-bundle'),
|
|
725
|
+
'official CLI reinstall did not restore the context-compression Bundle layer')
|
|
672
726
|
|
|
673
727
|
return {
|
|
674
728
|
tag: before.tag,
|
|
@@ -1,320 +0,0 @@
|
|
|
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
|
-
}
|