@runtypelabs/persona 3.30.0 → 3.31.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.
Files changed (44) hide show
  1. package/README.md +15 -2602
  2. package/dist/animations/glyph-cycle.d.cts +1 -1
  3. package/dist/animations/glyph-cycle.d.ts +1 -1
  4. package/dist/animations/{types-B_Qazlm4.d.cts → types-quh7NmYD.d.cts} +9 -0
  5. package/dist/animations/{types-B_Qazlm4.d.ts → types-quh7NmYD.d.ts} +9 -0
  6. package/dist/animations/wipe.d.cts +1 -1
  7. package/dist/animations/wipe.d.ts +1 -1
  8. package/dist/codegen.cjs +1 -1
  9. package/dist/codegen.js +1 -1
  10. package/dist/index.cjs +46 -46
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.cts +52 -0
  13. package/dist/index.d.ts +52 -0
  14. package/dist/index.global.js +55 -55
  15. package/dist/index.global.js.map +1 -1
  16. package/dist/index.js +45 -45
  17. package/dist/index.js.map +1 -1
  18. package/dist/launcher.global.js +2 -2
  19. package/dist/launcher.global.js.map +1 -1
  20. package/dist/smart-dom-reader.d.cts +50 -0
  21. package/dist/smart-dom-reader.d.ts +50 -0
  22. package/dist/theme-editor.cjs +39 -39
  23. package/dist/theme-editor.d.cts +50 -0
  24. package/dist/theme-editor.d.ts +50 -0
  25. package/dist/theme-editor.js +39 -39
  26. package/dist/theme-reference.cjs +1 -1
  27. package/dist/theme-reference.js +1 -1
  28. package/dist/widget.css +19 -0
  29. package/package.json +1 -1
  30. package/src/client.ts +6 -0
  31. package/src/components/approval-bubble.test.ts +52 -0
  32. package/src/components/approval-bubble.ts +20 -0
  33. package/src/components/panel.ts +7 -1
  34. package/src/defaults.ts +14 -0
  35. package/src/generated/runtype-openapi-contract.ts +4 -0
  36. package/src/session.test.ts +62 -0
  37. package/src/session.ts +27 -0
  38. package/src/styles/widget.css +19 -0
  39. package/src/theme-reference.ts +1 -1
  40. package/src/types.ts +50 -0
  41. package/src/ui.scroll.test.ts +383 -0
  42. package/src/ui.ts +390 -40
  43. package/src/utils/auto-follow.test.ts +91 -0
  44. package/src/utils/auto-follow.ts +68 -0
@@ -1,8 +1,11 @@
1
1
  import { describe, expect, it } from "vitest";
2
2
 
