@agent-native/core 0.124.2 → 0.124.3

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.
Files changed (27) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +6 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/shared/streaming-text-smoothing.ts +15 -14
  5. package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +34 -11
  6. package/corpus/templates/design/.agents/skills/responsive-breakpoints/SKILL.md +8 -0
  7. package/corpus/templates/design/actions/generate-design.ts +259 -36
  8. package/corpus/templates/design/actions/generate-screens.ts +51 -14
  9. package/corpus/templates/design/actions/present-design-variants.ts +6 -3
  10. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +8 -0
  11. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +158 -9
  12. package/corpus/templates/design/app/components/design/design-canvas/content-size-report.ts +124 -0
  13. package/corpus/templates/design/app/components/design/multi-screen/frame-geometry.ts +61 -9
  14. package/corpus/templates/design/changelog/2026-07-24-ai-designs-now-default-to-a-full-size-desktop-mobile-pair-fr.md +6 -0
  15. package/corpus/templates/design/changelog/2026-07-24-design-frames-now-fill-the-viewport-height-and-grow-to-fit-t.md +6 -0
  16. package/corpus/templates/design/changelog/2026-07-24-generating-an-additional-screen-now-places-it-beside-the-exi.md +6 -0
  17. package/dist/collab/awareness.d.ts +2 -2
  18. package/dist/collab/awareness.d.ts.map +1 -1
  19. package/dist/collab/routes.d.ts +1 -1
  20. package/dist/collab/struct-routes.d.ts +1 -1
  21. package/dist/resources/handlers.d.ts +1 -1
  22. package/dist/secrets/routes.d.ts +3 -3
  23. package/dist/shared/streaming-text-smoothing.d.ts.map +1 -1
  24. package/dist/shared/streaming-text-smoothing.js +15 -8
  25. package/dist/shared/streaming-text-smoothing.js.map +1 -1
  26. package/package.json +1 -1
  27. package/src/shared/streaming-text-smoothing.ts +15 -14
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-24
4
+ ---
5
+
6
+ AI designs now default to a full-size desktop + mobile pair (Framer-style breakpoint frames) instead of duplicated desktop/tablet/home device frames
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-24
4
+ ---
5
+
6
+ Design frames now fill the viewport height and grow to fit their content at each breakpoint width, so mobile and tablet frames no longer clip below the fold
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-24
4
+ ---
5
+
6
+ Generating an additional screen now places it beside the existing screens instead of stacking it on top at the same coordinates
@@ -62,11 +62,11 @@ export declare const postAwareness: import("h3").EventHandlerWithFetch<import("h
62
62
  error: string;
63
63
  states?: undefined;
64
64
  } | {
65
+ error?: undefined;
65
66
  states: {
66
67
  clientId: number;
67
68
  state: string;
68
69
  }[];
69
- error?: undefined;
70
70
  }>>;
