@ayni.org/ayni-ui-core 0.3.0 → 0.3.2

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/README.md CHANGED
@@ -24,5 +24,20 @@ The browser's stylesheet is the reference: tests in this repository compare it,
24
24
  with these values, so a change made on one side only fails. The app builds its styles from the same values. A change
25
25
  of look or wording on one of these surfaces starts here.
26
26
 
27
+ ## One conversation, every device
28
+
29
+ A conversation shows the same transcript on every device of a person (AG-UI profile PROF-STREAM-005,
30
+ PROF-HIST-007). The agent sends a session what the person's other devices add; the reducer places a text message
31
+ by its role and its id, so a message typed elsewhere lands on the person's side. For what a client missed while it
32
+ was away:
33
+
34
+ | Function | What it does |
35
+ | --- | --- |
36
+ | `readCatchUp(transcript, read)` | Reads the newest pages of history, going back until one reaches what the transcript holds, history ends, or four pages were read; then merges them. |
37
+ | `catchUp(transcript, pages)` | The merge itself. The transcript keeps its own paging position; when more was missed than was read, it starts over from the pages, so it never shows a hole. |
38
+ | `reachesTranscript(transcript, page)` | Whether a page of history holds a message the transcript already has. |
39
+
40
+ A client calls `readCatchUp` every time a session's stream attaches.
41
+
27
42
  Published to npm from this repository by the `npm-publish` workflow whenever `package.json` carries a version that is
28
43
  not on npm yet.
@@ -53,6 +53,27 @@ export declare const HOME_DESIGN: {
53
53
  readonly pinSize: 11;
54
54
  readonly pinPaddingX: 3;
55
55
  };
56
+ /**
57
+ * The two zones a tile is dragged between, the pinned one above the line and the rest below: while a tile is
58
+ * dragged both show a dashed outline, the one under the pointer a solid one; the empty pinned zone grows to
59
+ * one slot.
60
+ */
61
+ readonly zone: {
62
+ readonly minHeight: 12;
63
+ readonly radius: 12;
64
+ readonly droppableMinHeight: 48;
65
+ readonly droppableOutlineWidth: 1;
66
+ /** the share of the accent in the dashed outline */
67
+ readonly droppableOutlineShare: 0.5;
68
+ readonly outlineOffset: -2;
69
+ readonly targetOutlineWidth: 2;
70
+ };
71
+ /** the tile being dragged, where it stood; and the line above the pinned tile it would land before */
72
+ readonly drag: {
73
+ readonly opacity: 0.4;
74
+ readonly scale: 0.96;
75
+ readonly dropLineWidth: 3;
76
+ };
56
77
  };
