pi-midcompact 0.5.3 → 0.6.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 CHANGED
@@ -55,7 +55,7 @@ The raw session JSONL still contains:
55
55
 
56
56
  ### A reviewed draft can reclaim meaningful context
57
57
 
58
- The earlier browser and TUI captures below illustrate a draft with **2 ranges** covering **42 of 73 atoms**, while the other 31 atoms remain verbatim. The current UI reports Pi-provided anchor usage separately from factual content chars and image counts; it does not derive projected token savings from local character estimates. Click either image to open it at full resolution.
58
+ The earlier browser and TUI captures below illustrate a draft with **2 ranges** covering **42 of 73 atoms**, while the other 31 atoms remain verbatim. The current UI reports Pi-provided anchor usage as the baseline and derives a **display-only** projection of post-commit usage from documented char-class assumptions (labeled `est.`, shown as a range, never used for gating); factual content chars and image counts stay alongside it. Click either image to open it at full resolution.
59
59
 
60
60
  <p align="center">
61
61
  <a href="./figures/review-webui.png">
@@ -222,6 +222,23 @@ Enter/Esc/q close
222
222
 
223
223
  The extension shows planning status in Pi's footer only while a transaction is active. It disappears after commit or abort.
224
224
 
225
+ ## Web UI Development
226
+
227
+ From a source checkout, run the browser workbench against in-memory fixtures
228
+ without starting Pi:
229
+
230
+ ```bash
231
+ npm run dev:webui
232
+ npm run dev:webui -- --port=4180 --no-open
233
+ ```
234
+
235
+ The command opens a fixture router for `review-ready`, `review-pending`,
236
+ `selection-mixed`, `no-telemetry`, and `wide-content`. Each button opens an
237
+ isolated workbench with its own in-memory draft. Fixture pages survive browser
238
+ refresh and the page's Close action, reload HTML changes automatically, and
239
+ restart for imported TypeScript changes. Stop the router with `Ctrl+C`. Use
240
+ `dev/midcompact-debug-ui.ts` only when validating against an actual Pi session.
241
+
225
242
  ## Guarantees and Limits
226
243
 
227
244
  - **Original history is retained.** Compression changes what later model requests see, not the stored Pi messages.
package/README.zh-CN.md CHANGED
@@ -55,7 +55,7 @@ Pi 内置的 `/compact` 可理解为**前缀压缩**(prefix compaction):
55
55
 
56
56
  ### 实际压缩效果
57
57
 
58
- 下面的早期浏览器和 TUI 截图展示了一份包含 **2 个区段**、覆盖 **73 个 atom 中 42 个**的草案,其余 31 个 atom 保留原文。当前 UI 会把 Pi 提供的锚点 usage 与扩展统计的 content chars、图片数量分开显示,不再根据本地字符估算推导预计 token 节省量。点击图片可查看原图。
58
+ 下面的早期浏览器和 TUI 截图展示了一份包含 **2 个区段**、覆盖 **73 个 atom 中 42 个**的草案,其余 31 个 atom 保留原文。当前 UI Pi 上报的锚点 usage 为基线,并根据文档化的字符分类假设推导**仅供展示**的 commit 后占用预计(标注 `est.`、以区间呈现、绝不参与门禁);事实性的 content chars 与图片数量仍然并列展示。点击图片可查看原图。
59
59
 
60
60
  <p align="center">
61
61
  <a href="./figures/review-webui.png">
@@ -222,6 +222,21 @@ Enter/Esc/q 关闭
222
222
 
223
223
  扩展只在事务进行期间在 Pi 页脚显示规划状态;提交或放弃后会自动清除。
224
224
 
225
+ ## Web UI 开发
226
+
227
+ 在源码 checkout 中,无需启动 Pi 即可用内存 fixture 运行浏览器工作台:
228
+
229
+ ```bash
230
+ npm run dev:webui
231
+ npm run dev:webui -- --port=4180 --no-open
232
+ ```
233
+
234
+ 命令会打开一个 fixture router,其中包含 `review-ready`、`review-pending`、
235
+ `selection-mixed`、`no-telemetry` 和 `wide-content`。每个按钮都会打开一套独立的
236
+ 工作台和内存草案。刷新浏览器或点击 Close 不会终止 fixture;HTML 修改会自动刷新
237
+ 页面,导入的 TypeScript 修改会触发进程重启。用 `Ctrl+C` 停止 router。只有在验证
238
+ 真实 Pi 会话集成时,才需要使用 `dev/midcompact-debug-ui.ts`。
239
+
225
240
  ## 保证与限制
226
241
 
227
242
  - **保留原始历史。** 压缩只改变后续模型请求看到的内容,不改写存储的 Pi 消息。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-midcompact",
3
- "version": "0.5.3",
3
+ "version": "0.6.0",
4
4
  "description": "Branch-aware mid-context compression for the Pi coding agent",
5
5
  "author": "frostime",
6
6
  "repository": {
@@ -32,7 +32,8 @@
32
32
  ],
