@opengeni/react 0.36.0 → 0.37.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 +4 -0
- package/dist/{chunk-J2RVJ6JX.js → chunk-4IJCL7YO.js} +13 -2
- package/dist/chunk-4IJCL7YO.js.map +1 -0
- package/dist/{chunk-OKKMZDQF.js → chunk-FSPDND3P.js} +2 -2
- package/dist/{chunk-FT2TXNGZ.js → chunk-HJ4OQVGW.js} +174 -14
- package/dist/chunk-HJ4OQVGW.js.map +1 -0
- package/dist/{chunk-PVW56EVR.js → chunk-IP22SLO6.js} +447 -13
- package/dist/chunk-IP22SLO6.js.map +1 -0
- package/dist/{chunk-U255M5EZ.js → chunk-SJKT4TKW.js} +39 -6
- package/dist/chunk-SJKT4TKW.js.map +1 -0
- package/dist/{chunk-XT7JF2EH.js → chunk-UCZPNXV3.js} +96 -17
- package/dist/chunk-UCZPNXV3.js.map +1 -0
- package/dist/components/chat-composer.d.ts +3 -1
- package/dist/components/model-policy-picker.d.ts +78 -0
- package/dist/components/session-chrome.d.ts +1 -1
- package/dist/composer.d.ts +2 -0
- package/dist/composer.js +17 -3
- package/dist/hooks/use-composer.d.ts +9 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +30 -9
- package/dist/index.js.map +1 -1
- package/dist/model-policy.d.ts +12 -4
- package/dist/model-policy.js +5 -1
- package/dist/session-ui.js +2 -2
- package/dist/session.js +3 -3
- package/dist/timeline/types.d.ts +5 -0
- package/package.json +2 -2
- package/src/components/chat-composer.tsx +8 -3
- package/src/components/composer.tsx +17 -1
- package/src/components/copy-button.tsx +13 -6
- package/src/components/model-picker.tsx +2 -2
- package/src/components/model-policy-picker.tsx +582 -0
- package/src/components/session-chrome.tsx +138 -14
- package/src/composer.ts +13 -0
- package/src/hooks/use-composer.ts +213 -16
- package/src/index.ts +15 -0
- package/src/model-policy.ts +69 -14
- package/src/timeline/projection.ts +21 -1
- package/src/timeline/types.ts +5 -0
- package/dist/chunk-FT2TXNGZ.js.map +0 -1
- package/dist/chunk-J2RVJ6JX.js.map +0 -1
- package/dist/chunk-PVW56EVR.js.map +0 -1
- package/dist/chunk-U255M5EZ.js.map +0 -1
- package/dist/chunk-XT7JF2EH.js.map +0 -1
- /package/dist/{chunk-OKKMZDQF.js.map → chunk-FSPDND3P.js.map} +0 -0
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
*/
|
|
33
33
|
import type { SessionGoal, SessionPendingInputPreview, SessionTurn } from "@opengeni/sdk";
|
|
34
34
|
import {
|
|
35
|
+
AudioLinesIcon,
|
|
35
36
|
ArrowDownIcon,
|
|
36
37
|
ArrowUpIcon,
|
|
37
38
|
BotIcon,
|
|
@@ -64,7 +65,7 @@ import { cn } from "../lib/cn";
|
|
|
64
65
|
import { requestQueueDraftEdit } from "./queue-draft-policy";
|
|
65
66
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./tooltip";
|
|
66
67
|
|
|
67
|
-
export type SessionChromeSignalId = "incoming" | "queue" | "goal" | "agents";
|
|
68
|
+
export type SessionChromeSignalId = "incoming" | "steering" | "queue" | "goal" | "agents";
|
|
68
69
|
|
|
69
70
|
export type SessionChromeSignalTone = "neutral" | "accent" | "waiting" | "running";
|
|
70
71
|
|
|
@@ -105,6 +106,11 @@ type GoalPillState =
|
|
|
105
106
|
| "invariant_broken"
|
|
106
107
|
| "completed";
|
|
107
108
|
|
|
109
|
+
type QueuedTurnPresentation = {
|
|
110
|
+
kind: "prompt" | "realtime_voice" | "realtime_voice_handoff";
|
|
111
|
+
text: string;
|
|
112
|
+
};
|
|
113
|
+
|
|
108
114
|
const GOAL_LABEL: Record<GoalPillState, string> = {
|
|
109
115
|
pursuing: "Pursuing",
|
|
110
116
|
scheduled: "Scheduled",
|
|
@@ -115,6 +121,28 @@ const GOAL_LABEL: Record<GoalPillState, string> = {
|
|
|
115
121
|
completed: "Done",
|
|
116
122
|
};
|
|
117
123
|
|
|
124
|
+
function queuedTurnPresentation(turn: SessionTurn): QueuedTurnPresentation {
|
|
125
|
+
const realtimeDelegation = objectValue(turn.metadata.realtimeDelegation);
|
|
126
|
+
const inputTranscript = realtimeDelegation?.inputTranscript;
|
|
127
|
+
if (typeof inputTranscript === "string" && inputTranscript.trim()) {
|
|
128
|
+
return { kind: "realtime_voice", text: inputTranscript.trim() };
|
|
129
|
+
}
|
|
130
|
+
if (objectValue(turn.metadata.realtimeTailFlush)) {
|
|
131
|
+
return { kind: "realtime_voice_handoff", text: "Remaining voice context" };
|
|
132
|
+
}
|
|
133
|
+
return { kind: "prompt", text: turn.prompt };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function isSteeringTurn(turn: SessionTurn): boolean {
|
|
137
|
+
return turn.metadata.delivery === "steer";
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function objectValue(value: unknown): Record<string, unknown> | null {
|
|
141
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
142
|
+
? (value as Record<string, unknown>)
|
|
143
|
+
: null;
|
|
144
|
+
}
|
|
145
|
+
|
|
118
146
|
/** Select pill state from the goal's authoritative continuation projection. */
|
|
119
147
|
export function sessionChromeGoalPillState(
|
|
120
148
|
goalStatus: "active" | "paused" | "completed",
|
|
@@ -215,6 +243,34 @@ export function SessionChrome({
|
|
|
215
243
|
const record = goal?.goal ?? null;
|
|
216
244
|
const incoming = queue.pendingInputs;
|
|
217
245
|
const turns = queue.queue;
|
|
246
|
+
const composerSteering = composer?.steering ?? null;
|
|
247
|
+
const { steering, queuedTurns } = useMemo(() => {
|
|
248
|
+
const pendingQueueSteer =
|
|
249
|
+
turns.find((turn) => queue.pendingByTurn[turn.id] === "steer") ?? null;
|
|
250
|
+
const durableQueueSteer =
|
|
251
|
+
!pendingQueueSteer && turns[0] && isSteeringTurn(turns[0]) ? turns[0] : null;
|
|
252
|
+
const currentSteering =
|
|
253
|
+
composerSteering?.phase === "submitting"
|
|
254
|
+
? composerSteering
|
|
255
|
+
: pendingQueueSteer
|
|
256
|
+
? {
|
|
257
|
+
phase: "submitting" as const,
|
|
258
|
+
text: queuedTurnPresentation(pendingQueueSteer).text,
|
|
259
|
+
turnId: pendingQueueSteer.id,
|
|
260
|
+
}
|
|
261
|
+
: durableQueueSteer
|
|
262
|
+
? {
|
|
263
|
+
phase: "accepted" as const,
|
|
264
|
+
text: queuedTurnPresentation(durableQueueSteer).text,
|
|
265
|
+
turnId: durableQueueSteer.id,
|
|
266
|
+
}
|
|
267
|
+
: composerSteering;
|
|
268
|
+
const currentTurnId = currentSteering?.turnId ?? null;
|
|
269
|
+
return {
|
|
270
|
+
steering: currentSteering,
|
|
271
|
+
queuedTurns: currentTurnId ? turns.filter((turn) => turn.id !== currentTurnId) : turns,
|
|
272
|
+
};
|
|
273
|
+
}, [composerSteering, queue.pendingByTurn, turns]);
|
|
218
274
|
const canMutateQueue = !readOnly && composer !== undefined;
|
|
219
275
|
|
|
220
276
|
const liveGoal =
|
|
@@ -250,14 +306,44 @@ export function SessionChrome({
|
|
|
250
306
|
icon: <InboxIcon className="size-3" />,
|
|
251
307
|
});
|
|
252
308
|
}
|
|
253
|
-
if (
|
|
254
|
-
|
|
309
|
+
if (steering) {
|
|
310
|
+
rows.push({
|
|
311
|
+
id: "steering",
|
|
312
|
+
label: "Changing direction…",
|
|
313
|
+
detail: steering.text,
|
|
314
|
+
tone: "accent",
|
|
315
|
+
icon:
|
|
316
|
+
steering.phase === "submitting" ? (
|
|
317
|
+
<Loader2Icon className="size-3 animate-og-spin" />
|
|
318
|
+
) : (
|
|
319
|
+
<ZapIcon className="size-3" />
|
|
320
|
+
),
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
if (queuedTurns.length > 0) {
|
|
324
|
+
const presentations = queuedTurns.map(queuedTurnPresentation);
|
|
325
|
+
const first = presentations[0];
|
|
326
|
+
const allVoiceRequests = presentations.every(({ kind }) => kind === "realtime_voice");
|
|
327
|
+
const onlyVoiceHandoff =
|
|
328
|
+
presentations.length === 1 && first?.kind === "realtime_voice_handoff";
|
|
329
|
+
const voiceOnly = allVoiceRequests || onlyVoiceHandoff;
|
|
330
|
+
const detail = first?.text;
|
|
255
331
|
rows.push({
|
|
256
332
|
id: "queue",
|
|
257
|
-
label:
|
|
333
|
+
label: allVoiceRequests
|
|
334
|
+
? queuedTurns.length === 1
|
|
335
|
+
? "Voice request queued"
|
|
336
|
+
: `${queuedTurns.length} voice requests queued`
|
|
337
|
+
: onlyVoiceHandoff
|
|
338
|
+
? "Voice handoff queued"
|
|
339
|
+
: `${queuedTurns.length} queued prompt${queuedTurns.length === 1 ? "" : "s"}`,
|
|
258
340
|
...(detail ? { detail } : {}),
|
|
259
341
|
tone: "neutral",
|
|
260
|
-
icon:
|
|
342
|
+
icon: voiceOnly ? (
|
|
343
|
+
<AudioLinesIcon className="size-3" />
|
|
344
|
+
) : (
|
|
345
|
+
<ListOrderedIcon className="size-3" />
|
|
346
|
+
),
|
|
261
347
|
});
|
|
262
348
|
}
|
|
263
349
|
if (record && goalState) {
|
|
@@ -292,7 +378,7 @@ export function SessionChrome({
|
|
|
292
378
|
});
|
|
293
379
|
}
|
|
294
380
|
return rows;
|
|
295
|
-
}, [agentsSignal, elapsed, goalState, incoming, record,
|
|
381
|
+
}, [agentsSignal, elapsed, goalState, incoming, queuedTurns, record, steering]);
|
|
296
382
|
|
|
297
383
|
const [activeUncontrolled, setActiveUncontrolled] = useState<SessionChromeSignalId | null>(
|
|
298
384
|
defaultActive,
|
|
@@ -320,10 +406,10 @@ export function SessionChrome({
|
|
|
320
406
|
|
|
321
407
|
useEffect(() => {
|
|
322
408
|
if (active !== "queue" || !replaceDraftFor) return;
|
|
323
|
-
if (!
|
|
409
|
+
if (!queuedTurns.some((turn) => turn.id === replaceDraftFor)) {
|
|
324
410
|
setReplaceDraftFor(null);
|
|
325
411
|
}
|
|
326
|
-
}, [active,
|
|
412
|
+
}, [active, queuedTurns, replaceDraftFor]);
|
|
327
413
|
|
|
328
414
|
useEffect(() => {
|
|
329
415
|
const rail = railRef.current;
|
|
@@ -363,7 +449,7 @@ export function SessionChrome({
|
|
|
363
449
|
const node = panelBodyRef.current;
|
|
364
450
|
if (!node) return;
|
|
365
451
|
setPanelHeight(node.offsetHeight);
|
|
366
|
-
}, [open, active, incoming,
|
|
452
|
+
}, [open, active, incoming, queuedTurns, record, goalState, agentsPanel, agentsSignal, steering]);
|
|
367
453
|
|
|
368
454
|
useEffect(() => {
|
|
369
455
|
if (!open) return;
|
|
@@ -385,9 +471,11 @@ export function SessionChrome({
|
|
|
385
471
|
const panelBody =
|
|
386
472
|
active === "incoming" ? (
|
|
387
473
|
<IncomingPanel inputs={incoming} onDismiss={onDismissIncoming} />
|
|
474
|
+
) : active === "steering" && steering ? (
|
|
475
|
+
<SteeringPanel phase={steering.phase} text={steering.text} />
|
|
388
476
|
) : active === "queue" ? (
|
|
389
477
|
<QueuePanel
|
|
390
|
-
turns={
|
|
478
|
+
turns={queuedTurns}
|
|
391
479
|
readOnly={!canMutateQueue}
|
|
392
480
|
mutationFor={queue.mutationFor}
|
|
393
481
|
replaceDraftFor={replaceDraftFor}
|
|
@@ -661,6 +749,33 @@ function IncomingPanel({
|
|
|
661
749
|
);
|
|
662
750
|
}
|
|
663
751
|
|
|
752
|
+
function SteeringPanel({ phase, text }: { phase: "submitting" | "accepted"; text: string }) {
|
|
753
|
+
return (
|
|
754
|
+
<div
|
|
755
|
+
className="flex items-start gap-2 rounded-og-sm px-1.5 py-1"
|
|
756
|
+
role="status"
|
|
757
|
+
aria-live="polite"
|
|
758
|
+
data-og-session-chrome-panel="steering"
|
|
759
|
+
>
|
|
760
|
+
<span className="mt-0.5 inline-flex size-5 shrink-0 items-center justify-center rounded-full bg-og-accent-soft text-og-accent">
|
|
761
|
+
{phase === "submitting" ? (
|
|
762
|
+
<Loader2Icon className="size-3 animate-og-spin" aria-hidden="true" />
|
|
763
|
+
) : (
|
|
764
|
+
<ZapIcon className="size-3" aria-hidden="true" />
|
|
765
|
+
)}
|
|
766
|
+
</span>
|
|
767
|
+
<div className="min-w-0">
|
|
768
|
+
<p className="truncate text-og-xs font-medium leading-4 text-og-fg">{text}</p>
|
|
769
|
+
<p className="mt-0.5 text-[10px] leading-4 text-og-fg-muted">
|
|
770
|
+
{phase === "submitting"
|
|
771
|
+
? "Sending your latest direction…"
|
|
772
|
+
: "Direction accepted. The agent will continue from it."}
|
|
773
|
+
</p>
|
|
774
|
+
</div>
|
|
775
|
+
</div>
|
|
776
|
+
);
|
|
777
|
+
}
|
|
778
|
+
|
|
664
779
|
function QueuePanel({
|
|
665
780
|
turns,
|
|
666
781
|
readOnly,
|
|
@@ -691,6 +806,8 @@ function QueuePanel({
|
|
|
691
806
|
data-og-session-chrome-panel="queue"
|
|
692
807
|
>
|
|
693
808
|
{turns.map((turn, index) => {
|
|
809
|
+
const presentation = queuedTurnPresentation(turn);
|
|
810
|
+
const voice = presentation.kind !== "prompt";
|
|
694
811
|
const pending = mutationFor(turn.id);
|
|
695
812
|
const beforeUp = index > 0 ? (turns[index - 1]?.id ?? null) : null;
|
|
696
813
|
const beforeDown = index < turns.length - 1 ? (turns[index + 2]?.id ?? null) : null;
|
|
@@ -703,11 +820,18 @@ function QueuePanel({
|
|
|
703
820
|
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
821
|
>
|
|
705
822
|
<div className="flex items-start gap-1.5">
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
823
|
+
{voice ? (
|
|
824
|
+
<AudioLinesIcon
|
|
825
|
+
aria-hidden="true"
|
|
826
|
+
className="mt-0.5 size-3 shrink-0 text-og-accent"
|
|
827
|
+
/>
|
|
828
|
+
) : (
|
|
829
|
+
<span className="mt-px shrink-0 font-og-mono text-[10px] leading-4 text-og-fg-subtle">
|
|
830
|
+
{index + 1}
|
|
831
|
+
</span>
|
|
832
|
+
)}
|
|
709
833
|
<p className="min-w-0 flex-1 truncate text-og-xs leading-4 text-og-fg">
|
|
710
|
-
{
|
|
834
|
+
{presentation.text}
|
|
711
835
|
</p>
|
|
712
836
|
{showActions ? (
|
|
713
837
|
<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">
|
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,
|
|
@@ -225,6 +225,8 @@ export type ComposerState = {
|
|
|
225
225
|
send: (text?: string) => Promise<boolean>;
|
|
226
226
|
/** Supersede current direction with the draft. */
|
|
227
227
|
steer: (text?: string) => Promise<boolean>;
|
|
228
|
+
/** Optimistic-to-durable projection for a Steer that has not started yet. */
|
|
229
|
+
steering?: ComposerSteeringState | null | undefined;
|
|
228
230
|
sending: boolean;
|
|
229
231
|
canSend: boolean;
|
|
230
232
|
/** Pause the session without deleting its prompt queue. */
|
|
@@ -250,6 +252,59 @@ export type ComposerState = {
|
|
|
250
252
|
clearError: () => void;
|
|
251
253
|
};
|
|
252
254
|
|
|
255
|
+
export type ComposerSteeringState = {
|
|
256
|
+
phase: "submitting" | "accepted";
|
|
257
|
+
text: string;
|
|
258
|
+
clientEventId: string | null;
|
|
259
|
+
triggerEventId: string | null;
|
|
260
|
+
turnId: string | null;
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const STEERING_SETTLEMENT_EVENT_TYPES = new Set([
|
|
264
|
+
"turn.started",
|
|
265
|
+
"turn.completed",
|
|
266
|
+
"turn.failed",
|
|
267
|
+
"turn.cancelled",
|
|
268
|
+
"turn.superseded",
|
|
269
|
+
]);
|
|
270
|
+
|
|
271
|
+
function isSteeringSettlementEvent(event: SessionEvent): boolean {
|
|
272
|
+
return STEERING_SETTLEMENT_EVENT_TYPES.has(event.type);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function steeringAcceptedEvent(
|
|
276
|
+
steering: ComposerSteeringState,
|
|
277
|
+
events: readonly SessionEvent[],
|
|
278
|
+
): SessionEvent | undefined {
|
|
279
|
+
return events.find(
|
|
280
|
+
(event) =>
|
|
281
|
+
event.type === "user.message" &&
|
|
282
|
+
steering.clientEventId !== null &&
|
|
283
|
+
event.clientEventId === steering.clientEventId,
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function steeringSettledByEvents(
|
|
288
|
+
steering: ComposerSteeringState,
|
|
289
|
+
events: readonly SessionEvent[],
|
|
290
|
+
): boolean {
|
|
291
|
+
const acceptedEventId =
|
|
292
|
+
steering.triggerEventId ?? steeringAcceptedEvent(steering, events)?.id ?? null;
|
|
293
|
+
return events.some((event) => {
|
|
294
|
+
if (steering.turnId && event.turnId === steering.turnId && isSteeringSettlementEvent(event)) {
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
if (event.type !== "turn.started" || !acceptedEventId) return false;
|
|
298
|
+
const payload = event.payload;
|
|
299
|
+
return (
|
|
300
|
+
typeof payload === "object" &&
|
|
301
|
+
payload !== null &&
|
|
302
|
+
"triggerEventId" in payload &&
|
|
303
|
+
payload.triggerEventId === acceptedEventId
|
|
304
|
+
);
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
253
308
|
/**
|
|
254
309
|
* Draft + send + Pause/Resume state for the chat composer — the only
|
|
255
310
|
* human-to-agent input surface. The draft survives a failed send (nothing is
|
|
@@ -271,6 +326,17 @@ export function useComposer(
|
|
|
271
326
|
// a parent may switch sessionId without remounting this public hook.
|
|
272
327
|
const [stateTargetKey, setStateTargetKey] = useState(targetKey);
|
|
273
328
|
const [sending, setSending] = useState(false);
|
|
329
|
+
const [steering, setSteering] = useState<ComposerSteeringState | null>(() =>
|
|
330
|
+
initialPendingOperation?.delivery === "steer"
|
|
331
|
+
? {
|
|
332
|
+
phase: "submitting",
|
|
333
|
+
text: initialPendingOperation.input.text,
|
|
334
|
+
clientEventId: initialPendingOperation.input.clientEventId ?? null,
|
|
335
|
+
triggerEventId: null,
|
|
336
|
+
turnId: null,
|
|
337
|
+
}
|
|
338
|
+
: null,
|
|
339
|
+
);
|
|
274
340
|
const [pausing, setPausing] = useState(false);
|
|
275
341
|
const [resuming, setResuming] = useState(false);
|
|
276
342
|
const [error, setError] = useState<Error | null>(null);
|
|
@@ -282,6 +348,8 @@ export function useComposer(
|
|
|
282
348
|
() => initialShadow?.resources ?? [],
|
|
283
349
|
);
|
|
284
350
|
const pendingOperationRef = useRef<PendingComposerOperation | null>(initialPendingOperation);
|
|
351
|
+
const steeringSettlementEventsRef = useRef<SessionEvent[]>([]);
|
|
352
|
+
const steeringRef = useRef(steering);
|
|
285
353
|
const pendingClientEventId = useRef<string | null>(
|
|
286
354
|
initialPendingOperation?.input.clientEventId ?? null,
|
|
287
355
|
);
|
|
@@ -303,6 +371,9 @@ export function useComposer(
|
|
|
303
371
|
useLayoutEffect(() => {
|
|
304
372
|
onDraftAppliedRef.current = onDraftApplied;
|
|
305
373
|
}, [onDraftApplied]);
|
|
374
|
+
useLayoutEffect(() => {
|
|
375
|
+
steeringRef.current = steering;
|
|
376
|
+
}, [steering]);
|
|
306
377
|
// Read through a ref so a new extras closure (created every render by
|
|
307
378
|
// callers passing inline functions) does not invalidate `send`.
|
|
308
379
|
const sendExtrasRef = useRef(options.sendExtras);
|
|
@@ -320,6 +391,7 @@ export function useComposer(
|
|
|
320
391
|
targetGeneration.current += 1;
|
|
321
392
|
draftReadGeneration.current += 1;
|
|
322
393
|
pendingOperationRef.current = restorePendingComposerOperation(pendingOperationKey);
|
|
394
|
+
steeringSettlementEventsRef.current = [];
|
|
323
395
|
pendingClientEventId.current = pendingOperationRef.current?.input.clientEventId ?? null;
|
|
324
396
|
const shadow = pendingOperationRef.current?.newerShadow;
|
|
325
397
|
localEditRevision.current = shadow ? 1 : 0;
|
|
@@ -333,6 +405,17 @@ export function useComposer(
|
|
|
333
405
|
setStateTargetKey(targetKey);
|
|
334
406
|
setValue(shadow?.text ?? "");
|
|
335
407
|
setSending(false);
|
|
408
|
+
setSteering(
|
|
409
|
+
pendingOperationRef.current?.delivery === "steer"
|
|
410
|
+
? {
|
|
411
|
+
phase: "submitting",
|
|
412
|
+
text: pendingOperationRef.current.input.text,
|
|
413
|
+
clientEventId: pendingOperationRef.current.input.clientEventId ?? null,
|
|
414
|
+
triggerEventId: null,
|
|
415
|
+
turnId: null,
|
|
416
|
+
}
|
|
417
|
+
: null,
|
|
418
|
+
);
|
|
336
419
|
setPausing(false);
|
|
337
420
|
setResuming(false);
|
|
338
421
|
setError(null);
|
|
@@ -503,18 +586,76 @@ export function useComposer(
|
|
|
503
586
|
if (!sessionId || !durableDrafts) return;
|
|
504
587
|
return registerSessionReconciler(sessionId, "composer", async () => await loadDraft(false));
|
|
505
588
|
}, [durableDrafts, loadDraft, registerSessionReconciler, sessionId]);
|
|
589
|
+
const reconcileSteering = useCallback(async (): Promise<void> => {
|
|
590
|
+
if (!sessionId || !steeringRef.current) return;
|
|
591
|
+
const ownedTargetKey = targetKey;
|
|
592
|
+
let events: SessionEvent[];
|
|
593
|
+
try {
|
|
594
|
+
events = await client.listEvents(workspaceId, sessionId, {
|
|
595
|
+
includeTypes: [
|
|
596
|
+
"user.message",
|
|
597
|
+
"turn.started",
|
|
598
|
+
"turn.completed",
|
|
599
|
+
"turn.failed",
|
|
600
|
+
"turn.cancelled",
|
|
601
|
+
"turn.superseded",
|
|
602
|
+
],
|
|
603
|
+
limit: 250,
|
|
604
|
+
payloadMode: "full",
|
|
605
|
+
});
|
|
606
|
+
} catch {
|
|
607
|
+
// Best effort: the live stream still settles steering when its event arrives.
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
if (targetKeyRef.current !== ownedTargetKey) return;
|
|
611
|
+
setSteering((current) => {
|
|
612
|
+
if (!current) return current;
|
|
613
|
+
if (steeringSettledByEvents(current, events)) {
|
|
614
|
+
steeringSettlementEventsRef.current = [];
|
|
615
|
+
return null;
|
|
616
|
+
}
|
|
617
|
+
const accepted = steeringAcceptedEvent(current, events);
|
|
618
|
+
if (!accepted || current.triggerEventId) return current;
|
|
619
|
+
return {
|
|
620
|
+
...current,
|
|
621
|
+
phase: "accepted",
|
|
622
|
+
triggerEventId: accepted.id,
|
|
623
|
+
};
|
|
624
|
+
});
|
|
625
|
+
}, [client, sessionId, targetKey, workspaceId]);
|
|
506
626
|
useSessionEventTrigger(
|
|
507
627
|
client,
|
|
508
628
|
workspaceId,
|
|
509
629
|
sessionId,
|
|
510
|
-
isComposerDraftEvent,
|
|
511
|
-
() =>
|
|
630
|
+
(event) => isComposerDraftEvent(event) || isSteeringSettlementEvent(event),
|
|
631
|
+
(event) => {
|
|
632
|
+
if (isComposerDraftEvent(event)) void loadDraft(false);
|
|
633
|
+
if (!isSteeringSettlementEvent(event)) return;
|
|
634
|
+
steeringSettlementEventsRef.current = [
|
|
635
|
+
...steeringSettlementEventsRef.current.slice(-15),
|
|
636
|
+
event,
|
|
637
|
+
];
|
|
638
|
+
setSteering((current) => {
|
|
639
|
+
if (!current || !steeringSettledByEvents(current, [event])) return current;
|
|
640
|
+
steeringSettlementEventsRef.current = [];
|
|
641
|
+
return null;
|
|
642
|
+
});
|
|
643
|
+
},
|
|
512
644
|
{
|
|
513
|
-
enabled: Boolean(sessionId) && durableDrafts,
|
|
645
|
+
enabled: Boolean(sessionId) && (durableDrafts || steering !== null),
|
|
514
646
|
...(options.events !== undefined ? { events: options.events } : {}),
|
|
515
647
|
},
|
|
648
|
+
reconcileSteering,
|
|
516
649
|
);
|
|
517
650
|
|
|
651
|
+
useEffect(() => {
|
|
652
|
+
if (!steering) return;
|
|
653
|
+
const observed = [...(options.events ?? []), ...steeringSettlementEventsRef.current];
|
|
654
|
+
if (!steeringSettledByEvents(steering, observed)) return;
|
|
655
|
+
steeringSettlementEventsRef.current = [];
|
|
656
|
+
setSteering(null);
|
|
657
|
+
}, [options.events, steering]);
|
|
658
|
+
|
|
518
659
|
const currentDraftPayload = useCallback((): SaveComposerDraftRequest | null => {
|
|
519
660
|
if (!durableDrafts || targetKeyRef.current !== targetKey) return null;
|
|
520
661
|
const base = draftRef.current;
|
|
@@ -667,6 +808,8 @@ export function useComposer(
|
|
|
667
808
|
forgetPendingComposerOperation(operationKey);
|
|
668
809
|
};
|
|
669
810
|
|
|
811
|
+
let keepSteering = pending?.delivery === "steer";
|
|
812
|
+
|
|
670
813
|
const settleAccepted = (operation: PendingComposerOperation): void => {
|
|
671
814
|
clearPending();
|
|
672
815
|
const draftWasUnchanged = valueRef.current === operation.draftAtSend;
|
|
@@ -699,30 +842,40 @@ export function useComposer(
|
|
|
699
842
|
onSent?.(operation.input.text, operation.input);
|
|
700
843
|
};
|
|
701
844
|
|
|
702
|
-
const deliver = async (operation: PendingComposerOperation)
|
|
845
|
+
const deliver = async (operation: PendingComposerOperation) => {
|
|
703
846
|
if (operation.delivery === "steer") {
|
|
704
|
-
await client.steerMessage(workspaceId, sessionId, operation.input);
|
|
705
|
-
} else {
|
|
706
|
-
await client.sendMessage(workspaceId, sessionId, operation.input);
|
|
847
|
+
return await client.steerMessage(workspaceId, sessionId, operation.input);
|
|
707
848
|
}
|
|
849
|
+
await client.sendMessage(workspaceId, sessionId, operation.input);
|
|
850
|
+
return null;
|
|
708
851
|
};
|
|
709
852
|
|
|
853
|
+
if (delivery === "steer") {
|
|
854
|
+
setSteering({
|
|
855
|
+
phase: "submitting",
|
|
856
|
+
text: rawText,
|
|
857
|
+
clientEventId: pending?.input.clientEventId ?? pendingClientEventId.current,
|
|
858
|
+
triggerEventId: null,
|
|
859
|
+
turnId: null,
|
|
860
|
+
});
|
|
861
|
+
}
|
|
710
862
|
setSending(true);
|
|
711
863
|
setError(null);
|
|
712
864
|
try {
|
|
713
865
|
if (pending) {
|
|
714
|
-
let
|
|
866
|
+
let acceptedEvent: SessionEvent | null = null;
|
|
715
867
|
try {
|
|
716
868
|
const events = await client.listEvents(workspaceId, sessionId, {
|
|
717
869
|
includeTypes: ["user.message"],
|
|
718
870
|
limit: 100,
|
|
719
871
|
payloadMode: "none",
|
|
720
872
|
});
|
|
721
|
-
|
|
722
|
-
(
|
|
723
|
-
event
|
|
724
|
-
|
|
725
|
-
|
|
873
|
+
acceptedEvent =
|
|
874
|
+
events.find(
|
|
875
|
+
(event) =>
|
|
876
|
+
event.type === "user.message" &&
|
|
877
|
+
event.clientEventId === pending.input.clientEventId,
|
|
878
|
+
) ?? null;
|
|
726
879
|
} catch (cause) {
|
|
727
880
|
if (
|
|
728
881
|
targetKeyRef.current === ownedTargetKey &&
|
|
@@ -738,7 +891,17 @@ export function useComposer(
|
|
|
738
891
|
) {
|
|
739
892
|
return false;
|
|
740
893
|
}
|
|
741
|
-
if (
|
|
894
|
+
if (acceptedEvent) {
|
|
895
|
+
if (pending.delivery === "steer") {
|
|
896
|
+
keepSteering = true;
|
|
897
|
+
setSteering({
|
|
898
|
+
phase: "accepted",
|
|
899
|
+
text: pending.input.text,
|
|
900
|
+
clientEventId: pending.input.clientEventId ?? null,
|
|
901
|
+
triggerEventId: acceptedEvent.id,
|
|
902
|
+
turnId: null,
|
|
903
|
+
});
|
|
904
|
+
}
|
|
742
905
|
settleAccepted(pending);
|
|
743
906
|
return true;
|
|
744
907
|
}
|
|
@@ -751,8 +914,19 @@ export function useComposer(
|
|
|
751
914
|
return false;
|
|
752
915
|
}
|
|
753
916
|
try {
|
|
754
|
-
await deliver(pending);
|
|
917
|
+
const result = await deliver(pending);
|
|
918
|
+
if (pending.delivery === "steer" && result) {
|
|
919
|
+
keepSteering = true;
|
|
920
|
+
setSteering({
|
|
921
|
+
phase: "accepted",
|
|
922
|
+
text: pending.input.text,
|
|
923
|
+
clientEventId: pending.input.clientEventId ?? null,
|
|
924
|
+
triggerEventId: result.accepted.id,
|
|
925
|
+
turnId: result.turn.id,
|
|
926
|
+
});
|
|
927
|
+
}
|
|
755
928
|
} catch (cause) {
|
|
929
|
+
if (pending.delivery === "steer") keepSteering = true;
|
|
756
930
|
if (
|
|
757
931
|
targetKeyRef.current === ownedTargetKey &&
|
|
758
932
|
targetGeneration.current === ownedGeneration
|
|
@@ -812,11 +986,32 @@ export function useComposer(
|
|
|
812
986
|
};
|
|
813
987
|
pendingOperationRef.current = operation;
|
|
814
988
|
rememberPendingComposerOperation(operationKey, operation);
|
|
989
|
+
if (delivery === "steer") {
|
|
990
|
+
setSteering({
|
|
991
|
+
phase: "submitting",
|
|
992
|
+
text: sendText,
|
|
993
|
+
clientEventId: input.clientEventId ?? null,
|
|
994
|
+
triggerEventId: null,
|
|
995
|
+
turnId: null,
|
|
996
|
+
});
|
|
997
|
+
}
|
|
815
998
|
try {
|
|
816
|
-
await deliver(operation);
|
|
999
|
+
const result = await deliver(operation);
|
|
1000
|
+
if (delivery === "steer" && result) {
|
|
1001
|
+
keepSteering = true;
|
|
1002
|
+
setSteering({
|
|
1003
|
+
phase: "accepted",
|
|
1004
|
+
text: sendText,
|
|
1005
|
+
clientEventId: input.clientEventId ?? null,
|
|
1006
|
+
triggerEventId: result.accepted.id,
|
|
1007
|
+
turnId: result.turn.id,
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
817
1010
|
} catch (cause) {
|
|
818
1011
|
if (!isOutcomeUnknownError(cause)) {
|
|
819
1012
|
clearPending();
|
|
1013
|
+
} else if (delivery === "steer") {
|
|
1014
|
+
keepSteering = true;
|
|
820
1015
|
}
|
|
821
1016
|
if (
|
|
822
1017
|
targetKeyRef.current === ownedTargetKey &&
|
|
@@ -840,6 +1035,7 @@ export function useComposer(
|
|
|
840
1035
|
targetGeneration.current === ownedGeneration
|
|
841
1036
|
) {
|
|
842
1037
|
setSending(false);
|
|
1038
|
+
if (delivery === "steer" && !keepSteering) setSteering(null);
|
|
843
1039
|
}
|
|
844
1040
|
}
|
|
845
1041
|
},
|
|
@@ -1095,6 +1291,7 @@ export function useComposer(
|
|
|
1095
1291
|
hasDraftContent,
|
|
1096
1292
|
send,
|
|
1097
1293
|
steer,
|
|
1294
|
+
steering: identityMatches ? steering : null,
|
|
1098
1295
|
sending: identityMatches ? sending : false,
|
|
1099
1296
|
canSend:
|
|
1100
1297
|
identityMatches &&
|
package/src/index.ts
CHANGED
|
@@ -393,6 +393,19 @@ export { defaultChatComposerMessages } from "./components/composer";
|
|
|
393
393
|
export type { ChatComposerMessages } from "./components/composer";
|
|
394
394
|
export { ModelPicker } from "./components/model-picker";
|
|
395
395
|
export type { ModelPickerProps } from "./components/model-picker";
|
|
396
|
+
export {
|
|
397
|
+
BillingClassMark,
|
|
398
|
+
ModelPolicyPicker,
|
|
399
|
+
ModelPolicyPickerMenu,
|
|
400
|
+
PickerAnimatedPage,
|
|
401
|
+
PickerBackHeader,
|
|
402
|
+
PickerNavRow,
|
|
403
|
+
defaultModelPolicyPickerMessages,
|
|
404
|
+
} from "./components/model-policy-picker";
|
|
405
|
+
export type {
|
|
406
|
+
ModelPolicyPickerMessages,
|
|
407
|
+
ModelPolicyPickerProps,
|
|
408
|
+
} from "./components/model-policy-picker";
|
|
396
409
|
export {
|
|
397
410
|
advancedSourceSummary,
|
|
398
411
|
availabilityReasonLabel,
|
|
@@ -404,7 +417,9 @@ export {
|
|
|
404
417
|
findPickerRow,
|
|
405
418
|
groupPickerRowsByBillingClass,
|
|
406
419
|
labelLatencyMode,
|
|
420
|
+
labelReasoningEffort,
|
|
407
421
|
payerSummaryForModel,
|
|
422
|
+
projectClientModelRows,
|
|
408
423
|
projectPickerRows,
|
|
409
424
|
runnableLatencyModesForModel,
|
|
410
425
|
sortPickerRows,
|