@opengeni/react 0.25.0 → 0.25.5
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/dist/{chunk-AVPU5PMC.js → chunk-F4CFWKVI.js} +332 -64
- package/dist/chunk-F4CFWKVI.js.map +1 -0
- package/dist/chunk-GQN2QIR2.js +3767 -0
- package/dist/chunk-GQN2QIR2.js.map +1 -0
- package/dist/{chunk-I3BJZIG5.js → chunk-HIWPQYWI.js} +2 -120
- package/dist/chunk-HIWPQYWI.js.map +1 -0
- package/dist/chunk-M7X4JZOD.js +121 -0
- package/dist/chunk-M7X4JZOD.js.map +1 -0
- package/dist/{chunk-SHOFILHJ.js → chunk-MP6237JB.js} +570 -984
- package/dist/chunk-MP6237JB.js.map +1 -0
- package/dist/chunk-OZDLELJQ.js +937 -0
- package/dist/chunk-OZDLELJQ.js.map +1 -0
- package/dist/{chunk-RDPDU4TA.js → chunk-TMH6HZWF.js} +3 -3
- package/dist/{chunk-6XUS5VFM.js → chunk-YU5PGUK7.js} +6 -4
- package/dist/{chunk-6XUS5VFM.js.map → chunk-YU5PGUK7.js.map} +1 -1
- package/dist/{composer-BXb0Q1HF.d.ts → composer-DoC4veX1.d.ts} +4 -75
- package/dist/composer.d.ts +2 -1
- package/dist/composer.js +4 -3
- package/dist/index.d.ts +14 -259
- package/dist/index.js +1016 -5140
- package/dist/index.js.map +1 -1
- package/dist/machines.js +3 -2
- package/dist/session-Du_FsrZ1.d.ts +264 -0
- package/dist/session-ui-BqQDH7YV.d.ts +183 -0
- package/dist/session-ui.d.ts +7 -0
- package/dist/session-ui.js +17 -0
- package/dist/session-ui.js.map +1 -0
- package/dist/session.d.ts +3 -1
- package/dist/session.js +26 -9
- package/dist/use-file-attachments-C0kpHrs9.d.ts +76 -0
- package/dist/{session-BEvtFWhe.d.ts → use-turn-queue-3rkJwjFv.d.ts} +3 -185
- package/package.json +6 -2
- package/src/components/message-timeline.tsx +92 -52
- package/src/hooks/use-composer.ts +456 -69
- package/src/hooks/use-file-attachments.ts +1 -1
- package/src/hooks/use-goal.ts +1 -1
- package/src/hooks/use-session-events.ts +5 -0
- package/src/hooks/use-session-lineage.ts +1 -1
- package/src/hooks/use-session.ts +9 -6
- package/src/session-ui.ts +13 -0
- package/src/session.ts +15 -0
- package/src/timeline/activity-rail.tsx +1 -1
- package/dist/chunk-AVPU5PMC.js.map +0 -1
- package/dist/chunk-I3BJZIG5.js.map +0 -1
- package/dist/chunk-SHOFILHJ.js.map +0 -1
- /package/dist/{chunk-RDPDU4TA.js.map → chunk-TMH6HZWF.js.map} +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { ToolAuthNeededPayload, ResourceRef, ToolRef, SessionStatus, SessionQueueSnapshot, SessionTurn, EffectiveSessionControl, ComposerDraft, SessionEvent } from '@opengeni/sdk';
|
|
2
|
+
import { E as EmbeddedSessionClientOverride } from './session-context-rgrfElMl.js';
|
|
3
3
|
import { S as SessionEventFeedOptions } from './use-composer-BnEzheSk.js';
|
|
4
4
|
|
|
5
5
|
type UserMessageItem = {
|
|
@@ -249,70 +249,6 @@ type TimelineGroup = {
|
|
|
249
249
|
groups: TimelineGroup[];
|
|
250
250
|
};
|
|
251
251
|
|
|
252
|
-
type SessionEventsConnectionState = StreamConnectionState | "idle" | "ended" | "error";
|
|
253
|
-
type UseSessionEventsOptions = EmbeddedSessionClientOverride & {
|
|
254
|
-
/** Resume after this sequence (exclusive). Nonzero keeps full replay/resume semantics. */
|
|
255
|
-
after?: number | undefined;
|
|
256
|
-
/** Load a bounded tail by default, or opt back into full replay from `after`. */
|
|
257
|
-
replay?: "windowed" | "full" | undefined;
|
|
258
|
-
/** Pause the stream without unmounting (e.g. hidden tab). Defaults to true. */
|
|
259
|
-
enabled?: boolean | undefined;
|
|
260
|
-
};
|
|
261
|
-
type UseSessionEventsResult = {
|
|
262
|
-
/** Replayed + live events, ordered by sequence, no gaps, no duplicates. */
|
|
263
|
-
events: SessionEvent[];
|
|
264
|
-
/** Projected, renderable timeline (memoized over `events`). */
|
|
265
|
-
timeline: TimelineItem[];
|
|
266
|
-
/** Latest session status observed in the event log, if any. */
|
|
267
|
-
sessionStatus: SessionStatus | null;
|
|
268
|
-
connectionState: SessionEventsConnectionState;
|
|
269
|
-
/** Highest sequence seen so far (0 before the first event). */
|
|
270
|
-
lastSequence: number;
|
|
271
|
-
/** Exact serialized bytes retained in the current browser event window. */
|
|
272
|
-
windowBytes: number;
|
|
273
|
-
/** Whether older delivered events were evicted from the browser window. */
|
|
274
|
-
windowTruncated: boolean;
|
|
275
|
-
/** True until the initial tail window has been applied (windowed mode). */
|
|
276
|
-
initialLoading: boolean;
|
|
277
|
-
/** Whether older durable events are available before the current window. */
|
|
278
|
-
hasOlder: boolean;
|
|
279
|
-
/** True while an older window is being fetched. */
|
|
280
|
-
loadingOlder: boolean;
|
|
281
|
-
/** Prepend an older density-bounded window; resolves true when more remain. */
|
|
282
|
-
loadOlder: () => Promise<boolean>;
|
|
283
|
-
error: Error | null;
|
|
284
|
-
};
|
|
285
|
-
declare const SESSION_EVENT_BROWSER_MAX_BYTES: number;
|
|
286
|
-
declare const SESSION_EVENT_BROWSER_MAX_COUNT = 10000;
|
|
287
|
-
declare const SESSION_EVENT_BROWSER_SINGLE_EVENT_MAX_BYTES: number;
|
|
288
|
-
declare const SESSION_EVENT_BROWSER_PENDING_MAX_BYTES: number;
|
|
289
|
-
declare const SESSION_EVENT_BROWSER_PENDING_MAX_COUNT = 256;
|
|
290
|
-
type BrowserSessionEventWindow = {
|
|
291
|
-
events: SessionEvent[];
|
|
292
|
-
bytes: number;
|
|
293
|
-
truncated: boolean;
|
|
294
|
-
};
|
|
295
|
-
/**
|
|
296
|
-
* Live-stream a session's event log with replay-by-sequence, reconnect, and
|
|
297
|
-
* batched React updates. Fresh loads default to a bounded tail window; pass
|
|
298
|
-
* `replay: "full"` or a nonzero `after` for the previous full replay path.
|
|
299
|
-
*/
|
|
300
|
-
declare function useSessionEvents(sessionId: string | null | undefined, options?: UseSessionEventsOptions): UseSessionEventsResult;
|
|
301
|
-
/**
|
|
302
|
-
* Keep one direction-aware count+byte-bounded browser window. Live/default
|
|
303
|
-
* accumulation retains the newest suffix; backward paging retains the oldest
|
|
304
|
-
* prefix so newly fetched history cannot be immediately evicted. This is
|
|
305
|
-
* deliberately separate from durable history and transport paging: when a
|
|
306
|
-
* backward page evicts the live tail, the hook reconnects from the retained
|
|
307
|
-
* high-water mark while preserving the highest-ever-observed sequence
|
|
308
|
-
* separately. The source event remains durable in PostgreSQL throughout.
|
|
309
|
-
*/
|
|
310
|
-
declare function boundBrowserSessionEventWindow(events: readonly SessionEvent[], options?: {
|
|
311
|
-
maxBytes?: number;
|
|
312
|
-
maxCount?: number;
|
|
313
|
-
direction?: "newest" | "oldest";
|
|
314
|
-
}): BrowserSessionEventWindow;
|
|
315
|
-
|
|
316
252
|
/** Events that can change the authoritative prompt queue or effective control. */
|
|
317
253
|
declare function isTurnQueueEvent(event: Pick<SessionEvent, "type">): boolean;
|
|
318
254
|
type QueueMutationKind = "move" | "edit" | "steer" | "delete";
|
|
@@ -351,122 +287,4 @@ type UseTurnQueueResult = {
|
|
|
351
287
|
*/
|
|
352
288
|
declare function useTurnQueue(sessionId: string | null | undefined, options?: UseTurnQueueOptions): UseTurnQueueResult;
|
|
353
289
|
|
|
354
|
-
type
|
|
355
|
-
type UseSessionControlResult = {
|
|
356
|
-
pause: (reason?: string) => Promise<SessionControlResponse | null>;
|
|
357
|
-
resume: (reason?: string) => Promise<SessionControlResponse | null>;
|
|
358
|
-
controlling: boolean;
|
|
359
|
-
/** Approve a pending `requires_action` approval. */
|
|
360
|
-
approve: (approvalId: string, message?: string) => Promise<SessionEvent | null>;
|
|
361
|
-
/** Reject a pending `requires_action` approval. */
|
|
362
|
-
reject: (approvalId: string, message?: string) => Promise<SessionEvent | null>;
|
|
363
|
-
/** True while an approval decision is in flight. */
|
|
364
|
-
responding: boolean;
|
|
365
|
-
error: Error | null;
|
|
366
|
-
clearError: () => void;
|
|
367
|
-
};
|
|
368
|
-
/**
|
|
369
|
-
* Session pause/resume and approval decisions. Pair with
|
|
370
|
-
* `useSessionEvents` (for `session.requiresAction` payloads carrying the
|
|
371
|
-
* `approvalId`) to render an approval bar.
|
|
372
|
-
*/
|
|
373
|
-
declare function useSessionControl(sessionId: string | null | undefined, options?: UseSessionControlOptions): UseSessionControlResult;
|
|
374
|
-
|
|
375
|
-
declare function isSessionMcpApprovalPolicyEvent(event: Pick<SessionEvent, "type" | "payload">, serverId?: string): boolean;
|
|
376
|
-
type UseSessionMcpApprovalPolicyOptions = EmbeddedSessionMcpApprovalPolicyClientOverride & SessionEventFeedOptions & {
|
|
377
|
-
/** Optional safety-net polling (ms). Off by default; policy events drive refreshes. */
|
|
378
|
-
pollIntervalMs?: number | undefined;
|
|
379
|
-
};
|
|
380
|
-
type UseSessionMcpApprovalPolicyResult = {
|
|
381
|
-
server: SessionMcpServerMetadata | null;
|
|
382
|
-
policy: SessionMcpApprovalPolicy | null;
|
|
383
|
-
loading: boolean;
|
|
384
|
-
error: Error | null;
|
|
385
|
-
refresh: () => Promise<void>;
|
|
386
|
-
update: (policy: SessionMcpApprovalPolicy) => Promise<UpdateSessionMcpApprovalPolicyResponse | null>;
|
|
387
|
-
updating: boolean;
|
|
388
|
-
clearError: () => void;
|
|
389
|
-
};
|
|
390
|
-
/**
|
|
391
|
-
* Read and update one existing session MCP server's approval policy. Updates
|
|
392
|
-
* become effective for the next claimed attempt; current work is unchanged.
|
|
393
|
-
*/
|
|
394
|
-
declare function useSessionMcpApprovalPolicy(sessionId: string | null | undefined, serverId: string | null | undefined, options?: UseSessionMcpApprovalPolicyOptions): UseSessionMcpApprovalPolicyResult;
|
|
395
|
-
|
|
396
|
-
type PendingApproval = {
|
|
397
|
-
/** The id to send back via `user.approvalDecision` (`approvalId`). */
|
|
398
|
-
id: string;
|
|
399
|
-
/** Tool/function name awaiting the decision. */
|
|
400
|
-
name: string;
|
|
401
|
-
arguments?: unknown;
|
|
402
|
-
/** The raw approval entry from the `session.requiresAction` payload. */
|
|
403
|
-
raw?: unknown;
|
|
404
|
-
};
|
|
405
|
-
/** The approvals carried by one `session.requiresAction` payload. */
|
|
406
|
-
declare function approvalsFromRequiresAction(payload: unknown): PendingApproval[];
|
|
407
|
-
/** The approvals still awaiting a decision after replaying `events` in order. */
|
|
408
|
-
declare function projectPendingApprovals(events: SessionEvent[]): PendingApproval[];
|
|
409
|
-
|
|
410
|
-
/** Minimal actionable request shape available from the durable event log. */
|
|
411
|
-
type PendingHumanInputRequest = {
|
|
412
|
-
id: string;
|
|
413
|
-
turnId: string | null;
|
|
414
|
-
questions: HumanInputQuestion[];
|
|
415
|
-
allowSkip: boolean;
|
|
416
|
-
expiresAt: string | null;
|
|
417
|
-
};
|
|
418
|
-
/** Parse one generic `session.humanInput.requested` event defensively. */
|
|
419
|
-
declare function humanInputRequestFromEvent(event: Pick<SessionEvent, "type" | "payload" | "turnId">): PendingHumanInputRequest | null;
|
|
420
|
-
/**
|
|
421
|
-
* Fold a durable event log into the structured requests that are actionable
|
|
422
|
-
* now. Responses remove one request; a terminal owning turn clears any
|
|
423
|
-
* unresolved requests that died with it. Replaying history cannot resurrect a
|
|
424
|
-
* previously answered card.
|
|
425
|
-
*/
|
|
426
|
-
declare function projectPendingHumanInputRequests(events: SessionEvent[]): PendingHumanInputRequest[];
|
|
427
|
-
|
|
428
|
-
/** Events that can create, settle, or invalidate an actionable request. */
|
|
429
|
-
declare function isHumanInputEvent(event: Pick<SessionEvent, "type">): boolean;
|
|
430
|
-
type UseHumanInputRequestsOptions = EmbeddedHumanInputClientOverride & SessionEventFeedOptions & {
|
|
431
|
-
/** Optional safety-net polling. Durable events drive refresh by default. */
|
|
432
|
-
pollIntervalMs?: number | undefined;
|
|
433
|
-
};
|
|
434
|
-
type UseHumanInputRequestsResult = {
|
|
435
|
-
requests: SessionHumanInputRequest[];
|
|
436
|
-
loading: boolean;
|
|
437
|
-
error: Error | null;
|
|
438
|
-
refresh: () => Promise<void>;
|
|
439
|
-
respond: (requestId: string, response: SubmitHumanInputResponseRequest) => Promise<SessionEvent | null>;
|
|
440
|
-
respondingRequestId: string | null;
|
|
441
|
-
mutationError: Error | null;
|
|
442
|
-
clearMutationError: () => void;
|
|
443
|
-
};
|
|
444
|
-
/**
|
|
445
|
-
* Authoritative structured-human-input hook. The server's pending-request
|
|
446
|
-
* table is the read model; events only trigger reconciliation. This lets an
|
|
447
|
-
* embedded host use the same primitive with a shared event feed or its own SDK
|
|
448
|
-
* proxy without rebuilding request lifecycle logic.
|
|
449
|
-
*/
|
|
450
|
-
declare function useHumanInputRequests(sessionId: string | null | undefined, options?: UseHumanInputRequestsOptions): UseHumanInputRequestsResult;
|
|
451
|
-
|
|
452
|
-
/** Readable label for a tool call, without leaking an MCP server prefix. */
|
|
453
|
-
declare function toolDisplayName(name: string): string;
|
|
454
|
-
declare function buildTimeline(events: SessionEvent[]): TimelineItem[];
|
|
455
|
-
/**
|
|
456
|
-
* Whether the session's most recent turn ended in credit exhaustion — the
|
|
457
|
-
* terminal credit state apps key their "add credits" affordances on. Derived
|
|
458
|
-
* from the LAST turn-end event (completed/failed/cancelled): a later turn that
|
|
459
|
-
* settles any other way (someone topped up and kept working) clears it.
|
|
460
|
-
*/
|
|
461
|
-
declare function creditExhaustedFromEvents(events: SessionEvent[]): boolean;
|
|
462
|
-
/** The latest session status carried in the event log, if any. */
|
|
463
|
-
declare function sessionStatusFromEvents(events: SessionEvent[]): SessionStatus | null;
|
|
464
|
-
declare function groupTimeline(items: TimelineItem[]): TimelineGroup[];
|
|
465
|
-
/**
|
|
466
|
-
* Find a session id in orchestration tool arguments or output. Handles raw
|
|
467
|
-
* objects, JSON strings, and MCP tool results (`{ content: [{ type: "text",
|
|
468
|
-
* text: "{...}" }], structuredContent? }`).
|
|
469
|
-
*/
|
|
470
|
-
declare function extractSessionRef(value: unknown, depth?: number): string | null;
|
|
471
|
-
|
|
472
|
-
export { useTurnQueue as $, type ActivityItem as A, type BrowserSessionEventWindow as B, buildTimeline as C, creditExhaustedFromEvents as D, extractSessionRef as E, groupTimeline as F, type GoalItem as G, humanInputRequestFromEvent as H, isHumanInputEvent as I, isSessionMcpApprovalPolicyEvent as J, isTurnQueueEvent as K, projectPendingApprovals as L, projectPendingHumanInputRequests as M, type NoticeItem as N, sessionStatusFromEvents as O, type PendingApproval as P, type QueueMutationKind as Q, type ReasoningItem as R, SESSION_EVENT_BROWSER_MAX_BYTES as S, type ToolCallItem as T, type UseTurnQueueResult as U, toolDisplayName as V, type WorkerItem as W, useHumanInputRequests as X, useSessionControl as Y, useSessionEvents as Z, useSessionMcpApprovalPolicy as _, type TurnOutcome as a, type MemoryItem as a0, type WorkerCompletionItem as a1, type TimelineItem as b, type AgentMessageItem as c, type UserMessageItem as d, type AuthNeededItem as e, type PendingHumanInputRequest as f, SESSION_EVENT_BROWSER_MAX_COUNT as g, SESSION_EVENT_BROWSER_PENDING_MAX_BYTES as h, SESSION_EVENT_BROWSER_PENDING_MAX_COUNT as i, SESSION_EVENT_BROWSER_SINGLE_EVENT_MAX_BYTES as j, type SandboxItem as k, type SessionEventsConnectionState as l, type SessionStatusItem as m, type TimelineGroup as n, type TurnEndItem as o, type UseHumanInputRequestsOptions as p, type UseHumanInputRequestsResult as q, type UseSessionControlOptions as r, type UseSessionControlResult as s, type UseSessionEventsOptions as t, type UseSessionEventsResult as u, type UseSessionMcpApprovalPolicyOptions as v, type UseSessionMcpApprovalPolicyResult as w, type UseTurnQueueOptions as x, approvalsFromRequiresAction as y, boundBrowserSessionEventWindow as z };
|
|
290
|
+
export { type ActivityItem as A, type GoalItem as G, type MemoryItem as M, type NoticeItem as N, type QueueMutationKind as Q, type ReasoningItem as R, type SandboxItem as S, type TurnOutcome as T, type UseTurnQueueOptions as U, type WorkerItem as W, type AgentMessageItem as a, type AuthNeededItem as b, type SessionStatusItem as c, type TimelineGroup as d, type TimelineItem as e, type ToolCallItem as f, type TurnEndItem as g, type UseTurnQueueResult as h, type UserMessageItem as i, isTurnQueueEvent as j, type WorkerCompletionItem as k, useTurnQueue as u };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengeni/react",
|
|
3
|
-
"version": "0.25.
|
|
3
|
+
"version": "0.25.5",
|
|
4
4
|
"description": "React hooks and styled components for OpenGeni: live session streaming, chat composer, message timeline, session status, and fleet views — token-themed (CSS variables), dark-first, built on Tailwind v4 + Radix + Motion.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -31,6 +31,10 @@
|
|
|
31
31
|
"types": "./dist/session.d.ts",
|
|
32
32
|
"import": "./dist/session.js"
|
|
33
33
|
},
|
|
34
|
+
"./session-ui": {
|
|
35
|
+
"types": "./dist/session-ui.d.ts",
|
|
36
|
+
"import": "./dist/session-ui.js"
|
|
37
|
+
},
|
|
34
38
|
"./machines": {
|
|
35
39
|
"types": "./dist/machines.d.ts",
|
|
36
40
|
"import": "./dist/machines.js"
|
|
@@ -62,7 +66,7 @@
|
|
|
62
66
|
"@dnd-kit/utilities": "3.2.2",
|
|
63
67
|
"@fontsource-variable/noto-sans-arabic": "5.2.10",
|
|
64
68
|
"@fontsource-variable/noto-sans-jp": "5.2.10",
|
|
65
|
-
"@opengeni/sdk": "^0.25.
|
|
69
|
+
"@opengeni/sdk": "^0.25.5",
|
|
66
70
|
"clsx": "^2.1.1",
|
|
67
71
|
"lucide-react": "^1.8.0",
|
|
68
72
|
"motion": "^12.0.0",
|
|
@@ -150,18 +150,23 @@ export function MessageTimeline({
|
|
|
150
150
|
// structurally impossible — the reader only ever sees it already at the
|
|
151
151
|
// bottom. An empty timeline reveals immediately (there is nothing to anchor).
|
|
152
152
|
const [revealed, setRevealed] = useState(false);
|
|
153
|
-
// Our own scrollTop assignments echo back as scroll events
|
|
154
|
-
//
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
|
|
153
|
+
// Our own scrollTop assignments echo back as delayed scroll events. Keep the
|
|
154
|
+
// ordered, clamped targets rather than only the latest one: successive
|
|
155
|
+
// ResizeObserver corrections can queue several echoes before the browser
|
|
156
|
+
// dispatches them. If reader input lands at a different position, its value
|
|
157
|
+
// wins and clears the pending echoes. A matching echo must not recapture the
|
|
158
|
+
// visual anchor while progressive prepend is still mounting older rows.
|
|
159
|
+
const programmaticScrollTargetsRef = useRef<number[]>([]);
|
|
159
160
|
const assignScrollTop = useCallback((node: HTMLElement, value: number) => {
|
|
160
161
|
const previous = node.scrollTop;
|
|
161
|
-
programmaticScrollRef.current = true;
|
|
162
162
|
node.scrollTop = value;
|
|
163
163
|
if (node.scrollTop === previous) {
|
|
164
|
-
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const pending = programmaticScrollTargetsRef.current;
|
|
167
|
+
pending.push(node.scrollTop);
|
|
168
|
+
if (pending.length > 32) {
|
|
169
|
+
pending.splice(0, pending.length - 32);
|
|
165
170
|
}
|
|
166
171
|
}, []);
|
|
167
172
|
// Mirror `pinned` into a ref so the ResizeObserver callback (a stable closure)
|
|
@@ -187,9 +192,12 @@ export function MessageTimeline({
|
|
|
187
192
|
allGroups.length > 0 && (bulkActive || firstKeyChangedForBulk || mountingOlderGroups);
|
|
188
193
|
|
|
189
194
|
// Snapshot the topmost visible element and where it sits in the viewport, so a
|
|
190
|
-
// later reflow can restore it to the same spot.
|
|
191
|
-
//
|
|
192
|
-
//
|
|
195
|
+
// later reflow can restore it to the same spot. Prefer durable row wrappers
|
|
196
|
+
// over their containing group: an older activity item can merge into the
|
|
197
|
+
// existing group, leaving that outer group in place while shifting every row
|
|
198
|
+
// after the insertion. Transient chrome (the backfill sentinel and shimmer) is
|
|
199
|
+
// skipped — anchoring to a row that unmounts when the older window lands would
|
|
200
|
+
// drop the correction mid-prepend.
|
|
193
201
|
const captureAnchor = useCallback(() => {
|
|
194
202
|
const node = scrollRef.current;
|
|
195
203
|
const inner = node?.firstElementChild;
|
|
@@ -198,31 +206,63 @@ export function MessageTimeline({
|
|
|
198
206
|
return;
|
|
199
207
|
}
|
|
200
208
|
const containerTop = node.getBoundingClientRect().top;
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
+
const findVisible = (selector: string): Element | null => {
|
|
210
|
+
for (const candidate of inner.querySelectorAll(selector)) {
|
|
211
|
+
if (candidate instanceof HTMLElement && candidate.dataset.ogTimelineChrome !== undefined) {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
const rect = candidate.getBoundingClientRect();
|
|
215
|
+
if (rect.bottom > containerTop + 1 && rect.height > 0) {
|
|
216
|
+
return candidate;
|
|
217
|
+
}
|
|
209
218
|
}
|
|
219
|
+
return null;
|
|
220
|
+
};
|
|
221
|
+
const anchor =
|
|
222
|
+
findVisible("[data-og-timeline-row-anchor]") ??
|
|
223
|
+
findVisible("[data-og-timeline-group-anchor]");
|
|
224
|
+
if (anchor) {
|
|
225
|
+
const rect = anchor.getBoundingClientRect();
|
|
226
|
+
anchorRef.current = { el: anchor, top: rect.top - containerTop };
|
|
227
|
+
return;
|
|
210
228
|
}
|
|
211
229
|
anchorRef.current = null;
|
|
212
230
|
}, []);
|
|
213
231
|
|
|
214
|
-
//
|
|
232
|
+
// Restore the user-owned anchor synchronously after React commits a frame.
|
|
233
|
+
// ResizeObserver remains a late-measurement fallback, but progressive history
|
|
234
|
+
// frames must be corrected before the browser can paint an intermediate jump.
|
|
235
|
+
const restoreAnchor = useCallback(
|
|
236
|
+
(node: HTMLElement) => {
|
|
237
|
+
if (autoFollow && pinnedRef.current) {
|
|
238
|
+
assignScrollTop(node, node.scrollHeight);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const anchor = anchorRef.current;
|
|
242
|
+
if (!anchor || !anchor.el.isConnected) {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
const containerTop = node.getBoundingClientRect().top;
|
|
246
|
+
const now = anchor.el.getBoundingClientRect().top - containerTop;
|
|
247
|
+
const diff = now - anchor.top;
|
|
248
|
+
if (diff !== 0) {
|
|
249
|
+
assignScrollTop(node, node.scrollTop + diff);
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
[assignScrollTop, autoFollow],
|
|
253
|
+
);
|
|
254
|
+
|
|
215
255
|
// A LAYOUT effect so the very first paint of a freshly loaded session is
|
|
216
256
|
// already anchored at the bottom — no visible traversal down the history.
|
|
217
257
|
useLayoutEffect(() => {
|
|
218
258
|
const node = scrollRef.current;
|
|
219
|
-
if (node
|
|
220
|
-
|
|
259
|
+
if (node) {
|
|
260
|
+
restoreAnchor(node);
|
|
221
261
|
}
|
|
222
262
|
if (!revealed && groups.length > 0) {
|
|
223
263
|
setRevealed(true);
|
|
224
264
|
}
|
|
225
|
-
}, [resolvedItems, working,
|
|
265
|
+
}, [resolvedItems, working, revealed, groups.length, restoreAnchor]);
|
|
226
266
|
|
|
227
267
|
// A cleared timeline (stream identity change) re-arms the reveal so the next
|
|
228
268
|
// session also first paints at its bottom.
|
|
@@ -297,32 +337,26 @@ export function MessageTimeline({
|
|
|
297
337
|
if (!current) {
|
|
298
338
|
return;
|
|
299
339
|
}
|
|
300
|
-
|
|
301
|
-
assignScrollTop(current, current.scrollHeight);
|
|
302
|
-
} else {
|
|
303
|
-
const anchor = anchorRef.current;
|
|
304
|
-
if (anchor && anchor.el.isConnected) {
|
|
305
|
-
const containerTop = current.getBoundingClientRect().top;
|
|
306
|
-
const now = anchor.el.getBoundingClientRect().top - containerTop;
|
|
307
|
-
const diff = now - anchor.top;
|
|
308
|
-
if (diff !== 0) {
|
|
309
|
-
assignScrollTop(current, current.scrollTop + diff);
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
captureAnchor();
|
|
340
|
+
restoreAnchor(current);
|
|
314
341
|
});
|
|
315
342
|
observer.observe(inner);
|
|
316
343
|
return () => observer.disconnect();
|
|
317
|
-
}, [
|
|
344
|
+
}, [restoreAnchor]);
|
|
318
345
|
|
|
319
346
|
const onScroll = () => {
|
|
320
347
|
const node = scrollRef.current;
|
|
321
348
|
if (!node) {
|
|
322
349
|
return;
|
|
323
350
|
}
|
|
324
|
-
const
|
|
325
|
-
|
|
351
|
+
const pending = programmaticScrollTargetsRef.current;
|
|
352
|
+
const programmaticIndex = pending.findIndex((target) => Math.abs(node.scrollTop - target) <= 1);
|
|
353
|
+
const programmatic = programmaticIndex >= 0;
|
|
354
|
+
if (programmatic) {
|
|
355
|
+
pending.splice(0, programmaticIndex + 1);
|
|
356
|
+
} else {
|
|
357
|
+
// A real reader scroll supersedes every delayed programmatic echo.
|
|
358
|
+
pending.length = 0;
|
|
359
|
+
}
|
|
326
360
|
const nextPinned = node.scrollHeight - node.scrollTop - node.clientHeight < 48;
|
|
327
361
|
// Echoes of our own assignments may PIN but never UNPIN — only the reader
|
|
328
362
|
// scrolling away releases the bottom-follow.
|
|
@@ -330,7 +364,12 @@ export function MessageTimeline({
|
|
|
330
364
|
pinnedRef.current = nextPinned;
|
|
331
365
|
setPinned(nextPinned);
|
|
332
366
|
}
|
|
333
|
-
|
|
367
|
+
// The assignment site already captured the corrected anchor. Recapturing
|
|
368
|
+
// it from a delayed echo can observe the next prepended row before that
|
|
369
|
+
// row's ResizeObserver correction and make the viewport walk up the page.
|
|
370
|
+
if (!programmatic) {
|
|
371
|
+
captureAnchor();
|
|
372
|
+
}
|
|
334
373
|
};
|
|
335
374
|
|
|
336
375
|
return (
|
|
@@ -366,17 +405,18 @@ export function MessageTimeline({
|
|
|
366
405
|
</div>
|
|
367
406
|
) : null}
|
|
368
407
|
{groups.map(({ group, key }, index) => (
|
|
369
|
-
<
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
408
|
+
<div key={key} data-og-timeline-group-anchor="">
|
|
409
|
+
<TimelineGroupView
|
|
410
|
+
group={group}
|
|
411
|
+
renderMessageText={renderMessageText}
|
|
412
|
+
onOpenSession={onOpenSession}
|
|
413
|
+
onMemoryClick={onMemoryClick}
|
|
414
|
+
onReconnect={onReconnect}
|
|
415
|
+
resolveProviderLogo={resolveProviderLogo}
|
|
416
|
+
toolRegistry={toolRegistry}
|
|
417
|
+
foldLiveCluster={isAgentProgress(groups[index + 1]?.group)}
|
|
418
|
+
/>
|
|
419
|
+
</div>
|
|
380
420
|
))}
|
|
381
421
|
{working ? (
|
|
382
422
|
<div className="flex items-center gap-2 text-sm">
|