@opengeni/react 0.46.1 → 0.52.1
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 +158 -14
- package/dist/artifacts-document.d.ts +3 -0
- package/dist/artifacts-document.js +17 -0
- package/dist/artifacts-document.js.map +1 -0
- package/dist/artifacts-presentation.d.ts +3 -0
- package/dist/artifacts-presentation.js +17 -0
- package/dist/artifacts-presentation.js.map +1 -0
- package/dist/artifacts-spreadsheet.d.ts +3 -0
- package/dist/artifacts-spreadsheet.js +17 -0
- package/dist/artifacts-spreadsheet.js.map +1 -0
- package/dist/artifacts.d.ts +2 -0
- package/dist/artifacts.js +170 -0
- package/dist/artifacts.js.map +1 -0
- package/dist/chunk-6BBWWOOU.js +82 -0
- package/dist/chunk-6BBWWOOU.js.map +1 -0
- package/dist/{chunk-HFO4ERGQ.js → chunk-724BW4TW.js} +586 -35
- package/dist/chunk-724BW4TW.js.map +1 -0
- package/dist/chunk-7I2KB3OK.js +3895 -0
- package/dist/chunk-7I2KB3OK.js.map +1 -0
- package/dist/chunk-7WCLAEFV.js +2828 -0
- package/dist/chunk-7WCLAEFV.js.map +1 -0
- package/dist/{chunk-U3CSCMWA.js → chunk-FK6VG4LL.js} +9 -13
- package/dist/chunk-FK6VG4LL.js.map +1 -0
- package/dist/{chunk-SRFUT2ZU.js → chunk-FWS3KC3Z.js} +120 -64
- package/dist/chunk-FWS3KC3Z.js.map +1 -0
- package/dist/{chunk-EKZH6IDG.js → chunk-HDBG7XGO.js} +62 -1
- package/dist/chunk-HDBG7XGO.js.map +1 -0
- package/dist/chunk-HUIKIDM5.js +2148 -0
- package/dist/chunk-HUIKIDM5.js.map +1 -0
- package/dist/{chunk-LGVMUIRV.js → chunk-JAGDUCZQ.js} +1053 -390
- package/dist/chunk-JAGDUCZQ.js.map +1 -0
- package/dist/{chunk-LWR4MXSS.js → chunk-JR3QFY6B.js} +420 -35
- package/dist/chunk-JR3QFY6B.js.map +1 -0
- package/dist/chunk-JVYQRRC4.js +53 -0
- package/dist/chunk-JVYQRRC4.js.map +1 -0
- package/dist/chunk-KCGXPP5V.js +157 -0
- package/dist/chunk-KCGXPP5V.js.map +1 -0
- package/dist/{chunk-EB67J347.js → chunk-KZ73RL76.js} +111 -31
- package/dist/chunk-KZ73RL76.js.map +1 -0
- package/dist/chunk-PEI6RANI.js +3234 -0
- package/dist/chunk-PEI6RANI.js.map +1 -0
- package/dist/{chunk-3PB7MIT6.js → chunk-ROIRTM37.js} +220 -72
- package/dist/chunk-ROIRTM37.js.map +1 -0
- package/dist/chunk-VZTQ73XT.js +86 -0
- package/dist/chunk-VZTQ73XT.js.map +1 -0
- package/dist/{chunk-VCBFUSUZ.js → chunk-WSK7G5LE.js} +72 -4
- package/dist/chunk-WSK7G5LE.js.map +1 -0
- package/dist/{chunk-FGZUCXZF.js → chunk-WYA65Y3F.js} +16 -16
- package/dist/chunk-WYA65Y3F.js.map +1 -0
- package/dist/{chunk-6EDV6YFE.js → chunk-X4S56Y2H.js} +147 -35
- package/dist/chunk-X4S56Y2H.js.map +1 -0
- package/dist/client.d.ts +7 -1
- package/dist/clipboard.d.ts +1 -0
- package/dist/clipboard.js +7 -0
- package/dist/clipboard.js.map +1 -0
- package/dist/commands/index.d.ts +3 -3
- package/dist/commands/registry.d.ts +1 -1
- package/dist/commands/types.d.ts +1 -1
- package/dist/components/approval-surface.d.ts +2 -2
- package/dist/components/artifacts/artifact-surface.d.ts +18 -0
- package/dist/components/artifacts/document-editor.d.ts +172 -0
- package/dist/components/artifacts/editable-artifact-ui.d.ts +13 -0
- package/dist/components/artifacts/editable-artifact-workbench.d.ts +49 -0
- package/dist/components/artifacts/editable-document.d.ts +24 -0
- package/dist/components/artifacts/editable-presentation.d.ts +24 -0
- package/dist/components/artifacts/editable-spreadsheet.d.ts +32 -0
- package/dist/components/artifacts/index.d.ts +8 -0
- package/dist/components/artifacts/presentation-editor.d.ts +297 -0
- package/dist/components/artifacts/spreadsheet-canvas.d.ts +120 -0
- package/dist/components/artifacts/spreadsheet-grid.d.ts +113 -0
- package/dist/components/browser-viewer.d.ts +29 -0
- package/dist/components/chat-composer.d.ts +13 -8
- package/dist/components/code-editor.d.ts +25 -3
- package/dist/components/command-palette.d.ts +2 -2
- package/dist/components/composer-transcription-control.d.ts +4 -2
- package/dist/components/composer.d.ts +29 -15
- package/dist/components/computer-viewer.d.ts +23 -0
- package/dist/components/copy-button.d.ts +2 -2
- package/dist/components/desktop-viewer.d.ts +25 -21
- package/dist/components/diff-view.d.ts +1 -1
- package/dist/components/enrollment-consent.d.ts +1 -1
- package/dist/components/enrollment-device-flow.d.ts +1 -1
- package/dist/components/file-browser.d.ts +2 -2
- package/dist/components/fleet-tile.d.ts +1 -1
- package/dist/components/generated-video-player.d.ts +10 -0
- package/dist/components/human-input-form.d.ts +1 -1
- package/dist/components/human-input-surface.d.ts +2 -2
- package/dist/components/interactive-workbench-viewers.d.ts +2 -0
- package/dist/components/machine-card.d.ts +2 -2
- package/dist/components/machine-dock-bar.d.ts +3 -3
- package/dist/components/machine-health-pill.d.ts +2 -2
- package/dist/components/machine-input-display.d.ts +1 -1
- package/dist/components/machine-metrics.d.ts +2 -2
- package/dist/components/machine-status-pill.d.ts +4 -4
- package/dist/components/machines/health.d.ts +1 -1
- package/dist/components/machines/machine-detail.d.ts +5 -3
- package/dist/components/machines/metric-history-chart.d.ts +1 -1
- package/dist/components/machines/metric-sparkline.d.ts +2 -2
- package/dist/components/machines/series.d.ts +2 -2
- package/dist/components/machines-dashboard.d.ts +5 -5
- package/dist/components/markdown.d.ts +1 -1
- package/dist/components/message-timeline.d.ts +22 -5
- package/dist/components/model-picker.d.ts +2 -2
- package/dist/components/model-policy-picker.d.ts +7 -7
- package/dist/components/pierre-diff.d.ts +1 -1
- package/dist/components/pierre-file.d.ts +1 -1
- package/dist/components/queue-draft-policy.d.ts +1 -1
- package/dist/components/queue-surface-implementation.d.ts +3 -3
- package/dist/components/queue-surface-state.d.ts +4 -4
- package/dist/components/queue-surface.d.ts +5 -5
- package/dist/components/sandbox-files.d.ts +5 -5
- package/dist/components/sandbox-terminal.d.ts +50 -4
- package/dist/components/sandbox-workspace.d.ts +30 -10
- package/dist/components/session-chrome.d.ts +4 -4
- package/dist/components/session-status.d.ts +2 -2
- package/dist/components/timeline-annotation-selection.d.ts +9 -0
- package/dist/components/timeline-annotations-dialog.d.ts +16 -0
- package/dist/components/timeline-annotations.d.ts +13 -0
- package/dist/components/tooltip.d.ts +4 -4
- package/dist/components/user-message-body.d.ts +2 -2
- package/dist/components/workbench-changes.d.ts +2 -2
- package/dist/components/workspace-dock.d.ts +3 -1
- package/dist/composer.d.ts +11 -11
- package/dist/composer.js +7 -7
- package/dist/hooks/internal.d.ts +7 -1
- package/dist/hooks/use-attached-browsers.d.ts +21 -0
- package/dist/hooks/use-available-models.d.ts +2 -2
- package/dist/hooks/use-billing-usage.d.ts +1 -1
- package/dist/hooks/use-browser-frame-stream.d.ts +28 -0
- package/dist/hooks/use-browser-identities.d.ts +26 -0
- package/dist/hooks/use-browser-session.d.ts +28 -0
- package/dist/hooks/use-browser-sessions.d.ts +32 -0
- package/dist/hooks/use-codex-accounts.d.ts +3 -3
- package/dist/hooks/use-composer.d.ts +36 -9
- package/dist/hooks/use-computer-frame-stream.d.ts +24 -0
- package/dist/hooks/use-computer-session.d.ts +28 -0
- package/dist/hooks/use-computer-sessions.d.ts +28 -0
- package/dist/hooks/use-desktop-stream.d.ts +1 -1
- package/dist/hooks/use-environments.d.ts +11 -5
- package/dist/hooks/use-file-attachments.d.ts +1 -1
- package/dist/hooks/use-goal.d.ts +2 -2
- package/dist/hooks/use-human-input.d.ts +2 -2
- package/dist/hooks/use-last-started-turn-policy.d.ts +2 -2
- package/dist/hooks/use-machine-chip.d.ts +2 -2
- package/dist/hooks/use-machines.d.ts +10 -3
- package/dist/hooks/use-packs.d.ts +7 -2
- package/dist/hooks/use-rigs.d.ts +1 -1
- package/dist/hooks/use-sandbox-files.d.ts +25 -4
- package/dist/hooks/use-sandbox-git.d.ts +13 -5
- package/dist/hooks/use-sandbox-terminal.d.ts +1 -1
- package/dist/hooks/use-scheduled-tasks.d.ts +1 -1
- package/dist/hooks/use-session-capabilities.d.ts +5 -4
- package/dist/hooks/use-session-control.d.ts +1 -1
- package/dist/hooks/use-session-events.d.ts +2 -2
- package/dist/hooks/use-session-lineage.d.ts +1 -1
- package/dist/hooks/use-session-mcp-approval-policy.d.ts +2 -2
- package/dist/hooks/use-session.d.ts +2 -2
- package/dist/hooks/use-slash-commands.d.ts +1 -1
- package/dist/hooks/use-terminal-stream.d.ts +8 -2
- package/dist/hooks/use-transcription.d.ts +2 -2
- package/dist/hooks/use-turn-queue.d.ts +2 -2
- package/dist/hooks/use-video-artifact-playback.d.ts +9 -0
- package/dist/hooks/use-voice-input.d.ts +6 -4
- package/dist/hooks/use-workspace-capture.d.ts +1 -1
- package/dist/hooks/use-workspace-edit.d.ts +1 -1
- package/dist/hooks/use-workspace-sessions.d.ts +1 -1
- package/dist/hooks/use-workspaces.d.ts +1 -1
- package/dist/index.d.ts +158 -154
- package/dist/index.js +1718 -908
- package/dist/index.js.map +1 -1
- package/dist/interaction.d.ts +23 -0
- package/dist/interaction.js +36 -0
- package/dist/interaction.js.map +1 -0
- package/dist/interactive-workbench-viewers-KHNOHWQE.js +13 -0
- package/dist/interactive-workbench-viewers-KHNOHWQE.js.map +1 -0
- package/dist/lib/composer-responsive-context.d.ts +9 -0
- package/dist/lib/relay-wire.d.ts +4 -0
- package/dist/lib/sandbox-liveness.d.ts +9 -0
- package/dist/lib/terminal-capability.d.ts +5 -0
- package/dist/lib/use-portal-token-style.d.ts +14 -3
- package/dist/lib/xterm-theme.d.ts +1 -1
- package/dist/machines.d.ts +30 -30
- package/dist/machines.js +3 -3
- package/dist/provider.d.ts +4 -4
- package/dist/{queue-surface-implementation-J4XJFKRW.js → queue-surface-implementation-2KVJTYXH.js} +117 -64
- package/dist/queue-surface-implementation-2KVJTYXH.js.map +1 -0
- package/dist/realtime/dropdown-menu.d.ts +8 -8
- package/dist/realtime/realtime-control.d.ts +15 -6
- package/dist/realtime.d.ts +4 -4
- package/dist/realtime.js +30 -33
- package/dist/realtime.js.map +1 -1
- package/dist/session-context.d.ts +19 -1
- package/dist/session-ui.d.ts +14 -14
- package/dist/session-ui.js +6 -5
- package/dist/session.d.ts +28 -28
- package/dist/session.js +5 -5
- package/dist/timeline/activity-rail.d.ts +6 -3
- package/dist/timeline/disclosure-context.d.ts +1 -1
- package/dist/timeline/fleet-decision-projection.d.ts +1 -1
- package/dist/timeline/fleet-decision-row.d.ts +3 -3
- package/dist/timeline/index.d.ts +19 -19
- package/dist/timeline/parsers.d.ts +6 -3
- package/dist/timeline/projection.d.ts +3 -3
- package/dist/timeline/registry.d.ts +10 -1
- package/dist/timeline/screenshot-lightbox.d.ts +2 -2
- package/dist/timeline/shared.d.ts +21 -10
- package/dist/timeline/tool-diff.d.ts +2 -2
- package/dist/timeline/tool-renderers.d.ts +1 -1
- package/dist/timeline/turn-summary.d.ts +3 -3
- package/dist/timeline/types.d.ts +42 -2
- package/dist/timeline-annotation-selection-3L7LHGG2.js +187 -0
- package/dist/timeline-annotation-selection-3L7LHGG2.js.map +1 -0
- package/dist/timeline-annotations-dialog-U7EVLR75.js +196 -0
- package/dist/timeline-annotations-dialog-U7EVLR75.js.map +1 -0
- package/dist/voice-recording-store.d.ts +8 -2
- package/package.json +52 -7
- package/src/artifacts-document.ts +31 -0
- package/src/artifacts-presentation.ts +36 -0
- package/src/artifacts-spreadsheet.ts +21 -0
- package/src/artifacts.ts +86 -0
- package/src/client.ts +90 -1
- package/src/clipboard.ts +2 -0
- package/src/components/artifacts/artifact-surface.tsx +93 -0
- package/src/components/artifacts/document-editor.tsx +2255 -0
- package/src/components/artifacts/editable-artifact-ui.tsx +89 -0
- package/src/components/artifacts/editable-artifact-workbench.tsx +203 -0
- package/src/components/artifacts/editable-document.tsx +657 -0
- package/src/components/artifacts/editable-presentation.tsx +1020 -0
- package/src/components/artifacts/editable-spreadsheet.tsx +733 -0
- package/src/components/artifacts/index.ts +99 -0
- package/src/components/artifacts/presentation-editor.tsx +3148 -0
- package/src/components/artifacts/spreadsheet-canvas.ts +799 -0
- package/src/components/artifacts/spreadsheet-grid.tsx +2129 -0
- package/src/components/browser-viewer.tsx +1592 -0
- package/src/components/chat-composer.tsx +25 -2
- package/src/components/code-editor.tsx +90 -31
- package/src/components/command-palette.tsx +1 -1
- package/src/components/composer-transcription-control.tsx +51 -22
- package/src/components/composer.tsx +49 -23
- package/src/components/computer-viewer.tsx +1139 -0
- package/src/components/copy-button.tsx +1 -1
- package/src/components/desktop-viewer.tsx +103 -75
- package/src/components/file-browser.tsx +39 -17
- package/src/components/generated-video-player.tsx +89 -0
- package/src/components/human-input-form.tsx +2 -2
- package/src/components/interactive-workbench-viewers.ts +2 -0
- package/src/components/machine-card.tsx +1 -1
- package/src/components/machine-input-display.ts +3 -0
- package/src/components/machines/machine-detail.tsx +39 -15
- package/src/components/machines-dashboard.tsx +7 -6
- package/src/components/message-timeline.tsx +327 -37
- package/src/components/model-picker.tsx +2 -2
- package/src/components/model-policy-picker.tsx +16 -15
- package/src/components/queue-surface-implementation.tsx +135 -75
- package/src/components/queue-surface-state.tsx +2 -2
- package/src/components/sandbox-files.tsx +142 -62
- package/src/components/sandbox-terminal.tsx +119 -35
- package/src/components/sandbox-workspace.tsx +394 -228
- package/src/components/session-chrome.tsx +67 -27
- package/src/components/timeline-annotation-selection.tsx +221 -0
- package/src/components/timeline-annotations-dialog.tsx +213 -0
- package/src/components/timeline-annotations.tsx +84 -0
- package/src/components/tooltip.tsx +13 -16
- package/src/components/user-message-body.tsx +1 -1
- package/src/components/workbench-changes.tsx +192 -35
- package/src/components/workspace-dock.tsx +13 -5
- package/src/composer.ts +3 -0
- package/src/hooks/internal.ts +132 -38
- package/src/hooks/use-attached-browsers.ts +152 -0
- package/src/hooks/use-browser-frame-stream.ts +316 -0
- package/src/hooks/use-browser-identities.ts +225 -0
- package/src/hooks/use-browser-session.ts +476 -0
- package/src/hooks/use-browser-sessions.ts +281 -0
- package/src/hooks/use-codex-accounts.ts +4 -4
- package/src/hooks/use-composer.ts +769 -24
- package/src/hooks/use-computer-frame-stream.ts +311 -0
- package/src/hooks/use-computer-session.ts +442 -0
- package/src/hooks/use-computer-sessions.ts +245 -0
- package/src/hooks/use-environments.ts +24 -5
- package/src/hooks/use-file-attachments.ts +70 -32
- package/src/hooks/use-goal.ts +17 -5
- package/src/hooks/use-machine-chip.ts +5 -3
- package/src/hooks/use-machines.ts +79 -3
- package/src/hooks/use-packs.ts +51 -0
- package/src/hooks/use-sandbox-files.ts +382 -88
- package/src/hooks/use-sandbox-git.ts +208 -46
- package/src/hooks/use-sandbox-terminal.ts +3 -2
- package/src/hooks/use-session-capabilities.ts +94 -70
- package/src/hooks/use-session-events.ts +18 -4
- package/src/hooks/use-terminal-stream.ts +102 -23
- package/src/hooks/use-transcription.ts +4 -14
- package/src/hooks/use-turn-queue.ts +17 -5
- package/src/hooks/use-video-artifact-playback.ts +28 -0
- package/src/hooks/use-voice-input.ts +163 -19
- package/src/hooks/use-windowed-sections.ts +9 -7
- package/src/hooks/use-workspace-edit.ts +2 -1
- package/src/index.ts +21 -6
- package/src/interaction.ts +71 -0
- package/src/lib/composer-responsive-context.ts +15 -0
- package/src/lib/relay-wire.ts +4 -0
- package/src/lib/sandbox-liveness.ts +11 -0
- package/src/lib/terminal-capability.ts +10 -0
- package/src/lib/use-portal-token-style.ts +60 -15
- package/src/provider.tsx +52 -7
- package/src/realtime/dropdown-menu.tsx +15 -19
- package/src/realtime/realtime-control.tsx +16 -5
- package/src/session-context.ts +90 -0
- package/src/timeline/activity-rail.tsx +31 -6
- package/src/timeline/index.ts +4 -1
- package/src/timeline/parsers.ts +46 -20
- package/src/timeline/projection.ts +233 -13
- package/src/timeline/registry.ts +19 -0
- package/src/timeline/screenshot-lightbox.tsx +30 -9
- package/src/timeline/shared.tsx +59 -11
- package/src/timeline/tool-renderers.tsx +421 -24
- package/src/timeline/turn-summary.tsx +2 -2
- package/src/timeline/types.ts +58 -2
- package/src/voice-recording-store.ts +15 -0
- package/styles/compiled.css +5298 -0
- package/styles/compiled.d.ts +2 -0
- package/styles/effective-tokens.css +27 -0
- package/styles/index.css +18 -0
- package/styles/responsive.css +208 -0
- package/styles/responsive.d.ts +2 -0
- package/styles/tokens.css +50 -17
- package/dist/chunk-3PB7MIT6.js.map +0 -1
- package/dist/chunk-6EDV6YFE.js.map +0 -1
- package/dist/chunk-EB67J347.js.map +0 -1
- package/dist/chunk-EKZH6IDG.js.map +0 -1
- package/dist/chunk-FGZUCXZF.js.map +0 -1
- package/dist/chunk-HFO4ERGQ.js.map +0 -1
- package/dist/chunk-JB5725BI.js +0 -48
- package/dist/chunk-JB5725BI.js.map +0 -1
- package/dist/chunk-LGVMUIRV.js.map +0 -1
- package/dist/chunk-LWR4MXSS.js.map +0 -1
- package/dist/chunk-SRFUT2ZU.js.map +0 -1
- package/dist/chunk-U3CSCMWA.js.map +0 -1
- package/dist/chunk-VCBFUSUZ.js.map +0 -1
- package/dist/fleet-decision-projection-SLNZMS42.js +0 -241
- package/dist/fleet-decision-projection-SLNZMS42.js.map +0 -1
- package/dist/queue-surface-implementation-J4XJFKRW.js.map +0 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
DEFAULT_FILE_RESOURCE_MOUNT_ROOT,
|
|
3
3
|
type ComposerDraft,
|
|
4
|
+
type DraftTimelineAnnotation,
|
|
4
5
|
type EffectiveControlResumeOption,
|
|
5
6
|
type EffectiveSessionControl,
|
|
6
7
|
type OpenGeniApiError,
|
|
@@ -13,10 +14,12 @@ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react
|
|
|
13
14
|
import { useEmbeddedSession, type EmbeddedSessionClientOverride } from "../session-context";
|
|
14
15
|
import { useSessionEventTrigger, type SessionEventFeedOptions } from "./internal";
|
|
15
16
|
|
|
16
|
-
export type ComposerSendExtras = Omit<SendMessageInput, "text" | "clientEventId">;
|
|
17
|
+
export type ComposerSendExtras = Omit<SendMessageInput, "text" | "clientEventId" | "annotations">;
|
|
17
18
|
|
|
18
19
|
export type UseComposerOptions = EmbeddedSessionClientOverride &
|
|
19
20
|
SessionEventFeedOptions & {
|
|
21
|
+
/** Called synchronously after an ordinary Send is accepted by the local UI. */
|
|
22
|
+
onSubmitted?: ((text: string, input: SendMessageInput) => void) | undefined;
|
|
20
23
|
/** Called with the exact accepted wire input after a successful send. */
|
|
21
24
|
onSent?: ((text: string, input: SendMessageInput) => void) | undefined;
|
|
22
25
|
/**
|
|
@@ -42,6 +45,7 @@ export type UseComposerOptions = EmbeddedSessionClientOverride &
|
|
|
42
45
|
type ComposerDraftShadow = {
|
|
43
46
|
text: string;
|
|
44
47
|
resources: ResourceRef[];
|
|
48
|
+
annotations: DraftTimelineAnnotation[];
|
|
45
49
|
};
|
|
46
50
|
|
|
47
51
|
type PendingComposerOperation = {
|
|
@@ -49,6 +53,7 @@ type PendingComposerOperation = {
|
|
|
49
53
|
input: SendMessageInput;
|
|
50
54
|
draftAtSend: string;
|
|
51
55
|
resourcesAtSend: ResourceRef[];
|
|
56
|
+
annotationsAtSend: DraftTimelineAnnotation[];
|
|
52
57
|
/** Latest local text/resources that must survive an uncertain delivery. */
|
|
53
58
|
newerShadow: ComposerDraftShadow;
|
|
54
59
|
clearDraftOnAccept: boolean;
|
|
@@ -61,13 +66,39 @@ type StoredPendingComposerOperation = Omit<PendingComposerOperation, "input" | "
|
|
|
61
66
|
hasMcpCredentialUpdates: boolean;
|
|
62
67
|
};
|
|
63
68
|
|
|
69
|
+
export type ComposerOptimisticMessage = {
|
|
70
|
+
clientEventId: string;
|
|
71
|
+
text: string;
|
|
72
|
+
annotations: DraftTimelineAnnotation[];
|
|
73
|
+
resources: ResourceRef[];
|
|
74
|
+
occurredAt: string;
|
|
75
|
+
state: "sending" | "queued" | "failed";
|
|
76
|
+
error?: string | undefined;
|
|
77
|
+
outcomeUnknown?: boolean | undefined;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
type OptimisticSendOperation = ComposerOptimisticMessage & {
|
|
81
|
+
input: SendMessageInput;
|
|
82
|
+
draftPayload: SaveComposerDraftRequest | null;
|
|
83
|
+
/** Latest unsent local draft that must survive this operation and remounts. */
|
|
84
|
+
newerShadow: ComposerDraftShadow;
|
|
85
|
+
canRetry: boolean;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
type StoredOptimisticSendOperation = Omit<OptimisticSendOperation, "input" | "canRetry"> & {
|
|
89
|
+
input: Omit<SendMessageInput, "mcpCredentialUpdates">;
|
|
90
|
+
hasMcpCredentialUpdates: boolean;
|
|
91
|
+
};
|
|
92
|
+
|
|
64
93
|
const PENDING_COMPOSER_STORAGE_PREFIX = "opengeni.pending-composer.v1:";
|
|
94
|
+
const OPTIMISTIC_SEND_STORAGE_PREFIX = "opengeni.optimistic-sends.v1:";
|
|
65
95
|
|
|
66
96
|
// A remount must not manufacture a new operation while the previous mutation
|
|
67
97
|
// is still outcome-unknown. Keep only non-credential request fields here; the
|
|
68
98
|
// mounted hook retains the exact input, including any credential updates. The
|
|
69
99
|
// safe shadow is also session-scoped so a refresh cannot lose newer edits.
|
|
70
100
|
const pendingComposerOperations = new Map<string, StoredPendingComposerOperation>();
|
|
101
|
+
const optimisticSendOperations = new Map<string, StoredOptimisticSendOperation[]>();
|
|
71
102
|
|
|
72
103
|
function pendingComposerOperationKey(
|
|
73
104
|
workspaceId: string,
|
|
@@ -88,6 +119,78 @@ function pendingComposerStorageKey(key: string): string {
|
|
|
88
119
|
return `${PENDING_COMPOSER_STORAGE_PREFIX}${encodeURIComponent(key)}`;
|
|
89
120
|
}
|
|
90
121
|
|
|
122
|
+
function optimisticSendStorageKey(key: string): string {
|
|
123
|
+
return `${OPTIMISTIC_SEND_STORAGE_PREFIX}${encodeURIComponent(key)}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function restoreOptimisticSendOperations(key: string | null): OptimisticSendOperation[] {
|
|
127
|
+
if (!key) return [];
|
|
128
|
+
let stored = optimisticSendOperations.get(key);
|
|
129
|
+
if (!stored) {
|
|
130
|
+
const storage = pendingComposerStorage();
|
|
131
|
+
try {
|
|
132
|
+
const parsed: unknown = JSON.parse(storage?.getItem(optimisticSendStorageKey(key)) ?? "[]");
|
|
133
|
+
stored = Array.isArray(parsed)
|
|
134
|
+
? (parsed.filter(
|
|
135
|
+
(candidate): candidate is StoredOptimisticSendOperation =>
|
|
136
|
+
typeof candidate === "object" &&
|
|
137
|
+
candidate !== null &&
|
|
138
|
+
typeof candidate.clientEventId === "string" &&
|
|
139
|
+
typeof candidate.text === "string" &&
|
|
140
|
+
typeof candidate.occurredAt === "string" &&
|
|
141
|
+
(candidate.state === "sending" || candidate.state === "failed") &&
|
|
142
|
+
typeof candidate.input === "object" &&
|
|
143
|
+
candidate.input !== null &&
|
|
144
|
+
typeof candidate.input.text === "string" &&
|
|
145
|
+
typeof candidate.hasMcpCredentialUpdates === "boolean",
|
|
146
|
+
) as StoredOptimisticSendOperation[])
|
|
147
|
+
: [];
|
|
148
|
+
} catch {
|
|
149
|
+
stored = [];
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return stored.map(({ hasMcpCredentialUpdates, ...operation }) => ({
|
|
153
|
+
...operation,
|
|
154
|
+
newerShadow: operation.newerShadow ?? { text: "", resources: [], annotations: [] },
|
|
155
|
+
state:
|
|
156
|
+
operation.state === "sending" || operation.state === "queued" ? "sending" : operation.state,
|
|
157
|
+
outcomeUnknown:
|
|
158
|
+
operation.state === "sending" || operation.state === "queued"
|
|
159
|
+
? true
|
|
160
|
+
: operation.outcomeUnknown,
|
|
161
|
+
error:
|
|
162
|
+
operation.state === "sending"
|
|
163
|
+
? "Delivery was interrupted; retry to reconcile this message."
|
|
164
|
+
: operation.error,
|
|
165
|
+
input: operation.input,
|
|
166
|
+
canRetry: !hasMcpCredentialUpdates,
|
|
167
|
+
}));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function rememberOptimisticSendOperations(
|
|
171
|
+
key: string | null,
|
|
172
|
+
operations: OptimisticSendOperation[],
|
|
173
|
+
): void {
|
|
174
|
+
if (!key) return;
|
|
175
|
+
const stored = operations
|
|
176
|
+
.filter((operation) => operation.state !== "queued")
|
|
177
|
+
.map((operation) => {
|
|
178
|
+
const { canRetry: _canRetry, input, ...rest } = operation;
|
|
179
|
+
const { mcpCredentialUpdates: _credentials, ...safeInput } = input;
|
|
180
|
+
return {
|
|
181
|
+
...rest,
|
|
182
|
+
input: safeInput,
|
|
183
|
+
hasMcpCredentialUpdates: input.mcpCredentialUpdates !== undefined,
|
|
184
|
+
} satisfies StoredOptimisticSendOperation;
|
|
185
|
+
});
|
|
186
|
+
optimisticSendOperations.set(key, stored);
|
|
187
|
+
try {
|
|
188
|
+
pendingComposerStorage()?.setItem(optimisticSendStorageKey(key), JSON.stringify(stored));
|
|
189
|
+
} catch {
|
|
190
|
+
// Best effort; the in-memory queue still owns this mount.
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
91
194
|
function resourceList(value: unknown): value is ResourceRef[] {
|
|
92
195
|
return (
|
|
93
196
|
Array.isArray(value) &&
|
|
@@ -103,6 +206,41 @@ function resourceList(value: unknown): value is ResourceRef[] {
|
|
|
103
206
|
);
|
|
104
207
|
}
|
|
105
208
|
|
|
209
|
+
function timelineAnnotationList(value: unknown): value is DraftTimelineAnnotation[] {
|
|
210
|
+
return (
|
|
211
|
+
Array.isArray(value) &&
|
|
212
|
+
value.every((candidate) => {
|
|
213
|
+
if (typeof candidate !== "object" || candidate === null) return false;
|
|
214
|
+
const annotation = candidate as Record<string, unknown>;
|
|
215
|
+
const source = annotation.source;
|
|
216
|
+
if (typeof source !== "object" || source === null) return false;
|
|
217
|
+
const sourceRecord = source as Record<string, unknown>;
|
|
218
|
+
return (
|
|
219
|
+
typeof annotation.id === "string" &&
|
|
220
|
+
typeof annotation.quote === "string" &&
|
|
221
|
+
typeof annotation.note === "string" &&
|
|
222
|
+
typeof sourceRecord.eventId === "string" &&
|
|
223
|
+
typeof sourceRecord.eventType === "string" &&
|
|
224
|
+
typeof sourceRecord.sequence === "number" &&
|
|
225
|
+
(typeof sourceRecord.turnId === "string" || sourceRecord.turnId === null) &&
|
|
226
|
+
typeof sourceRecord.startOffset === "number" &&
|
|
227
|
+
typeof sourceRecord.endOffset === "number" &&
|
|
228
|
+
typeof sourceRecord.contextBefore === "string" &&
|
|
229
|
+
typeof sourceRecord.contextAfter === "string"
|
|
230
|
+
);
|
|
231
|
+
})
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function cloneAnnotations(
|
|
236
|
+
annotations: readonly DraftTimelineAnnotation[],
|
|
237
|
+
): DraftTimelineAnnotation[] {
|
|
238
|
+
return annotations.map((annotation) => ({
|
|
239
|
+
...annotation,
|
|
240
|
+
source: { ...annotation.source },
|
|
241
|
+
}));
|
|
242
|
+
}
|
|
243
|
+
|
|
106
244
|
function readStoredPendingComposerOperation(
|
|
107
245
|
key: string | null,
|
|
108
246
|
): StoredPendingComposerOperation | null {
|
|
@@ -128,18 +266,34 @@ function readStoredPendingComposerOperation(
|
|
|
128
266
|
(record.delivery !== "send" && record.delivery !== "steer") ||
|
|
129
267
|
typeof record.draftAtSend !== "string" ||
|
|
130
268
|
!resourceList(record.resourcesAtSend) ||
|
|
269
|
+
(record.annotationsAtSend !== undefined &&
|
|
270
|
+
!timelineAnnotationList(record.annotationsAtSend)) ||
|
|
131
271
|
typeof shadowRecord.text !== "string" ||
|
|
132
272
|
!resourceList(shadowRecord.resources) ||
|
|
273
|
+
(shadowRecord.annotations !== undefined &&
|
|
274
|
+
!timelineAnnotationList(shadowRecord.annotations)) ||
|
|
133
275
|
typeof record.clearDraftOnAccept !== "boolean" ||
|
|
134
276
|
typeof record.hasMcpCredentialUpdates !== "boolean" ||
|
|
135
277
|
typeof inputRecord.text !== "string" ||
|
|
136
278
|
typeof inputRecord.clientEventId !== "string" ||
|
|
137
279
|
"mcpCredentialUpdates" in inputRecord ||
|
|
138
|
-
("resources" in inputRecord && !resourceList(inputRecord.resources))
|
|
280
|
+
("resources" in inputRecord && !resourceList(inputRecord.resources)) ||
|
|
281
|
+
("annotations" in inputRecord && !timelineAnnotationList(inputRecord.annotations))
|
|
139
282
|
) {
|
|
140
283
|
return null;
|
|
141
284
|
}
|
|
142
|
-
return
|
|
285
|
+
return {
|
|
286
|
+
...(record as StoredPendingComposerOperation),
|
|
287
|
+
annotationsAtSend: timelineAnnotationList(record.annotationsAtSend)
|
|
288
|
+
? record.annotationsAtSend
|
|
289
|
+
: [],
|
|
290
|
+
newerShadow: {
|
|
291
|
+
...(shadow as ComposerDraftShadow),
|
|
292
|
+
annotations: timelineAnnotationList(shadowRecord.annotations)
|
|
293
|
+
? shadowRecord.annotations
|
|
294
|
+
: [],
|
|
295
|
+
},
|
|
296
|
+
};
|
|
143
297
|
} catch {
|
|
144
298
|
return null;
|
|
145
299
|
}
|
|
@@ -168,6 +322,7 @@ function restorePendingComposerOperation(key: string | null): PendingComposerOpe
|
|
|
168
322
|
newerShadow: stored.newerShadow ?? {
|
|
169
323
|
text: stored.draftAtSend,
|
|
170
324
|
resources: stored.resourcesAtSend,
|
|
325
|
+
annotations: stored.annotationsAtSend,
|
|
171
326
|
},
|
|
172
327
|
canRetry: !stored.hasMcpCredentialUpdates,
|
|
173
328
|
};
|
|
@@ -188,6 +343,7 @@ function rememberPendingComposerOperation(
|
|
|
188
343
|
newerShadow: {
|
|
189
344
|
text: operation.newerShadow.text,
|
|
190
345
|
resources: [...operation.newerShadow.resources],
|
|
346
|
+
annotations: cloneAnnotations(operation.newerShadow.annotations),
|
|
191
347
|
},
|
|
192
348
|
} satisfies StoredPendingComposerOperation;
|
|
193
349
|
pendingComposerOperations.set(key, stored);
|
|
@@ -212,7 +368,14 @@ function updatePendingComposerShadow(
|
|
|
212
368
|
shadow: ComposerDraftShadow,
|
|
213
369
|
): PendingComposerOperation | null {
|
|
214
370
|
if (!key || !operation) return operation;
|
|
215
|
-
const next = {
|
|
371
|
+
const next = {
|
|
372
|
+
...operation,
|
|
373
|
+
newerShadow: {
|
|
374
|
+
...shadow,
|
|
375
|
+
resources: [...shadow.resources],
|
|
376
|
+
annotations: cloneAnnotations(shadow.annotations),
|
|
377
|
+
},
|
|
378
|
+
};
|
|
216
379
|
rememberPendingComposerOperation(key, next);
|
|
217
380
|
return next;
|
|
218
381
|
}
|
|
@@ -220,14 +383,27 @@ function updatePendingComposerShadow(
|
|
|
220
383
|
export type ComposerState = {
|
|
221
384
|
value: string;
|
|
222
385
|
setValue: (value: string) => void;
|
|
386
|
+
annotations?: DraftTimelineAnnotation[] | undefined;
|
|
387
|
+
addAnnotation?: ((annotation: DraftTimelineAnnotation) => void) | undefined;
|
|
388
|
+
updateAnnotation?: ((id: string, note: string) => void) | undefined;
|
|
389
|
+
removeAnnotation?: ((id: string) => void) | undefined;
|
|
390
|
+
/** Newly captured annotation the review surface should focus. */
|
|
391
|
+
annotationReviewTargetId?: string | null | undefined;
|
|
392
|
+
clearAnnotationReviewTarget?: (() => void) | undefined;
|
|
223
393
|
/** Read the current draft synchronously before a destructive replacement. */
|
|
224
394
|
hasDraftContent: () => boolean;
|
|
225
395
|
/** Append the draft behind prompts already visible in the queue. */
|
|
226
396
|
send: (text?: string) => Promise<boolean>;
|
|
397
|
+
/** Locally acknowledged ordinary sends awaiting durable timeline reconciliation. */
|
|
398
|
+
optimisticMessages?: ComposerOptimisticMessage[] | undefined;
|
|
399
|
+
retryOptimisticMessage?: ((clientEventId: string) => void) | undefined;
|
|
400
|
+
removeOptimisticMessage?: ((clientEventId: string) => void) | undefined;
|
|
227
401
|
/** Supersede current direction with the draft. */
|
|
228
402
|
steer: (text?: string) => Promise<boolean>;
|
|
229
403
|
/** Optimistic-to-durable projection for a Steer that has not started yet. */
|
|
230
404
|
steering?: ComposerSteeringState | null | undefined;
|
|
405
|
+
/** Immediate mutation-receipt projection while physical cancellation settles. */
|
|
406
|
+
stoppingAttempt?: "current" | "previous" | null | undefined;
|
|
231
407
|
sending: boolean;
|
|
232
408
|
canSend: boolean;
|
|
233
409
|
/** Pause the session without deleting its prompt queue. */
|
|
@@ -259,6 +435,13 @@ export type ComposerSteeringState = {
|
|
|
259
435
|
clientEventId: string | null;
|
|
260
436
|
triggerEventId: string | null;
|
|
261
437
|
turnId: string | null;
|
|
438
|
+
/** True only when the accepted Steer durably interrupted a live attempt. */
|
|
439
|
+
stoppingPreviousAttempt?: boolean | undefined;
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
type ComposerControlStoppingState = {
|
|
443
|
+
controlVersion: number;
|
|
444
|
+
controlEtag: string;
|
|
262
445
|
};
|
|
263
446
|
|
|
264
447
|
const STEERING_SETTLEMENT_EVENT_TYPES = new Set([
|
|
@@ -321,12 +504,20 @@ export function useComposer(
|
|
|
321
504
|
const targetKey = `${workspaceId}\u0000${sessionId ?? ""}\u0000${durableDrafts ? "durable" : "disabled"}`;
|
|
322
505
|
const pendingOperationKey = pendingComposerOperationKey(workspaceId, sessionId);
|
|
323
506
|
const initialPendingOperation = restorePendingComposerOperation(pendingOperationKey);
|
|
324
|
-
const
|
|
507
|
+
const initialOptimisticSends = restoreOptimisticSendOperations(pendingOperationKey);
|
|
508
|
+
const initialShadow =
|
|
509
|
+
initialPendingOperation?.newerShadow ?? initialOptimisticSends.at(-1)?.newerShadow;
|
|
325
510
|
const [value, setValue] = useState(() => initialShadow?.text ?? "");
|
|
511
|
+
const [annotations, setAnnotations] = useState<DraftTimelineAnnotation[]>(
|
|
512
|
+
() => initialShadow?.annotations ?? [],
|
|
513
|
+
);
|
|
514
|
+
const [annotationReviewTargetId, setAnnotationReviewTargetId] = useState<string | null>(null);
|
|
326
515
|
// Keep rendered state behind the committed target identity for one frame:
|
|
327
516
|
// a parent may switch sessionId without remounting this public hook.
|
|
328
517
|
const [stateTargetKey, setStateTargetKey] = useState(targetKey);
|
|
329
518
|
const [sending, setSending] = useState(false);
|
|
519
|
+
const [optimisticSends, setOptimisticSends] =
|
|
520
|
+
useState<OptimisticSendOperation[]>(initialOptimisticSends);
|
|
330
521
|
const [steering, setSteering] = useState<ComposerSteeringState | null>(() =>
|
|
331
522
|
initialPendingOperation?.delivery === "steer"
|
|
332
523
|
? {
|
|
@@ -339,6 +530,7 @@ export function useComposer(
|
|
|
339
530
|
: null,
|
|
340
531
|
);
|
|
341
532
|
const [pausing, setPausing] = useState(false);
|
|
533
|
+
const [controlStopping, setControlStopping] = useState<ComposerControlStoppingState | null>(null);
|
|
342
534
|
const [resuming, setResuming] = useState(false);
|
|
343
535
|
const [error, setError] = useState<Error | null>(null);
|
|
344
536
|
const [draft, setDraft] = useState<ComposerDraft | null>(null);
|
|
@@ -349,12 +541,16 @@ export function useComposer(
|
|
|
349
541
|
() => initialShadow?.resources ?? [],
|
|
350
542
|
);
|
|
351
543
|
const pendingOperationRef = useRef<PendingComposerOperation | null>(initialPendingOperation);
|
|
544
|
+
const optimisticSendsRef = useRef<OptimisticSendOperation[]>(initialOptimisticSends);
|
|
545
|
+
const optimisticProcessorBusyRef = useRef(false);
|
|
546
|
+
const optimisticCallbackIdsRef = useRef(new Set<string>());
|
|
352
547
|
const steeringSettlementEventsRef = useRef<SessionEvent[]>([]);
|
|
353
548
|
const steeringRef = useRef(steering);
|
|
354
549
|
const pendingClientEventId = useRef<string | null>(
|
|
355
550
|
initialPendingOperation?.input.clientEventId ?? null,
|
|
356
551
|
);
|
|
357
552
|
const valueRef = useRef(initialShadow?.text ?? "");
|
|
553
|
+
const annotationsRef = useRef<DraftTimelineAnnotation[]>(initialShadow?.annotations ?? []);
|
|
358
554
|
const draftRef = useRef<ComposerDraft | null>(null);
|
|
359
555
|
const restoredResourcesRef = useRef<ResourceRef[]>(initialShadow?.resources ?? []);
|
|
360
556
|
const localEditRevision = useRef(initialShadow ? 1 : 0);
|
|
@@ -363,6 +559,7 @@ export function useComposer(
|
|
|
363
559
|
const lastSavedSignature = useRef<string | null>(null);
|
|
364
560
|
const saveChain = useRef<Promise<void>>(Promise.resolve());
|
|
365
561
|
const onSent = options.onSent;
|
|
562
|
+
const onSubmitted = options.onSubmitted;
|
|
366
563
|
const onDraftApplied = options.onDraftApplied;
|
|
367
564
|
// Read through a ref so live session/policy projections can replace their
|
|
368
565
|
// apply callback without invalidating the draft loader and re-running its
|
|
@@ -392,11 +589,17 @@ export function useComposer(
|
|
|
392
589
|
targetGeneration.current += 1;
|
|
393
590
|
draftReadGeneration.current += 1;
|
|
394
591
|
pendingOperationRef.current = restorePendingComposerOperation(pendingOperationKey);
|
|
592
|
+
const restoredOptimisticSends = restoreOptimisticSendOperations(pendingOperationKey);
|
|
593
|
+
optimisticSendsRef.current = restoredOptimisticSends;
|
|
594
|
+
optimisticProcessorBusyRef.current = false;
|
|
595
|
+
optimisticCallbackIdsRef.current = new Set();
|
|
395
596
|
steeringSettlementEventsRef.current = [];
|
|
396
597
|
pendingClientEventId.current = pendingOperationRef.current?.input.clientEventId ?? null;
|
|
397
|
-
const shadow =
|
|
598
|
+
const shadow =
|
|
599
|
+
pendingOperationRef.current?.newerShadow ?? restoredOptimisticSends.at(-1)?.newerShadow;
|
|
398
600
|
localEditRevision.current = shadow ? 1 : 0;
|
|
399
601
|
valueRef.current = shadow?.text ?? "";
|
|
602
|
+
annotationsRef.current = shadow?.annotations ?? [];
|
|
400
603
|
draftRef.current = null;
|
|
401
604
|
restoredResourcesRef.current = shadow?.resources ?? [];
|
|
402
605
|
lastSavedSignature.current = null;
|
|
@@ -405,7 +608,10 @@ export function useComposer(
|
|
|
405
608
|
saveChain.current = Promise.resolve();
|
|
406
609
|
setStateTargetKey(targetKey);
|
|
407
610
|
setValue(shadow?.text ?? "");
|
|
611
|
+
setAnnotations(shadow?.annotations ?? []);
|
|
612
|
+
setAnnotationReviewTargetId(null);
|
|
408
613
|
setSending(false);
|
|
614
|
+
setOptimisticSends(restoredOptimisticSends);
|
|
409
615
|
setSteering(
|
|
410
616
|
pendingOperationRef.current?.delivery === "steer"
|
|
411
617
|
? {
|
|
@@ -418,6 +624,7 @@ export function useComposer(
|
|
|
418
624
|
: null,
|
|
419
625
|
);
|
|
420
626
|
setPausing(false);
|
|
627
|
+
setControlStopping(null);
|
|
421
628
|
setResuming(false);
|
|
422
629
|
setError(null);
|
|
423
630
|
setDraft(null);
|
|
@@ -427,17 +634,37 @@ export function useComposer(
|
|
|
427
634
|
setRestoredResources(shadow?.resources ?? []);
|
|
428
635
|
}, [durableDrafts, pendingOperationKey, sessionId, targetKey]);
|
|
429
636
|
|
|
637
|
+
const setOptimisticDraftShadow = useCallback(
|
|
638
|
+
(shadow: ComposerDraftShadow): void => {
|
|
639
|
+
if (optimisticSendsRef.current.length === 0) return;
|
|
640
|
+
const next = optimisticSendsRef.current.map((operation) => ({
|
|
641
|
+
...operation,
|
|
642
|
+
newerShadow: {
|
|
643
|
+
text: shadow.text,
|
|
644
|
+
resources: [...shadow.resources],
|
|
645
|
+
annotations: cloneAnnotations(shadow.annotations),
|
|
646
|
+
},
|
|
647
|
+
}));
|
|
648
|
+
optimisticSendsRef.current = next;
|
|
649
|
+
rememberOptimisticSendOperations(pendingOperationKey, next);
|
|
650
|
+
setOptimisticSends(next);
|
|
651
|
+
},
|
|
652
|
+
[pendingOperationKey],
|
|
653
|
+
);
|
|
654
|
+
|
|
430
655
|
const applyDraft = useCallback(
|
|
431
656
|
(next: ComposerDraft): void => {
|
|
432
657
|
if (targetKeyRef.current !== targetKey) return;
|
|
433
658
|
if (!durableDrafts) {
|
|
434
659
|
localEditRevision.current += 1;
|
|
435
660
|
valueRef.current = next.text;
|
|
661
|
+
annotationsRef.current = next.annotations ?? [];
|
|
436
662
|
restoredResourcesRef.current = next.resources;
|
|
437
663
|
draftRef.current = null;
|
|
438
664
|
lastSavedSignature.current = null;
|
|
439
665
|
setDraft(null);
|
|
440
666
|
setValue(next.text);
|
|
667
|
+
setAnnotations(next.annotations ?? []);
|
|
441
668
|
setRestoredResources(next.resources);
|
|
442
669
|
pendingOperationRef.current = updatePendingComposerShadow(
|
|
443
670
|
pendingOperationKey,
|
|
@@ -448,18 +675,29 @@ export function useComposer(
|
|
|
448
675
|
next.resources,
|
|
449
676
|
resolveSendExtras(sendExtrasRef.current).resources ?? [],
|
|
450
677
|
),
|
|
678
|
+
annotations: next.annotations ?? [],
|
|
451
679
|
},
|
|
452
680
|
);
|
|
681
|
+
setOptimisticDraftShadow({
|
|
682
|
+
text: next.text,
|
|
683
|
+
resources: mergeResources(
|
|
684
|
+
next.resources,
|
|
685
|
+
resolveSendExtras(sendExtrasRef.current).resources ?? [],
|
|
686
|
+
),
|
|
687
|
+
annotations: next.annotations ?? [],
|
|
688
|
+
});
|
|
453
689
|
setDraftConflict(null);
|
|
454
690
|
return;
|
|
455
691
|
}
|
|
456
692
|
valueRef.current = next.text;
|
|
693
|
+
annotationsRef.current = next.annotations ?? [];
|
|
457
694
|
draftRef.current = next;
|
|
458
695
|
restoredResourcesRef.current = next.resources;
|
|
459
696
|
lastSavedSignature.current = draftSignature(draftPayload(next));
|
|
460
697
|
localEditRevision.current += 1;
|
|
461
698
|
setDraft(next);
|
|
462
699
|
setValue(next.text);
|
|
700
|
+
setAnnotations(next.annotations ?? []);
|
|
463
701
|
setRestoredResources(next.resources);
|
|
464
702
|
pendingOperationRef.current = updatePendingComposerShadow(
|
|
465
703
|
pendingOperationKey,
|
|
@@ -470,12 +708,21 @@ export function useComposer(
|
|
|
470
708
|
next.resources,
|
|
471
709
|
resolveSendExtras(sendExtrasRef.current).resources ?? [],
|
|
472
710
|
),
|
|
711
|
+
annotations: next.annotations ?? [],
|
|
473
712
|
},
|
|
474
713
|
);
|
|
714
|
+
setOptimisticDraftShadow({
|
|
715
|
+
text: next.text,
|
|
716
|
+
resources: mergeResources(
|
|
717
|
+
next.resources,
|
|
718
|
+
resolveSendExtras(sendExtrasRef.current).resources ?? [],
|
|
719
|
+
),
|
|
720
|
+
annotations: next.annotations ?? [],
|
|
721
|
+
});
|
|
475
722
|
setDraftConflict(null);
|
|
476
723
|
onDraftAppliedRef.current?.(next);
|
|
477
724
|
},
|
|
478
|
-
[durableDrafts, pendingOperationKey, targetKey],
|
|
725
|
+
[durableDrafts, pendingOperationKey, setOptimisticDraftShadow, targetKey],
|
|
479
726
|
);
|
|
480
727
|
|
|
481
728
|
const loadDraft = useCallback(
|
|
@@ -496,6 +743,7 @@ export function useComposer(
|
|
|
496
743
|
baseAtStart,
|
|
497
744
|
valueRef.current,
|
|
498
745
|
restoredResourcesRef.current,
|
|
746
|
+
annotationsRef.current,
|
|
499
747
|
extrasAtStart,
|
|
500
748
|
),
|
|
501
749
|
)
|
|
@@ -525,15 +773,19 @@ export function useComposer(
|
|
|
525
773
|
draftRef.current = fetched;
|
|
526
774
|
setDraft(fetched);
|
|
527
775
|
setDraftConflict(null);
|
|
528
|
-
const shadow =
|
|
776
|
+
const shadow =
|
|
777
|
+
pendingOperationRef.current?.newerShadow ??
|
|
778
|
+
optimisticSendsRef.current.at(-1)?.newerShadow;
|
|
529
779
|
if (shadow) {
|
|
530
780
|
// The server can only know the original operation. Never replace
|
|
531
781
|
// newer local edits while that operation is still uncertain.
|
|
532
782
|
valueRef.current = shadow.text;
|
|
533
783
|
restoredResourcesRef.current = shadow.resources;
|
|
784
|
+
annotationsRef.current = shadow.annotations;
|
|
534
785
|
localEditRevision.current ||= 1;
|
|
535
786
|
setValue(shadow.text);
|
|
536
787
|
setRestoredResources(shadow.resources);
|
|
788
|
+
setAnnotations(shadow.annotations);
|
|
537
789
|
} else if (
|
|
538
790
|
replaceLocal ||
|
|
539
791
|
(!localWasDirtyAtStart && localAtStart === localEditRevision.current)
|
|
@@ -549,9 +801,11 @@ export function useComposer(
|
|
|
549
801
|
extrasNow.latencyMode !== extrasAtStart.latencyMode;
|
|
550
802
|
valueRef.current = fetched.text;
|
|
551
803
|
restoredResourcesRef.current = fetched.resources;
|
|
804
|
+
annotationsRef.current = fetched.annotations ?? [];
|
|
552
805
|
lastSavedSignature.current = draftSignature(draftPayload(fetched));
|
|
553
806
|
setValue(fetched.text);
|
|
554
807
|
setRestoredResources(fetched.resources);
|
|
808
|
+
setAnnotations(fetched.annotations ?? []);
|
|
555
809
|
if (replaceLocal || !pickerChangedDuringFetch) {
|
|
556
810
|
onDraftAppliedRef.current?.(fetched);
|
|
557
811
|
}
|
|
@@ -675,13 +929,32 @@ export function useComposer(
|
|
|
675
929
|
setSteering(null);
|
|
676
930
|
}, [options.events, steering]);
|
|
677
931
|
|
|
932
|
+
useEffect(() => {
|
|
933
|
+
if (!controlStopping) return;
|
|
934
|
+
const effectiveControl = options.effectiveControl;
|
|
935
|
+
if (!effectiveControl || effectiveControl.controlVersion < controlStopping.controlVersion) {
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
if (
|
|
939
|
+
effectiveControl.controlVersion === controlStopping.controlVersion &&
|
|
940
|
+
effectiveControl.controlEtag === controlStopping.controlEtag &&
|
|
941
|
+
effectiveControl.settlement !== null
|
|
942
|
+
) {
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
// A newer control revision, an impossible same-version/different-etag
|
|
946
|
+
// projection, or quiescence at this exact revision supersedes the local
|
|
947
|
+
// mutation receipt. SessionChrome then falls back to durable queue truth.
|
|
948
|
+
setControlStopping((current) => (current === controlStopping ? null : current));
|
|
949
|
+
}, [controlStopping, options.effectiveControl]);
|
|
950
|
+
|
|
678
951
|
const currentDraftPayload = useCallback((): SaveComposerDraftRequest | null => {
|
|
679
952
|
if (!durableDrafts || targetKeyRef.current !== targetKey) return null;
|
|
680
953
|
const base = draftRef.current;
|
|
681
954
|
if (!base) return null;
|
|
682
955
|
const extras = resolveSendExtras(sendExtrasRef.current);
|
|
683
|
-
return composerDraftPayload(base, value, restoredResources, extras);
|
|
684
|
-
}, [durableDrafts, restoredResources, targetKey, value]);
|
|
956
|
+
return composerDraftPayload(base, value, restoredResources, annotations, extras);
|
|
957
|
+
}, [annotations, durableDrafts, restoredResources, targetKey, value]);
|
|
685
958
|
|
|
686
959
|
const persistPayload = useCallback(
|
|
687
960
|
async (payload: SaveComposerDraftRequest): Promise<boolean> => {
|
|
@@ -758,6 +1031,177 @@ export function useComposer(
|
|
|
758
1031
|
[client, durableDrafts, sessionId, targetKey, workspaceId],
|
|
759
1032
|
);
|
|
760
1033
|
|
|
1034
|
+
const replaceOptimisticSends = useCallback(
|
|
1035
|
+
(
|
|
1036
|
+
update: (current: OptimisticSendOperation[]) => OptimisticSendOperation[],
|
|
1037
|
+
): OptimisticSendOperation[] => {
|
|
1038
|
+
const next = update(optimisticSendsRef.current);
|
|
1039
|
+
optimisticSendsRef.current = next;
|
|
1040
|
+
rememberOptimisticSendOperations(pendingOperationKey, next);
|
|
1041
|
+
setOptimisticSends(next);
|
|
1042
|
+
return next;
|
|
1043
|
+
},
|
|
1044
|
+
[pendingOperationKey],
|
|
1045
|
+
);
|
|
1046
|
+
|
|
1047
|
+
const markOptimisticAccepted = useCallback(
|
|
1048
|
+
(operation: OptimisticSendOperation): void => {
|
|
1049
|
+
if (!optimisticCallbackIdsRef.current.has(operation.clientEventId)) {
|
|
1050
|
+
optimisticCallbackIdsRef.current.add(operation.clientEventId);
|
|
1051
|
+
onSent?.(operation.input.text, operation.input);
|
|
1052
|
+
}
|
|
1053
|
+
replaceOptimisticSends((current) =>
|
|
1054
|
+
current.filter((candidate) => candidate.clientEventId !== operation.clientEventId),
|
|
1055
|
+
);
|
|
1056
|
+
},
|
|
1057
|
+
[onSent, replaceOptimisticSends],
|
|
1058
|
+
);
|
|
1059
|
+
|
|
1060
|
+
useEffect(() => {
|
|
1061
|
+
const acceptedIds = new Set(
|
|
1062
|
+
(options.events ?? [])
|
|
1063
|
+
.filter((event) => event.type === "user.message" && event.clientEventId)
|
|
1064
|
+
.map((event) => event.clientEventId as string),
|
|
1065
|
+
);
|
|
1066
|
+
if (acceptedIds.size === 0) return;
|
|
1067
|
+
const accepted = optimisticSendsRef.current.filter((operation) =>
|
|
1068
|
+
acceptedIds.has(operation.clientEventId),
|
|
1069
|
+
);
|
|
1070
|
+
for (const operation of accepted) markOptimisticAccepted(operation);
|
|
1071
|
+
}, [markOptimisticAccepted, options.events]);
|
|
1072
|
+
|
|
1073
|
+
const processOptimisticSends = useCallback((): void => {
|
|
1074
|
+
if (!sessionId || optimisticProcessorBusyRef.current) return;
|
|
1075
|
+
const currentOperations = optimisticSendsRef.current;
|
|
1076
|
+
const operationIndex = currentOperations.findIndex(
|
|
1077
|
+
(candidate) => candidate.state === "sending",
|
|
1078
|
+
);
|
|
1079
|
+
const operation = operationIndex < 0 ? undefined : currentOperations[operationIndex];
|
|
1080
|
+
if (!operation) return;
|
|
1081
|
+
const ownedTargetKey = targetKey;
|
|
1082
|
+
const ownedGeneration = targetGeneration.current;
|
|
1083
|
+
optimisticProcessorBusyRef.current = true;
|
|
1084
|
+
void (async () => {
|
|
1085
|
+
try {
|
|
1086
|
+
if (operation.outcomeUnknown) {
|
|
1087
|
+
const events = await client.listEvents(workspaceId, sessionId, {
|
|
1088
|
+
includeTypes: ["user.message"],
|
|
1089
|
+
limit: 100,
|
|
1090
|
+
payloadMode: "none",
|
|
1091
|
+
});
|
|
1092
|
+
if (
|
|
1093
|
+
events.some(
|
|
1094
|
+
(event) =>
|
|
1095
|
+
event.type === "user.message" && event.clientEventId === operation.clientEventId,
|
|
1096
|
+
)
|
|
1097
|
+
) {
|
|
1098
|
+
markOptimisticAccepted(operation);
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
if (!operation.canRetry) {
|
|
1102
|
+
throw new Error(
|
|
1103
|
+
"OpenGeni cannot safely retry this uncertain request after remount; reconcile the session before sending again.",
|
|
1104
|
+
);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
let expectedDraftRevision: number | undefined;
|
|
1108
|
+
if (durableDrafts && operation.draftPayload) {
|
|
1109
|
+
if (!(await persistPayload(operation.draftPayload))) {
|
|
1110
|
+
throw new Error("The message draft could not be saved before delivery.");
|
|
1111
|
+
}
|
|
1112
|
+
expectedDraftRevision = draftRef.current?.revision;
|
|
1113
|
+
}
|
|
1114
|
+
if (
|
|
1115
|
+
targetKeyRef.current !== ownedTargetKey ||
|
|
1116
|
+
targetGeneration.current !== ownedGeneration
|
|
1117
|
+
) {
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1120
|
+
const wireInput = {
|
|
1121
|
+
...operation.input,
|
|
1122
|
+
...(expectedDraftRevision !== undefined ? { expectedDraftRevision } : {}),
|
|
1123
|
+
};
|
|
1124
|
+
await client.sendMessage(workspaceId, sessionId, wireInput);
|
|
1125
|
+
if (
|
|
1126
|
+
targetKeyRef.current !== ownedTargetKey ||
|
|
1127
|
+
targetGeneration.current !== ownedGeneration
|
|
1128
|
+
) {
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
if (options.events === undefined) {
|
|
1132
|
+
markOptimisticAccepted({ ...operation, input: wireInput });
|
|
1133
|
+
} else {
|
|
1134
|
+
replaceOptimisticSends((current) =>
|
|
1135
|
+
current.map((candidate) =>
|
|
1136
|
+
candidate.clientEventId === operation.clientEventId
|
|
1137
|
+
? { ...candidate, input: wireInput, state: "queued", error: undefined }
|
|
1138
|
+
: candidate,
|
|
1139
|
+
),
|
|
1140
|
+
);
|
|
1141
|
+
}
|
|
1142
|
+
if (!optimisticCallbackIdsRef.current.has(operation.clientEventId)) {
|
|
1143
|
+
optimisticCallbackIdsRef.current.add(operation.clientEventId);
|
|
1144
|
+
onSent?.(wireInput.text, wireInput);
|
|
1145
|
+
}
|
|
1146
|
+
if (durableDrafts && draftRef.current) {
|
|
1147
|
+
const cleared = {
|
|
1148
|
+
...draftRef.current,
|
|
1149
|
+
revision: 0,
|
|
1150
|
+
text: "",
|
|
1151
|
+
resources: [],
|
|
1152
|
+
annotations: [],
|
|
1153
|
+
sourceTurnId: null,
|
|
1154
|
+
sourceTurnVersion: null,
|
|
1155
|
+
updatedAt: null,
|
|
1156
|
+
};
|
|
1157
|
+
draftRef.current = cleared;
|
|
1158
|
+
setDraft(cleared);
|
|
1159
|
+
lastSavedSignature.current = draftSignature(draftPayload(cleared));
|
|
1160
|
+
}
|
|
1161
|
+
} catch (cause) {
|
|
1162
|
+
const problem = asError(cause);
|
|
1163
|
+
const outcomeUnknown = isOutcomeUnknownError(cause) || operation.outcomeUnknown === true;
|
|
1164
|
+
if (
|
|
1165
|
+
targetKeyRef.current === ownedTargetKey &&
|
|
1166
|
+
targetGeneration.current === ownedGeneration
|
|
1167
|
+
) {
|
|
1168
|
+
replaceOptimisticSends((current) =>
|
|
1169
|
+
current.map((candidate) =>
|
|
1170
|
+
candidate.clientEventId === operation.clientEventId
|
|
1171
|
+
? {
|
|
1172
|
+
...candidate,
|
|
1173
|
+
state: "failed",
|
|
1174
|
+
error: problem.message,
|
|
1175
|
+
outcomeUnknown,
|
|
1176
|
+
}
|
|
1177
|
+
: candidate,
|
|
1178
|
+
),
|
|
1179
|
+
);
|
|
1180
|
+
}
|
|
1181
|
+
} finally {
|
|
1182
|
+
optimisticProcessorBusyRef.current = false;
|
|
1183
|
+
if (targetKeyRef.current === ownedTargetKey) {
|
|
1184
|
+
setOptimisticSends([...optimisticSendsRef.current]);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
})();
|
|
1188
|
+
}, [
|
|
1189
|
+
client,
|
|
1190
|
+
durableDrafts,
|
|
1191
|
+
markOptimisticAccepted,
|
|
1192
|
+
onSent,
|
|
1193
|
+
options.events,
|
|
1194
|
+
persistPayload,
|
|
1195
|
+
replaceOptimisticSends,
|
|
1196
|
+
sessionId,
|
|
1197
|
+
targetKey,
|
|
1198
|
+
workspaceId,
|
|
1199
|
+
]);
|
|
1200
|
+
|
|
1201
|
+
useEffect(() => {
|
|
1202
|
+
processOptimisticSends();
|
|
1203
|
+
}, [optimisticSends, processOptimisticSends]);
|
|
1204
|
+
|
|
761
1205
|
// Private durable autosave. A newer local edit is never replaced by an older
|
|
762
1206
|
// response; saves serialize and each reads the latest acknowledged revision.
|
|
763
1207
|
useEffect(() => {
|
|
@@ -769,10 +1213,12 @@ export function useComposer(
|
|
|
769
1213
|
restoredResourcesRef.current,
|
|
770
1214
|
resolveSendExtras(sendExtrasRef.current).resources ?? [],
|
|
771
1215
|
),
|
|
1216
|
+
annotations: annotationsRef.current,
|
|
772
1217
|
};
|
|
773
1218
|
if (
|
|
774
1219
|
pending.newerShadow.text !== shadow.text ||
|
|
775
|
-
JSON.stringify(pending.newerShadow.resources) !== JSON.stringify(shadow.resources)
|
|
1220
|
+
JSON.stringify(pending.newerShadow.resources) !== JSON.stringify(shadow.resources) ||
|
|
1221
|
+
JSON.stringify(pending.newerShadow.annotations) !== JSON.stringify(shadow.annotations)
|
|
776
1222
|
) {
|
|
777
1223
|
pendingOperationRef.current = updatePendingComposerShadow(
|
|
778
1224
|
pendingOperationKey,
|
|
@@ -782,6 +1228,27 @@ export function useComposer(
|
|
|
782
1228
|
}
|
|
783
1229
|
return;
|
|
784
1230
|
}
|
|
1231
|
+
const optimistic = optimisticSendsRef.current.at(-1);
|
|
1232
|
+
if (optimistic) {
|
|
1233
|
+
const shadow = {
|
|
1234
|
+
text: valueRef.current,
|
|
1235
|
+
resources: mergeResources(
|
|
1236
|
+
restoredResourcesRef.current,
|
|
1237
|
+
resolveSendExtras(sendExtrasRef.current).resources ?? [],
|
|
1238
|
+
),
|
|
1239
|
+
annotations: annotationsRef.current,
|
|
1240
|
+
};
|
|
1241
|
+
if (
|
|
1242
|
+
optimistic.newerShadow.text !== shadow.text ||
|
|
1243
|
+
JSON.stringify(optimistic.newerShadow.resources) !== JSON.stringify(shadow.resources) ||
|
|
1244
|
+
JSON.stringify(optimistic.newerShadow.annotations) !== JSON.stringify(shadow.annotations)
|
|
1245
|
+
) {
|
|
1246
|
+
setOptimisticDraftShadow(shadow);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
if (optimisticSendsRef.current.some((operation) => operation.state === "sending")) {
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
785
1252
|
if (
|
|
786
1253
|
!durableDrafts ||
|
|
787
1254
|
!sessionId ||
|
|
@@ -803,6 +1270,7 @@ export function useComposer(
|
|
|
803
1270
|
liveExtrasVersion,
|
|
804
1271
|
pendingOperationKey,
|
|
805
1272
|
persistPayload,
|
|
1273
|
+
setOptimisticDraftShadow,
|
|
806
1274
|
sending,
|
|
807
1275
|
sessionId,
|
|
808
1276
|
]);
|
|
@@ -818,14 +1286,19 @@ export function useComposer(
|
|
|
818
1286
|
pendingClientEventId.current = pending.input.clientEventId ?? null;
|
|
819
1287
|
}
|
|
820
1288
|
const draftAtSend = value;
|
|
1289
|
+
const annotationsAtSend = annotations;
|
|
821
1290
|
const rawText = explicit ?? draftAtSend;
|
|
822
1291
|
const hasText = rawText.trim().length > 0;
|
|
1292
|
+
const hasAnnotations = annotationsAtSend.length > 0;
|
|
1293
|
+
const annotationsComplete = annotationsAtSend.every(
|
|
1294
|
+
(annotation) => annotation.note.trim().length > 0,
|
|
1295
|
+
);
|
|
823
1296
|
// Resolve the extras once: a file-only message (empty text + ≥1 ready
|
|
824
1297
|
// resource) is legitimate, so we must not bail on empty text alone.
|
|
825
1298
|
const extras = pending ? {} : resolveSendExtras(sendExtrasRef.current);
|
|
826
1299
|
const hasResources = restoredResources.length > 0 || (extras.resources?.length ?? 0) > 0;
|
|
827
1300
|
if (
|
|
828
|
-
(!pending && !hasText && !hasResources) ||
|
|
1301
|
+
(!pending && (!annotationsComplete || (!hasText && !hasResources && !hasAnnotations))) ||
|
|
829
1302
|
!sessionId ||
|
|
830
1303
|
sending ||
|
|
831
1304
|
sendBlockedRef.current?.() === true ||
|
|
@@ -848,6 +1321,8 @@ export function useComposer(
|
|
|
848
1321
|
const resourcesWereUnchanged =
|
|
849
1322
|
JSON.stringify(restoredResourcesRef.current) ===
|
|
850
1323
|
JSON.stringify(operation.resourcesAtSend);
|
|
1324
|
+
const annotationsWereUnchanged =
|
|
1325
|
+
JSON.stringify(annotationsRef.current) === JSON.stringify(operation.annotationsAtSend);
|
|
851
1326
|
const previousDraft = draftRef.current;
|
|
852
1327
|
if (previousDraft) {
|
|
853
1328
|
const cleared = {
|
|
@@ -855,6 +1330,7 @@ export function useComposer(
|
|
|
855
1330
|
revision: 0,
|
|
856
1331
|
text: "",
|
|
857
1332
|
resources: [],
|
|
1333
|
+
annotations: [],
|
|
858
1334
|
sourceTurnId: null,
|
|
859
1335
|
sourceTurnVersion: null,
|
|
860
1336
|
updatedAt: null,
|
|
@@ -867,6 +1343,11 @@ export function useComposer(
|
|
|
867
1343
|
restoredResourcesRef.current = [];
|
|
868
1344
|
setRestoredResources([]);
|
|
869
1345
|
}
|
|
1346
|
+
if (annotationsWereUnchanged) {
|
|
1347
|
+
annotationsRef.current = [];
|
|
1348
|
+
setAnnotations([]);
|
|
1349
|
+
setAnnotationReviewTargetId(null);
|
|
1350
|
+
}
|
|
870
1351
|
if (operation.clearDraftOnAccept && draftWasUnchanged) {
|
|
871
1352
|
valueRef.current = "";
|
|
872
1353
|
setValue("");
|
|
@@ -955,6 +1436,8 @@ export function useComposer(
|
|
|
955
1436
|
clientEventId: pending.input.clientEventId ?? null,
|
|
956
1437
|
triggerEventId: result.accepted.id,
|
|
957
1438
|
turnId: result.turn.id,
|
|
1439
|
+
stoppingPreviousAttempt:
|
|
1440
|
+
result.replay !== true && (result.interruptionCount ?? 0) > 0,
|
|
958
1441
|
});
|
|
959
1442
|
}
|
|
960
1443
|
} catch (cause) {
|
|
@@ -981,7 +1464,7 @@ export function useComposer(
|
|
|
981
1464
|
// and submitted byte-for-byte, while file-only sends use the same
|
|
982
1465
|
// placeholder for both operations so the server content fence cannot
|
|
983
1466
|
// reject its own client.
|
|
984
|
-
const sendText = hasText ? rawText : FILE_ONLY_MESSAGE_TEXT;
|
|
1467
|
+
const sendText = hasText ? rawText : hasAnnotations ? "" : FILE_ONLY_MESSAGE_TEXT;
|
|
985
1468
|
const currentPayload = currentDraftPayload();
|
|
986
1469
|
const payload = currentPayload ? { ...currentPayload, text: sendText } : null;
|
|
987
1470
|
if (payload && !(await persistPayload(payload))) return false;
|
|
@@ -1003,15 +1486,18 @@ export function useComposer(
|
|
|
1003
1486
|
? { expectedDraftRevision: draftRef.current.revision }
|
|
1004
1487
|
: {}),
|
|
1005
1488
|
resources: mergeResources(restoredResources, extras.resources ?? []),
|
|
1489
|
+
annotations: cloneAnnotations(annotationsAtSend),
|
|
1006
1490
|
});
|
|
1007
1491
|
const operation: PendingComposerOperation = {
|
|
1008
1492
|
delivery,
|
|
1009
1493
|
input,
|
|
1010
1494
|
draftAtSend,
|
|
1011
1495
|
resourcesAtSend: [...restoredResources],
|
|
1496
|
+
annotationsAtSend: cloneAnnotations(annotationsAtSend),
|
|
1012
1497
|
newerShadow: {
|
|
1013
1498
|
text: draftAtSend,
|
|
1014
1499
|
resources: mergeResources(restoredResources, extras.resources ?? []),
|
|
1500
|
+
annotations: cloneAnnotations(annotationsAtSend),
|
|
1015
1501
|
},
|
|
1016
1502
|
clearDraftOnAccept: explicit === undefined,
|
|
1017
1503
|
canRetry: true,
|
|
@@ -1037,6 +1523,8 @@ export function useComposer(
|
|
|
1037
1523
|
clientEventId: input.clientEventId ?? null,
|
|
1038
1524
|
triggerEventId: result.accepted.id,
|
|
1039
1525
|
turnId: result.turn.id,
|
|
1526
|
+
stoppingPreviousAttempt:
|
|
1527
|
+
result.replay !== true && (result.interruptionCount ?? 0) > 0,
|
|
1040
1528
|
});
|
|
1041
1529
|
}
|
|
1042
1530
|
} catch (cause) {
|
|
@@ -1079,6 +1567,7 @@ export function useComposer(
|
|
|
1079
1567
|
options.effectiveControl?.controlEtag,
|
|
1080
1568
|
persistPayload,
|
|
1081
1569
|
restoredResources,
|
|
1570
|
+
annotations,
|
|
1082
1571
|
sending,
|
|
1083
1572
|
sessionId,
|
|
1084
1573
|
targetKey,
|
|
@@ -1087,9 +1576,140 @@ export function useComposer(
|
|
|
1087
1576
|
],
|
|
1088
1577
|
);
|
|
1089
1578
|
|
|
1090
|
-
const send = useCallback(
|
|
1579
|
+
const send = useCallback(
|
|
1580
|
+
async (explicit?: string): Promise<boolean> => {
|
|
1581
|
+
if (pendingOperationRef.current?.delivery === "send") {
|
|
1582
|
+
return await dispatch("send", explicit);
|
|
1583
|
+
}
|
|
1584
|
+
const rawText = explicit ?? valueRef.current;
|
|
1585
|
+
const annotationsAtSend = cloneAnnotations(annotationsRef.current);
|
|
1586
|
+
const hasText = rawText.trim().length > 0;
|
|
1587
|
+
const hasAnnotations = annotationsAtSend.length > 0;
|
|
1588
|
+
const annotationsComplete = annotationsAtSend.every(
|
|
1589
|
+
(annotation) => annotation.note.trim().length > 0,
|
|
1590
|
+
);
|
|
1591
|
+
const extras = resolveSendExtras(sendExtrasRef.current);
|
|
1592
|
+
const resources = mergeResources(restoredResourcesRef.current, extras.resources ?? []);
|
|
1593
|
+
if (
|
|
1594
|
+
!sessionId ||
|
|
1595
|
+
sending ||
|
|
1596
|
+
!annotationsComplete ||
|
|
1597
|
+
(!hasText && !hasAnnotations && resources.length === 0) ||
|
|
1598
|
+
sendBlockedRef.current?.() === true ||
|
|
1599
|
+
targetKeyRef.current !== targetKey
|
|
1600
|
+
) {
|
|
1601
|
+
return false;
|
|
1602
|
+
}
|
|
1603
|
+
const sendText = hasText ? rawText : hasAnnotations ? "" : FILE_ONLY_MESSAGE_TEXT;
|
|
1604
|
+
const clientEventId = generateClientEventId();
|
|
1605
|
+
const input = composeSendInput(sendText, clientEventId, extras, {
|
|
1606
|
+
...(options.effectiveControl?.controlEtag
|
|
1607
|
+
? { controlEtag: options.effectiveControl.controlEtag }
|
|
1608
|
+
: {}),
|
|
1609
|
+
resources,
|
|
1610
|
+
annotations: annotationsAtSend,
|
|
1611
|
+
});
|
|
1612
|
+
const currentPayload = currentDraftPayload();
|
|
1613
|
+
const operation: OptimisticSendOperation = {
|
|
1614
|
+
clientEventId,
|
|
1615
|
+
text: sendText,
|
|
1616
|
+
annotations: annotationsAtSend,
|
|
1617
|
+
resources,
|
|
1618
|
+
occurredAt: new Date().toISOString(),
|
|
1619
|
+
state: "sending",
|
|
1620
|
+
input,
|
|
1621
|
+
draftPayload: currentPayload ? { ...currentPayload, text: sendText } : null,
|
|
1622
|
+
newerShadow: {
|
|
1623
|
+
text: explicit === undefined ? "" : valueRef.current,
|
|
1624
|
+
resources: explicit === undefined ? [] : [...restoredResourcesRef.current],
|
|
1625
|
+
annotations: explicit === undefined ? [] : cloneAnnotations(annotationsRef.current),
|
|
1626
|
+
},
|
|
1627
|
+
canRetry: true,
|
|
1628
|
+
};
|
|
1629
|
+
replaceOptimisticSends((current) => [...current, operation]);
|
|
1630
|
+
queueMicrotask(processOptimisticSends);
|
|
1631
|
+
setError(null);
|
|
1632
|
+
onSubmitted?.(sendText, input);
|
|
1633
|
+
if (explicit === undefined) {
|
|
1634
|
+
valueRef.current = "";
|
|
1635
|
+
annotationsRef.current = [];
|
|
1636
|
+
restoredResourcesRef.current = [];
|
|
1637
|
+
localEditRevision.current += 1;
|
|
1638
|
+
setValue("");
|
|
1639
|
+
setAnnotations([]);
|
|
1640
|
+
setAnnotationReviewTargetId(null);
|
|
1641
|
+
setRestoredResources([]);
|
|
1642
|
+
}
|
|
1643
|
+
return true;
|
|
1644
|
+
},
|
|
1645
|
+
[
|
|
1646
|
+
currentDraftPayload,
|
|
1647
|
+
dispatch,
|
|
1648
|
+
onSubmitted,
|
|
1649
|
+
options.effectiveControl?.controlEtag,
|
|
1650
|
+
processOptimisticSends,
|
|
1651
|
+
replaceOptimisticSends,
|
|
1652
|
+
sending,
|
|
1653
|
+
sessionId,
|
|
1654
|
+
targetKey,
|
|
1655
|
+
],
|
|
1656
|
+
);
|
|
1091
1657
|
const steer = useCallback(async (text?: string) => await dispatch("steer", text), [dispatch]);
|
|
1092
1658
|
|
|
1659
|
+
const retryOptimisticMessage = useCallback(
|
|
1660
|
+
(clientEventId: string): void => {
|
|
1661
|
+
replaceOptimisticSends((current) =>
|
|
1662
|
+
current.map((operation) => {
|
|
1663
|
+
if (operation.clientEventId !== clientEventId || operation.state !== "failed") {
|
|
1664
|
+
return operation;
|
|
1665
|
+
}
|
|
1666
|
+
if (operation.outcomeUnknown) {
|
|
1667
|
+
return { ...operation, state: "sending", error: undefined };
|
|
1668
|
+
}
|
|
1669
|
+
const nextClientEventId = generateClientEventId();
|
|
1670
|
+
const retryExtras = resolveSendExtras(sendExtrasRef.current);
|
|
1671
|
+
const retryInput = composeSendInput(operation.text, nextClientEventId, retryExtras, {
|
|
1672
|
+
...(options.effectiveControl?.controlEtag
|
|
1673
|
+
? { controlEtag: options.effectiveControl.controlEtag }
|
|
1674
|
+
: {}),
|
|
1675
|
+
resources: operation.resources,
|
|
1676
|
+
annotations: operation.annotations,
|
|
1677
|
+
});
|
|
1678
|
+
return {
|
|
1679
|
+
...operation,
|
|
1680
|
+
clientEventId: nextClientEventId,
|
|
1681
|
+
input: retryInput,
|
|
1682
|
+
draftPayload: operation.draftPayload
|
|
1683
|
+
? {
|
|
1684
|
+
...operation.draftPayload,
|
|
1685
|
+
model: retryExtras.model ?? operation.draftPayload.model,
|
|
1686
|
+
reasoningEffort:
|
|
1687
|
+
retryExtras.reasoningEffort ?? operation.draftPayload.reasoningEffort,
|
|
1688
|
+
latencyMode: retryExtras.latencyMode ?? operation.draftPayload.latencyMode,
|
|
1689
|
+
}
|
|
1690
|
+
: null,
|
|
1691
|
+
occurredAt: new Date().toISOString(),
|
|
1692
|
+
state: "sending",
|
|
1693
|
+
error: undefined,
|
|
1694
|
+
outcomeUnknown: false,
|
|
1695
|
+
canRetry: true,
|
|
1696
|
+
};
|
|
1697
|
+
}),
|
|
1698
|
+
);
|
|
1699
|
+
queueMicrotask(processOptimisticSends);
|
|
1700
|
+
},
|
|
1701
|
+
[options.effectiveControl?.controlEtag, processOptimisticSends, replaceOptimisticSends],
|
|
1702
|
+
);
|
|
1703
|
+
|
|
1704
|
+
const removeOptimisticMessage = useCallback(
|
|
1705
|
+
(clientEventId: string): void => {
|
|
1706
|
+
replaceOptimisticSends((current) =>
|
|
1707
|
+
current.filter((operation) => operation.clientEventId !== clientEventId),
|
|
1708
|
+
);
|
|
1709
|
+
},
|
|
1710
|
+
[replaceOptimisticSends],
|
|
1711
|
+
);
|
|
1712
|
+
|
|
1093
1713
|
// A send is possible with non-empty text OR with ≥1 attached resource (a
|
|
1094
1714
|
// file-only message). Resources ride in `sendExtras`, so we resolve them here
|
|
1095
1715
|
// — keeping useComposer attachment-agnostic while still lighting up the send
|
|
@@ -1099,6 +1719,7 @@ export function useComposer(
|
|
|
1099
1719
|
restoredResources.length > 0 ||
|
|
1100
1720
|
(resolveSendExtras(sendExtrasRef.current).resources?.length ?? 0) > 0;
|
|
1101
1721
|
const hasPendingOperation = pendingOperationRef.current !== null;
|
|
1722
|
+
const annotationsComplete = annotations.every((annotation) => annotation.note.trim().length > 0);
|
|
1102
1723
|
|
|
1103
1724
|
const pause = useCallback(
|
|
1104
1725
|
async (reason?: string): Promise<void> => {
|
|
@@ -1110,12 +1731,27 @@ export function useComposer(
|
|
|
1110
1731
|
setPausing(true);
|
|
1111
1732
|
setError(null);
|
|
1112
1733
|
try {
|
|
1113
|
-
await client.pauseSession(workspaceId, sessionId, {
|
|
1734
|
+
const result = await client.pauseSession(workspaceId, sessionId, {
|
|
1114
1735
|
...(reason !== undefined ? { reason } : {}),
|
|
1115
1736
|
...(options.effectiveControl?.controlEtag
|
|
1116
1737
|
? { expectedControlEtag: options.effectiveControl.controlEtag }
|
|
1117
1738
|
: {}),
|
|
1118
1739
|
});
|
|
1740
|
+
if (
|
|
1741
|
+
targetKeyRef.current === ownedTargetKey &&
|
|
1742
|
+
targetGeneration.current === ownedGeneration
|
|
1743
|
+
) {
|
|
1744
|
+
setControlStopping(
|
|
1745
|
+
result.interruptionCount > 0 &&
|
|
1746
|
+
result.effectiveControl.state === "paused" &&
|
|
1747
|
+
result.effectiveControl.settlement !== null
|
|
1748
|
+
? {
|
|
1749
|
+
controlVersion: result.effectiveControl.controlVersion,
|
|
1750
|
+
controlEtag: result.effectiveControl.controlEtag,
|
|
1751
|
+
}
|
|
1752
|
+
: null,
|
|
1753
|
+
);
|
|
1754
|
+
}
|
|
1119
1755
|
} catch (cause) {
|
|
1120
1756
|
if (
|
|
1121
1757
|
targetKeyRef.current === ownedTargetKey &&
|
|
@@ -1241,13 +1877,87 @@ export function useComposer(
|
|
|
1241
1877
|
restoredResourcesRef.current,
|
|
1242
1878
|
resolveSendExtras(sendExtrasRef.current).resources ?? [],
|
|
1243
1879
|
),
|
|
1880
|
+
annotations: annotationsRef.current,
|
|
1244
1881
|
},
|
|
1245
1882
|
);
|
|
1883
|
+
setOptimisticDraftShadow({
|
|
1884
|
+
text: next,
|
|
1885
|
+
resources: mergeResources(
|
|
1886
|
+
restoredResourcesRef.current,
|
|
1887
|
+
resolveSendExtras(sendExtrasRef.current).resources ?? [],
|
|
1888
|
+
),
|
|
1889
|
+
annotations: annotationsRef.current,
|
|
1890
|
+
});
|
|
1246
1891
|
setValue(next);
|
|
1247
1892
|
},
|
|
1248
|
-
[pendingOperationKey, targetKey],
|
|
1893
|
+
[pendingOperationKey, setOptimisticDraftShadow, targetKey],
|
|
1249
1894
|
);
|
|
1250
1895
|
|
|
1896
|
+
const updateAnnotations = useCallback(
|
|
1897
|
+
(next: DraftTimelineAnnotation[], reviewTargetId: string | null = null) => {
|
|
1898
|
+
if (targetKeyRef.current !== targetKey) return;
|
|
1899
|
+
localEditRevision.current += 1;
|
|
1900
|
+
annotationsRef.current = next;
|
|
1901
|
+
pendingOperationRef.current = updatePendingComposerShadow(
|
|
1902
|
+
pendingOperationKey,
|
|
1903
|
+
pendingOperationRef.current,
|
|
1904
|
+
{
|
|
1905
|
+
text: valueRef.current,
|
|
1906
|
+
resources: mergeResources(
|
|
1907
|
+
restoredResourcesRef.current,
|
|
1908
|
+
resolveSendExtras(sendExtrasRef.current).resources ?? [],
|
|
1909
|
+
),
|
|
1910
|
+
annotations: next,
|
|
1911
|
+
},
|
|
1912
|
+
);
|
|
1913
|
+
setOptimisticDraftShadow({
|
|
1914
|
+
text: valueRef.current,
|
|
1915
|
+
resources: mergeResources(
|
|
1916
|
+
restoredResourcesRef.current,
|
|
1917
|
+
resolveSendExtras(sendExtrasRef.current).resources ?? [],
|
|
1918
|
+
),
|
|
1919
|
+
annotations: next,
|
|
1920
|
+
});
|
|
1921
|
+
setAnnotations(next);
|
|
1922
|
+
setAnnotationReviewTargetId(reviewTargetId);
|
|
1923
|
+
},
|
|
1924
|
+
[pendingOperationKey, setOptimisticDraftShadow, targetKey],
|
|
1925
|
+
);
|
|
1926
|
+
|
|
1927
|
+
const addAnnotation = useCallback(
|
|
1928
|
+
(annotation: DraftTimelineAnnotation) => {
|
|
1929
|
+
if (annotationsRef.current.length >= 12) {
|
|
1930
|
+
setError(new Error("A message can include at most 12 timeline annotations."));
|
|
1931
|
+
return;
|
|
1932
|
+
}
|
|
1933
|
+
const next = [...annotationsRef.current, annotation];
|
|
1934
|
+
updateAnnotations(next, annotation.id);
|
|
1935
|
+
},
|
|
1936
|
+
[updateAnnotations],
|
|
1937
|
+
);
|
|
1938
|
+
|
|
1939
|
+
const updateAnnotation = useCallback(
|
|
1940
|
+
(id: string, note: string) => {
|
|
1941
|
+
updateAnnotations(
|
|
1942
|
+
annotationsRef.current.map((annotation) =>
|
|
1943
|
+
annotation.id === id ? { ...annotation, note } : annotation,
|
|
1944
|
+
),
|
|
1945
|
+
);
|
|
1946
|
+
},
|
|
1947
|
+
[updateAnnotations],
|
|
1948
|
+
);
|
|
1949
|
+
|
|
1950
|
+
const removeAnnotation = useCallback(
|
|
1951
|
+
(id: string) => {
|
|
1952
|
+
updateAnnotations(annotationsRef.current.filter((annotation) => annotation.id !== id));
|
|
1953
|
+
},
|
|
1954
|
+
[updateAnnotations],
|
|
1955
|
+
);
|
|
1956
|
+
|
|
1957
|
+
const clearAnnotationReviewTarget = useCallback(() => {
|
|
1958
|
+
setAnnotationReviewTargetId(null);
|
|
1959
|
+
}, []);
|
|
1960
|
+
|
|
1251
1961
|
const removeRestoredResource = useCallback(
|
|
1252
1962
|
(index: number) => {
|
|
1253
1963
|
if (targetKeyRef.current !== targetKey) return;
|
|
@@ -1260,11 +1970,17 @@ export function useComposer(
|
|
|
1260
1970
|
{
|
|
1261
1971
|
text: valueRef.current,
|
|
1262
1972
|
resources: mergeResources(next, resolveSendExtras(sendExtrasRef.current).resources ?? []),
|
|
1973
|
+
annotations: annotationsRef.current,
|
|
1263
1974
|
},
|
|
1264
1975
|
);
|
|
1976
|
+
setOptimisticDraftShadow({
|
|
1977
|
+
text: valueRef.current,
|
|
1978
|
+
resources: mergeResources(next, resolveSendExtras(sendExtrasRef.current).resources ?? []),
|
|
1979
|
+
annotations: annotationsRef.current,
|
|
1980
|
+
});
|
|
1265
1981
|
setRestoredResources(next);
|
|
1266
1982
|
},
|
|
1267
|
-
[pendingOperationKey, targetKey],
|
|
1983
|
+
[pendingOperationKey, setOptimisticDraftShadow, targetKey],
|
|
1268
1984
|
);
|
|
1269
1985
|
|
|
1270
1986
|
const hasDraftContent = useCallback((): boolean => {
|
|
@@ -1272,6 +1988,7 @@ export function useComposer(
|
|
|
1272
1988
|
const extras = resolveSendExtras(sendExtrasRef.current);
|
|
1273
1989
|
return (
|
|
1274
1990
|
valueRef.current.length > 0 ||
|
|
1991
|
+
annotationsRef.current.length > 0 ||
|
|
1275
1992
|
restoredResourcesRef.current.length > 0 ||
|
|
1276
1993
|
(extras.resources?.length ?? 0) > 0 ||
|
|
1277
1994
|
(current?.sourceTurnId !== null && current?.sourceTurnId !== undefined)
|
|
@@ -1312,6 +2029,7 @@ export function useComposer(
|
|
|
1312
2029
|
);
|
|
1313
2030
|
|
|
1314
2031
|
const identityMatches = stateTargetKey === targetKey;
|
|
2032
|
+
const visibleSteering = identityMatches ? steering : null;
|
|
1315
2033
|
const reloadDraft = useCallback(async () => await loadDraft(true), [loadDraft]);
|
|
1316
2034
|
const clearError = useCallback(() => {
|
|
1317
2035
|
if (targetKeyRef.current !== targetKey) return;
|
|
@@ -1322,17 +2040,41 @@ export function useComposer(
|
|
|
1322
2040
|
return {
|
|
1323
2041
|
value: identityMatches ? value : "",
|
|
1324
2042
|
setValue: updateValue,
|
|
2043
|
+
annotations: identityMatches ? annotations : [],
|
|
2044
|
+
addAnnotation,
|
|
2045
|
+
updateAnnotation,
|
|
2046
|
+
removeAnnotation,
|
|
2047
|
+
annotationReviewTargetId: identityMatches ? annotationReviewTargetId : null,
|
|
2048
|
+
clearAnnotationReviewTarget,
|
|
1325
2049
|
hasDraftContent,
|
|
1326
2050
|
send,
|
|
2051
|
+
optimisticMessages: identityMatches
|
|
2052
|
+
? optimisticSends.map(
|
|
2053
|
+
({ input: _input, draftPayload: _payload, canRetry: _retry, ...item }) => item,
|
|
2054
|
+
)
|
|
2055
|
+
: [],
|
|
2056
|
+
retryOptimisticMessage,
|
|
2057
|
+
removeOptimisticMessage,
|
|
1327
2058
|
steer,
|
|
1328
|
-
steering:
|
|
2059
|
+
steering: visibleSteering,
|
|
2060
|
+
stoppingAttempt: identityMatches
|
|
2061
|
+
? controlStopping
|
|
2062
|
+
? "current"
|
|
2063
|
+
: visibleSteering?.phase === "accepted" && visibleSteering.stoppingPreviousAttempt === true
|
|
2064
|
+
? "previous"
|
|
2065
|
+
: null
|
|
2066
|
+
: null,
|
|
1329
2067
|
sending: identityMatches ? sending : false,
|
|
1330
2068
|
canSend:
|
|
1331
2069
|
identityMatches &&
|
|
1332
2070
|
Boolean(sessionId) &&
|
|
1333
2071
|
!sending &&
|
|
1334
2072
|
sendBlockedRef.current?.() !== true &&
|
|
1335
|
-
|
|
2073
|
+
annotationsComplete &&
|
|
2074
|
+
(hasPendingOperation ||
|
|
2075
|
+
value.trim().length > 0 ||
|
|
2076
|
+
hasReadyResources ||
|
|
2077
|
+
annotations.length > 0),
|
|
1336
2078
|
pause,
|
|
1337
2079
|
pausing: identityMatches ? pausing : false,
|
|
1338
2080
|
resume,
|
|
@@ -1360,12 +2102,12 @@ export function isComposerDraftEvent(event: Pick<SessionEvent, "type">): boolean
|
|
|
1360
2102
|
}
|
|
1361
2103
|
|
|
1362
2104
|
/**
|
|
1363
|
-
* Default text for a
|
|
1364
|
-
* Kept non-empty so the wire contract (`text: z.string().min(1)`) and
|
|
1365
|
-
* worker's non-whitespace guard accept it
|
|
1366
|
-
* `resources`.
|
|
2105
|
+
* Default text for a resource-only message (attachments present, no typed
|
|
2106
|
+
* draft). Kept non-empty so the wire contract (`text: z.string().min(1)`) and
|
|
2107
|
+
* the worker's non-whitespace guard accept it. The export name is retained for
|
|
2108
|
+
* compatibility now that repositories can ride beside files in `resources`.
|
|
1367
2109
|
*/
|
|
1368
|
-
export const FILE_ONLY_MESSAGE_TEXT = "(see attached
|
|
2110
|
+
export const FILE_ONLY_MESSAGE_TEXT = "(see attached context)";
|
|
1369
2111
|
|
|
1370
2112
|
/** Resolve possibly-deferred extras to a concrete bag (function evaluated now). */
|
|
1371
2113
|
export function resolveSendExtras(
|
|
@@ -1477,6 +2219,7 @@ function draftPayload(draft: ComposerDraft): SaveComposerDraftRequest {
|
|
|
1477
2219
|
expectedRevision: draft.revision,
|
|
1478
2220
|
text: draft.text,
|
|
1479
2221
|
resources: draft.resources,
|
|
2222
|
+
annotations: draft.annotations ?? [],
|
|
1480
2223
|
model: draft.model,
|
|
1481
2224
|
reasoningEffort: draft.reasoningEffort,
|
|
1482
2225
|
latencyMode: draft.latencyMode ?? "standard",
|
|
@@ -1487,12 +2230,14 @@ function composerDraftPayload(
|
|
|
1487
2230
|
base: ComposerDraft,
|
|
1488
2231
|
text: string,
|
|
1489
2232
|
restoredResources: ResourceRef[],
|
|
2233
|
+
annotations: DraftTimelineAnnotation[],
|
|
1490
2234
|
extras: ComposerSendExtras,
|
|
1491
2235
|
): SaveComposerDraftRequest {
|
|
1492
2236
|
return {
|
|
1493
2237
|
expectedRevision: base.revision,
|
|
1494
2238
|
text,
|
|
1495
2239
|
resources: mergeResources(restoredResources, extras.resources ?? []),
|
|
2240
|
+
annotations,
|
|
1496
2241
|
model: extras.model ?? base.model,
|
|
1497
2242
|
reasoningEffort: extras.reasoningEffort ?? base.reasoningEffort,
|
|
1498
2243
|
latencyMode: extras.latencyMode ?? base.latencyMode ?? "standard",
|