33
33
  "scripts": {
34
34
  "typecheck": "tsc -p tsconfig.json --noEmit",
35
- "test": "rm -rf .test-dist && tsc -p tsconfig.test.json && node test/install-mocks.mjs && node --test test/schema-contract.test.mjs test/core.test.mjs test/runtime-start.test.mjs test/runtime-agent.test.mjs test/runtime-user.test.mjs test/runtime-transaction.test.mjs test/runtime-lock.test.mjs",
35
+ "dev:webui": "tsx watch dev/review-webui-preview.ts",
36
+ "test": "rm -rf .test-dist && tsc -p tsconfig.test.json && node test/install-mocks.mjs && node --test test/schema-contract.test.mjs test/core.test.mjs test/runtime-start.test.mjs test/runtime-agent.test.mjs test/runtime-user.test.mjs test/runtime-transaction.test.mjs test/runtime-lock.test.mjs && node test/review-webui-dom.test.mjs",
36
37
  "typecheck:contract": "tsc -p tsconfig.test.json --noEmit",
37
38
  "pack:check": "npm pack --dry-run"
38
39
  },
@@ -45,6 +46,7 @@
45
46
  "@earendil-works/pi-ai": "^0.84.1",
46
47
  "@earendil-works/pi-coding-agent": "^0.84.1",
47
48
  "typescript": "^5.9.3",
49
+ "tsx": "^4.23.13",
48
50
  "@earendil-works/pi-tui": "^0.84.1"
49
51
  },
50
52
  "pi": {
package/src/SPEC.md CHANGED
@@ -32,7 +32,12 @@ package.json → `pi.extensions`).
32
32
  then appends state, abort appends nothing. Both refuse to run while the
33
33
  Agent holds the planning lock.
34
34
  - `anchorUsage` is informational only — Pi-reported awareness, never an
35
- optimization target (same rule as `projectedTokens` / estimates).
35
+ optimization target. The web workbench may additionally render a
36
+ **display-level** post-commit projection derived from the documented
37
+ char-class assumption table in `content-metrics` (`TOKEN_ESTIMATE`): always
38
+ labeled `est.`, shown as a propagated band, and never feeding commit
39
+ gating, range validation, or any decision (same rule as `projectedTokens`
40
+ / `approxTokens`).
36
41
  - Atom refs are transaction-local: re-run inspect/locate in a later
37
42
  transaction; group refs (`g...`) are never locate refs.
38
43
 
@@ -78,6 +83,24 @@ package.json → `pi.extensions`).
78
83
  `skill:<name>`).
79
84
  - The tool never starts a transaction and never commits; both are command-
80
85
  or user-gated. Recall is the only action valid without a transaction.
86
+ - Web workbench (`review-webui.html` + `review-webui.ts`): the state payload
87
+ carries per-atom char-class counts (`narrowChars`/`wideChars`), per-range
88
+ replacement char-class counts (including the actual wrapper), and the
89
+ assumption table (`est`), so the page renders the projection band and can
90
+ update estimates while a summary is edited; `GET /api/atom/:ref` serves the
91
+ frozen atom's full text for the original-text drawer (read-only, snapshot-local).
92
+ `startReviewWebUiServer` owns the Pi-independent loopback HTTP contract;
93
+ `showReviewWebUi` adapts it to Pi notification, browser launch, and page-bound
94
+ lifetime. Development may opt into a persistent server without changing the
95
+ production defaults.
96
+ User-facing copy says "can't compress" for protected atoms; "protected"
97
+ stays the agent/tool-side term.
98
+ Page invariants that broke once and must hold: the `<!--MIDCOMPACT_STATE-->`
99
+ script tag is the server's template injection point (renaming it breaks
100
+ state loading); `selectionRefs` initialization depends on helpers defined
101
+ later in the page script (order is load-bearing, TDZ); `gbody` visibility
102
+ is driven by the render-time `hidden` attribute, so collapse handlers must
103
+ sync that attribute, not just a class.
81
104
 
82
105
  ## Change rules
83
106
 
@@ -1,6 +1,8 @@
1
- // Sole owner of factual message content statistics. Never converts local char or
2
- // image byte counts into token claims. Image base64 never contributes to text
3
- // char counts.
1
+ // Sole owner of message content statistics. Factual char/image counts are the
2
+ // authority; the only token conversion allowed is the explicitly scoped,
3
+ // display-level estimator at the bottom of this file (web UI presentation
4
+ // only — it never gates decisions, commits, or range validity). Image base64
5
+ // never contributes to text char counts.
4
6
 
5
7
  import type { ContentMetrics, ImageFact, MessageLike } from "./types.js";
6
8
 
@@ -183,3 +185,73 @@ export function aggregateMetrics(parts: readonly ContentMetrics[]): ContentMetri
183
185
  }
184
186
  return { contentChars, imageCount, images };
185
187
  }
