@opengeni/react 0.15.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/README.md +110 -10
- package/dist/{chunk-TOJR776I.js → chunk-6XUS5VFM.js} +45 -369
- package/dist/chunk-6XUS5VFM.js.map +1 -0
- package/dist/chunk-CATATY77.js +2393 -0
- package/dist/chunk-CATATY77.js.map +1 -0
- package/dist/chunk-I3BJZIG5.js +439 -0
- package/dist/chunk-I3BJZIG5.js.map +1 -0
- package/dist/chunk-TK7G6XLT.js +18 -0
- package/dist/chunk-TK7G6XLT.js.map +1 -0
- package/dist/chunk-UAHPKLB6.js +2237 -0
- package/dist/chunk-UAHPKLB6.js.map +1 -0
- package/dist/chunk-UQS7OQDE.js +591 -0
- package/dist/chunk-UQS7OQDE.js.map +1 -0
- package/dist/chunk-YDNWMKXO.js +102 -0
- package/dist/chunk-YDNWMKXO.js.map +1 -0
- package/dist/composer-CVm5uoUI.d.ts +585 -0
- package/dist/composer.d.ts +6 -0
- package/dist/composer.js +56 -0
- package/dist/composer.js.map +1 -0
- package/dist/index.d.ts +1288 -1785
- package/dist/index.js +7140 -8817
- package/dist/index.js.map +1 -1
- package/dist/machines.d.ts +412 -3
- package/dist/machines.js +3 -1
- package/dist/noto-sans-arabic-loader-IA2T4X2A.js +21 -0
- package/dist/noto-sans-arabic-loader-IA2T4X2A.js.map +1 -0
- package/dist/noto-sans-jp-loader.generated-HFGLYBR3.js +292 -0
- package/dist/noto-sans-jp-loader.generated-HFGLYBR3.js.map +1 -0
- package/dist/queue-surface-implementation-3UW3MIIR.js +890 -0
- package/dist/queue-surface-implementation-3UW3MIIR.js.map +1 -0
- package/dist/session-Db00TRwl.d.ts +446 -0
- package/dist/session-context-rgrfElMl.d.ts +58 -0
- package/dist/session.d.ts +4 -0
- package/dist/session.js +54 -0
- package/dist/session.js.map +1 -0
- package/dist/use-composer-By1wn3MM.d.ts +99 -0
- package/package.json +20 -4
- package/src/approvals.ts +5 -3
- package/src/client.ts +44 -0
- package/src/components/approval-surface.tsx +198 -0
- package/src/components/chat-composer.tsx +94 -993
- package/src/components/code-editor.tsx +104 -26
- package/src/components/command-palette.tsx +8 -2
- package/src/components/composer-transcription-control.tsx +211 -0
- package/src/components/composer.tsx +1536 -0
- package/src/components/desktop-viewer.tsx +3 -3
- package/src/components/enrollment-consent.tsx +2 -2
- package/src/components/file-browser.tsx +367 -29
- package/src/components/human-input-form.tsx +417 -0
- package/src/components/machine-card.tsx +1 -1
- package/src/components/machines-dashboard.tsx +3 -3
- package/src/components/message-timeline.tsx +205 -25
- package/src/components/model-picker.tsx +12 -3
- package/src/components/pierre-diff.tsx +32 -6
- package/src/components/queue-draft-policy.ts +18 -0
- package/src/components/queue-surface-implementation.tsx +1037 -0
- package/src/components/queue-surface-state.tsx +94 -0
- package/src/components/queue-surface.tsx +42 -559
- package/src/components/sandbox-files.tsx +138 -14
- package/src/components/sandbox-terminal.tsx +8 -2
- package/src/components/sandbox-workspace.tsx +339 -119
- package/src/components/workbench-changes.tsx +84 -33
- package/src/components/workspace-dock.tsx +188 -18
- package/src/composer.ts +60 -0
- package/src/hooks/internal.ts +115 -34
- package/src/hooks/use-composer.ts +385 -104
- package/src/hooks/use-human-input.ts +131 -0
- package/src/hooks/use-machine-chip.ts +2 -2
- package/src/hooks/use-machines.ts +27 -11
- package/src/hooks/use-sandbox-files.ts +310 -62
- package/src/hooks/use-sandbox-git.ts +154 -40
- package/src/hooks/use-sandbox-terminal.ts +28 -6
- package/src/hooks/use-session-capabilities.ts +38 -9
- package/src/hooks/use-session-control.ts +37 -8
- package/src/hooks/use-session-events.ts +522 -48
- package/src/hooks/use-session-mcp-approval-policy.ts +161 -0
- package/src/hooks/use-slash-commands.ts +38 -38
- package/src/hooks/use-transcription.ts +757 -0
- package/src/hooks/use-turn-queue.ts +134 -66
- package/src/hooks/use-windowed-sections.ts +2 -2
- package/src/hooks/use-workspace-capture.ts +146 -40
- package/src/hooks/use-workspace-edit.ts +50 -14
- package/src/hooks/use-workspace-sessions.ts +3 -0
- package/src/human-input.ts +72 -0
- package/src/index.ts +85 -6
- package/src/lib/noto-sans-arabic-loader.ts +21 -0
- package/src/lib/noto-sans-jp-loader.generated.ts +295 -0
- package/src/lib/use-portal-token-style.ts +51 -0
- package/src/lib/use-unicode-fonts.ts +57 -0
- package/src/provider.tsx +4 -69
- package/src/session-context.ts +160 -0
- package/src/session.ts +89 -0
- package/src/timeline/parsers.ts +35 -0
- package/src/timeline/projection.ts +12 -4
- package/src/timeline/tool-renderers.tsx +41 -0
- package/src/timeline/turn-summary.tsx +2 -2
- package/src/timeline/types.ts +10 -9
- package/styles/index.css +56 -0
- package/styles/index.d.ts +2 -0
- package/styles/tokens.css +11 -6
- package/styles/tokens.d.ts +2 -0
- package/dist/chunk-TOJR776I.js.map +0 -1
- package/dist/machines-BpdwuQcD.d.ts +0 -449
|
@@ -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
|
|
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,
|
|
@@ -1018,6 +1181,10 @@ function AuthNeededRow({
|
|
|
1018
1181
|
const [busy, setBusy] = useState(false);
|
|
1019
1182
|
const [failed, setFailed] = useState(false);
|
|
1020
1183
|
const provider = providerLabel(item.providerDomain);
|
|
1184
|
+
const unavailable =
|
|
1185
|
+
item.reason === "unsupported_auth" || item.reason === "resource_scope_unavailable";
|
|
1186
|
+
const missing = item.reason === "missing_connection";
|
|
1187
|
+
const actionLabel = missing ? "Connect" : "Reconnect";
|
|
1021
1188
|
|
|
1022
1189
|
const start = async () => {
|
|
1023
1190
|
if (!onReconnect || busy) {
|
|
@@ -1028,6 +1195,7 @@ function AuthNeededRow({
|
|
|
1028
1195
|
try {
|
|
1029
1196
|
// On success the app redirects to consent (or routes to credential entry),
|
|
1030
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.
|
|
1031
1199
|
await onReconnect(item);
|
|
1032
1200
|
setBusy(false);
|
|
1033
1201
|
} catch {
|
|
@@ -1045,11 +1213,13 @@ function AuthNeededRow({
|
|
|
1045
1213
|
label={provider}
|
|
1046
1214
|
/>
|
|
1047
1215
|
<div className="min-w-0">
|
|
1048
|
-
<p className="truncate text-og-md font-medium text-og-fg">
|
|
1216
|
+
<p className="truncate text-og-md font-medium text-og-fg">
|
|
1217
|
+
{unavailable ? `${provider} tools unavailable` : `${actionLabel} ${provider}`}
|
|
1218
|
+
</p>
|
|
1049
1219
|
<p className="truncate text-og-sm text-og-fg-subtle">{authReasonLine(item.reason)}</p>
|
|
1050
1220
|
</div>
|
|
1051
1221
|
</div>
|
|
1052
|
-
{onReconnect ? (
|
|
1222
|
+
{!unavailable && onReconnect ? (
|
|
1053
1223
|
<button
|
|
1054
1224
|
type="button"
|
|
1055
1225
|
onClick={() => void start()}
|
|
@@ -1060,9 +1230,9 @@ function AuthNeededRow({
|
|
|
1060
1230
|
)}
|
|
1061
1231
|
>
|
|
1062
1232
|
<RefreshCwIcon className={cn("size-3.5", busy && "animate-og-spin")} aria-hidden />
|
|
1063
|
-
{busy ? "
|
|
1233
|
+
{busy ? "Opening…" : actionLabel}
|
|
1064
1234
|
</button>
|
|
1065
|
-
) : item.authorizationUrl ? (
|
|
1235
|
+
) : !unavailable && item.authorizationUrl ? (
|
|
1066
1236
|
<a
|
|
1067
1237
|
href={item.authorizationUrl}
|
|
1068
1238
|
rel="noreferrer"
|
|
@@ -1073,13 +1243,19 @@ function AuthNeededRow({
|
|
|
1073
1243
|
)}
|
|
1074
1244
|
>
|
|
1075
1245
|
<RefreshCwIcon className="size-3.5" aria-hidden />
|
|
1076
|
-
|
|
1246
|
+
{actionLabel}
|
|
1077
1247
|
</a>
|
|
1078
1248
|
) : null}
|
|
1079
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}
|
|
1080
1256
|
{failed ? (
|
|
1081
1257
|
<p className="px-1 text-og-xs text-og-status-failed">
|
|
1082
|
-
Couldn't start reconnecting {provider}. Try again.
|
|
1258
|
+
Couldn't start {missing ? "connecting" : "reconnecting"} {provider}. Try again.
|
|
1083
1259
|
</p>
|
|
1084
1260
|
) : null}
|
|
1085
1261
|
</div>
|
|
@@ -1160,6 +1336,10 @@ function authReasonLine(reason: AuthNeededItem["reason"]): string {
|
|
|
1160
1336
|
case "expired":
|
|
1161
1337
|
case "refresh_failed":
|
|
1162
1338
|
return "Its access expired.";
|
|
1339
|
+
case "unsupported_auth":
|
|
1340
|
+
return "This connection cannot authenticate the configured tool endpoint.";
|
|
1341
|
+
case "resource_scope_unavailable":
|
|
1342
|
+
return "This tool endpoint cannot enforce the selected repository access.";
|
|
1163
1343
|
default:
|
|
1164
1344
|
return "Its connection needs attention.";
|
|
1165
1345
|
}
|
|
@@ -12,6 +12,8 @@ export type ModelPickerProps = {
|
|
|
12
12
|
onChange: (modelId: string) => void;
|
|
13
13
|
disabled?: boolean | undefined;
|
|
14
14
|
className?: string | undefined;
|
|
15
|
+
/** Visible-to-assistive-technology label. Defaults to "Model". */
|
|
16
|
+
label?: string | undefined;
|
|
15
17
|
};
|
|
16
18
|
|
|
17
19
|
/**
|
|
@@ -25,7 +27,14 @@ export type ModelPickerProps = {
|
|
|
25
27
|
* the send path. Renders nothing when no models are exposed, so a single-model
|
|
26
28
|
* deployment shows no chrome.
|
|
27
29
|
*/
|
|
28
|
-
export function ModelPicker({
|
|
30
|
+
export function ModelPicker({
|
|
31
|
+
models,
|
|
32
|
+
value,
|
|
33
|
+
onChange,
|
|
34
|
+
disabled,
|
|
35
|
+
className,
|
|
36
|
+
label = "Model",
|
|
37
|
+
}: ModelPickerProps) {
|
|
29
38
|
const selectId = useId();
|
|
30
39
|
// Group by provider, preserving first-seen order for both the providers and
|
|
31
40
|
// the models within each — the server already orders the list (default model
|
|
@@ -50,14 +59,14 @@ export function ModelPicker({ models, value, onChange, disabled, className }: Mo
|
|
|
50
59
|
return (
|
|
51
60
|
<span className={cn("relative inline-flex items-center", className)}>
|
|
52
61
|
<label htmlFor={selectId} className="sr-only">
|
|
53
|
-
|
|
62
|
+
{label}
|
|
54
63
|
</label>
|
|
55
64
|
<select
|
|
56
65
|
id={selectId}
|
|
57
66
|
value={value ?? ""}
|
|
58
67
|
onChange={(event) => onChange(event.target.value)}
|
|
59
68
|
disabled={disabled === true}
|
|
60
|
-
aria-label=
|
|
69
|
+
aria-label={label}
|
|
61
70
|
className={cn(
|
|
62
71
|
// Sized like the other footer controls; the chevron overlay needs the
|
|
63
72
|
// right padding so the value never collides with it.
|
|
@@ -78,6 +78,20 @@ export function PierreDiff({
|
|
|
78
78
|
className,
|
|
79
79
|
}: PierreDiffProps) {
|
|
80
80
|
const [failed, setFailed] = useState(false);
|
|
81
|
+
const [forcedColors, setForcedColors] = useState(false);
|
|
82
|
+
|
|
83
|
+
// Pierre's rich renderer distinguishes additions/deletions primarily through
|
|
84
|
+
// foreground and background colors inside a shadow root. In Windows forced-
|
|
85
|
+
// colors mode those paints intentionally collapse, so expose the literal
|
|
86
|
+
// unified patch (+/- markers) instead of leaving changed lines ambiguous.
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
|
|
89
|
+
const query = window.matchMedia("(forced-colors: active)");
|
|
90
|
+
const update = () => setForcedColors(query.matches);
|
|
91
|
+
update();
|
|
92
|
+
query.addEventListener?.("change", update);
|
|
93
|
+
return () => query.removeEventListener?.("change", update);
|
|
94
|
+
}, []);
|
|
81
95
|
|
|
82
96
|
// Probe the import once so a hard failure (peer missing) shows `fallback`
|
|
83
97
|
// rather than a Suspense boundary that never resolves. Skipped when `plain`.
|
|
@@ -92,11 +106,18 @@ export function PierreDiff({
|
|
|
92
106
|
};
|
|
93
107
|
}, [plain]);
|
|
94
108
|
|
|
95
|
-
if (plain || failed) {
|
|
109
|
+
if (plain || failed || forcedColors) {
|
|
96
110
|
// `plain` opts out of highlighting; `failed` = `@pierre/diffs` not installed.
|
|
97
111
|
// Render the caller's fallback, or a plain (unhighlighted) patch dump — NOT a
|
|
98
112
|
// second hunk renderer. One highlighted renderer (Pierre) + a text degrade.
|
|
99
|
-
return
|
|
113
|
+
return (
|
|
114
|
+
<div
|
|
115
|
+
className={className}
|
|
116
|
+
data-opengeni-plain-diff={forcedColors ? "forced-colors" : "fallback"}
|
|
117
|
+
>
|
|
118
|
+
{fallback ?? <PlainPatch diff={diff} />}
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
100
121
|
}
|
|
101
122
|
|
|
102
123
|
// Default to the dark theme: the host UI is dark-first, and Pierre's own
|
|
@@ -131,13 +152,15 @@ export function PierreDiff({
|
|
|
131
152
|
"--diffs-fg-number-addition-override": "var(--og-color-status-idle)",
|
|
132
153
|
"--diffs-fg-number-deletion-override": "var(--og-color-status-failed)",
|
|
133
154
|
"--diffs-bg-addition-override":
|
|
134
|
-
"color-mix(in
|
|
155
|
+
"color-mix(in oklab, var(--og-color-status-idle) 12%, var(--og-color-bg))",
|
|
135
156
|
"--diffs-bg-deletion-override":
|
|
136
|
-
"color-mix(in
|
|
157
|
+
"color-mix(in oklab, var(--og-color-status-failed) 12%, var(--og-color-bg))",
|
|
137
158
|
"--diffs-bg-addition-emphasis-override":
|
|
138
|
-
"color-mix(in
|
|
159
|
+
"color-mix(in oklab, var(--og-color-status-idle) 22%, var(--og-color-bg))",
|
|
139
160
|
"--diffs-bg-deletion-emphasis-override":
|
|
140
|
-
"color-mix(in
|
|
161
|
+
"color-mix(in oklab, var(--og-color-status-failed) 22%, var(--og-color-bg))",
|
|
162
|
+
"--diffs-header-font-family": "var(--og-font-sans)",
|
|
163
|
+
"--diffs-font-family": "var(--og-font-mono)",
|
|
141
164
|
"--diffs-font-size": "var(--og-code-font-size)",
|
|
142
165
|
"--diffs-line-height": "var(--og-code-line-height)",
|
|
143
166
|
} as CSSProperties;
|
|
@@ -177,7 +200,10 @@ function PlainPatch({ diff }: { diff: GitFileDiff[] }) {
|
|
|
177
200
|
{diff.map((file) => (
|
|
178
201
|
<pre
|
|
179
202
|
key={file.path}
|
|
203
|
+
aria-label={`Diff for ${file.path}`}
|
|
180
204
|
className="mb-2 overflow-auto whitespace-pre rounded-og-sm border border-og-border bg-og-bg/60 p-2.5 font-og-mono text-og-xs leading-5"
|
|
205
|
+
role="region"
|
|
206
|
+
tabIndex={0}
|
|
181
207
|
>
|
|
182
208
|
{gitFileDiffToPatch(file)
|
|
183
209
|
.split("\n")
|
|
@@ -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
|
+
}
|