@oh-my-pi/snapcompact 18.14.2 → 18.15.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/CHANGELOG.md +6 -0
- package/dist/types/snapcompact.d.ts +20 -0
- package/package.json +6 -6
- package/src/prompts/snapcompact-summary.md +1 -0
- package/src/snapcompact.ts +108 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.15.0] - 2026-11-01
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added `viewWithoutFrames`: what a reader without image input reads for an archive, its kept text in place of its frames ([#17510](https://github.com/can1357/oh-my-pi/pull/17510) by [@will-bogusz](https://github.com/will-bogusz))
|
|
10
|
+
|
|
5
11
|
## [18.13.3] - 2026-10-28
|
|
6
12
|
|
|
7
13
|
### Changed
|
|
@@ -676,6 +676,26 @@ export declare function textWithoutThinking(text: string): string;
|
|
|
676
676
|
* file list.
|
|
677
677
|
*/
|
|
678
678
|
export declare function viewWithoutThinking(summary: string, archive: Archive, details?: CompactionDetails): ArchiveView;
|
|
679
|
+
/** Options for {@link viewWithoutFrames}. */
|
|
680
|
+
export interface ViewWithoutFramesOptions {
|
|
681
|
+
/** `false` for a reader that excludes assistant reasoning ({@link readerIncludesThinking}). */
|
|
682
|
+
includeThinking?: boolean;
|
|
683
|
+
/** Most archive characters the reader takes; the middle past it is dropped, both ends kept. */
|
|
684
|
+
maxChars?: number;
|
|
685
|
+
}
|
|
686
|
+
/**
|
|
687
|
+
* What a reader without image input reads for a compaction whose summary is
|
|
688
|
+
* `summary` and archive `archive`: the archive's kept source
|
|
689
|
+
* ({@link Archive.text}) as plain text in place of its frames, and the summary
|
|
690
|
+
* re-rendered without the frame legend, its file list kept. With
|
|
691
|
+
* `includeThinking: false` the text and legend also leave out reasoning, as
|
|
692
|
+
* {@link viewWithoutThinking} does, without rendering frames this reader
|
|
693
|
+
* cannot see; past `maxChars` the middle of the text is dropped. An archive
|
|
694
|
+
* persisted without that source keeps its text edges and counts the imaged
|
|
695
|
+
* middle as dropped. An archive with no frames, no reasoning to drop and no
|
|
696
|
+
* text past `maxChars` is returned as is.
|
|
697
|
+
*/
|
|
698
|
+
export declare function viewWithoutFrames(summary: string, archive: Archive, options?: ViewWithoutFramesOptions): ArchiveView;
|
|
679
699
|
/**
|
|
680
700
|
* Run a snapcompact compaction over prepared messages. Fully local: serializes
|
|
681
701
|
* the discarded history, appends it to the accumulated archive source text, and
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/snapcompact",
|
|
4
|
-
"version": "18.
|
|
4
|
+
"version": "18.15.0",
|
|
5
5
|
"description": "Bitmap-frame context compression for vision-capable LLMs",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": {
|
|
@@ -34,11 +34,11 @@
|
|
|
34
34
|
"fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@oh-my-pi/pi-ai": "18.
|
|
38
|
-
"@oh-my-pi/pi-catalog": "18.
|
|
39
|
-
"@oh-my-pi/pi-natives": "18.
|
|
40
|
-
"@oh-my-pi/pi-utils": "18.
|
|
41
|
-
"@oh-my-pi/pi-wire": "18.
|
|
37
|
+
"@oh-my-pi/pi-ai": "18.15.0",
|
|
38
|
+
"@oh-my-pi/pi-catalog": "18.15.0",
|
|
39
|
+
"@oh-my-pi/pi-natives": "18.15.0",
|
|
40
|
+
"@oh-my-pi/pi-utils": "18.15.0",
|
|
41
|
+
"@oh-my-pi/pi-wire": "18.15.0"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@types/bun": "^1.3.14"
|
|
@@ -19,6 +19,7 @@ Reading HISTORY:
|
|
|
19
19
|
{{/if}}{{#if truncatedChars}}- About {{truncatedChars}} characters of older middle history dropped to fit archive budget.
|
|
20
20
|
{{/if}}- If an exact earlier detail matters and a section is unclear, re-derive from workspace (re-read files, re-run commands), rather than guess.
|
|
21
21
|
|
|
22
|
+
{{!-- viewWithoutFrames (snapcompact.ts) reads this FILES section back out of a rendered summary: keep FILES_SECTION_HEAD and HISTORY_SECTION_HEAD in step with these headings. --}}
|
|
22
23
|
{{#if files}}FILES
|
|
23
24
|
===================
|
|
24
25
|
{{files}}
|
package/src/snapcompact.ts
CHANGED
|
@@ -2332,6 +2332,114 @@ export function viewWithoutThinking(summary: string, archive: Archive, details?:
|
|
|
2332
2332
|
};
|
|
2333
2333
|
}
|
|
2334
2334
|
|
|
2335
|
+
const FILES_SECTION_HEAD = "FILES\n===================\n";
|
|
2336
|
+
const HISTORY_SECTION_HEAD = "HISTORY\n===================";
|
|
2337
|
+
|
|
2338
|
+
/** The FILES section of a snapcompact summary as it was rendered, or `undefined` when it lists none. */
|
|
2339
|
+
function summaryFileSection(summary: string): string | undefined {
|
|
2340
|
+
const start = summary.indexOf(FILES_SECTION_HEAD);
|
|
2341
|
+
const end = summary.lastIndexOf(HISTORY_SECTION_HEAD);
|
|
2342
|
+
if (start < 0 || end < start) return undefined;
|
|
2343
|
+
const files = summary.slice(start + FILES_SECTION_HEAD.length, end).replace(TRAILING_LINE_BREAKS, "");
|
|
2344
|
+
return files.length > 0 ? files : undefined;
|
|
2345
|
+
}
|
|
2346
|
+
|
|
2347
|
+
/** Options for {@link viewWithoutFrames}. */
|
|
2348
|
+
export interface ViewWithoutFramesOptions {
|
|
2349
|
+
/** `false` for a reader that excludes assistant reasoning ({@link readerIncludesThinking}). */
|
|
2350
|
+
includeThinking?: boolean;
|
|
2351
|
+
/** Most archive characters the reader takes; the middle past it is dropped, both ends kept. */
|
|
2352
|
+
maxChars?: number;
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
/** `archive`'s text regions cut to `maxChars`, keeping both ends and counting the cut middle as dropped. */
|
|
2356
|
+
function keepEnds(archive: Archive, maxChars: number): Archive {
|
|
2357
|
+
if (archive.totalChars <= maxChars) return archive;
|
|
2358
|
+
let head = archive.textHead ?? "";
|
|
2359
|
+
let tail = archive.textTail ?? "";
|
|
2360
|
+
if (tail.length === 0) {
|
|
2361
|
+
const headChars = Math.ceil(maxChars / 2);
|
|
2362
|
+
tail = head.slice(head.length - (maxChars - headChars));
|
|
2363
|
+
head = head.slice(0, headChars);
|
|
2364
|
+
} else {
|
|
2365
|
+
const headChars = Math.min(head.length, Math.max(Math.ceil(maxChars / 2), maxChars - tail.length));
|
|
2366
|
+
const tailChars = Math.min(tail.length, maxChars - headChars);
|
|
2367
|
+
head = head.slice(0, headChars);
|
|
2368
|
+
tail = tail.slice(tail.length - tailChars);
|
|
2369
|
+
}
|
|
2370
|
+
const { textHead: _head, textTail: _tail, ...rest } = archive;
|
|
2371
|
+
return {
|
|
2372
|
+
...rest,
|
|
2373
|
+
totalChars: head.length + tail.length,
|
|
2374
|
+
truncatedChars: archive.truncatedChars + archive.totalChars - head.length - tail.length,
|
|
2375
|
+
...(head ? { textHead: head } : {}),
|
|
2376
|
+
...(tail ? { textTail: tail } : {}),
|
|
2377
|
+
};
|
|
2378
|
+
}
|
|
2379
|
+
|
|
2380
|
+
/**
|
|
2381
|
+
* What a reader without image input reads for a compaction whose summary is
|
|
2382
|
+
* `summary` and archive `archive`: the archive's kept source
|
|
2383
|
+
* ({@link Archive.text}) as plain text in place of its frames, and the summary
|
|
2384
|
+
* re-rendered without the frame legend, its file list kept. With
|
|
2385
|
+
* `includeThinking: false` the text and legend also leave out reasoning, as
|
|
2386
|
+
* {@link viewWithoutThinking} does, without rendering frames this reader
|
|
2387
|
+
* cannot see; past `maxChars` the middle of the text is dropped. An archive
|
|
2388
|
+
* persisted without that source keeps its text edges and counts the imaged
|
|
2389
|
+
* middle as dropped. An archive with no frames, no reasoning to drop and no
|
|
2390
|
+
* text past `maxChars` is returned as is.
|
|
2391
|
+
*/
|
|
2392
|
+
export function viewWithoutFrames(
|
|
2393
|
+
summary: string,
|
|
2394
|
+
archive: Archive,
|
|
2395
|
+
options: ViewWithoutFramesOptions = {},
|
|
2396
|
+
): ArchiveView {
|
|
2397
|
+
const dropThinking = options.includeThinking === false && summaryIncludesThinking(summary);
|
|
2398
|
+
const fits = options.maxChars === undefined || archive.totalChars <= options.maxChars;
|
|
2399
|
+
if (archive.frames.length === 0 && !dropThinking && fits) return { summary, archive };
|
|
2400
|
+
const scrub = (text: string): string => (dropThinking ? textWithoutThinking(text) : text);
|
|
2401
|
+
let textArchive: Archive;
|
|
2402
|
+
if (archive.text) {
|
|
2403
|
+
const text = scrub(archive.text);
|
|
2404
|
+
textArchive = {
|
|
2405
|
+
frames: [],
|
|
2406
|
+
totalChars: text.length,
|
|
2407
|
+
truncatedChars: archive.truncatedChars,
|
|
2408
|
+
text,
|
|
2409
|
+
textHead: text,
|
|
2410
|
+
};
|
|
2411
|
+
} else {
|
|
2412
|
+
// Without the whole source the tail may open inside a section cut off in
|
|
2413
|
+
// the dropped middle, whose scope is unknown; a reader without reasoning
|
|
2414
|
+
// skips those lines (see sourceForReaderWithoutThinking).
|
|
2415
|
+
const tail = archive.textTail ?? "";
|
|
2416
|
+
const labelled = dropThinking ? tail.search(SECTION_START) : 0;
|
|
2417
|
+
const keptTail = labelled < 0 ? "" : tail.slice(labelled);
|
|
2418
|
+
const textHead = scrub(archive.textHead ?? "");
|
|
2419
|
+
const textTail = scrub(keptTail);
|
|
2420
|
+
const framedChars = archive.frames.reduce((sum, frame) => sum + frame.chars, 0);
|
|
2421
|
+
textArchive = {
|
|
2422
|
+
frames: [],
|
|
2423
|
+
totalChars: textHead.length + textTail.length,
|
|
2424
|
+
truncatedChars: archive.truncatedChars + framedChars + tail.length - keptTail.length,
|
|
2425
|
+
...(textHead ? { textHead } : {}),
|
|
2426
|
+
...(textTail ? { textTail } : {}),
|
|
2427
|
+
};
|
|
2428
|
+
}
|
|
2429
|
+
if (options.maxChars !== undefined) textArchive = keepEnds(textArchive, Math.max(0, Math.floor(options.maxChars)));
|
|
2430
|
+
const source = textArchive.text ?? textArchive.textHead ?? "";
|
|
2431
|
+
return {
|
|
2432
|
+
summary: prompt.render(snapcompactSummaryPrompt, {
|
|
2433
|
+
frameCount: 0,
|
|
2434
|
+
truncatedChars: textArchive.truncatedChars,
|
|
2435
|
+
includedPreviousSummary: source.startsWith(PREVIOUS_SUMMARY_HEAD),
|
|
2436
|
+
files: summaryFileSection(summary),
|
|
2437
|
+
includeThinking: summaryIncludesThinking(summary) && !dropThinking,
|
|
2438
|
+
}),
|
|
2439
|
+
archive: textArchive,
|
|
2440
|
+
};
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2335
2443
|
/**
|
|
2336
2444
|
* Run a snapcompact compaction over prepared messages. Fully local: serializes
|
|
2337
2445
|
* the discarded history, appends it to the accumulated archive source text, and
|