@oh-my-pi/snapcompact 18.14.1 → 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 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.14.1",
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.14.1",
38
- "@oh-my-pi/pi-catalog": "18.14.1",
39
- "@oh-my-pi/pi-natives": "18.14.1",
40
- "@oh-my-pi/pi-utils": "18.14.1",
41
- "@oh-my-pi/pi-wire": "18.14.1"
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}}
@@ -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