57
78
  readonly column: {
58
79
  readonly header: {
@@ -183,6 +204,13 @@ export declare const HOME_COLORS: {
183
204
  readonly line: "border";
184
205
  readonly pin: "muted";
185
206
  readonly pinBackground: "surface";
207
+ readonly target: "accent";
208
+ };
209
+ readonly zone: {
210
+ readonly outline: "accent";
211
+ };
212
+ readonly drag: {
213
+ readonly dropLine: "accent";
186
214
  };
187
215
  readonly column: {
188
216
  readonly background: "bg";
@@ -16,6 +16,23 @@ export const HOME_DESIGN = {
16
16
  dot: { bottom: 4, right: 4, size: 9, borderWidth: 2 },
17
17
  /** the line under the pinned tiles, with its pin */
18
18
  separator: { width: 48, height: 14, lineWidth: 1, pinSize: 11, pinPaddingX: 3 },
19
+ /**
20
+ * The two zones a tile is dragged between, the pinned one above the line and the rest below: while a tile is
21
+ * dragged both show a dashed outline, the one under the pointer a solid one; the empty pinned zone grows to
22
+ * one slot.
23
+ */
24
+ zone: {
25
+ minHeight: 12,
26
+ radius: 12,
27
+ droppableMinHeight: 48,
28
+ droppableOutlineWidth: 1,
29
+ /** the share of the accent in the dashed outline */
30
+ droppableOutlineShare: 0.5,
31
+ outlineOffset: -2,
32
+ targetOutlineWidth: 2,
33
+ },
34
+ /** the tile being dragged, where it stood; and the line above the pinned tile it would land before */
35
+ drag: { opacity: 0.4, scale: 0.96, dropLineWidth: 3 },
19
36
  },
20
37
  column: {
21
38
  header: { height: 64, paddingLeft: 12, paddingRight: 8, gap: 8, fontSize: 16, iconSize: 18, flagFontSize: 16 },
@@ -62,7 +79,9 @@ export const HOME_COLORS = {
62
79
  selectedBar: { background: 'accent' },
63
80
  badge: { background: 'danger' },
64
81
  dot: { background: 'warning', border: 'surface' },
65
- separator: { line: 'border', pin: 'muted', pinBackground: 'surface' },
82
+ separator: { line: 'border', pin: 'muted', pinBackground: 'surface', target: 'accent' },
83
+ zone: { outline: 'accent' },
84
+ drag: { dropLine: 'accent' },
66
85
  column: { background: 'bg', border: 'border' },
67
86
  header: { icon: 'muted' },
68
87
  selectedCard: { background: 'surface' },
package/dist/reducer.d.ts CHANGED
@@ -19,3 +19,23 @@ export declare function reactionsOf(page: Record<string, unknown>[]): Record<str
19
19
  }[]>;
20
20
  /** Merges a history page (newest last) into the transcript without duplicates (UI-CONV-012, UI-PRIV-003). */
21
21
  export declare function mergeHistory(transcript: Transcript, page: Record<string, unknown>[], nextBefore: string | null): Transcript;
22
+ /** A page of history as the agent returns it (PROF-HIST-001): newest last, with where the page before it starts. */
23
+ export interface HistoryPage {
24
+ messages: Record<string, unknown>[];
25
+ nextBefore: string | null;
26
+ }
27
+ /** Whether a page of history reaches what the transcript holds: one of its messages is already there. */
28
+ export declare function reachesTranscript(transcript: Transcript, page: Record<string, unknown>[]): boolean;
29
+ /**
30
+ * What a device missed while it was away, merged into the transcript it holds (UI-CONV-022, PROF-HIST-007): a
31
+ * conversation continued from another device, or events its stream could not replay. `pages` are the newest pages
32
+ * of history, newest first, read until one reached the transcript or history ended. The transcript keeps its own
33
+ * paging position when a page reached it. When none did, more was missed than was read: the transcript starts
34
+ * over from the pages, so it never shows a hole between what it held and what it read.
35
+ */
36
+ export declare function catchUp(transcript: Transcript, pages: readonly HistoryPage[]): Transcript;
37
+ /**
38
+ * Reads what `catchUp` needs: the newest pages of history, going back until one reaches the transcript, history
39
+ * ends, or `maxPages` were read; then merges them.
40
+ */
41
+ export declare function readCatchUp(transcript: Transcript, read: (before?: string) => Promise<HistoryPage>, maxPages?: number): Promise<Transcript>;
package/dist/reducer.js CHANGED
@@ -75,9 +75,13 @@ export function reduce(transcript, session, event, fromRun, isOnScreen) {
75
75
  break;
76
76
  case 'TEXT_MESSAGE_START': {
77
77
  const role = event.role === 'user' ? 'user' : event.role === 'system' ? 'system' : 'assistant';
78
+ const id = String(event.messageId);
79
+ // a message already held comes again when a stream replays it after a reconnect: it keeps its time, so
80
+ // it stays where it is in the transcript
81
+ const held = t.messages.find((m) => m.id === id);
78
82
  t = {
79
83
  ...t,
80
- messages: upsertMessage(t.messages, { id: String(event.messageId), role, content: '', at, streaming: true }),
84
+ messages: upsertMessage(t.messages, { id, role, content: '', at: held?.at ?? at, streaming: true }),
81
85
  };
82
86
  break;
83
87
  }
@@ -308,3 +312,50 @@ export function mergeHistory(transcript, page, nextBefore) {
308
312
  complete: nextBefore === null || page.length === 0,
309
313
  };
310
314
  }
315
+ /** Whether a page of history reaches what the transcript holds: one of its messages is already there. */
316
+ export function reachesTranscript(transcript, page) {
317
+ const known = new Set(transcript.messages.map((m) => m.id));
318
+ return page.some((m) => {
319
+ const external = m.metadata?.externalId;
320
+ return known.has(String(m.id)) || (!!external && known.has(external));
321
+ });
322
+ }
323
+ /**
324
+ * What a device missed while it was away, merged into the transcript it holds (UI-CONV-022, PROF-HIST-007): a
325
+ * conversation continued from another device, or events its stream could not replay. `pages` are the newest pages
326
+ * of history, newest first, read until one reached the transcript or history ended. The transcript keeps its own
327
+ * paging position when a page reached it. When none did, more was missed than was read: the transcript starts
328
+ * over from the pages, so it never shows a hole between what it held and what it read.
329
+ */
330
+ export function catchUp(transcript, pages) {
331
+ if (!pages.length)
332
+ return transcript;
333
+ const held = transcript.messages.length > 0;
334
+ const reached = !held || pages.some((p) => p.nextBefore === null || reachesTranscript(transcript, p.messages));
335
+ let merged = reached ? transcript : { ...transcript, messages: [], reactions: {} };
336
+ for (const page of pages)
337
+ merged = mergeHistory(merged, page.messages, page.nextBefore);
338
+ return held && reached
339
+ ? { ...merged, oldestLoadedId: transcript.oldestLoadedId, complete: transcript.complete }
340
+ : merged;
341
+ }
342
+ /**
343
+ * Reads what `catchUp` needs: the newest pages of history, going back until one reaches the transcript, history
344
+ * ends, or `maxPages` were read; then merges them.
345
+ */
346
+ export async function readCatchUp(transcript, read, maxPages = 4) {
347
+ const pages = [];
348
+ let before;
349
+ for (let i = 0; i < maxPages; i++) {
350
+ const page = await read(before);
351
+ pages.push(page);
352
+ const done = page.nextBefore === null ||
353
+ page.messages.length === 0 ||
354
+ transcript.messages.length === 0 ||
355
+ reachesTranscript(transcript, page.messages);
356
+ if (done)
357
+ break;
358
+ before = page.nextBefore ?? undefined;
359
+ }
360
+ return catchUp(transcript, pages);
361
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ayni.org/ayni-ui-core",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Framework-independent core of the Ayni UI (UI-SHELL-004): agents and controllers, trust resolution, sessions and binding, the conversation reducer, the local store and sync; shared by the browser UI and the Ayni app",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",