71
71
  /**
72
72
  * GET /_agent-native/collab/:docId/users
@@ -77,10 +77,10 @@ export declare const getActiveUsers: import("h3").EventHandlerWithFetch<import("
77
77
  error: string;
78
78
  users?: undefined;
79
79
  } | {
80
+ error?: undefined;
80
81
  users: {
81
82
  clientId: number;
82
83
  lastSeen: number;
83
84
  }[];
84
- error?: undefined;
85
85
  }>>;
86
86
  //# sourceMappingURL=awareness.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"awareness.d.ts","sourceRoot":"","sources":["../../src/collab/awareness.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAgB3C,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CAClB;AAOD,eAAO,MAAM,sBAAsB,EAAG,kBAA2B,CAAC;AAElE,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,WAAW,CAAC;IACpB,IAAI,EAAE,kBAAkB,CAAC;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,2EAA2E;IAC3E,MAAM,EAAE,KAAK,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACnD,gFAAgF;IAChF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sCAAsC;IACtC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2EAA2E;IAC3E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,yEAAyE;IACzE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAOD,wBAAgB,mBAAmB,IAAI,YAAY,CAElD;AAED,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,cAAc,GAAG,SAAS,GAChC,IAAI,CAON;AAED,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,KAAK,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,EAClD,KAAK,CAAC,EAAE,cAAc,GACrB,IAAI,CAgBN;AAoBD,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,SAAS,GAAE,MAAmB,GAC7B,IAAI,CAON;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAE1E;AAiBD,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,CAO1E;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,GAAG,IAAI,CAOnE;AAkCD;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa;;;;;kBAwDa,MAAM;eAAS,MAAM;;;GAoB1D,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,cAAc;;;;;kBAYM,MAAM;kBAAY,MAAM;;;GAMvD,CAAC"}
1
+ {"version":3,"file":"awareness.d.ts","sourceRoot":"","sources":["../../src/collab/awareness.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAgB3C,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CAClB;AAOD,eAAO,MAAM,sBAAsB,EAAG,kBAA2B,CAAC;AAElE,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,WAAW,CAAC;IACpB,IAAI,EAAE,kBAAkB,CAAC;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,2EAA2E;IAC3E,MAAM,EAAE,KAAK,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACnD,gFAAgF;IAChF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sCAAsC;IACtC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2EAA2E;IAC3E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,yEAAyE;IACzE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAOD,wBAAgB,mBAAmB,IAAI,YAAY,CAElD;AAED,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,cAAc,GAAG,SAAS,GAChC,IAAI,CAON;AAED,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,KAAK,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,EAClD,KAAK,CAAC,EAAE,cAAc,GACrB,IAAI,CAgBN;AAoBD,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,SAAS,GAAE,MAAmB,GAC7B,IAAI,CAON;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAE1E;AAiBD,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,CAO1E;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,GAAG,IAAI,CAOnE;AAkCD;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa;;;;;;kBAwDa,MAAM;eAAS,MAAM;;GAoB1D,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,cAAc;;;;;;kBAYM,MAAM;kBAAY,MAAM;;GAMvD,CAAC"}
@@ -41,9 +41,9 @@ export declare const postCollabUpdate: import("h3").EventHandlerWithFetch<import
41
41
  * Body: { text: string, fieldName?: string, requestSource?: string }
42
42
  */
43
43
  export declare const postCollabText: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
44
- text?: undefined;
45
44
  ok?: undefined;
46
45
  error: string;