3
3
  import {
4
+ computeAnchorScrollState,
5
+ computeShrunkSpacerHeight,
4
6
  createFollowStateController,
5
7
  getScrollBottomOffset,
8
+ hasSelectionWithin,
6
9
  isElementNearBottom,
7
10
  resolveFollowStateFromScroll,
8
11
  resolveFollowStateFromWheel
@@ -90,6 +93,94 @@ describe("auto-follow utilities", () => {
90
93
  expect(resume.action).toBe("resume");
91
94
  });
92
95
 
96
+ it("detects an active selection touching the container", () => {
97
+ const inside = { name: "inside" } as unknown as Node;
98
+ const outside = { name: "outside" } as unknown as Node;
99
+ const container = {
100
+ contains: (node: Node | null) => node === inside
101
+ };
102
+
103
+ expect(hasSelectionWithin(null, container)).toBe(false);
104
+ expect(
105
+ hasSelectionWithin(
106
+ { isCollapsed: true, anchorNode: inside, focusNode: inside },
107
+ container
108
+ )
109
+ ).toBe(false);
110
+ expect(
111
+ hasSelectionWithin(
112
+ { isCollapsed: false, anchorNode: outside, focusNode: outside },
113
+ container
114
+ )
115
+ ).toBe(false);
116
+ expect(
117
+ hasSelectionWithin(
118
+ { isCollapsed: false, anchorNode: inside, focusNode: outside },
119
+ container
120
+ )
121
+ ).toBe(true);
122
+ expect(
123
+ hasSelectionWithin(
124
+ { isCollapsed: false, anchorNode: outside, focusNode: inside },
125
+ container
126
+ )
127
+ ).toBe(true);
128
+ });
129
+
130
+ it("computes anchor-top scroll geometry", () => {
131
+ // Anchored message deep in the transcript: spacer fills the shortfall so
132
+ // the target position is reachable.
133
+ expect(
134
+ computeAnchorScrollState({
135
+ anchorOffsetTop: 700,
136
+ topOffset: 16,
137
+ viewportHeight: 400,
138
+ contentHeight: 1000
139
+ })
140
+ ).toEqual({ targetScrollTop: 684, spacerHeight: 84 });
141
+
142
+ // Message near the top of the transcript: target clamps to 0.
143
+ expect(
144
+ computeAnchorScrollState({
145
+ anchorOffsetTop: 10,
146
+ topOffset: 16,
147
+ viewportHeight: 400,
148
+ contentHeight: 1000
149
+ }).targetScrollTop
150
+ ).toBe(0);
151
+
152
+ // Already enough content below: no spacer needed.
153
+ expect(
154
+ computeAnchorScrollState({
155
+ anchorOffsetTop: 500,
156
+ topOffset: 16,
157
+ viewportHeight: 400,
158
+ contentHeight: 2000
159
+ }).spacerHeight
160
+ ).toBe(0);
161
+ });
162
+
163
+ it("shrinks the anchor spacer as content grows, never below zero or above initial", () => {
164
+ const base = {
165
+ initialSpacerHeight: 100,
166
+ contentHeightAtAnchor: 1000
167
+ };
168
+
169
+ expect(
170
+ computeShrunkSpacerHeight({ ...base, currentContentHeight: 1000 })
171
+ ).toBe(100);
172
+ expect(
173
+ computeShrunkSpacerHeight({ ...base, currentContentHeight: 1040 })
174
+ ).toBe(60);
175
+ expect(
176
+ computeShrunkSpacerHeight({ ...base, currentContentHeight: 1500 })
177
+ ).toBe(0);
178
+ // Content shrank (e.g. a collapsed tool row): spacer never grows back.
179
+ expect(
180
+ computeShrunkSpacerHeight({ ...base, currentContentHeight: 900 })
181
+ ).toBe(100);
182
+ });
183
+
93
184
  it("resolves wheel intent for pause and resume", () => {
94
185
  expect(
95
186
  resolveFollowStateFromWheel({
@@ -110,3 +110,71 @@ export function resolveFollowStateFromWheel(
110
110
 
111
111
  return "none";
112
112
  }
113
+
114
+ export type SelectionLike = Pick<Selection, "isCollapsed"> & {
115
+ anchorNode: Node | null;
116
+ focusNode: Node | null;
117
+ };
118
+
119
+ /**
120
+ * True when the user has a non-collapsed text selection that touches the
121
+ * given container. Used to pause auto-follow while the user is selecting
122
+ * transcript text, so the streaming scroll doesn't drag content out from
123
+ * under an in-progress drag-selection.
124
+ */
125
+ export function hasSelectionWithin(
126
+ selection: SelectionLike | null,
127
+ container: Pick<Node, "contains">
128
+ ): boolean {
129
+ if (!selection || selection.isCollapsed) return false;
130
+ return (
131
+ container.contains(selection.anchorNode) ||
132
+ container.contains(selection.focusNode)
133
+ );
134
+ }
135
+
136
+ export type AnchorScrollInput = {
137
+ /** Top of the anchored user message, relative to the scroll content. */
138
+ anchorOffsetTop: number;
139
+ /** Desired gap kept between the anchored message and the viewport top. */
140
+ topOffset: number;
141
+ /** clientHeight of the scroll container. */
142
+ viewportHeight: number;
143
+ /** scrollHeight of the scroll container, excluding any anchor spacer. */
144
+ contentHeight: number;
145
+ };
146
+
147
+ /**
148
+ * Geometry for anchor-top ("pin the sent user message near the viewport
149
+ * top") scrolling. The spacer height is the extra scrollable room needed so
150
+ * the target scroll position is reachable before the streamed response has
151
+ * grown tall enough to provide it naturally.
152
+ */
153
+ export function computeAnchorScrollState(input: AnchorScrollInput): {
154
+ targetScrollTop: number;
155
+ spacerHeight: number;
156
+ } {
157
+ const targetScrollTop = Math.max(0, input.anchorOffsetTop - input.topOffset);
158
+ const spacerHeight = Math.max(
159
+ 0,
160
+ targetScrollTop + input.viewportHeight - input.contentHeight
161
+ );
162
+ return { targetScrollTop, spacerHeight };
163
+ }
164
+
165
+ /**
166
+ * Shrink-only spacer update: as streamed content grows beneath the anchored
167
+ * message, the spacer gives back exactly that much room so total scroll
168
+ * height stays constant and nothing jumps. Never grows after anchoring.
169
+ */
170
+ export function computeShrunkSpacerHeight(input: {
171
+ initialSpacerHeight: number;
172
+ contentHeightAtAnchor: number;
173
+ currentContentHeight: number;
174
+ }): number {
175
+ const growth = Math.max(
176
+ 0,
177
+ input.currentContentHeight - input.contentHeightAtAnchor
178
+ );
179
+ return Math.max(0, input.initialSpacerHeight - growth);
180
+ }