@opengeni/react 0.20.0 → 0.23.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/dist/{chunk-6BXBGJ3B.js → chunk-6XUS5VFM.js} +2 -2
- package/dist/{chunk-LAZ2A743.js → chunk-CATATY77.js} +3 -3
- package/dist/{chunk-HHFA4AFX.js → chunk-I3BJZIG5.js} +15 -1
- package/dist/chunk-I3BJZIG5.js.map +1 -0
- package/dist/{chunk-6UT5OC3P.js → chunk-UAHPKLB6.js} +113 -9
- package/dist/chunk-UAHPKLB6.js.map +1 -0
- package/dist/{chunk-MRSECXRP.js → chunk-UQS7OQDE.js} +82 -54
- package/dist/chunk-UQS7OQDE.js.map +1 -0
- package/dist/{chunk-EBTT3MYN.js → chunk-YDNWMKXO.js} +11 -1
- package/dist/chunk-YDNWMKXO.js.map +1 -0
- package/dist/{composer-DvUaa5ki.d.ts → composer-CVm5uoUI.d.ts} +2 -2
- package/dist/composer.d.ts +3 -3
- package/dist/composer.js +3 -3
- package/dist/index.d.ts +8 -8
- package/dist/index.js +140 -26
- package/dist/index.js.map +1 -1
- package/dist/machines.d.ts +1 -1
- package/dist/machines.js +2 -2
- package/dist/{queue-surface-implementation-NQMV2ML3.js → queue-surface-implementation-3UW3MIIR.js} +9 -9
- package/dist/queue-surface-implementation-3UW3MIIR.js.map +1 -0
- package/dist/{session-Gd4iVPYw.d.ts → session-Db00TRwl.d.ts} +32 -11
- package/dist/{session-context-DIFFlXKc.d.ts → session-context-rgrfElMl.d.ts} +7 -1
- package/dist/session.d.ts +3 -3
- package/dist/session.js +7 -3
- package/dist/{use-composer-BCfm4mzX.d.ts → use-composer-By1wn3MM.d.ts} +5 -18
- package/package.json +2 -2
- package/src/client.ts +4 -0
- package/src/components/message-timeline.tsx +195 -23
- package/src/components/queue-draft-policy.ts +18 -0
- package/src/components/queue-surface-implementation.tsx +6 -10
- package/src/components/queue-surface.tsx +1 -0
- package/src/hooks/use-composer.ts +128 -78
- package/src/hooks/use-session-mcp-approval-policy.ts +161 -0
- package/src/index.ts +9 -0
- package/src/session-context.ts +25 -0
- package/src/session.ts +10 -0
- package/src/timeline/types.ts +8 -15
- package/dist/chunk-6UT5OC3P.js.map +0 -1
- package/dist/chunk-EBTT3MYN.js.map +0 -1
- package/dist/chunk-HHFA4AFX.js.map +0 -1
- package/dist/chunk-MRSECXRP.js.map +0 -1
- package/dist/queue-surface-implementation-NQMV2ML3.js.map +0 -1
- /package/dist/{chunk-6BXBGJ3B.js.map → chunk-6XUS5VFM.js.map} +0 -0
- /package/dist/{chunk-LAZ2A743.js.map → chunk-CATATY77.js.map} +0 -0
|
@@ -20,6 +20,7 @@ import type { ComponentType } from "react";
|
|
|
20
20
|
import { AnimatePresence, motion } from "motion/react";
|
|
21
21
|
import { Collapsible } from "radix-ui";
|
|
22
22
|
import {
|
|
23
|
+
memo,
|
|
23
24
|
useCallback,
|
|
24
25
|
useEffect,
|
|
25
26
|
useLayoutEffect,
|
|
@@ -108,6 +109,8 @@ export type MessageTimelineProps = {
|
|
|
108
109
|
className?: string | undefined;
|
|
109
110
|
};
|
|
110
111
|
|
|
112
|
+
const INITIAL_MOUNTED_GROUPS = 1;
|
|
113
|
+
|
|
111
114
|
/**
|
|
112
115
|
* The session timeline: chat messages with streaming deltas, collapsed
|
|
113
116
|
* activity clusters (reasoning, tool calls, sandbox work), spawned-worker
|
|
@@ -132,8 +135,10 @@ export function MessageTimeline({
|
|
|
132
135
|
className,
|
|
133
136
|
}: MessageTimelineProps) {
|
|
134
137
|
const resolvedItems = useMemo(() => items ?? buildTimeline(events ?? []), [items, events]);
|
|
135
|
-
const
|
|
136
|
-
const
|
|
138
|
+
const allGroups = useMemo(() => groupTimeline(resolvedItems), [resolvedItems]);
|
|
139
|
+
const keyedGroups = useStableTimelineGroupKeys(allGroups);
|
|
140
|
+
const { mountedGroups: groups, mountingOlderGroups } = useProgressivelyMountedGroups(keyedGroups);
|
|
141
|
+
const firstGroupKey = allGroups[0] ? timelineGroupKey(allGroups[0]) : null;
|
|
137
142
|
|
|
138
143
|
const scrollRef = useRef<HTMLDivElement | null>(null);
|
|
139
144
|
const topSentinelRef = useRef<HTMLDivElement | null>(null);
|
|
@@ -178,7 +183,8 @@ export function MessageTimeline({
|
|
|
178
183
|
const firstKeyChangedForBulk =
|
|
179
184
|
previousBulkFirstKeyRef.current !== undefined &&
|
|
180
185
|
previousBulkFirstKeyRef.current !== firstGroupKey;
|
|
181
|
-
const bulkRender =
|
|
186
|
+
const bulkRender =
|
|
187
|
+
allGroups.length > 0 && (bulkActive || firstKeyChangedForBulk || mountingOlderGroups);
|
|
182
188
|
|
|
183
189
|
// Snapshot the topmost visible element and where it sits in the viewport, so a
|
|
184
190
|
// later reflow can restore it to the same spot. Transient chrome (the backfill
|
|
@@ -221,10 +227,10 @@ export function MessageTimeline({
|
|
|
221
227
|
// A cleared timeline (stream identity change) re-arms the reveal so the next
|
|
222
228
|
// session also first paints at its bottom.
|
|
223
229
|
useLayoutEffect(() => {
|
|
224
|
-
if (
|
|
230
|
+
if (allGroups.length === 0 && revealed) {
|
|
225
231
|
setRevealed(false);
|
|
226
232
|
}
|
|
227
|
-
}, [
|
|
233
|
+
}, [allGroups.length, revealed]);
|
|
228
234
|
|
|
229
235
|
// Clear the bulk-paint marker a frame after it renders, so rows appended
|
|
230
236
|
// live (streams, new turns) animate exactly as before.
|
|
@@ -234,9 +240,15 @@ export function MessageTimeline({
|
|
|
234
240
|
return;
|
|
235
241
|
}
|
|
236
242
|
setBulkActive(true);
|
|
243
|
+
// Initial tails and prepended history mount from newest to oldest across
|
|
244
|
+
// frames. Keep every row born during that bulk window animation-free; only
|
|
245
|
+
// clear the marker after the authoritative group list is fully mounted.
|
|
246
|
+
if (mountingOlderGroups) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
237
249
|
const frame = requestFrame(() => setBulkActive(false));
|
|
238
250
|
return () => cancelFrame(frame);
|
|
239
|
-
}, [bulkRender, firstGroupKey]);
|
|
251
|
+
}, [bulkRender, firstGroupKey, mountingOlderGroups]);
|
|
240
252
|
|
|
241
253
|
// Prefetch older history well before the reader reaches the top: the
|
|
242
254
|
// sentinel sits above the first group and trips 1600px early, so backfill
|
|
@@ -248,6 +260,7 @@ export function MessageTimeline({
|
|
|
248
260
|
if (
|
|
249
261
|
!root ||
|
|
250
262
|
!target ||
|
|
263
|
+
mountingOlderGroups ||
|
|
251
264
|
!hasOlder ||
|
|
252
265
|
loadingOlder ||
|
|
253
266
|
!onLoadOlder ||
|
|
@@ -265,7 +278,7 @@ export function MessageTimeline({
|
|
|
265
278
|
);
|
|
266
279
|
observer.observe(target);
|
|
267
280
|
return () => observer.disconnect();
|
|
268
|
-
}, [hasOlder, loadingOlder, onLoadOlder, firstGroupKey]);
|
|
281
|
+
}, [hasOlder, loadingOlder, onLoadOlder, firstGroupKey, mountingOlderGroups]);
|
|
269
282
|
|
|
270
283
|
// Scroll anchoring: when the content reflows (a fold expands/collapses, a
|
|
271
284
|
// stream appends), keep following the bottom if pinned; otherwise pin the
|
|
@@ -338,7 +351,7 @@ export function MessageTimeline({
|
|
|
338
351
|
<p className="py-10 text-center text-sm text-og-fg-subtle">No activity yet.</p>
|
|
339
352
|
))
|
|
340
353
|
: null}
|
|
341
|
-
{hasOlder ? (
|
|
354
|
+
{hasOlder && !mountingOlderGroups ? (
|
|
342
355
|
<div
|
|
343
356
|
ref={topSentinelRef}
|
|
344
357
|
data-og-top-sentinel=""
|
|
@@ -352,9 +365,9 @@ export function MessageTimeline({
|
|
|
352
365
|
<span className="og-shimmer-text font-medium">Loading earlier activity…</span>
|
|
353
366
|
</div>
|
|
354
367
|
) : null}
|
|
355
|
-
{groups.map((group, index) => (
|
|
368
|
+
{groups.map(({ group, key }, index) => (
|
|
356
369
|
<TimelineGroupView
|
|
357
|
-
key={
|
|
370
|
+
key={key}
|
|
358
371
|
group={group}
|
|
359
372
|
renderMessageText={renderMessageText}
|
|
360
373
|
onOpenSession={onOpenSession}
|
|
@@ -362,7 +375,7 @@ export function MessageTimeline({
|
|
|
362
375
|
onReconnect={onReconnect}
|
|
363
376
|
resolveProviderLogo={resolveProviderLogo}
|
|
364
377
|
toolRegistry={toolRegistry}
|
|
365
|
-
foldLiveCluster={isAgentProgress(groups[index + 1])}
|
|
378
|
+
foldLiveCluster={isAgentProgress(groups[index + 1]?.group)}
|
|
366
379
|
/>
|
|
367
380
|
))}
|
|
368
381
|
{working ? (
|
|
@@ -406,6 +419,141 @@ export function MessageTimeline({
|
|
|
406
419
|
);
|
|
407
420
|
}
|
|
408
421
|
|
|
422
|
+
type MountedGroupWindow = {
|
|
423
|
+
groupKeys: string[];
|
|
424
|
+
visibleStart: number;
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
type KeyedTimelineGroup = {
|
|
428
|
+
group: TimelineGroup;
|
|
429
|
+
key: string;
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Projection can legitimately change a group's content-derived key while
|
|
434
|
+
* retaining its existing rows. The common pagination case is an older activity
|
|
435
|
+
* item merging into the first activity group; live appends grow the same group
|
|
436
|
+
* from the other side. Match the new authoritative groups to the previous
|
|
437
|
+
* committed groups by their durable item IDs so both the React key and the
|
|
438
|
+
* progressive-window anchor survive either change.
|
|
439
|
+
*/
|
|
440
|
+
function useStableTimelineGroupKeys(allGroups: TimelineGroup[]): KeyedTimelineGroup[] {
|
|
441
|
+
const previousRef = useRef<KeyedTimelineGroup[]>([]);
|
|
442
|
+
const keyedGroups = useMemo(() => {
|
|
443
|
+
const previousByItemId = new Map<string, KeyedTimelineGroup>();
|
|
444
|
+
for (const previous of previousRef.current) {
|
|
445
|
+
for (const itemId of timelineGroupItemIds(previous.group)) {
|
|
446
|
+
previousByItemId.set(itemId, previous);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const usedKeys = new Set<string>();
|
|
451
|
+
return allGroups.map((group, index) => {
|
|
452
|
+
const itemIds = timelineGroupItemIds(group);
|
|
453
|
+
let retainedKey: string | undefined;
|
|
454
|
+
for (const itemId of itemIds) {
|
|
455
|
+
const previous = previousByItemId.get(itemId);
|
|
456
|
+
if (previous && !usedKeys.has(previous.key)) {
|
|
457
|
+
retainedKey = previous.key;
|
|
458
|
+
break;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const canonicalKey = timelineGroupKey(group);
|
|
463
|
+
let key = retainedKey ?? canonicalKey;
|
|
464
|
+
let collision = 0;
|
|
465
|
+
while (usedKeys.has(key)) {
|
|
466
|
+
key = `${canonicalKey}:${index}:${collision}`;
|
|
467
|
+
collision += 1;
|
|
468
|
+
}
|
|
469
|
+
usedKeys.add(key);
|
|
470
|
+
return { group, key };
|
|
471
|
+
});
|
|
472
|
+
}, [allGroups]);
|
|
473
|
+
|
|
474
|
+
useLayoutEffect(() => {
|
|
475
|
+
previousRef.current = keyedGroups;
|
|
476
|
+
}, [keyedGroups]);
|
|
477
|
+
|
|
478
|
+
return keyedGroups;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Bulk history is already projected into its authoritative order before this
|
|
483
|
+
* hook runs. Mount its newest group first, then prepend one older group per
|
|
484
|
+
* animation frame so low-end browsers can paint and service input between
|
|
485
|
+
* React/Markdown commits instead of doing the entire tail in one long task.
|
|
486
|
+
*
|
|
487
|
+
* A live append does not move the first mounted key, so it is included
|
|
488
|
+
* immediately in the mounted suffix. A history prepend moves that key deeper
|
|
489
|
+
* in the array; shifting `visibleStart` by that exact prefix keeps every
|
|
490
|
+
* existing row mounted while the new prefix is revealed. If projection
|
|
491
|
+
* coalesces the seam item itself away, the earliest surviving mounted key
|
|
492
|
+
* provides the same anchor. Replacements with no shared mounted key (for
|
|
493
|
+
* example a session switch or clear-view) start a fresh suffix.
|
|
494
|
+
*/
|
|
495
|
+
function useProgressivelyMountedGroups(allGroups: KeyedTimelineGroup[]): {
|
|
496
|
+
mountedGroups: KeyedTimelineGroup[];
|
|
497
|
+
mountingOlderGroups: boolean;
|
|
498
|
+
} {
|
|
499
|
+
const previousGroupKeysRef = useRef<string[]>([]);
|
|
500
|
+
const groupKeys = useMemo(() => {
|
|
501
|
+
const nextKeys = allGroups.map((group) => group.key);
|
|
502
|
+
const previousKeys = previousGroupKeysRef.current;
|
|
503
|
+
return nextKeys.length === previousKeys.length &&
|
|
504
|
+
nextKeys.every((key, index) => key === previousKeys[index])
|
|
505
|
+
? previousKeys
|
|
506
|
+
: nextKeys;
|
|
507
|
+
}, [allGroups]);
|
|
508
|
+
useLayoutEffect(() => {
|
|
509
|
+
previousGroupKeysRef.current = groupKeys;
|
|
510
|
+
}, [groupKeys]);
|
|
511
|
+
const [window, setWindow] = useState<MountedGroupWindow>(() => ({
|
|
512
|
+
groupKeys,
|
|
513
|
+
visibleStart: Math.max(0, allGroups.length - INITIAL_MOUNTED_GROUPS),
|
|
514
|
+
}));
|
|
515
|
+
|
|
516
|
+
const lastPossibleStart = Math.max(0, allGroups.length - INITIAL_MOUNTED_GROUPS);
|
|
517
|
+
let visibleStart = 0;
|
|
518
|
+
if (allGroups.length > 0) {
|
|
519
|
+
const currentIndexByKey = new Map(groupKeys.map((key, index) => [key, index]));
|
|
520
|
+
let retainedStart: number | undefined;
|
|
521
|
+
for (const key of window.groupKeys.slice(window.visibleStart)) {
|
|
522
|
+
const index = currentIndexByKey.get(key);
|
|
523
|
+
if (index !== undefined) {
|
|
524
|
+
retainedStart = index;
|
|
525
|
+
break;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
visibleStart =
|
|
529
|
+
retainedStart === undefined ? lastPossibleStart : Math.min(retainedStart, lastPossibleStart);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
useEffect(() => {
|
|
533
|
+
// Synchronize a prepend/replacement before scheduling its first reveal.
|
|
534
|
+
if (window.groupKeys !== groupKeys || window.visibleStart !== visibleStart) {
|
|
535
|
+
setWindow({ groupKeys, visibleStart });
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
if (visibleStart === 0) {
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
const frame = requestFrame(() => {
|
|
542
|
+
setWindow((current) =>
|
|
543
|
+
current.groupKeys === groupKeys && current.visibleStart > 0
|
|
544
|
+
? { ...current, visibleStart: current.visibleStart - 1 }
|
|
545
|
+
: current,
|
|
546
|
+
);
|
|
547
|
+
});
|
|
548
|
+
return () => cancelFrame(frame);
|
|
549
|
+
}, [groupKeys, visibleStart, window.groupKeys, window.visibleStart]);
|
|
550
|
+
|
|
551
|
+
return {
|
|
552
|
+
mountedGroups: allGroups.slice(visibleStart),
|
|
553
|
+
mountingOlderGroups: visibleStart > 0,
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
|
|
409
557
|
function requestFrame(callback: FrameRequestCallback): number {
|
|
410
558
|
if (typeof requestAnimationFrame === "function") {
|
|
411
559
|
return requestAnimationFrame(callback);
|
|
@@ -421,7 +569,11 @@ function cancelFrame(id: number): void {
|
|
|
421
569
|
window.clearTimeout(id);
|
|
422
570
|
}
|
|
423
571
|
|
|
424
|
-
|
|
572
|
+
// Progressive history mounting reuses the exact group objects from `allGroups`.
|
|
573
|
+
// Skip repainting those stable rows on every prefix reveal; live projection
|
|
574
|
+
// creates a new group object, and behavior/callback changes are separate props,
|
|
575
|
+
// so ordinary streaming and host updates still invalidate immediately.
|
|
576
|
+
const TimelineGroupView = memo(function TimelineGroupView({
|
|
425
577
|
group,
|
|
426
578
|
renderMessageText,
|
|
427
579
|
onOpenSession,
|
|
@@ -531,7 +683,7 @@ function TimelineGroupView({
|
|
|
531
683
|
/>
|
|
532
684
|
);
|
|
533
685
|
}
|
|
534
|
-
}
|
|
686
|
+
});
|
|
535
687
|
|
|
536
688
|
function timelineGroupKey(group: TimelineGroup): string {
|
|
537
689
|
switch (group.kind) {
|
|
@@ -544,6 +696,17 @@ function timelineGroupKey(group: TimelineGroup): string {
|
|
|
544
696
|
}
|
|
545
697
|
}
|
|
546
698
|
|
|
699
|
+
function timelineGroupItemIds(group: TimelineGroup): string[] {
|
|
700
|
+
switch (group.kind) {
|
|
701
|
+
case "item":
|
|
702
|
+
return [group.item.id];
|
|
703
|
+
case "activity":
|
|
704
|
+
return group.items.map((item) => item.id);
|
|
705
|
+
case "turn":
|
|
706
|
+
return group.groups.flatMap(timelineGroupItemIds);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
547
710
|
/** The agent has moved PAST a cluster only when what follows is more agent
|
|
548
711
|
progress — new activity, a settled turn, or narration. A waiting notice
|
|
549
712
|
(approval pause), a pending queued message, a goal pill, or nothing at all
|
|
@@ -998,12 +1161,12 @@ function NoticeRow({ item }: { item: NoticeItem }) {
|
|
|
998
1161
|
}
|
|
999
1162
|
|
|
1000
1163
|
/**
|
|
1001
|
-
* The inline connection card:
|
|
1002
|
-
*
|
|
1003
|
-
*
|
|
1004
|
-
*
|
|
1005
|
-
*
|
|
1006
|
-
*
|
|
1164
|
+
* The inline connection-recovery card: missing or lapsed access surfaces as a
|
|
1165
|
+
* calm, tappable affordance instead of a raw provider-domain error. The `reason`
|
|
1166
|
+
* only shapes human copy; no domain or enum code is shown as a label.
|
|
1167
|
+
* `onReconnect` (from the app, which owns the SDK client) starts the flow;
|
|
1168
|
+
* without it, a pre-minted authorization link is offered, or the card stays
|
|
1169
|
+
* informative. Recovery never claims to resume/replay the failed tool call.
|
|
1007
1170
|
*/
|
|
1008
1171
|
function AuthNeededRow({
|
|
1009
1172
|
item,
|
|
@@ -1020,6 +1183,8 @@ function AuthNeededRow({
|
|
|
1020
1183
|
const provider = providerLabel(item.providerDomain);
|
|
1021
1184
|
const unavailable =
|
|
1022
1185
|
item.reason === "unsupported_auth" || item.reason === "resource_scope_unavailable";
|
|
1186
|
+
const missing = item.reason === "missing_connection";
|
|
1187
|
+
const actionLabel = missing ? "Connect" : "Reconnect";
|
|
1023
1188
|
|
|
1024
1189
|
const start = async () => {
|
|
1025
1190
|
if (!onReconnect || busy) {
|
|
@@ -1030,6 +1195,7 @@ function AuthNeededRow({
|
|
|
1030
1195
|
try {
|
|
1031
1196
|
// On success the app redirects to consent (or routes to credential entry),
|
|
1032
1197
|
// so this row unmounts; a resolve without navigation just relaxes the button.
|
|
1198
|
+
// The callback starts authorization only. It never resumes this tool call.
|
|
1033
1199
|
await onReconnect(item);
|
|
1034
1200
|
setBusy(false);
|
|
1035
1201
|
} catch {
|
|
@@ -1048,7 +1214,7 @@ function AuthNeededRow({
|
|
|
1048
1214
|
/>
|
|
1049
1215
|
<div className="min-w-0">
|
|
1050
1216
|
<p className="truncate text-og-md font-medium text-og-fg">
|
|
1051
|
-
{unavailable ? `${provider} tools unavailable` :
|
|
1217
|
+
{unavailable ? `${provider} tools unavailable` : `${actionLabel} ${provider}`}
|
|
1052
1218
|
</p>
|
|
1053
1219
|
<p className="truncate text-og-sm text-og-fg-subtle">{authReasonLine(item.reason)}</p>
|
|
1054
1220
|
</div>
|
|
@@ -1064,7 +1230,7 @@ function AuthNeededRow({
|
|
|
1064
1230
|
)}
|
|
1065
1231
|
>
|
|
1066
1232
|
<RefreshCwIcon className={cn("size-3.5", busy && "animate-og-spin")} aria-hidden />
|
|
1067
|
-
{busy ? "
|
|
1233
|
+
{busy ? "Opening…" : actionLabel}
|
|
1068
1234
|
</button>
|
|
1069
1235
|
) : !unavailable && item.authorizationUrl ? (
|
|
1070
1236
|
<a
|
|
@@ -1077,13 +1243,19 @@ function AuthNeededRow({
|
|
|
1077
1243
|
)}
|
|
1078
1244
|
>
|
|
1079
1245
|
<RefreshCwIcon className="size-3.5" aria-hidden />
|
|
1080
|
-
|
|
1246
|
+
{actionLabel}
|
|
1081
1247
|
</a>
|
|
1082
1248
|
) : null}
|
|
1083
1249
|
</div>
|
|
1250
|
+
{!unavailable ? (
|
|
1251
|
+
<p className="px-1 text-og-xs text-og-fg-subtle">
|
|
1252
|
+
This tool call wasn't replayed. After {missing ? "connecting" : "reconnecting"}, send a
|
|
1253
|
+
new message to try again.
|
|
1254
|
+
</p>
|
|
1255
|
+
) : null}
|
|
1084
1256
|
{failed ? (
|
|
1085
1257
|
<p className="px-1 text-og-xs text-og-status-failed">
|
|
1086
|
-
Couldn't start reconnecting {provider}. Try again.
|
|
1258
|
+
Couldn't start {missing ? "connecting" : "reconnecting"} {provider}. Try again.
|
|
1087
1259
|
</p>
|
|
1088
1260
|
) : null}
|
|
1089
1261
|
</div>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ComposerState } from "../hooks/use-composer";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Decide whether checking out a queued prompt would replace current composer
|
|
5
|
+
* content. `hasDraftContent` reads the synchronous draft refs, so this stays
|
|
6
|
+
* correct even when a controlled input update has not rendered yet.
|
|
7
|
+
*/
|
|
8
|
+
export function requestQueueDraftEdit(
|
|
9
|
+
composer: Pick<ComposerState, "hasDraftContent">,
|
|
10
|
+
confirmReplacement: () => void,
|
|
11
|
+
editImmediately: () => void,
|
|
12
|
+
): void {
|
|
13
|
+
if (composer.hasDraftContent()) {
|
|
14
|
+
confirmReplacement();
|
|
15
|
+
} else {
|
|
16
|
+
editImmediately();
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
import { DropdownMenu } from "radix-ui";
|
|
41
41
|
import type { ComposerState } from "../hooks/use-composer";
|
|
42
42
|
import type { QueueMutationKind, UseTurnQueueResult } from "../hooks/use-turn-queue";
|
|
43
|
+
import { requestQueueDraftEdit } from "./queue-draft-policy";
|
|
43
44
|
import { QueueErrorAlert, QueueStoppingStatus } from "./queue-surface-state";
|
|
44
45
|
|
|
45
46
|
/** The sole human prompt queue: compact above Goal, Agents, and composer. */
|
|
@@ -217,16 +218,11 @@ export function QueueSurface({
|
|
|
217
218
|
const requestEdit = useCallback(
|
|
218
219
|
(turn: SessionTurn) => {
|
|
219
220
|
if (!composer || !canEditInComposer) return;
|
|
220
|
-
|
|
221
|
-
composer
|
|
222
|
-
|
|
223
|
-
(
|
|
224
|
-
|
|
225
|
-
if (draftDirty) {
|
|
226
|
-
setReplaceDraftFor(turn.id);
|
|
227
|
-
} else {
|
|
228
|
-
void edit(turn, false);
|
|
229
|
-
}
|
|
221
|
+
requestQueueDraftEdit(
|
|
222
|
+
composer,
|
|
223
|
+
() => setReplaceDraftFor(turn.id),
|
|
224
|
+
() => void edit(turn, false),
|
|
225
|
+
);
|
|
230
226
|
},
|
|
231
227
|
[canEditInComposer, composer, edit],
|
|
232
228
|
);
|
|
@@ -2,6 +2,7 @@ import { Loader2Icon } from "lucide-react";
|
|
|
2
2
|
import { lazy, Suspense, type ComponentType } from "react";
|
|
3
3
|
|
|
4
4
|
import type { QueueSurfaceProps } from "./queue-surface-implementation";
|
|
5
|
+
export { requestQueueDraftEdit } from "./queue-draft-policy";
|
|
5
6
|
import { EmptyQueueStateSurface } from "./queue-surface-state";
|
|
6
7
|
|
|
7
8
|
export type { QueueSurfaceProps } from "./queue-surface-implementation";
|