46
+ text?: undefined;
47
47
  } | {
48
48
  error?: undefined;
49
49
  ok: boolean;
@@ -13,8 +13,8 @@
13
13
  * Body: { json: any, fieldName?: string, type?: "map"|"array", requestSource?: string }
14
14
  */
15
15
  export declare const postCollabJson: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
16
- ok?: undefined;
17
16
  error: string;
17
+ ok?: undefined;
18
18
  } | {
19
19
  error?: undefined;
20
20
  ok: boolean;
@@ -49,8 +49,8 @@ export declare function handleUpdateResource(event: any): Promise<import("./stor
49
49
  }>;
50
50
  /** DELETE /_agent-native/resources/:id — delete a resource */
51
51
  export declare function handleDeleteResource(event: any): Promise<{
52
- ok?: undefined;
53
52
  error: string;
53
+ ok?: undefined;
54
54
  } | {
55
55
  error?: undefined;
56
56
  ok: boolean;
@@ -34,16 +34,16 @@ export declare function createListSecretsHandler(): import("h3").EventHandlerWit
34
34
  /** POST /_agent-native/secrets/:key — write a secret. */
35
35
  export declare function createWriteSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
36
36
  error: string;
37
- ok?: undefined;
38
37
  status?: undefined;
38
+ ok?: undefined;
39
39
  } | {
40
40
  ok: boolean;
41
41
  status: string;
42
42
  error?: undefined;
43
43
  } | {
44
- ok?: undefined;
45
44
  error: string;
46
45
  removed?: undefined;
46
+ ok?: undefined;
47
47
  } | {
48
48
  ok: boolean;
49
49
  removed: boolean;
@@ -54,9 +54,9 @@ export declare function createWriteSecretHandler(): import("h3").EventHandlerWit
54
54
  * current stored value without changing anything. Useful for the "Test" button.
55
55
  */
56
56
  export declare function createTestSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
57
- ok?: undefined;
58
57
  error: string;
59
58
  note?: undefined;
59
+ ok?: undefined;
60
60
  } | {
61
61
  ok: boolean;
62
62
  note?: undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"streaming-text-smoothing.d.ts","sourceRoot":"","sources":["../../src/shared/streaming-text-smoothing.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,mCAAmC,KAAK,CAAC;AACtD,eAAO,MAAM,8CAA8C,MAAM,CAAC;AAClE,eAAO,MAAM,yCAAyC,MAAM,CAAC;AA4C7D;;;;;;;;;;GAUG;AACH,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CA6ClE;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,IAAI,IAAI,CAE1C;AAED,wBAAgB,mCAAmC,CACjD,SAAS,EAAE,SAAS,MAAM,EAAE,GAC3B,MAAM,CASR;AAED,wBAAgB,0BAA0B,CAAC,EACzC,OAAO,EACP,SAAS,EACT,SAAiB,GAClB,EAAE;IACD,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB,GAAG,MAAM,CAyBT;AAED,wBAAgB,iCAAiC,CAC/C,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,OAAO,EAAE,MAAM,GACd,MAAM,CAkBR"}
1
+ {"version":3,"file":"streaming-text-smoothing.d.ts","sourceRoot":"","sources":["../../src/shared/streaming-text-smoothing.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,mCAAmC,KAAK,CAAC;AACtD,eAAO,MAAM,8CAA8C,MAAM,CAAC;AAClE,eAAO,MAAM,yCAAyC,MAAM,CAAC;AA4C7D;;;;;;;;;;GAUG;AACH,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CA8ClE;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,IAAI,IAAI,CAE1C;AAED,wBAAgB,mCAAmC,CACjD,SAAS,EAAE,SAAS,MAAM,EAAE,GAC3B,MAAM,CASR;AAED,wBAAgB,0BAA0B,CAAC,EACzC,OAAO,EACP,SAAS,EACT,SAAiB,GAClB,EAAE;IACD,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB,GAAG,MAAM,CAyBT;AAED,wBAAgB,iCAAiC,CAC/C,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,OAAO,EAAE,MAAM,GACd,MAAM,CAkBR"}
@@ -49,16 +49,23 @@ export function splitStreamingTextGraphemes(text) {
49
49
  // Incremental path: new text extends the cached text
50
50
  if (_segCache && text.startsWith(_segCache.text)) {
51
51
  const prevLen = _segCache.text.length;
52
- // Re-segment from (prevLen - OVERLAP) to include the overlap so any
53
- // grapheme that was split across the boundary is correctly re-assembled.
54
- const overlapStart = Math.max(0, prevLen - SEGMENTER_OVERLAP);
52
+ const cached = _segCache.graphemes;
53
+ // Walk back over cached graphemes until at least OVERLAP characters have
54
+ // been released, so the re-segmented suffix starts on a known grapheme
55
+ // boundary. Slicing at a fixed character offset instead would cut a
56
+ // cluster (surrogate pair, ZWJ sequence) in half, and segmenting that
57
+ // partial cluster in isolation yields a different grapheme count than the
58
+ // cache holds for the same region — which silently drops characters.
59
+ let stableCount = cached.length;
60
+ let releasedChars = 0;
61
+ while (stableCount > 0 && releasedChars < SEGMENTER_OVERLAP) {
62
+ stableCount--;
63
+ releasedChars += cached[stableCount].length;
64
+ }
65
+ const overlapStart = prevLen - releasedChars;
55
66
  const suffix = text.slice(overlapStart);
56
67
  const newGraphemes = Array.from(segmenter.segment(suffix), (entry) => entry.segment);
57
- // Drop graphemes that correspond to the overlap region from the cache so
58
- // we don't double-count them.
59
- const overlapGraphemes = Array.from(segmenter.segment(text.slice(overlapStart, prevLen)), (entry) => entry.segment);
60
- const stableGraphemes = _segCache.graphemes.slice(0, _segCache.graphemes.length - overlapGraphemes.length);
61
- const merged = stableGraphemes.concat(newGraphemes);
68
+ const merged = cached.slice(0, stableCount).concat(newGraphemes);
62
69
  _segCache = { text, graphemes: merged };
63
70
  return merged;
64
71
  }
@@ -1 +1 @@
1
- {"version":3,"file":"streaming-text-smoothing.js","sourceRoot":"","sources":["../../src/shared/streaming-text-smoothing.ts"],"names":[],"mappings":"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","sourcesContent":["export const SMOOTH_STREAMING_COMMIT_INTERVAL_MS = 16;\nexport const SMOOTH_STREAMING_LONG_TEXT_THRESHOLD_GRAPHEMES = 640;\nexport const SMOOTH_STREAMING_LONG_TEXT_TAIL_GRAPHEMES = 180;\nconst SMOOTH_STREAMING_SPEED_MULTIPLIER = 2;\n\ntype SegmenterInstance = {\n segment(input: string): Iterable<{ segment: string }>;\n};\n\ntype SegmenterConstructor = new (\n locales?: string | string[],\n options?: { granularity?: \"grapheme\" },\n) => SegmenterInstance;\n\n// Module-level singleton Segmenter instance — creating a new Segmenter on every\n// call was previously the hot path cost (allocation + engine init per chunk).\nlet _segmenter: SegmenterInstance | null | undefined = undefined; // undefined = not yet checked\n\nfunction getSegmenter(): SegmenterInstance | null {\n if (_segmenter !== undefined) return _segmenter;\n const Segmenter = (Intl as typeof Intl & { Segmenter?: SegmenterConstructor })\n .Segmenter;\n _segmenter = Segmenter\n ? new Segmenter(undefined, { granularity: \"grapheme\" })\n : null;\n return _segmenter;\n}\n\n/**\n * Incremental segmentation cache. Instead of re-segmenting the full target\n * text on every frame, we extend the cached grapheme array by only segmenting\n * newly appended text.\n *\n * Because multi-byte grapheme clusters (emoji, ZWJ sequences) can straddle a\n * chunk boundary we re-segment the last OVERLAP characters of the previous text\n * together with the new suffix, then splice the results in.\n */\nconst SEGMENTER_OVERLAP = 16; // chars — safely covers any real grapheme cluster\n\ninterface SegmenterCache {\n text: string;\n graphemes: string[];\n}\n\nlet _segCache: SegmenterCache | null = null;\n\n/**\n * Split `text` into grapheme clusters.\n *\n * Results are cached: when `text` is an append of a previously segmented\n * string, only the appended suffix is segmented (plus a small overlap) and\n * merged with the cached result, making this O(delta) instead of O(n) per\n * call.\n *\n * When the new text is not a prefix-extension of the cache (e.g. a full reset\n * or non-append change) we fall back to full segmentation and reset the cache.\n */\nexport function splitStreamingTextGraphemes(text: string): string[] {\n const segmenter = getSegmenter();\n\n if (!segmenter) {\n return Array.from(text);\n }\n\n // Fast path: exact cache hit\n if (_segCache && _segCache.text === text) {\n return _segCache.graphemes;\n }\n\n // Incremental path: new text extends the cached text\n if (_segCache && text.startsWith(_segCache.text)) {\n const prevLen = _segCache.text.length;\n // Re-segment from (prevLen - OVERLAP) to include the overlap so any\n // grapheme that was split across the boundary is correctly re-assembled.\n const overlapStart = Math.max(0, prevLen - SEGMENTER_OVERLAP);\n const suffix = text.slice(overlapStart);\n const newGraphemes = Array.from(\n segmenter.segment(suffix),\n (entry) => entry.segment,\n );\n // Drop graphemes that correspond to the overlap region from the cache so\n // we don't double-count them.\n const overlapGraphemes = Array.from(\n segmenter.segment(text.slice(overlapStart, prevLen)),\n (entry) => entry.segment,\n );\n const stableGraphemes = _segCache.graphemes.slice(\n 0,\n _segCache.graphemes.length - overlapGraphemes.length,\n );\n const merged = stableGraphemes.concat(newGraphemes);\n _segCache = { text, graphemes: merged };\n return merged;\n }\n\n // Full fallback: text is not an append of cached text (reset or replacement)\n const graphemes = Array.from(\n segmenter.segment(text),\n (entry) => entry.segment,\n );\n _segCache = { text, graphemes };\n return graphemes;\n}\n\n/**\n * Reset the incremental segmentation cache. Call this when switching to a\n * new message so the cache from the previous message isn't carried over.\n * (Not strictly necessary for correctness — the cache is keyed on content —\n * but avoids holding a reference to the last message's text string.)\n */\nexport function resetSegmenterCache(): void {\n _segCache = null;\n}\n\nexport function initialSmoothStreamingGraphemeCount(\n graphemes: readonly string[],\n): number {\n if (graphemes.length <= SMOOTH_STREAMING_LONG_TEXT_THRESHOLD_GRAPHEMES) {\n return 0;\n }\n\n return Math.max(\n 0,\n graphemes.length - SMOOTH_STREAMING_LONG_TEXT_TAIL_GRAPHEMES,\n );\n}\n\nexport function smoothStreamingRevealCount({\n backlog,\n elapsedMs,\n inputDone = false,\n}: {\n backlog: number;\n elapsedMs: number;\n inputDone?: boolean;\n}): number {\n if (backlog <= 0 || elapsedMs <= 0) {\n return 0;\n }\n\n const baseCharactersPerSecond = inputDone\n ? backlog > 800\n ? 900\n : 420\n : backlog > 1400\n ? 640\n : backlog > 520\n ? 360\n : backlog > 180\n ? 190\n : 95;\n const charactersPerSecond =\n baseCharactersPerSecond * SMOOTH_STREAMING_SPEED_MULTIPLIER;\n\n const maxBurst =\n (inputDone ? 160 : backlog > 1400 ? 120 : 72) *\n SMOOTH_STREAMING_SPEED_MULTIPLIER;\n const count = Math.floor((elapsedMs / 1000) * charactersPerSecond);\n\n return Math.min(backlog, Math.max(1, count), maxBurst);\n}\n\nexport function smoothStreamingPunctuationDelayMs(\n grapheme: string | undefined,\n backlog: number,\n): number {\n if (!grapheme || backlog > 220) {\n return 0;\n }\n\n if (grapheme === \"\\n\") {\n return 80 / SMOOTH_STREAMING_SPEED_MULTIPLIER;\n }\n\n if (/[.!?)]/.test(grapheme)) {\n return 70 / SMOOTH_STREAMING_SPEED_MULTIPLIER;\n }\n\n if (/[,;:]/.test(grapheme)) {\n return 35 / SMOOTH_STREAMING_SPEED_MULTIPLIER;\n }\n\n return 0;\n}\n"]}
1
+ {"version":3,"file":"streaming-text-smoothing.js","sourceRoot":"","sources":["../../src/shared/streaming-text-smoothing.ts"],"names":[],"mappings":"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","sourcesContent":["export const SMOOTH_STREAMING_COMMIT_INTERVAL_MS = 16;\nexport const SMOOTH_STREAMING_LONG_TEXT_THRESHOLD_GRAPHEMES = 640;\nexport const SMOOTH_STREAMING_LONG_TEXT_TAIL_GRAPHEMES = 180;\nconst SMOOTH_STREAMING_SPEED_MULTIPLIER = 2;\n\ntype SegmenterInstance = {\n segment(input: string): Iterable<{ segment: string }>;\n};\n\ntype SegmenterConstructor = new (\n locales?: string | string[],\n options?: { granularity?: \"grapheme\" },\n) => SegmenterInstance;\n\n// Module-level singleton Segmenter instance — creating a new Segmenter on every\n// call was previously the hot path cost (allocation + engine init per chunk).\nlet _segmenter: SegmenterInstance | null | undefined = undefined; // undefined = not yet checked\n\nfunction getSegmenter(): SegmenterInstance | null {\n if (_segmenter !== undefined) return _segmenter;\n const Segmenter = (Intl as typeof Intl & { Segmenter?: SegmenterConstructor })\n .Segmenter;\n _segmenter = Segmenter\n ? new Segmenter(undefined, { granularity: \"grapheme\" })\n : null;\n return _segmenter;\n}\n\n/**\n * Incremental segmentation cache. Instead of re-segmenting the full target\n * text on every frame, we extend the cached grapheme array by only segmenting\n * newly appended text.\n *\n * Because multi-byte grapheme clusters (emoji, ZWJ sequences) can straddle a\n * chunk boundary we re-segment the last OVERLAP characters of the previous text\n * together with the new suffix, then splice the results in.\n */\nconst SEGMENTER_OVERLAP = 16; // chars — safely covers any real grapheme cluster\n\ninterface SegmenterCache {\n text: string;\n graphemes: string[];\n}\n\nlet _segCache: SegmenterCache | null = null;\n\n/**\n * Split `text` into grapheme clusters.\n *\n * Results are cached: when `text` is an append of a previously segmented\n * string, only the appended suffix is segmented (plus a small overlap) and\n * merged with the cached result, making this O(delta) instead of O(n) per\n * call.\n *\n * When the new text is not a prefix-extension of the cache (e.g. a full reset\n * or non-append change) we fall back to full segmentation and reset the cache.\n */\nexport function splitStreamingTextGraphemes(text: string): string[] {\n const segmenter = getSegmenter();\n\n if (!segmenter) {\n return Array.from(text);\n }\n\n // Fast path: exact cache hit\n if (_segCache && _segCache.text === text) {\n return _segCache.graphemes;\n }\n\n // Incremental path: new text extends the cached text\n if (_segCache && text.startsWith(_segCache.text)) {\n const prevLen = _segCache.text.length;\n const cached = _segCache.graphemes;\n // Walk back over cached graphemes until at least OVERLAP characters have\n // been released, so the re-segmented suffix starts on a known grapheme\n // boundary. Slicing at a fixed character offset instead would cut a\n // cluster (surrogate pair, ZWJ sequence) in half, and segmenting that\n // partial cluster in isolation yields a different grapheme count than the\n // cache holds for the same region — which silently drops characters.\n let stableCount = cached.length;\n let releasedChars = 0;\n while (stableCount > 0 && releasedChars < SEGMENTER_OVERLAP) {\n stableCount--;\n releasedChars += cached[stableCount]!.length;\n }\n const overlapStart = prevLen - releasedChars;\n const suffix = text.slice(overlapStart);\n const newGraphemes = Array.from(\n segmenter.segment(suffix),\n (entry) => entry.segment,\n );\n const merged = cached.slice(0, stableCount).concat(newGraphemes);\n _segCache = { text, graphemes: merged };\n return merged;\n }\n\n // Full fallback: text is not an append of cached text (reset or replacement)\n const graphemes = Array.from(\n segmenter.segment(text),\n (entry) => entry.segment,\n );\n _segCache = { text, graphemes };\n return graphemes;\n}\n\n/**\n * Reset the incremental segmentation cache. Call this when switching to a\n * new message so the cache from the previous message isn't carried over.\n * (Not strictly necessary for correctness — the cache is keyed on content —\n * but avoids holding a reference to the last message's text string.)\n */\nexport function resetSegmenterCache(): void {\n _segCache = null;\n}\n\nexport function initialSmoothStreamingGraphemeCount(\n graphemes: readonly string[],\n): number {\n if (graphemes.length <= SMOOTH_STREAMING_LONG_TEXT_THRESHOLD_GRAPHEMES) {\n return 0;\n }\n\n return Math.max(\n 0,\n graphemes.length - SMOOTH_STREAMING_LONG_TEXT_TAIL_GRAPHEMES,\n );\n}\n\nexport function smoothStreamingRevealCount({\n backlog,\n elapsedMs,\n inputDone = false,\n}: {\n backlog: number;\n elapsedMs: number;\n inputDone?: boolean;\n}): number {\n if (backlog <= 0 || elapsedMs <= 0) {\n return 0;\n }\n\n const baseCharactersPerSecond = inputDone\n ? backlog > 800\n ? 900\n : 420\n : backlog > 1400\n ? 640\n : backlog > 520\n ? 360\n : backlog > 180\n ? 190\n : 95;\n const charactersPerSecond =\n baseCharactersPerSecond * SMOOTH_STREAMING_SPEED_MULTIPLIER;\n\n const maxBurst =\n (inputDone ? 160 : backlog > 1400 ? 120 : 72) *\n SMOOTH_STREAMING_SPEED_MULTIPLIER;\n const count = Math.floor((elapsedMs / 1000) * charactersPerSecond);\n\n return Math.min(backlog, Math.max(1, count), maxBurst);\n}\n\nexport function smoothStreamingPunctuationDelayMs(\n grapheme: string | undefined,\n backlog: number,\n): number {\n if (!grapheme || backlog > 220) {\n return 0;\n }\n\n if (grapheme === \"\\n\") {\n return 80 / SMOOTH_STREAMING_SPEED_MULTIPLIER;\n }\n\n if (/[.!?)]/.test(grapheme)) {\n return 70 / SMOOTH_STREAMING_SPEED_MULTIPLIER;\n }\n\n if (/[,;:]/.test(grapheme)) {\n return 35 / SMOOTH_STREAMING_SPEED_MULTIPLIER;\n }\n\n return 0;\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.124.2",
3
+ "version": "0.124.3",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -70,25 +70,26 @@ export function splitStreamingTextGraphemes(text: string): string[] {
70
70
  // Incremental path: new text extends the cached text
71
71
  if (_segCache && text.startsWith(_segCache.text)) {
72
72
  const prevLen = _segCache.text.length;
73
- // Re-segment from (prevLen - OVERLAP) to include the overlap so any
74
- // grapheme that was split across the boundary is correctly re-assembled.
75
- const overlapStart = Math.max(0, prevLen - SEGMENTER_OVERLAP);
73
+ const cached = _segCache.graphemes;
74
+ // Walk back over cached graphemes until at least OVERLAP characters have
75
+ // been released, so the re-segmented suffix starts on a known grapheme
76
+ // boundary. Slicing at a fixed character offset instead would cut a
77
+ // cluster (surrogate pair, ZWJ sequence) in half, and segmenting that
78
+ // partial cluster in isolation yields a different grapheme count than the
79
+ // cache holds for the same region — which silently drops characters.
80
+ let stableCount = cached.length;
81
+ let releasedChars = 0;
82
+ while (stableCount > 0 && releasedChars < SEGMENTER_OVERLAP) {
83
+ stableCount--;
84
+ releasedChars += cached[stableCount]!.length;
85
+ }
86
+ const overlapStart = prevLen - releasedChars;
76
87
  const suffix = text.slice(overlapStart);
77
88
  const newGraphemes = Array.from(
78
89
  segmenter.segment(suffix),
79
90
  (entry) => entry.segment,
80
91
  );
81
- // Drop graphemes that correspond to the overlap region from the cache so
82
- // we don't double-count them.
83
- const overlapGraphemes = Array.from(
84
- segmenter.segment(text.slice(overlapStart, prevLen)),
85
- (entry) => entry.segment,
86
- );
87
- const stableGraphemes = _segCache.graphemes.slice(
88
- 0,
89
- _segCache.graphemes.length - overlapGraphemes.length,
90
- );
91
- const merged = stableGraphemes.concat(newGraphemes);
92
+ const merged = cached.slice(0, stableCount).concat(newGraphemes);
92
93
  _segCache = { text, graphemes: merged };
93
94
  return merged;
94
95
  }