188
+
189
+ // ---- Display-level token estimation (web UI presentation only) ----
190
+ //
191
+ // The web UI shows a projected post-commit usage band. Because the consumer
192
+ // model is provider-dependent, tokens are estimated from char classes with a
193
+ // documented assumption table instead of a real tokenizer: the [low, high]
194
+ // pairs express published tokenizer spread and are propagated as a band, never
195
+ // collapsed into a single authoritative number. The band is derived from the
196
+ // content mix of the atoms involved (ASCII-heavy content lands near the tight
197
+ // end, CJK-heavy near the wide end). Nothing here feeds commit gating, range
198
+ // validation, or any decision.
199
+
200
+ /** Per-char-class token-cost assumptions: [low, high] tokens per char. */
201
+ export const TOKEN_ESTIMATE = {
202
+ /** ASCII/code: roughly 3.3–4.5 chars per token across common tokenizers. */
203
+ narrowTokPerChar: [0.22, 0.3],
204
+ /** Non-ASCII scripts (CJK, kana, hangul, emoji, …): the dominant spread. */
205
+ wideTokPerChar: [0.5, 1.0],
206
+ /** Images: provider- and resolution-dependent flat allowance. */
207
+ imageTok: [700, 1600],
208
+ } as const;
209
+
210
+ /** Char classes the estimator distinguishes. */
211
+ export interface CharMix {
212
+ /** ASCII code points. */
213
+ narrowChars: number;
214
+ /** Everything non-ASCII (counted conservatively at the wide rate). */
215
+ wideChars: number;
216
+ }
217
+
218
+ // Classification is deliberately coarse: ASCII is estimated at the narrow
219
+ // rate; every non-ASCII code point (CJK, kana, hangul, emoji, Cyrillic, …) is
220
+ // counted at the wide rate, which is the conservative side for CJK-heavy
221
+ // content and keeps the table honest without per-script modeling.
222
+
223
+ /** Split a text into narrow (ASCII) and wide (everything else) code points. */
224
+ export function charClassCounts(text: string): CharMix {
225
+ let narrowChars = 0;
226
+ let wideChars = 0;
227
+ for (const ch of text) {
228
+ const cp = ch.codePointAt(0)!;
229
+ if (cp <= 0x7f) {
230
+ narrowChars += 1;
231
+ continue;
232
+ }
233
+ wideChars += 1;
234
+ }
235
+ return { narrowChars, wideChars };
236
+ }
237
+
238
+ export interface TokenEstimate {
239
+ point: number;
240
+ low: number;
241
+ high: number;
242
+ }
243
+
244
+ /** Estimate tokens for a char mix plus images, as a propagated band. */
245
+ export function estimateTokens(mix: CharMix, imageCount: number): TokenEstimate {
246
+ const [aLo, aHi] = TOKEN_ESTIMATE.narrowTokPerChar;
247
+ const [cLo, cHi] = TOKEN_ESTIMATE.wideTokPerChar;
248
+ const [iLo, iHi] = TOKEN_ESTIMATE.imageTok;
249
+ const mid = (lo: number, hi: number) => (lo + hi) / 2;
250
+ return {
251
+ point: Math.round(mix.narrowChars * mid(aLo, aHi)
252
+ + mix.wideChars * mid(cLo, cHi)
253
+ + imageCount * mid(iLo, iHi)),
254
+ low: Math.round(mix.narrowChars * aLo + mix.wideChars * cLo + imageCount * iLo),
255
+ high: Math.round(mix.narrowChars * aHi + mix.wideChars * cHi + imageCount * iHi),
256
+ };
257
+ }
package/src/projection.ts CHANGED
@@ -74,8 +74,8 @@ export function replacementMetrics(block: CompressionBlock): ContentMetrics {
74
74
  * Factual replacement content chars for a range, given its topic and summary.
75
75
  * Computed from the actual summary message wrapper text that projection emits.
76
76
  */
77
- export function replacementContentChars(summary: string, topic?: string): number {
78
- const block: CompressionBlock = {
77
+ function draftReplacementBlock(summary: string, topic?: string): CompressionBlock {
78
+ return {
79
79
  id: "draft",
80
80
  summary,
81
81
  topic,
@@ -89,7 +89,16 @@ export function replacementContentChars(summary: string, topic?: string): number
89
89
  originalApproxTokens: 0,
90
90
  compressedApproxTokens: 0,
91
91
  };
92
- return replacementMetrics(block).contentChars;
92
+ }
93
+
94
+ /** Render the exact replacement content used for draft display estimates. */
95
+ export function replacementContentText(summary: string, topic?: string): string {
96
+ const content = summaryMessage(draftReplacementBlock(summary, topic)).content;
97
+ return typeof content === "string" ? content : "";
98
+ }
99
+
100
+ export function replacementContentChars(summary: string, topic?: string): number {
101
+ return replacementMetrics(draftReplacementBlock(summary, topic)).contentChars;
93
102
  }
94
103
 
95
104
  /** @deprecated legacy heuristic; kept only for old approximate-token field compat. */