@oh-my-pi/snapcompact 16.0.9 → 16.0.11
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 +8 -0
- package/dist/types/snapcompact.d.ts +8 -5
- package/package.json +5 -4
- package/src/prompts/snapcompact-summary.md +9 -1
- package/src/snapcompact.ts +85 -14
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [16.0.11] - 2026-06-19
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Refined elision markers for file operations and truncated text for better display consistency
|
|
10
|
+
- Updated summary text for consistent descriptions of archived tool output
|
|
11
|
+
- Folded a much wider range of Unicode to ASCII in `normalize()` before native rendering: added a per-character Unicode NFKD decomposition fallback (fullwidth forms, super/subscripts, ligatures, circled and math-styled alphanumerics, Roman numerals, vulgar fractions) and expanded the `CHAR_FOLD` punctuation table (more quotes/primes, hyphens, the fraction slash, dot leaders, bullets, and arrows) so undrawable glyphs land on close ASCII equivalents instead of `?`
|
|
12
|
+
|
|
5
13
|
## [16.0.8] - 2026-06-18
|
|
6
14
|
|
|
7
15
|
### Added
|
|
@@ -450,11 +450,14 @@ export declare const NEWLINE_GLYPH = "\u2588";
|
|
|
450
450
|
* Prepare text for printing: strip ANSI escape sequences, collapse horizontal
|
|
451
451
|
* whitespace runs to single spaces and newline-bearing runs to one
|
|
452
452
|
* {@link NEWLINE_GLYPH} (drawn as a pitch-black cell), then fold everything
|
|
453
|
-
* outside the fonts' ASCII + Latin-1 coverage to ASCII approximations
|
|
454
|
-
*
|
|
455
|
-
*
|
|
456
|
-
*
|
|
457
|
-
*
|
|
453
|
+
* outside the fonts' ASCII + Latin-1 coverage to ASCII approximations — first
|
|
454
|
+
* through the {@link CHAR_FOLD} punctuation table, then via an NFKD
|
|
455
|
+
* decomposition that recovers the ASCII skeleton of compatibility characters
|
|
456
|
+
* (fullwidth, super/subscripts, ligatures, circled/math-styled alphanumerics,
|
|
457
|
+
* Roman numerals, vulgar fractions). Unrenderable control/format/combining
|
|
458
|
+
* characters are dropped without occupying a cell; `?` remains the fallback
|
|
459
|
+
* for unsupported graphic characters. The zero-width ink toggles
|
|
460
|
+
* {@link DIM_ON}/{@link DIM_OFF} pass through untouched.
|
|
458
461
|
*/
|
|
459
462
|
export declare function normalize(text: string): string;
|
|
460
463
|
/**
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/snapcompact",
|
|
4
|
-
"version": "16.0.
|
|
4
|
+
"version": "16.0.11",
|
|
5
5
|
"description": "Bitmap-frame context compression for vision-capable LLMs",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -31,9 +31,10 @@
|
|
|
31
31
|
"fmt": "biome format --write ."
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@oh-my-pi/pi-ai": "16.0.
|
|
35
|
-
"@oh-my-pi/pi-natives": "16.0.
|
|
36
|
-
"@oh-my-pi/pi-utils": "16.0.
|
|
34
|
+
"@oh-my-pi/pi-ai": "16.0.11",
|
|
35
|
+
"@oh-my-pi/pi-natives": "16.0.11",
|
|
36
|
+
"@oh-my-pi/pi-utils": "16.0.11",
|
|
37
|
+
"@oh-my-pi/pi-wire": "16.0.11"
|
|
37
38
|
},
|
|
38
39
|
"devDependencies": {
|
|
39
40
|
"@types/bun": "^1.3.14"
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
Prior conversation history has been archived verbatim onto {{frameCount}} snapcompact frame{{#if multipleFrames}}s{{/if}} — the bitmap image{{#if multipleFrames}}s{{/if}} attached below{{#if multipleFrames}}, ordered oldest to newest{{/if}}.
|
|
2
2
|
|
|
3
|
-
Reading a frame:
|
|
3
|
+
Reading a frame: a solid black cell marks a newline and runs of spaces collapse to one; each turn opens with a heading — # User ¶, # Assistant ¶, or # Tool call ¶ — with assistant reasoning in _italics_ and tool output inside <out>…</out>.
|
|
4
|
+
{{#if docColumns}}- Two side-by-side text columns, each {{cols}} characters wide and up to {{rows}} rows tall: read the left column top to bottom, then the right.
|
|
5
|
+
{{else}}- A single grid {{cols}} characters wide and up to {{rows}} rows tall: read left to right, top to bottom — no word wrap, so words may break across rows.
|
|
6
|
+
{{/if}}
|
|
7
|
+
{{#if sentenceInk}}- Ink cycles six colors, one per sentence.
|
|
8
|
+
{{/if}}{{#if stopwordDimmed}}- Function words are dim gray; content words keep full ink.
|
|
9
|
+
{{/if}}{{#if dimmedToolResults}}- Text inside <out> is dim gray; that gray is archived tool output, not conversation.
|
|
10
|
+
{{/if}}{{#if lineRepeated}}- Each line is printed twice (white, then a pale-yellow band); the copies are identical.
|
|
11
|
+
{{/if}}
|
|
4
12
|
{{#if mixedShapes}}
|
|
5
13
|
|
|
6
14
|
Older frames may use a different font, grid, or ink coloring than described above; the reading order is always the same (left to right, top to bottom, oldest frame first).
|
package/src/snapcompact.ts
CHANGED
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
import type { Api, ImageContent, Message, Model } from "@oh-my-pi/pi-ai";
|
|
51
51
|
import { renderSnapcompactPng } from "@oh-my-pi/pi-natives";
|
|
52
52
|
import { formatGroupedPaths, prompt } from "@oh-my-pi/pi-utils";
|
|
53
|
+
import { INTENT_FIELD } from "@oh-my-pi/pi-wire";
|
|
53
54
|
import fileOperationsTemplate from "./prompts/file-operations.md" with { type: "text" };
|
|
54
55
|
import snapcompactSummaryPrompt from "./prompts/snapcompact-summary.md" with { type: "text" };
|
|
55
56
|
|
|
@@ -592,7 +593,7 @@ function formatFileOperations(readFiles: string[], modifiedFiles: string[], read
|
|
|
592
593
|
const all = [...mode.keys()].sort();
|
|
593
594
|
let files = formatGroupedPaths(all.slice(0, FILE_OPERATION_SUMMARY_LIMIT), path => ` (${mode.get(path)})`);
|
|
594
595
|
if (all.length > FILE_OPERATION_SUMMARY_LIMIT) {
|
|
595
|
-
files += `\n
|
|
596
|
+
files += `\n[…${all.length - FILE_OPERATION_SUMMARY_LIMIT} files elided…]`;
|
|
596
597
|
}
|
|
597
598
|
return prompt.render(fileOperationsTemplate, { files });
|
|
598
599
|
}
|
|
@@ -657,7 +658,7 @@ function truncateForSummary(text: string, maxChars: number, headRatio: number):
|
|
|
657
658
|
const tailChars = maxChars - headChars;
|
|
658
659
|
const elided = text.length - maxChars;
|
|
659
660
|
const tail = tailChars > 0 ? text.slice(-tailChars) : "";
|
|
660
|
-
return `${text.slice(0, headChars)} [
|
|
661
|
+
return `${text.slice(0, headChars)} […${elided}ch elided…] ${tail}`;
|
|
661
662
|
}
|
|
662
663
|
|
|
663
664
|
const DIM_MARKERS = /[\u000e\u000f]/g;
|
|
@@ -740,9 +741,11 @@ export function serializeConversation(messages: Message[], options?: SerializeOp
|
|
|
740
741
|
|
|
741
742
|
for (const block of msg.content) {
|
|
742
743
|
if (block.type === "text") {
|
|
743
|
-
|
|
744
|
+
const text = stripDimMarkers(block.text);
|
|
745
|
+
if (text.trim()) pendingText.push(text);
|
|
744
746
|
} else if (block.type === "thinking") {
|
|
745
|
-
|
|
747
|
+
const thinking = stripDimMarkers(block.thinking);
|
|
748
|
+
if (thinking.trim()) pendingThinking.push(thinking);
|
|
746
749
|
} else if (block.type === "toolCall") {
|
|
747
750
|
if (uselessCallIds.has(block.id)) continue;
|
|
748
751
|
flushAssistant();
|
|
@@ -750,11 +753,15 @@ export function serializeConversation(messages: Message[], options?: SerializeOp
|
|
|
750
753
|
// Prefer the harness-derived intent, else the raw `_i` arg; render it as
|
|
751
754
|
// a one-line `//comment` and drop `_i` from the args below.
|
|
752
755
|
const rawIntent =
|
|
753
|
-
typeof block.intent === "string"
|
|
756
|
+
typeof block.intent === "string"
|
|
757
|
+
? block.intent
|
|
758
|
+
: typeof args[INTENT_FIELD] === "string"
|
|
759
|
+
? (args[INTENT_FIELD] as string)
|
|
760
|
+
: "";
|
|
754
761
|
const intent = stripDimMarkers(rawIntent).replace(/\s+/g, " ").trim();
|
|
755
762
|
const argsStr = truncateForSummary(
|
|
756
763
|
Object.entries(args)
|
|
757
|
-
.filter(([key]) => key !==
|
|
764
|
+
.filter(([key]) => key !== INTENT_FIELD)
|
|
758
765
|
.map(
|
|
759
766
|
([key, value]) =>
|
|
760
767
|
`${key}=${truncateForSummary(JSON.stringify(value) ?? "undefined", toolArgMaxChars, headRatio)}`,
|
|
@@ -807,8 +814,11 @@ function stripOpenAiRemoteCompactionPreserveData(
|
|
|
807
814
|
// Text normalization
|
|
808
815
|
// ============================================================================
|
|
809
816
|
|
|
810
|
-
/**
|
|
817
|
+
/** Punctuation and symbol folds applied before the NFKD fallback in
|
|
818
|
+
* {@link normalize}: quotes, dashes, bullets, arrows, and dot leaders that
|
|
819
|
+
* have no compatibility decomposition (or one that is itself non-ASCII). */
|
|
811
820
|
const CHAR_FOLD: Record<string, string> = {
|
|
821
|
+
// Quotation marks and primes.
|
|
812
822
|
"\u2018": "'",
|
|
813
823
|
"\u2019": "'",
|
|
814
824
|
"\u201a": "'",
|
|
@@ -816,18 +826,44 @@ const CHAR_FOLD: Record<string, string> = {
|
|
|
816
826
|
"\u201c": '"',
|
|
817
827
|
"\u201d": '"',
|
|
818
828
|
"\u201e": '"',
|
|
829
|
+
"\u2032": "'",
|
|
830
|
+
"\u2033": '"',
|
|
831
|
+
"\u2035": "'",
|
|
832
|
+
"\u2036": '"',
|
|
833
|
+
"\u2039": "<",
|
|
834
|
+
"\u203a": ">",
|
|
835
|
+
// Dashes, hyphens, and the fraction slash NFKD leaves in vulgar fractions.
|
|
836
|
+
"\u2010": "-",
|
|
837
|
+
"\u2011": "-",
|
|
838
|
+
"\u2012": "-",
|
|
819
839
|
"\u2013": "-",
|
|
820
840
|
"\u2014": "-",
|
|
821
841
|
"\u2015": "-",
|
|
822
842
|
"\u2212": "-",
|
|
843
|
+
"\u2044": "/",
|
|
844
|
+
// Dot leaders and ellipses.
|
|
845
|
+
"\u2024": ".",
|
|
846
|
+
"\u2025": "..",
|
|
823
847
|
"\u2026": "...",
|
|
848
|
+
"\u22ef": "...",
|
|
849
|
+
// Bullets.
|
|
824
850
|
"\u2022": "*",
|
|
851
|
+
"\u2023": "*",
|
|
852
|
+
"\u2043": "-",
|
|
853
|
+
"\u2219": "*",
|
|
825
854
|
"\u25cf": "*",
|
|
826
855
|
"\u25a0": "*",
|
|
827
856
|
"\u25aa": "*",
|
|
857
|
+
// Arrows.
|
|
828
858
|
"\u2190": "<-",
|
|
859
|
+
"\u2191": "^",
|
|
829
860
|
"\u2192": "->",
|
|
861
|
+
"\u2193": "v",
|
|
862
|
+
"\u2194": "<->",
|
|
863
|
+
"\u21d0": "<=",
|
|
830
864
|
"\u21d2": "=>",
|
|
865
|
+
"\u21d4": "<=>",
|
|
866
|
+
// Check marks and crosses.
|
|
831
867
|
"\u2713": "v",
|
|
832
868
|
"\u2714": "v",
|
|
833
869
|
"\u2717": "x",
|
|
@@ -855,15 +891,48 @@ const EDGE_RUNS = /^[ \u2588]+|[ \u2588]+$/g;
|
|
|
855
891
|
* combining marks the fonts cannot compose, and lone surrogates. */
|
|
856
892
|
const UNRENDERABLE = /[\p{Cc}\p{Mn}\p{Me}\p{Cs}]/u;
|
|
857
893
|
|
|
894
|
+
/** Combining marks NFKD splits off accented letters; dropped so the base
|
|
895
|
+
* letter prints without the diacritic the bundled fonts cannot compose. */
|
|
896
|
+
const COMBINING_MARKS = /\p{M}+/gu;
|
|
897
|
+
|
|
898
|
+
/**
|
|
899
|
+
* Aggressive single-code-point ASCII fold via Unicode NFKD: decompose the
|
|
900
|
+
* compatibility form (fullwidth, super/subscripts, ligatures, circled and
|
|
901
|
+
* math-styled alphanumerics, Roman numerals, vulgar fractions, …), strip the
|
|
902
|
+
* combining marks, and keep the ASCII/Latin-1 skeleton — routing any residual
|
|
903
|
+
* punctuation back through {@link CHAR_FOLD}. Returns `undefined` when the code
|
|
904
|
+
* point has no decomposition or still leaves an undrawable glyph, so the
|
|
905
|
+
* caller falls back to `?`.
|
|
906
|
+
*/
|
|
907
|
+
function foldToAscii(ch: string): string | undefined {
|
|
908
|
+
const decomposed = ch.normalize("NFKD").replace(COMBINING_MARKS, "");
|
|
909
|
+
if (decomposed === ch) return undefined;
|
|
910
|
+
let out = "";
|
|
911
|
+
for (const part of decomposed) {
|
|
912
|
+
const cp = part.codePointAt(0) as number;
|
|
913
|
+
if ((cp >= 0x20 && cp < 0x7f) || (cp >= 0xa0 && cp <= 0xff)) {
|
|
914
|
+
out += part;
|
|
915
|
+
continue;
|
|
916
|
+
}
|
|
917
|
+
const fold = CHAR_FOLD[part];
|
|
918
|
+
if (fold === undefined) return undefined;
|
|
919
|
+
out += fold;
|
|
920
|
+
}
|
|
921
|
+
return out;
|
|
922
|
+
}
|
|
923
|
+
|
|
858
924
|
/**
|
|
859
925
|
* Prepare text for printing: strip ANSI escape sequences, collapse horizontal
|
|
860
926
|
* whitespace runs to single spaces and newline-bearing runs to one
|
|
861
927
|
* {@link NEWLINE_GLYPH} (drawn as a pitch-black cell), then fold everything
|
|
862
|
-
* outside the fonts' ASCII + Latin-1 coverage to ASCII approximations
|
|
863
|
-
*
|
|
864
|
-
*
|
|
865
|
-
*
|
|
866
|
-
*
|
|
928
|
+
* outside the fonts' ASCII + Latin-1 coverage to ASCII approximations — first
|
|
929
|
+
* through the {@link CHAR_FOLD} punctuation table, then via an NFKD
|
|
930
|
+
* decomposition that recovers the ASCII skeleton of compatibility characters
|
|
931
|
+
* (fullwidth, super/subscripts, ligatures, circled/math-styled alphanumerics,
|
|
932
|
+
* Roman numerals, vulgar fractions). Unrenderable control/format/combining
|
|
933
|
+
* characters are dropped without occupying a cell; `?` remains the fallback
|
|
934
|
+
* for unsupported graphic characters. The zero-width ink toggles
|
|
935
|
+
* {@link DIM_ON}/{@link DIM_OFF} pass through untouched.
|
|
867
936
|
*/
|
|
868
937
|
export function normalize(text: string): string {
|
|
869
938
|
const stripped = text.includes("\u001b") ? Bun.stripANSI(text) : text;
|
|
@@ -889,8 +958,10 @@ export function normalize(text: string): string {
|
|
|
889
958
|
} else if (cp >= 0x2500 && cp <= 0x257f) {
|
|
890
959
|
// Box drawing: keep table skeletons legible.
|
|
891
960
|
out += cp === 0x2502 || cp === 0x2503 ? "|" : cp === 0x2500 || cp === 0x2501 ? "-" : "+";
|
|
892
|
-
} else
|
|
893
|
-
|
|
961
|
+
} else {
|
|
962
|
+
const folded = foldToAscii(ch);
|
|
963
|
+
if (folded !== undefined) out += folded;
|
|
964
|
+
else if (!UNRENDERABLE.test(ch)) out += "?";
|
|
894
965
|
}
|
|
895
966
|
}
|
|
896
967
|
return out;
|