@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 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
- * Unrenderable control/format/combining characters are dropped without
455
- * occupying a cell; `?` remains the fallback for unsupported graphic
456
- * characters. The zero-width ink toggles {@link DIM_ON}/{@link DIM_OFF} pass
457
- * through untouched.
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.9",
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.9",
35
- "@oh-my-pi/pi-natives": "16.0.9",
36
- "@oh-my-pi/pi-utils": "16.0.9"
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: monospace {{fontCell}} pixel font on a white background, {{#if docColumns}}typeset as two word-wrapped newspaper columns of {{cols}} characters by {{rows}} lines each — read the left column top to bottom, then the right column{{else}}{{cols}} characters per row, {{rows}} text rows per frame; read left to right, top to bottom. Text flows continuously with no word wrap, so words may break across row ends{{/if}}. Horizontal whitespace runs were collapsed to single spaces; line breaks print as a solid black cell (one character wide) treat each as a newline. {{#if sentenceInk}}Ink color cycles through six colors, advancing at sentence boundaries — a color change marks a new sentence.{{else}}Glyphs are plain black ink.{{/if}}{{#if stopwordDimmed}} Common function words (the, of, and, …) are printed in dim gray; content words carry the full ink.{{/if}}{{#if dimmedToolResults}} Tool output is printed in dim gray ink gray text is archived tool output, not conversation.{{/if}}{{#if lineRepeated}} Every text line is printed twice in a row — first on the white background, then repeated on a pale yellow band. The copies are identical: read each line once and use the duplicate only to double-check hard glyphs.{{/if}} Roles are tagged inline as [User]:, [Assistant]:, [Think]:, [Tool Call]:, and [Tool Result]:.
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).
@@ -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… (${all.length - FILE_OPERATION_SUMMARY_LIMIT} more files omitted)`;
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)} [... ${elided} chars elided ...] ${tail}`;
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
- pendingText.push(stripDimMarkers(block.text));
744
+ const text = stripDimMarkers(block.text);
745
+ if (text.trim()) pendingText.push(text);
744
746
  } else if (block.type === "thinking") {
745
- pendingThinking.push(stripDimMarkers(block.thinking));
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" ? block.intent : typeof args._i === "string" ? args._i : "";
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 !== "_i")
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
- /** Folds for common non-Latin-1 characters the bundled fonts cannot draw. */
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
- * Unrenderable control/format/combining characters are dropped without
864
- * occupying a cell; `?` remains the fallback for unsupported graphic
865
- * characters. The zero-width ink toggles {@link DIM_ON}/{@link DIM_OFF} pass
866
- * through untouched.
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 if (!UNRENDERABLE.test(ch)) {
893
- out += "?";
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;