@opengeni/react 0.36.1 → 0.40.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.
- package/README.md +92 -2
- package/dist/{chunk-J2RVJ6JX.js → chunk-4IJCL7YO.js} +13 -2
- package/dist/chunk-4IJCL7YO.js.map +1 -0
- package/dist/{chunk-7CJOAW3V.js → chunk-EB67J347.js} +1 -135
- package/dist/chunk-EB67J347.js.map +1 -0
- package/dist/chunk-EKZH6IDG.js +141 -0
- package/dist/chunk-EKZH6IDG.js.map +1 -0
- package/dist/{chunk-OKKMZDQF.js → chunk-JALF5FI3.js} +9 -7
- package/dist/{chunk-OKKMZDQF.js.map → chunk-JALF5FI3.js.map} +1 -1
- package/dist/{chunk-Z5FV355Z.js → chunk-KR2SK5GJ.js} +1382 -724
- package/dist/chunk-KR2SK5GJ.js.map +1 -0
- package/dist/{chunk-N4XEBE23.js → chunk-OMCFRWHL.js} +22 -13
- package/dist/{chunk-N4XEBE23.js.map → chunk-OMCFRWHL.js.map} +1 -1
- package/dist/{chunk-FT2TXNGZ.js → chunk-Q2NCKWTK.js} +183 -18
- package/dist/chunk-Q2NCKWTK.js.map +1 -0
- package/dist/{chunk-22RM4GMO.js → chunk-SJKT4TKW.js} +26 -2
- package/dist/chunk-SJKT4TKW.js.map +1 -0
- package/dist/{chunk-ALA3UGWB.js → chunk-SXIQK54Z.js} +8 -6
- package/dist/{chunk-ALA3UGWB.js.map → chunk-SXIQK54Z.js.map} +1 -1
- package/dist/chunk-WZT5G5OR.js +433 -0
- package/dist/chunk-WZT5G5OR.js.map +1 -0
- package/dist/client.d.ts +2 -0
- package/dist/components/chat-composer.d.ts +7 -1
- package/dist/components/human-input-form.d.ts +16 -6
- package/dist/components/human-input-surface.d.ts +19 -0
- package/dist/components/machine-input-display.d.ts +12 -0
- package/dist/components/message-timeline.d.ts +6 -1
- package/dist/components/model-policy-picker.d.ts +78 -0
- package/dist/components/session-chrome.d.ts +2 -1
- package/dist/components/tip-follow.d.ts +30 -0
- package/dist/composer.d.ts +2 -0
- package/dist/composer.js +22 -5
- package/dist/hooks/use-composer.d.ts +10 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.js +2261 -2230
- package/dist/index.js.map +1 -1
- package/dist/machines.js +4 -3
- package/dist/model-policy.d.ts +12 -4
- package/dist/model-policy.js +5 -1
- package/dist/realtime/dropdown-menu.d.ts +16 -0
- package/dist/realtime/realtime-control.d.ts +126 -0
- package/dist/realtime.d.ts +11 -0
- package/dist/realtime.js +1023 -0
- package/dist/realtime.js.map +1 -0
- package/dist/session-context.d.ts +7 -1
- package/dist/session-ui.d.ts +2 -0
- package/dist/session-ui.js +6 -4
- package/dist/session.js +5 -4
- package/dist/timeline/compute-label.d.ts +2 -0
- package/dist/timeline/index.d.ts +1 -0
- package/dist/timeline/types.d.ts +5 -0
- package/package.json +7 -2
- package/src/client.ts +13 -0
- package/src/components/chat-composer.tsx +19 -5
- package/src/components/composer-transcription-control.tsx +2 -2
- package/src/components/composer.tsx +14 -5
- package/src/components/human-input-form.tsx +506 -186
- package/src/components/human-input-surface.tsx +80 -0
- package/src/components/machine-input-display.ts +83 -0
- package/src/components/message-timeline.tsx +348 -308
- package/src/components/model-policy-picker.tsx +587 -0
- package/src/components/sandbox-files.tsx +1 -1
- package/src/components/session-chrome.tsx +141 -15
- package/src/components/tip-follow.ts +127 -6
- package/src/composer.ts +13 -0
- package/src/hooks/use-composer.ts +229 -29
- package/src/index.ts +19 -0
- package/src/model-policy.ts +51 -7
- package/src/realtime/dropdown-menu.tsx +123 -0
- package/src/realtime/realtime-control.tsx +1178 -0
- package/src/realtime.ts +27 -0
- package/src/session-context.ts +16 -0
- package/src/session-ui.ts +2 -0
- package/src/timeline/compute-label.tsx +18 -0
- package/src/timeline/index.ts +3 -0
- package/src/timeline/projection.ts +21 -1
- package/src/timeline/tool-renderers.tsx +232 -43
- package/src/timeline/types.ts +5 -0
- package/styles/index.css +61 -0
- package/styles/tokens.css +2 -0
- package/dist/chunk-22RM4GMO.js.map +0 -1
- package/dist/chunk-7CJOAW3V.js.map +0 -1
- package/dist/chunk-FT2TXNGZ.js.map +0 -1
- package/dist/chunk-J2RVJ6JX.js.map +0 -1
- package/dist/chunk-Z5FV355Z.js.map +0 -1
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* | `--og-session-chrome-chip-pad-x` / `--og-session-chrome-chip-gap` | Chip padding / rail gap |
|
|
19
19
|
* | `--og-session-chrome-rail-pad` | Outer rail inset |
|
|
20
20
|
* | `--og-session-chrome-panel-pad-x` / `-y` | Expanded panel padding |
|
|
21
|
+
* | `--og-session-chrome-panel-max-height` | Cap for expanded body (scrolls inside) |
|
|
21
22
|
* | `--og-session-chrome-duration` / `--og-session-chrome-ease` | Expand + pill motion |
|
|
22
23
|
* | `--og-session-chrome-crossfade-duration` | Segment content opacity crossfade |
|
|
23
24
|
* | `--og-session-chrome-row-hover` | Inbox / queue row hover wash |
|
|
@@ -32,6 +33,7 @@
|
|
|
32
33
|
*/
|
|
33
34
|
import type { SessionGoal, SessionPendingInputPreview, SessionTurn } from "@opengeni/sdk";
|
|
34
35
|
import {
|
|
36
|
+
AudioLinesIcon,
|
|
35
37
|
ArrowDownIcon,
|
|
36
38
|
ArrowUpIcon,
|
|
37
39
|
BotIcon,
|
|
@@ -64,7 +66,7 @@ import { cn } from "../lib/cn";
|
|
|
64
66
|
import { requestQueueDraftEdit } from "./queue-draft-policy";
|
|
65
67
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./tooltip";
|
|
66
68
|
|
|
67
|
-
export type SessionChromeSignalId = "incoming" | "queue" | "goal" | "agents";
|
|
69
|
+
export type SessionChromeSignalId = "incoming" | "steering" | "queue" | "goal" | "agents";
|
|
68
70
|
|
|
69
71
|
export type SessionChromeSignalTone = "neutral" | "accent" | "waiting" | "running";
|
|
70
72
|
|
|
@@ -105,6 +107,11 @@ type GoalPillState =
|
|
|
105
107
|
| "invariant_broken"
|
|
106
108
|
| "completed";
|
|
107
109
|
|
|
110
|
+
type QueuedTurnPresentation = {
|
|
111
|
+
kind: "prompt" | "realtime_voice" | "realtime_voice_handoff";
|
|
112
|
+
text: string;
|
|
113
|
+
};
|
|
114
|
+
|
|
108
115
|
const GOAL_LABEL: Record<GoalPillState, string> = {
|
|
109
116
|
pursuing: "Pursuing",
|
|
110
117
|
scheduled: "Scheduled",
|
|
@@ -115,6 +122,28 @@ const GOAL_LABEL: Record<GoalPillState, string> = {
|
|
|
115
122
|
completed: "Done",
|
|
116
123
|
};
|
|
117
124
|
|
|
125
|
+
function queuedTurnPresentation(turn: SessionTurn): QueuedTurnPresentation {
|
|
126
|
+
const realtimeDelegation = objectValue(turn.metadata.realtimeDelegation);
|
|
127
|
+
const inputTranscript = realtimeDelegation?.inputTranscript;
|
|
128
|
+
if (typeof inputTranscript === "string" && inputTranscript.trim()) {
|
|
129
|
+
return { kind: "realtime_voice", text: inputTranscript.trim() };
|
|
130
|
+
}
|
|
131
|
+
if (objectValue(turn.metadata.realtimeTailFlush)) {
|
|
132
|
+
return { kind: "realtime_voice_handoff", text: "Remaining voice context" };
|
|
133
|
+
}
|
|
134
|
+
return { kind: "prompt", text: turn.prompt };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function isSteeringTurn(turn: SessionTurn): boolean {
|
|
138
|
+
return turn.metadata.delivery === "steer";
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function objectValue(value: unknown): Record<string, unknown> | null {
|
|
142
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
143
|
+
? (value as Record<string, unknown>)
|
|
144
|
+
: null;
|
|
145
|
+
}
|
|
146
|
+
|
|
118
147
|
/** Select pill state from the goal's authoritative continuation projection. */
|
|
119
148
|
export function sessionChromeGoalPillState(
|
|
120
149
|
goalStatus: "active" | "paused" | "completed",
|
|
@@ -215,6 +244,34 @@ export function SessionChrome({
|
|
|
215
244
|
const record = goal?.goal ?? null;
|
|
216
245
|
const incoming = queue.pendingInputs;
|
|
217
246
|
const turns = queue.queue;
|
|
247
|
+
const composerSteering = composer?.steering ?? null;
|
|
248
|
+
const { steering, queuedTurns } = useMemo(() => {
|
|
249
|
+
const pendingQueueSteer =
|
|
250
|
+
turns.find((turn) => queue.pendingByTurn[turn.id] === "steer") ?? null;
|
|
251
|
+
const durableQueueSteer =
|
|
252
|
+
!pendingQueueSteer && turns[0] && isSteeringTurn(turns[0]) ? turns[0] : null;
|
|
253
|
+
const currentSteering =
|
|
254
|
+
composerSteering?.phase === "submitting"
|
|
255
|
+
? composerSteering
|
|
256
|
+
: pendingQueueSteer
|
|
257
|
+
? {
|
|
258
|
+
phase: "submitting" as const,
|
|
259
|
+
text: queuedTurnPresentation(pendingQueueSteer).text,
|
|
260
|
+
turnId: pendingQueueSteer.id,
|
|
261
|
+
}
|
|
262
|
+
: durableQueueSteer
|
|
263
|
+
? {
|
|
264
|
+
phase: "accepted" as const,
|
|
265
|
+
text: queuedTurnPresentation(durableQueueSteer).text,
|
|
266
|
+
turnId: durableQueueSteer.id,
|
|
267
|
+
}
|
|
268
|
+
: composerSteering;
|
|
269
|
+
const currentTurnId = currentSteering?.turnId ?? null;
|
|
270
|
+
return {
|
|
271
|
+
steering: currentSteering,
|
|
272
|
+
queuedTurns: currentTurnId ? turns.filter((turn) => turn.id !== currentTurnId) : turns,
|
|
273
|
+
};
|
|
274
|
+
}, [composerSteering, queue.pendingByTurn, turns]);
|
|
218
275
|
const canMutateQueue = !readOnly && composer !== undefined;
|
|
219
276
|
|
|
220
277
|
const liveGoal =
|
|
@@ -250,14 +307,44 @@ export function SessionChrome({
|
|
|
250
307
|
icon: <InboxIcon className="size-3" />,
|
|
251
308
|
});
|
|
252
309
|
}
|
|
253
|
-
if (
|
|
254
|
-
|
|
310
|
+
if (steering) {
|
|
311
|
+
rows.push({
|
|
312
|
+
id: "steering",
|
|
313
|
+
label: "Changing direction…",
|
|
314
|
+
detail: steering.text,
|
|
315
|
+
tone: "accent",
|
|
316
|
+
icon:
|
|
317
|
+
steering.phase === "submitting" ? (
|
|
318
|
+
<Loader2Icon className="size-3 animate-og-spin" />
|
|
319
|
+
) : (
|
|
320
|
+
<ZapIcon className="size-3" />
|
|
321
|
+
),
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
if (queuedTurns.length > 0) {
|
|
325
|
+
const presentations = queuedTurns.map(queuedTurnPresentation);
|
|
326
|
+
const first = presentations[0];
|
|
327
|
+
const allVoiceRequests = presentations.every(({ kind }) => kind === "realtime_voice");
|
|
328
|
+
const onlyVoiceHandoff =
|
|
329
|
+
presentations.length === 1 && first?.kind === "realtime_voice_handoff";
|
|
330
|
+
const voiceOnly = allVoiceRequests || onlyVoiceHandoff;
|
|
331
|
+
const detail = first?.text;
|
|
255
332
|
rows.push({
|
|
256
333
|
id: "queue",
|
|
257
|
-
label:
|
|
334
|
+
label: allVoiceRequests
|
|
335
|
+
? queuedTurns.length === 1
|
|
336
|
+
? "Voice request queued"
|
|
337
|
+
: `${queuedTurns.length} voice requests queued`
|
|
338
|
+
: onlyVoiceHandoff
|
|
339
|
+
? "Voice handoff queued"
|
|
340
|
+
: `${queuedTurns.length} queued prompt${queuedTurns.length === 1 ? "" : "s"}`,
|
|
258
341
|
...(detail ? { detail } : {}),
|
|
259
342
|
tone: "neutral",
|
|
260
|
-
icon:
|
|
343
|
+
icon: voiceOnly ? (
|
|
344
|
+
<AudioLinesIcon className="size-3" />
|
|
345
|
+
) : (
|
|
346
|
+
<ListOrderedIcon className="size-3" />
|
|
347
|
+
),
|
|
261
348
|
});
|
|
262
349
|
}
|
|
263
350
|
if (record && goalState) {
|
|
@@ -292,7 +379,7 @@ export function SessionChrome({
|
|
|
292
379
|
});
|
|
293
380
|
}
|
|
294
381
|
return rows;
|
|
295
|
-
}, [agentsSignal, elapsed, goalState, incoming, record,
|
|
382
|
+
}, [agentsSignal, elapsed, goalState, incoming, queuedTurns, record, steering]);
|
|
296
383
|
|
|
297
384
|
const [activeUncontrolled, setActiveUncontrolled] = useState<SessionChromeSignalId | null>(
|
|
298
385
|
defaultActive,
|
|
@@ -320,10 +407,10 @@ export function SessionChrome({
|
|
|
320
407
|
|
|
321
408
|
useEffect(() => {
|
|
322
409
|
if (active !== "queue" || !replaceDraftFor) return;
|
|
323
|
-
if (!
|
|
410
|
+
if (!queuedTurns.some((turn) => turn.id === replaceDraftFor)) {
|
|
324
411
|
setReplaceDraftFor(null);
|
|
325
412
|
}
|
|
326
|
-
}, [active,
|
|
413
|
+
}, [active, queuedTurns, replaceDraftFor]);
|
|
327
414
|
|
|
328
415
|
useEffect(() => {
|
|
329
416
|
const rail = railRef.current;
|
|
@@ -363,7 +450,7 @@ export function SessionChrome({
|
|
|
363
450
|
const node = panelBodyRef.current;
|
|
364
451
|
if (!node) return;
|
|
365
452
|
setPanelHeight(node.offsetHeight);
|
|
366
|
-
}, [open, active, incoming,
|
|
453
|
+
}, [open, active, incoming, queuedTurns, record, goalState, agentsPanel, agentsSignal, steering]);
|
|
367
454
|
|
|
368
455
|
useEffect(() => {
|
|
369
456
|
if (!open) return;
|
|
@@ -385,9 +472,11 @@ export function SessionChrome({
|
|
|
385
472
|
const panelBody =
|
|
386
473
|
active === "incoming" ? (
|
|
387
474
|
<IncomingPanel inputs={incoming} onDismiss={onDismissIncoming} />
|
|
475
|
+
) : active === "steering" && steering ? (
|
|
476
|
+
<SteeringPanel phase={steering.phase} text={steering.text} />
|
|
388
477
|
) : active === "queue" ? (
|
|
389
478
|
<QueuePanel
|
|
390
|
-
turns={
|
|
479
|
+
turns={queuedTurns}
|
|
391
480
|
readOnly={!canMutateQueue}
|
|
392
481
|
mutationFor={queue.mutationFor}
|
|
393
482
|
replaceDraftFor={replaceDraftFor}
|
|
@@ -576,8 +665,9 @@ export function SessionChrome({
|
|
|
576
665
|
>
|
|
577
666
|
<div
|
|
578
667
|
ref={panelBodyRef}
|
|
579
|
-
className="relative border-t border-og-border/50"
|
|
668
|
+
className="relative overflow-y-auto overscroll-contain border-t border-og-border/50"
|
|
580
669
|
style={{
|
|
670
|
+
maxHeight: "var(--og-session-chrome-panel-max-height)",
|
|
581
671
|
paddingInline: "var(--og-session-chrome-panel-pad-x)",
|
|
582
672
|
paddingBlock: "var(--og-session-chrome-panel-pad-y)",
|
|
583
673
|
}}
|
|
@@ -661,6 +751,33 @@ function IncomingPanel({
|
|
|
661
751
|
);
|
|
662
752
|
}
|
|
663
753
|
|
|
754
|
+
function SteeringPanel({ phase, text }: { phase: "submitting" | "accepted"; text: string }) {
|
|
755
|
+
return (
|
|
756
|
+
<div
|
|
757
|
+
className="flex items-start gap-2 rounded-og-sm px-1.5 py-1"
|
|
758
|
+
role="status"
|
|
759
|
+
aria-live="polite"
|
|
760
|
+
data-og-session-chrome-panel="steering"
|
|
761
|
+
>
|
|
762
|
+
<span className="mt-0.5 inline-flex size-5 shrink-0 items-center justify-center rounded-full bg-og-accent-soft text-og-accent">
|
|
763
|
+
{phase === "submitting" ? (
|
|
764
|
+
<Loader2Icon className="size-3 animate-og-spin" aria-hidden="true" />
|
|
765
|
+
) : (
|
|
766
|
+
<ZapIcon className="size-3" aria-hidden="true" />
|
|
767
|
+
)}
|
|
768
|
+
</span>
|
|
769
|
+
<div className="min-w-0">
|
|
770
|
+
<p className="truncate text-og-xs font-medium leading-4 text-og-fg">{text}</p>
|
|
771
|
+
<p className="mt-0.5 text-[10px] leading-4 text-og-fg-muted">
|
|
772
|
+
{phase === "submitting"
|
|
773
|
+
? "Sending your latest direction…"
|
|
774
|
+
: "Direction accepted. The agent will continue from it."}
|
|
775
|
+
</p>
|
|
776
|
+
</div>
|
|
777
|
+
</div>
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
|
|
664
781
|
function QueuePanel({
|
|
665
782
|
turns,
|
|
666
783
|
readOnly,
|
|
@@ -691,6 +808,8 @@ function QueuePanel({
|
|
|
691
808
|
data-og-session-chrome-panel="queue"
|
|
692
809
|
>
|
|
693
810
|
{turns.map((turn, index) => {
|
|
811
|
+
const presentation = queuedTurnPresentation(turn);
|
|
812
|
+
const voice = presentation.kind !== "prompt";
|
|
694
813
|
const pending = mutationFor(turn.id);
|
|
695
814
|
const beforeUp = index > 0 ? (turns[index - 1]?.id ?? null) : null;
|
|
696
815
|
const beforeDown = index < turns.length - 1 ? (turns[index + 2]?.id ?? null) : null;
|
|
@@ -703,11 +822,18 @@ function QueuePanel({
|
|
|
703
822
|
className="group flex flex-col gap-1 rounded-og-sm px-1.5 py-1 transition-colors hover:bg-[var(--og-session-chrome-row-hover)]"
|
|
704
823
|
>
|
|
705
824
|
<div className="flex items-start gap-1.5">
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
825
|
+
{voice ? (
|
|
826
|
+
<AudioLinesIcon
|
|
827
|
+
aria-hidden="true"
|
|
828
|
+
className="mt-0.5 size-3 shrink-0 text-og-accent"
|
|
829
|
+
/>
|
|
830
|
+
) : (
|
|
831
|
+
<span className="mt-px shrink-0 font-og-mono text-[10px] leading-4 text-og-fg-subtle">
|
|
832
|
+
{index + 1}
|
|
833
|
+
</span>
|
|
834
|
+
)}
|
|
709
835
|
<p className="min-w-0 flex-1 truncate text-og-xs leading-4 text-og-fg">
|
|
710
|
-
{
|
|
836
|
+
{presentation.text}
|
|
711
837
|
</p>
|
|
712
838
|
{showActions ? (
|
|
713
839
|
<div className="flex shrink-0 items-center gap-0.5 opacity-0 transition-opacity group-hover:opacity-100 focus-within:opacity-100 max-sm:opacity-100">
|
|
@@ -81,6 +81,13 @@ export const TIP_FOLLOW_VELOCITY_ARM_DEBT_PX = 72;
|
|
|
81
81
|
export const TIP_FOLLOW_VELOCITY_DECAY_MS = _t(180);
|
|
82
82
|
/** Reader-up pixels above clamp budget that count as leaving the tip. */
|
|
83
83
|
export const TIP_FOLLOW_READER_UP_EPS_PX = 2;
|
|
84
|
+
/**
|
|
85
|
+
* Browsers snap scrollTop writes to device pixels (whole px at dpr 1). A DOM
|
|
86
|
+
* position within this window of the camera's own fractional position is that
|
|
87
|
+
* quantization echo, not external motion — keep the fraction so sub-pixel
|
|
88
|
+
* settle steps accumulate. Anything larger (clamp, user, glue) re-bases.
|
|
89
|
+
*/
|
|
90
|
+
export const TIP_FOLLOW_QUANTIZE_WINDOW_PX = 1;
|
|
84
91
|
|
|
85
92
|
/** Test-only override for scrollend feature detection (`null` = probe DOM). */
|
|
86
93
|
let scrollEndSupportOverride: boolean | null = null;
|
|
@@ -122,11 +129,20 @@ export type TipFollowState = {
|
|
|
122
129
|
hotUntil: number;
|
|
123
130
|
lastTs: number;
|
|
124
131
|
lastHeight: number;
|
|
132
|
+
/** Last observed scroller clientHeight — viewport shrink heats like tip growth. */
|
|
133
|
+
lastClientHeight: number;
|
|
125
134
|
lastGrowthAt: number;
|
|
126
135
|
/** EMA of recent height growth (px/s); nudges τ / ceiling under bursts. */
|
|
127
136
|
growthVelocity: number;
|
|
128
137
|
/** Camera velocity toward tip (px/s). Smoothed — never jumps with debt. */
|
|
129
138
|
scrollVelocity: number;
|
|
139
|
+
/**
|
|
140
|
+
* Fractional camera position. The DOM quantizes scrollTop to device pixels,
|
|
141
|
+
* so settle steps under 1px would be discarded every frame if the camera
|
|
142
|
+
* re-based on the DOM — it then parks 20-50px short of the tip forever.
|
|
143
|
+
* `null` = no fraction in flight; adopt the DOM position.
|
|
144
|
+
*/
|
|
145
|
+
cameraTop: number | null;
|
|
130
146
|
};
|
|
131
147
|
|
|
132
148
|
export type TipFollowStepInput = {
|
|
@@ -150,9 +166,11 @@ export function createTipFollowState(): TipFollowState {
|
|
|
150
166
|
hotUntil: 0,
|
|
151
167
|
lastTs: 0,
|
|
152
168
|
lastHeight: 0,
|
|
169
|
+
lastClientHeight: 0,
|
|
153
170
|
lastGrowthAt: 0,
|
|
154
171
|
growthVelocity: 0,
|
|
155
172
|
scrollVelocity: 0,
|
|
173
|
+
cameraTop: null,
|
|
156
174
|
};
|
|
157
175
|
}
|
|
158
176
|
|
|
@@ -162,6 +180,7 @@ export function tipFollowCancel(state: TipFollowState): TipFollowState {
|
|
|
162
180
|
running: false,
|
|
163
181
|
lastTs: 0,
|
|
164
182
|
scrollVelocity: 0,
|
|
183
|
+
cameraTop: null,
|
|
165
184
|
};
|
|
166
185
|
}
|
|
167
186
|
|
|
@@ -196,6 +215,49 @@ export function tipFollowNoteGrowth(
|
|
|
196
215
|
};
|
|
197
216
|
}
|
|
198
217
|
|
|
218
|
+
/**
|
|
219
|
+
* Record scroller viewport height. A shrink (composer / SessionChrome /
|
|
220
|
+
* window) raises tip debt (`scrollHeight - clientHeight`) without growing
|
|
221
|
+
* content — the same debt content-growth would create. Heat the camera so
|
|
222
|
+
* catch-up does not fall into cold soft-settle (~42px/s) and leave the tip
|
|
223
|
+
* parked under the chrome.
|
|
224
|
+
*/
|
|
225
|
+
export function tipFollowNoteViewportShrink(
|
|
226
|
+
state: TipFollowState,
|
|
227
|
+
clientHeight: number,
|
|
228
|
+
now: number,
|
|
229
|
+
): TipFollowState {
|
|
230
|
+
const previous = state.lastClientHeight;
|
|
231
|
+
if (previous <= 0) {
|
|
232
|
+
return { ...state, lastClientHeight: clientHeight };
|
|
233
|
+
}
|
|
234
|
+
const shrink = previous - clientHeight;
|
|
235
|
+
if (shrink <= 0) {
|
|
236
|
+
return { ...state, lastClientHeight: clientHeight };
|
|
237
|
+
}
|
|
238
|
+
if (shrink <= TIP_FOLLOW_SHRINK_EPS_PX) {
|
|
239
|
+
// Sub-eps shrink: HOLD the baseline (deadband with memory). An animated
|
|
240
|
+
// chrome/composer ease-out delivers many ≤eps frames; adopting each one
|
|
241
|
+
// leaked the whole tail into cold debt with no glue and no heat.
|
|
242
|
+
return state;
|
|
243
|
+
}
|
|
244
|
+
const dt = state.lastGrowthAt > 0 ? now - state.lastGrowthAt : 0;
|
|
245
|
+
let growthVelocity = state.growthVelocity;
|
|
246
|
+
if (dt > 0) {
|
|
247
|
+
const instant = (shrink / dt) * 1000;
|
|
248
|
+
growthVelocity = growthVelocity * 0.65 + instant * 0.35;
|
|
249
|
+
} else {
|
|
250
|
+
growthVelocity = Math.max(growthVelocity, shrink * (1000 / TIP_FOLLOW_HOT_IDLE_MS));
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
...state,
|
|
254
|
+
lastClientHeight: clientHeight,
|
|
255
|
+
lastGrowthAt: now,
|
|
256
|
+
growthVelocity,
|
|
257
|
+
hotUntil: now + TIP_FOLLOW_HOT_IDLE_MS,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
199
261
|
/**
|
|
200
262
|
* Adaptive τ: small debt → calm; large debt / armed velocity → catch-up.
|
|
201
263
|
* While hot, floor urgency so live streams are not stuck on calm τ.
|
|
@@ -376,6 +438,29 @@ export function tipFollowCompensateShrink(
|
|
|
376
438
|
return Math.max(0, Math.min(maxScroll, scrollTop - delta));
|
|
377
439
|
}
|
|
378
440
|
|
|
441
|
+
/**
|
|
442
|
+
* Viewport shrank (SessionChrome / composer / window). maxScroll rises by ≈Δc;
|
|
443
|
+
* keep the same tip distance: scrollTop += Δc. Without this, pinned follow only
|
|
444
|
+
* sees new tip debt and soft-settles under the chrome.
|
|
445
|
+
*/
|
|
446
|
+
export function tipFollowCompensateViewportShrink(
|
|
447
|
+
scrollTop: number,
|
|
448
|
+
previousClientHeight: number,
|
|
449
|
+
nextClientHeight: number,
|
|
450
|
+
scrollHeight: number,
|
|
451
|
+
epsPx: number = TIP_FOLLOW_SHRINK_EPS_PX,
|
|
452
|
+
): number {
|
|
453
|
+
if (previousClientHeight <= 0) {
|
|
454
|
+
return scrollTop;
|
|
455
|
+
}
|
|
456
|
+
const shrink = previousClientHeight - nextClientHeight;
|
|
457
|
+
if (shrink <= epsPx) {
|
|
458
|
+
return scrollTop;
|
|
459
|
+
}
|
|
460
|
+
const maxScroll = Math.max(0, scrollHeight - nextClientHeight);
|
|
461
|
+
return Math.max(0, Math.min(maxScroll, scrollTop + shrink));
|
|
462
|
+
}
|
|
463
|
+
|
|
379
464
|
/**
|
|
380
465
|
* One camera step. Shell schedules the next rAF while `result.state.running`.
|
|
381
466
|
*/
|
|
@@ -385,12 +470,39 @@ export function tipFollowStep(
|
|
|
385
470
|
): TipFollowStepResult {
|
|
386
471
|
const { scrollTop, scrollHeight, clientHeight, now, pinned, reducedMotion, revealed } = input;
|
|
387
472
|
const previousHeight = state.lastHeight;
|
|
473
|
+
const previousClient = state.lastClientHeight;
|
|
388
474
|
const frameGrowth =
|
|
389
475
|
previousHeight > 0 && scrollHeight > previousHeight ? scrollHeight - previousHeight : 0;
|
|
390
|
-
const
|
|
476
|
+
const frameViewportShrink =
|
|
477
|
+
previousClient > 0 && clientHeight < previousClient - TIP_FOLLOW_SHRINK_EPS_PX
|
|
478
|
+
? previousClient - clientHeight
|
|
479
|
+
: 0;
|
|
480
|
+
const grew = frameGrowth > 0 || frameViewportShrink > 0;
|
|
391
481
|
let noted = tipFollowNoteGrowth(state, scrollHeight, now);
|
|
482
|
+
noted = tipFollowNoteViewportShrink(noted, clientHeight, now);
|
|
483
|
+
// Sub-device-pixel writes are floored by the engine: a DOM position within
|
|
484
|
+
// the quantization window of our own fractional camera is the echo of our
|
|
485
|
+
// last write, not external motion — keep the fraction so settle steps
|
|
486
|
+
// accumulate. Larger disagreement (clamp / reader / snap) re-bases.
|
|
487
|
+
let cameraTop = scrollTop;
|
|
488
|
+
if (
|
|
489
|
+
state.cameraTop !== null &&
|
|
490
|
+
Math.abs(state.cameraTop - scrollTop) <= TIP_FOLLOW_QUANTIZE_WINDOW_PX
|
|
491
|
+
) {
|
|
492
|
+
cameraTop = state.cameraTop;
|
|
493
|
+
}
|
|
494
|
+
// Chrome/composer dock: tip-glue before ease so debt from ΔclientHeight is
|
|
495
|
+
// not left for cold soft-settle (content still under the new bar).
|
|
496
|
+
if (pinned && frameViewportShrink > 0) {
|
|
497
|
+
cameraTop = tipFollowCompensateViewportShrink(
|
|
498
|
+
cameraTop,
|
|
499
|
+
previousClient,
|
|
500
|
+
clientHeight,
|
|
501
|
+
scrollHeight,
|
|
502
|
+
);
|
|
503
|
+
}
|
|
392
504
|
const target = targetScrollTop(scrollHeight, clientHeight);
|
|
393
|
-
const debt = target -
|
|
505
|
+
const debt = target - cameraTop;
|
|
394
506
|
const hot = now < noted.hotUntil;
|
|
395
507
|
const settling = !hot && Math.abs(debt) < TIP_FOLLOW_CATCHUP_DEBT_PX;
|
|
396
508
|
|
|
@@ -426,7 +538,7 @@ export function tipFollowStep(
|
|
|
426
538
|
|
|
427
539
|
if (Math.abs(debt) <= 0.75) {
|
|
428
540
|
return {
|
|
429
|
-
scrollTop: Math.abs(debt) > 0 ? target :
|
|
541
|
+
scrollTop: Math.abs(debt) > 0 ? target : cameraTop,
|
|
430
542
|
state: {
|
|
431
543
|
...tipFollowCancel(noted),
|
|
432
544
|
growthVelocity: hot ? noted.growthVelocity : 0,
|
|
@@ -445,13 +557,19 @@ export function tipFollowStep(
|
|
|
445
557
|
let scrollVelocity =
|
|
446
558
|
(noted.scrollVelocity ?? 0) + (desiredVel - (noted.scrollVelocity ?? 0)) * velAlpha;
|
|
447
559
|
|
|
448
|
-
const maxStep = tipFollowMaxStepPx(
|
|
560
|
+
const maxStep = tipFollowMaxStepPx(
|
|
561
|
+
debt,
|
|
562
|
+
noted.growthVelocity,
|
|
563
|
+
dt,
|
|
564
|
+
frameGrowth + frameViewportShrink,
|
|
565
|
+
hot,
|
|
566
|
+
);
|
|
449
567
|
const maxVel = maxStep / dtSec;
|
|
450
568
|
if (Math.abs(scrollVelocity) > maxVel) {
|
|
451
569
|
scrollVelocity = Math.sign(scrollVelocity) * maxVel;
|
|
452
570
|
}
|
|
453
571
|
|
|
454
|
-
let next =
|
|
572
|
+
let next = cameraTop + scrollVelocity * dtSec;
|
|
455
573
|
// Never overshoot the tip — stop and kill velocity on arrival.
|
|
456
574
|
if (debt > 0 && next >= target - 0.35) {
|
|
457
575
|
next = target;
|
|
@@ -462,13 +580,16 @@ export function tipFollowStep(
|
|
|
462
580
|
}
|
|
463
581
|
next = Math.max(0, next);
|
|
464
582
|
|
|
583
|
+
const running = Math.abs(target - next) > 0.35 || Math.abs(scrollVelocity) > 1;
|
|
465
584
|
return {
|
|
466
585
|
scrollTop: next,
|
|
467
586
|
state: {
|
|
468
587
|
...noted,
|
|
469
588
|
scrollVelocity,
|
|
470
|
-
running
|
|
589
|
+
running,
|
|
471
590
|
lastTs: now,
|
|
591
|
+
// Carry the fraction the DOM will floor away; drop it once landed.
|
|
592
|
+
cameraTop: running ? next : null,
|
|
472
593
|
},
|
|
473
594
|
};
|
|
474
595
|
}
|
package/src/composer.ts
CHANGED
|
@@ -4,6 +4,19 @@
|
|
|
4
4
|
* `import * as Composer from "@opengeni/react/composer"`.
|
|
5
5
|
*/
|
|
6
6
|
export { OPEN_WORKSTREAM_CONTROL_EVENT } from "./workstream-control-event";
|
|
7
|
+
export {
|
|
8
|
+
BillingClassMark,
|
|
9
|
+
ModelPolicyPicker,
|
|
10
|
+
ModelPolicyPickerMenu,
|
|
11
|
+
PickerAnimatedPage,
|
|
12
|
+
PickerBackHeader,
|
|
13
|
+
PickerNavRow,
|
|
14
|
+
defaultModelPolicyPickerMessages,
|
|
15
|
+
} from "./components/model-policy-picker";
|
|
16
|
+
export type {
|
|
17
|
+
ModelPolicyPickerMessages,
|
|
18
|
+
ModelPolicyPickerProps,
|
|
19
|
+
} from "./components/model-policy-picker";
|
|
7
20
|
export {
|
|
8
21
|
Actions,
|
|
9
22
|
AttachButton,
|