@opengeni/react 0.23.0 → 0.25.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-UQS7OQDE.js → chunk-F4CFWKVI.js} +334 -64
- package/dist/chunk-F4CFWKVI.js.map +1 -0
- package/dist/chunk-GQN2QIR2.js +3767 -0
- package/dist/chunk-GQN2QIR2.js.map +1 -0
- package/dist/{chunk-I3BJZIG5.js → chunk-HIWPQYWI.js} +2 -120
- package/dist/chunk-HIWPQYWI.js.map +1 -0
- package/dist/chunk-M7X4JZOD.js +121 -0
- package/dist/chunk-M7X4JZOD.js.map +1 -0
- package/dist/{chunk-UAHPKLB6.js → chunk-MP6237JB.js} +571 -975
- package/dist/chunk-MP6237JB.js.map +1 -0
- package/dist/chunk-OZDLELJQ.js +937 -0
- package/dist/chunk-OZDLELJQ.js.map +1 -0
- package/dist/{chunk-CATATY77.js → chunk-TMH6HZWF.js} +8 -7
- package/dist/chunk-TMH6HZWF.js.map +1 -0
- package/dist/{chunk-6XUS5VFM.js → chunk-YU5PGUK7.js} +6 -4
- package/dist/{chunk-6XUS5VFM.js.map → chunk-YU5PGUK7.js.map} +1 -1
- package/dist/{composer-CVm5uoUI.d.ts → composer-DoC4veX1.d.ts} +6 -63
- package/dist/composer.d.ts +3 -2
- package/dist/composer.js +4 -3
- package/dist/fleet-decision-projection-SLNZMS42.js +241 -0
- package/dist/fleet-decision-projection-SLNZMS42.js.map +1 -0
- package/dist/fleet-decision-row-GGCAT4E4.js +351 -0
- package/dist/fleet-decision-row-GGCAT4E4.js.map +1 -0
- package/dist/index.d.ts +17 -259
- package/dist/index.js +1042 -5058
- package/dist/index.js.map +1 -1
- package/dist/machines.js +3 -2
- package/dist/session-Du_FsrZ1.d.ts +264 -0
- package/dist/session-ui-BqQDH7YV.d.ts +183 -0
- package/dist/session-ui.d.ts +7 -0
- package/dist/session-ui.js +17 -0
- package/dist/session-ui.js.map +1 -0
- package/dist/session.d.ts +4 -2
- package/dist/session.js +26 -9
- package/dist/{use-composer-By1wn3MM.d.ts → use-composer-BnEzheSk.d.ts} +8 -2
- package/dist/use-file-attachments-C0kpHrs9.d.ts +76 -0
- package/dist/use-turn-queue-3rkJwjFv.d.ts +290 -0
- package/package.json +6 -2
- package/src/components/composer.tsx +7 -6
- package/src/components/message-timeline.tsx +94 -54
- package/src/hooks/use-composer.ts +470 -73
- package/src/hooks/use-file-attachments.ts +96 -3
- package/src/hooks/use-goal.ts +23 -5
- package/src/hooks/use-session-events.ts +5 -0
- package/src/hooks/use-session-lineage.ts +1 -1
- package/src/hooks/use-session.ts +9 -6
- package/src/hooks/use-workspace-sessions.ts +30 -5
- package/src/session-ui.ts +13 -0
- package/src/session.ts +15 -0
- package/src/timeline/activity-rail.tsx +18 -2
- package/src/timeline/fleet-decision-projection.ts +366 -0
- package/src/timeline/fleet-decision-row.tsx +412 -0
- package/src/timeline/index.ts +2 -0
- package/src/timeline/projection.ts +19 -8
- package/src/timeline/tool-display-name.ts +16 -0
- package/src/timeline/tool-renderers.tsx +1 -1
- package/src/timeline/types.ts +68 -1
- package/dist/chunk-CATATY77.js.map +0 -1
- package/dist/chunk-I3BJZIG5.js.map +0 -1
- package/dist/chunk-UAHPKLB6.js.map +0 -1
- package/dist/chunk-UQS7OQDE.js.map +0 -1
- package/dist/session-Db00TRwl.d.ts +0 -446
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { FileAsset, FileResourceRef } from '@opengeni/sdk';
|
|
2
|
+
import { C as ClientOverride } from './session-context-rgrfElMl.js';
|
|
3
|
+
|
|
4
|
+
type UseFileAttachmentsOptions = ClientOverride & {
|
|
5
|
+
/**
|
|
6
|
+
* Only files matching this predicate are accepted by {@link
|
|
7
|
+
* UseFileAttachmentsResult.addFromPaste} (the clipboard path). Defaults to
|
|
8
|
+
* `image/*` — the console's historical paste filter. {@link
|
|
9
|
+
* UseFileAttachmentsResult.addFiles} (the explicit picker / drop path)
|
|
10
|
+
* bypasses it.
|
|
11
|
+
*/
|
|
12
|
+
pasteFilter?: ((file: File) => boolean) | undefined;
|
|
13
|
+
};
|
|
14
|
+
type FileAttachment = {
|
|
15
|
+
id: string;
|
|
16
|
+
name: string;
|
|
17
|
+
contentType: string;
|
|
18
|
+
sizeBytes: number;
|
|
19
|
+
status: "uploading" | "ready" | "failed";
|
|
20
|
+
/** The SDK `FileAsset` once the upload finishes. */
|
|
21
|
+
file?: FileAsset | undefined;
|
|
22
|
+
/** Object-URL for an inline preview; minted for `image/*` files only. */
|
|
23
|
+
previewUrl?: string | undefined;
|
|
24
|
+
error?: string | undefined;
|
|
25
|
+
};
|
|
26
|
+
type UseFileAttachmentsResult = {
|
|
27
|
+
attachments: FileAttachment[];
|
|
28
|
+
/**
|
|
29
|
+
* `FileResourceRef[]` for every attachment that finished uploading — feed
|
|
30
|
+
* straight into `useComposer`'s `sendExtras.resources`.
|
|
31
|
+
*/
|
|
32
|
+
readyResources: FileResourceRef[];
|
|
33
|
+
/** True while any attachment is still uploading (drives progress UI). */
|
|
34
|
+
uploading: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* True while any attachment still needs an explicit outcome: wait for an
|
|
37
|
+
* upload, retry a failure, or remove it. This is the loss-prevention send
|
|
38
|
+
* gate; failed cards must never be silently omitted from a message.
|
|
39
|
+
*/
|
|
40
|
+
hasUnresolved: boolean;
|
|
41
|
+
/** Explicit picker / drop path — uploads every file, no filter. */
|
|
42
|
+
addFiles: (files: Iterable<File>) => void;
|
|
43
|
+
/** Clipboard path — applies `pasteFilter` (default `image/*`) then uploads. */
|
|
44
|
+
addFromPaste: (event: {
|
|
45
|
+
clipboardData: DataTransfer | null;
|
|
46
|
+
}) => void;
|
|
47
|
+
/** Restore already-ready server assets without recreating browser-local bytes. */
|
|
48
|
+
restoreReadyFiles: (files: Iterable<FileAsset>) => void;
|
|
49
|
+
/**
|
|
50
|
+
* Re-run the upload for a `failed` attachment, in place (same id, same
|
|
51
|
+
* source file). No-op for an id that isn't a known failed upload.
|
|
52
|
+
*/
|
|
53
|
+
retry: (id: string) => void;
|
|
54
|
+
/** Remove one attachment; revokes its object-URL. */
|
|
55
|
+
remove: (id: string) => void;
|
|
56
|
+
/**
|
|
57
|
+
* Remove only finalized files whose durable ids were accepted by a send.
|
|
58
|
+
* Attachments added while that request was in flight remain queued for the
|
|
59
|
+
* next message.
|
|
60
|
+
*/
|
|
61
|
+
removeReadyFiles: (fileIds: Iterable<string>) => void;
|
|
62
|
+
/** Remove all attachments and revoke every object-URL. */
|
|
63
|
+
clear: () => void;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Upload-and-track state for files attached to the next message. Owns the
|
|
67
|
+
* full client-side upload layer: a per-file `uploading | ready | failed`
|
|
68
|
+
* status machine driven by the SDK's `client.uploadFile`, object-URL image
|
|
69
|
+
* previews with create/revoke lifecycle, the `image/*` clipboard paste filter,
|
|
70
|
+
* and a `FileResourceRef[]` projection that drops straight into a message's
|
|
71
|
+
* `resources`. Workspace-scoped, so it resolves both client and workspace from
|
|
72
|
+
* the {@link OpenGeniProvider} (or a per-call `{ client, workspaceId }`).
|
|
73
|
+
*/
|
|
74
|
+
declare function useFileAttachments(options?: UseFileAttachmentsOptions): UseFileAttachmentsResult;
|
|
75
|
+
|
|
76
|
+
export { type FileAttachment as F, type UseFileAttachmentsResult as U, type UseFileAttachmentsOptions as a, useFileAttachments as u };
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { ToolAuthNeededPayload, ResourceRef, ToolRef, SessionStatus, SessionQueueSnapshot, SessionTurn, EffectiveSessionControl, ComposerDraft, SessionEvent } from '@opengeni/sdk';
|
|
2
|
+
import { E as EmbeddedSessionClientOverride } from './session-context-rgrfElMl.js';
|
|
3
|
+
import { S as SessionEventFeedOptions } from './use-composer-BnEzheSk.js';
|
|
4
|
+
|
|
5
|
+
type UserMessageItem = {
|
|
6
|
+
kind: "user-message";
|
|
7
|
+
id: string;
|
|
8
|
+
text: string;
|
|
9
|
+
/** Resources attached to this message (file uploads, repositories). */
|
|
10
|
+
resources: ResourceRef[];
|
|
11
|
+
/** Tools requested for the turn this message starts. */
|
|
12
|
+
tools: ToolRef[];
|
|
13
|
+
occurredAt: string;
|
|
14
|
+
};
|
|
15
|
+
type AgentMessageItem = {
|
|
16
|
+
kind: "agent-message";
|
|
17
|
+
id: string;
|
|
18
|
+
turnId: string | null;
|
|
19
|
+
text: string;
|
|
20
|
+
/** Still receiving deltas (no completed/turn-end seen yet). */
|
|
21
|
+
streaming: boolean;
|
|
22
|
+
occurredAt: string;
|
|
23
|
+
};
|
|
24
|
+
type ReasoningItem = {
|
|
25
|
+
kind: "reasoning";
|
|
26
|
+
id: string;
|
|
27
|
+
turnId: string | null;
|
|
28
|
+
text: string;
|
|
29
|
+
streaming: boolean;
|
|
30
|
+
occurredAt: string;
|
|
31
|
+
};
|
|
32
|
+
type ToolCallItem = {
|
|
33
|
+
kind: "tool-call";
|
|
34
|
+
id: string;
|
|
35
|
+
turnId: string | null;
|
|
36
|
+
callId: string | null;
|
|
37
|
+
name: string;
|
|
38
|
+
arguments: unknown;
|
|
39
|
+
output: unknown;
|
|
40
|
+
/**
|
|
41
|
+
* The provider-native tool item (`agent.toolCall.created.payload.raw`). Carries
|
|
42
|
+
* `type` (e.g. `apply_patch_call`, `computer_call`, `hosted_tool_call`) and the
|
|
43
|
+
* tool-specific fields the per-tool renderers read (`operation`, `action`,
|
|
44
|
+
* `providerData`, …). `undefined` for first-party MCP tools, which carry their
|
|
45
|
+
* payload in `arguments`/`output` instead.
|
|
46
|
+
*/
|
|
47
|
+
raw: unknown;
|
|
48
|
+
status: "running" | "complete" | "failed" | "cancelled";
|
|
49
|
+
occurredAt: string;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* An orchestration call against another session — the manager spawning or
|
|
53
|
+
* messaging a worker. Rendered as a first-class "worker" row, not a generic
|
|
54
|
+
* tool call.
|
|
55
|
+
*/
|
|
56
|
+
type WorkerItem = {
|
|
57
|
+
kind: "worker";
|
|
58
|
+
id: string;
|
|
59
|
+
turnId: string | null;
|
|
60
|
+
callId: string | null;
|
|
61
|
+
action: "spawn" | "message";
|
|
62
|
+
/** The worker's initial message / the message sent to it, when parseable. */
|
|
63
|
+
prompt: string | null;
|
|
64
|
+
/** The target/spawned worker session id, when parseable from args/output. */
|
|
65
|
+
workerSessionId: string | null;
|
|
66
|
+
status: "running" | "complete" | "failed" | "cancelled";
|
|
67
|
+
occurredAt: string;
|
|
68
|
+
};
|
|
69
|
+
type WorkerCompletionItem = {
|
|
70
|
+
kind: "worker-completion";
|
|
71
|
+
id: string;
|
|
72
|
+
turnId: string | null;
|
|
73
|
+
occurredAt: string;
|
|
74
|
+
childSessionId: string;
|
|
75
|
+
childStatus: string;
|
|
76
|
+
goalStatus: string | null;
|
|
77
|
+
goalText: string | null;
|
|
78
|
+
evidence: string | null;
|
|
79
|
+
pausedReason: string | null;
|
|
80
|
+
text: string;
|
|
81
|
+
};
|
|
82
|
+
type SandboxItem = {
|
|
83
|
+
kind: "sandbox";
|
|
84
|
+
id: string;
|
|
85
|
+
turnId: string | null;
|
|
86
|
+
name: string;
|
|
87
|
+
command: string | null;
|
|
88
|
+
output: string;
|
|
89
|
+
origin?: "created" | "restored" | "resumed" | null;
|
|
90
|
+
status: "running" | "complete" | "failed" | "cancelled";
|
|
91
|
+
occurredAt: string;
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* A first-party workspace-memory write the agent made mid-turn — a `memory.saved`
|
|
95
|
+
* (it committed a new preference / fact / procedure / decision / history) or a
|
|
96
|
+
* `memory.corrected` (it updated or archived an existing one). A settled save is
|
|
97
|
+
* ordinary progress, not an exceptional state, so it renders as a calm NEUTRAL
|
|
98
|
+
* step on the rail (never accent/color). When the host app supplies an
|
|
99
|
+
* `onMemoryClick` handler the row also deep-links to the record in its memory
|
|
100
|
+
* pane; without one it is non-interactive rich content.
|
|
101
|
+
*/
|
|
102
|
+
type MemoryItem = {
|
|
103
|
+
kind: "memory";
|
|
104
|
+
id: string;
|
|
105
|
+
turnId: string | null;
|
|
106
|
+
variant: "saved" | "corrected";
|
|
107
|
+
/** The memory's kind enum (`"preference" | "semantic" | …`); mapped to a human label at render. */
|
|
108
|
+
memoryKind: string;
|
|
109
|
+
/** The memory text, ellipsized to ≤120 chars server-side. For a supersede this is the OLD text. */
|
|
110
|
+
preview: string;
|
|
111
|
+
/** The save collapsed into an existing memory (no new row was written). Saved variant only. */
|
|
112
|
+
deduped?: boolean;
|
|
113
|
+
/** The NEW text when a correction superseded the memory with a replacement; absent = updated-in-place or archived. */
|
|
114
|
+
replacementPreview?: string;
|
|
115
|
+
/**
|
|
116
|
+
* What a `memory.corrected` did: `"superseded"` (replaced by a new record, see
|
|
117
|
+
* `replacementPreview`), `"updated"` (edited in place — the record lives on), or
|
|
118
|
+
* `"archived"` (retired). Distinguishes updated-in-place from archived when there
|
|
119
|
+
* is no replacement. Corrected variant only; read defensively (may be absent).
|
|
120
|
+
*/
|
|
121
|
+
action?: string;
|
|
122
|
+
/** The saved / corrected memory's id — the deep-link target for a save. */
|
|
123
|
+
memoryId: string;
|
|
124
|
+
/** The replacement memory's id when a correction produced one — the LIVE record the deep-link targets. */
|
|
125
|
+
replacementMemoryId?: string;
|
|
126
|
+
occurredAt: string;
|
|
127
|
+
};
|
|
128
|
+
type FleetDecisionScoreItem = {
|
|
129
|
+
candidateKey: string;
|
|
130
|
+
eligible: boolean;
|
|
131
|
+
rejectionReason: "allocator_disabled" | "unavailable" | "cooling" | "quota_ceiling" | "overlay_isolation" | null;
|
|
132
|
+
total: number;
|
|
133
|
+
confidence: "unknown" | "low" | "medium" | "high";
|
|
134
|
+
};
|
|
135
|
+
/**
|
|
136
|
+
* One production-vs-shadow placement explanation. Candidate keys are
|
|
137
|
+
* event-local aliases only; no credential/account identity reaches this item.
|
|
138
|
+
*/
|
|
139
|
+
type FleetDecisionItem = {
|
|
140
|
+
kind: "fleet-decision";
|
|
141
|
+
id: string;
|
|
142
|
+
turnId: string | null;
|
|
143
|
+
policyVersion: "adaptive-shadow-v1";
|
|
144
|
+
actualOutcome: "selected" | "waiting" | "none";
|
|
145
|
+
actualCandidateKey: string | null;
|
|
146
|
+
actualReason: "lease_reused" | "pin" | "rotation" | "active" | "all_capped" | "none";
|
|
147
|
+
shadowOutcome: "selected" | "paced" | "none";
|
|
148
|
+
shadowCandidateKey: string | null;
|
|
149
|
+
shadowReason: "fenced_in_flight" | "fenced_candidate_missing" | "admission_paced" | "no_eligible_candidate" | "overlay_isolated_empty" | "best_score" | "affinity_best" | "hysteresis_hold";
|
|
150
|
+
comparison: "match" | "different_candidate" | "different_outcome" | "not_comparable_truncated";
|
|
151
|
+
confidence: "unknown" | "low" | "medium" | "high";
|
|
152
|
+
admissionOutcome: "admit" | "pace";
|
|
153
|
+
admissionReason: "fenced_in_flight" | "pacing_disabled" | "capacity_unknown" | "capacity_available" | "work_conserving_borrow" | "manager_priority" | "standard_starvation_bound" | "capacity_saturated" | "emergency_fuse";
|
|
154
|
+
borrowedIdleCapacity: boolean;
|
|
155
|
+
borrowedOverlayCapacity: boolean;
|
|
156
|
+
strandedEligibleCount: number;
|
|
157
|
+
candidateCount: number;
|
|
158
|
+
truncatedCandidateCount: number;
|
|
159
|
+
scoreRowsTruncatedCount: number;
|
|
160
|
+
scores: FleetDecisionScoreItem[];
|
|
161
|
+
occurredAt: string;
|
|
162
|
+
};
|
|
163
|
+
type SessionStatusItem = {
|
|
164
|
+
kind: "session-status";
|
|
165
|
+
id: string;
|
|
166
|
+
status: SessionStatus;
|
|
167
|
+
occurredAt: string;
|
|
168
|
+
};
|
|
169
|
+
type GoalItem = {
|
|
170
|
+
kind: "goal";
|
|
171
|
+
id: string;
|
|
172
|
+
action: "set" | "updated" | "completed" | "paused" | "resumed" | "cleared" | "continuation";
|
|
173
|
+
text: string | null;
|
|
174
|
+
occurredAt: string;
|
|
175
|
+
};
|
|
176
|
+
type NoticeItem = {
|
|
177
|
+
kind: "notice";
|
|
178
|
+
id: string;
|
|
179
|
+
tone: "waiting" | "cancelled" | "failed";
|
|
180
|
+
text: string;
|
|
181
|
+
/** Optional evidence kept inspectable without overwhelming the main rail. */
|
|
182
|
+
details?: {
|
|
183
|
+
label: string;
|
|
184
|
+
value: unknown;
|
|
185
|
+
};
|
|
186
|
+
action?: {
|
|
187
|
+
label: string;
|
|
188
|
+
url: string;
|
|
189
|
+
};
|
|
190
|
+
occurredAt: string;
|
|
191
|
+
};
|
|
192
|
+
/**
|
|
193
|
+
* A tool call hit a missing or lapsed connection. The broker reports that
|
|
194
|
+
* condition as a tool error and the turn continues; reconnecting never resumes
|
|
195
|
+
* or replays the original call. Carries the structured `tool.auth_needed`
|
|
196
|
+
* payload so the renderer can draw a clean inline recovery affordance (provider
|
|
197
|
+
* logo + one human line + a Connect/Reconnect button) and the app can start the
|
|
198
|
+
* right flow (OAuth reconnect for the surviving connection, or credential
|
|
199
|
+
* re-entry for an api-key one). The `reason` shapes the human copy but is never
|
|
200
|
+
* shown raw.
|
|
201
|
+
*/
|
|
202
|
+
type AuthNeededItem = {
|
|
203
|
+
kind: "auth-needed";
|
|
204
|
+
id: string;
|
|
205
|
+
turnId: string | null;
|
|
206
|
+
/** The connection's registrable domain, e.g. "linear.app". */
|
|
207
|
+
providerDomain: string;
|
|
208
|
+
/** The lapsed connection to reconnect, when the row survived. */
|
|
209
|
+
connectionId: string | null;
|
|
210
|
+
reason: ToolAuthNeededPayload["reason"] | null;
|
|
211
|
+
/** Scopes the provider now needs; may inform the copy, never shown as a raw label. */
|
|
212
|
+
scopes: string[];
|
|
213
|
+
/** The OAuth `resource` (RFC 8707) the reconnect should target, when supplied. */
|
|
214
|
+
resource: string | null;
|
|
215
|
+
/** The tool whose call triggered the reauth, for context. */
|
|
216
|
+
toolName: string | null;
|
|
217
|
+
/** A pre-minted authorization URL, when the broker already produced one. */
|
|
218
|
+
authorizationUrl: string | null;
|
|
219
|
+
occurredAt: string;
|
|
220
|
+
};
|
|
221
|
+
type TurnOutcome = "complete" | "failed" | "cancelled";
|
|
222
|
+
type TurnEndItem = {
|
|
223
|
+
kind: "turn-end";
|
|
224
|
+
id: string;
|
|
225
|
+
turnId: string | null;
|
|
226
|
+
outcome: TurnOutcome;
|
|
227
|
+
failureText: string | null;
|
|
228
|
+
occurredAt: string;
|
|
229
|
+
};
|
|
230
|
+
type TimelineItem = UserMessageItem | AgentMessageItem | ReasoningItem | ToolCallItem | WorkerItem | WorkerCompletionItem | SandboxItem | SessionStatusItem | GoalItem | NoticeItem | AuthNeededItem | MemoryItem | FleetDecisionItem | TurnEndItem;
|
|
231
|
+
/** Activity items cluster between chat messages (reasoning, tools, workers, sandbox, memory). */
|
|
232
|
+
type ActivityItem = ReasoningItem | ToolCallItem | WorkerItem | SandboxItem | MemoryItem | FleetDecisionItem;
|
|
233
|
+
type TimelineGroup = {
|
|
234
|
+
kind: "item";
|
|
235
|
+
item: TimelineItem;
|
|
236
|
+
} | {
|
|
237
|
+
kind: "activity";
|
|
238
|
+
id: string;
|
|
239
|
+
items: ActivityItem[];
|
|
240
|
+
outcome?: TurnOutcome;
|
|
241
|
+
failureText?: string;
|
|
242
|
+
} | {
|
|
243
|
+
kind: "turn";
|
|
244
|
+
id: string;
|
|
245
|
+
outcome: TurnOutcome;
|
|
246
|
+
failureText?: string;
|
|
247
|
+
startedAt: string;
|
|
248
|
+
endedAt: string;
|
|
249
|
+
groups: TimelineGroup[];
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
/** Events that can change the authoritative prompt queue or effective control. */
|
|
253
|
+
declare function isTurnQueueEvent(event: Pick<SessionEvent, "type">): boolean;
|
|
254
|
+
type QueueMutationKind = "move" | "edit" | "steer" | "delete";
|
|
255
|
+
type UseTurnQueueOptions = EmbeddedSessionClientOverride & SessionEventFeedOptions & {
|
|
256
|
+
pollIntervalMs?: number | undefined;
|
|
257
|
+
};
|
|
258
|
+
type UseTurnQueueResult = {
|
|
259
|
+
snapshot: SessionQueueSnapshot | null;
|
|
260
|
+
/** Human/API prompts exactly in server execution order. Never client-sorted. */
|
|
261
|
+
queue: SessionTurn[];
|
|
262
|
+
effectiveControl: EffectiveSessionControl | null;
|
|
263
|
+
/** The latest interrupted attempt has not yet durably proved physical quiescence. */
|
|
264
|
+
stoppingPreviousAttempt: boolean;
|
|
265
|
+
loading: boolean;
|
|
266
|
+
error: Error | null;
|
|
267
|
+
refresh: () => Promise<void>;
|
|
268
|
+
moveTurn: (turnId: string, beforeTurnId: string | null) => Promise<boolean>;
|
|
269
|
+
/** Atomically withdraw a waiting prompt into the private durable composer draft. */
|
|
270
|
+
editTurn: (turnId: string, options: {
|
|
271
|
+
expectedDraftRevision: number;
|
|
272
|
+
replaceDraft: boolean;
|
|
273
|
+
}) => Promise<ComposerDraft | null>;
|
|
274
|
+
/** Advance the same durable waiting prompt; no duplicate prompt is created. */
|
|
275
|
+
steerTurn: (turnId: string) => Promise<boolean>;
|
|
276
|
+
removeTurn: (turnId: string) => Promise<boolean>;
|
|
277
|
+
pendingByTurn: Readonly<Record<string, QueueMutationKind>>;
|
|
278
|
+
mutationFor: (turnId: string) => QueueMutationKind | null;
|
|
279
|
+
mutating: boolean;
|
|
280
|
+
mutationError: Error | null;
|
|
281
|
+
clearMutationError: () => void;
|
|
282
|
+
};
|
|
283
|
+
/**
|
|
284
|
+
* The one authoritative human prompt queue. Every mutation carries the exact
|
|
285
|
+
* server versions the operator saw and accepts only monotonic snapshots. A
|
|
286
|
+
* conflict immediately reloads server truth; the client never invents order.
|
|
287
|
+
*/
|
|
288
|
+
declare function useTurnQueue(sessionId: string | null | undefined, options?: UseTurnQueueOptions): UseTurnQueueResult;
|
|
289
|
+
|
|
290
|
+
export { type ActivityItem as A, type GoalItem as G, type MemoryItem as M, type NoticeItem as N, type QueueMutationKind as Q, type ReasoningItem as R, type SandboxItem as S, type TurnOutcome as T, type UseTurnQueueOptions as U, type WorkerItem as W, type AgentMessageItem as a, type AuthNeededItem as b, type SessionStatusItem as c, type TimelineGroup as d, type TimelineItem as e, type ToolCallItem as f, type TurnEndItem as g, type UseTurnQueueResult as h, type UserMessageItem as i, isTurnQueueEvent as j, type WorkerCompletionItem as k, useTurnQueue as u };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengeni/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.5",
|
|
4
4
|
"description": "React hooks and styled components for OpenGeni: live session streaming, chat composer, message timeline, session status, and fleet views — token-themed (CSS variables), dark-first, built on Tailwind v4 + Radix + Motion.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -31,6 +31,10 @@
|
|
|
31
31
|
"types": "./dist/session.d.ts",
|
|
32
32
|
"import": "./dist/session.js"
|
|
33
33
|
},
|
|
34
|
+
"./session-ui": {
|
|
35
|
+
"types": "./dist/session-ui.d.ts",
|
|
36
|
+
"import": "./dist/session-ui.js"
|
|
37
|
+
},
|
|
34
38
|
"./machines": {
|
|
35
39
|
"types": "./dist/machines.d.ts",
|
|
36
40
|
"import": "./dist/machines.js"
|
|
@@ -62,7 +66,7 @@
|
|
|
62
66
|
"@dnd-kit/utilities": "3.2.2",
|
|
63
67
|
"@fontsource-variable/noto-sans-arabic": "5.2.10",
|
|
64
68
|
"@fontsource-variable/noto-sans-jp": "5.2.10",
|
|
65
|
-
"@opengeni/sdk": "^0.
|
|
69
|
+
"@opengeni/sdk": "^0.25.5",
|
|
66
70
|
"clsx": "^2.1.1",
|
|
67
71
|
"lucide-react": "^1.8.0",
|
|
68
72
|
"motion": "^12.0.0",
|
|
@@ -199,7 +199,7 @@ export const defaultChatComposerMessages: ChatComposerMessages = {
|
|
|
199
199
|
export type ComposerSubmitMode = "queue" | "steer";
|
|
200
200
|
export type ComposerSubmitBlocker =
|
|
201
201
|
| "disabled"
|
|
202
|
-
| "
|
|
202
|
+
| "attachment"
|
|
203
203
|
| "sending"
|
|
204
204
|
| "command"
|
|
205
205
|
| "empty"
|
|
@@ -292,7 +292,7 @@ export function useChatComposerController({
|
|
|
292
292
|
return () => document.removeEventListener(OPEN_WORKSTREAM_CONTROL_EVENT, openControl);
|
|
293
293
|
}, [id, paused]);
|
|
294
294
|
|
|
295
|
-
const
|
|
295
|
+
const blockedByAttachment = attachments?.hasUnresolved === true;
|
|
296
296
|
const hasReadyAttachment = (attachments?.readyResources.length ?? 0) > 0;
|
|
297
297
|
|
|
298
298
|
const [dragging, setDragging] = useState(false);
|
|
@@ -386,8 +386,8 @@ export function useChatComposerController({
|
|
|
386
386
|
|
|
387
387
|
const submitBlocker: ComposerSubmitBlocker = disabled
|
|
388
388
|
? "disabled"
|
|
389
|
-
:
|
|
390
|
-
? "
|
|
389
|
+
: blockedByAttachment
|
|
390
|
+
? "attachment"
|
|
391
391
|
: delivery.sending || submitting
|
|
392
392
|
? "sending"
|
|
393
393
|
: commandDraftBlocked
|
|
@@ -404,7 +404,8 @@ export function useChatComposerController({
|
|
|
404
404
|
delivery.clearError();
|
|
405
405
|
return false;
|
|
406
406
|
}
|
|
407
|
-
if (disabled ||
|
|
407
|
+
if (disabled || blockedByAttachment || delivery.sending || submittingRef.current)
|
|
408
|
+
return false;
|
|
408
409
|
if (!delivery.canSend && !hasReadyAttachment) return false;
|
|
409
410
|
submittingRef.current = true;
|
|
410
411
|
setSubmitting(true);
|
|
@@ -416,7 +417,7 @@ export function useChatComposerController({
|
|
|
416
417
|
}
|
|
417
418
|
},
|
|
418
419
|
[
|
|
419
|
-
|
|
420
|
+
blockedByAttachment,
|
|
420
421
|
commandDraftBlocked,
|
|
421
422
|
delivery,
|
|
422
423
|
disabled,
|
|
@@ -150,18 +150,23 @@ export function MessageTimeline({
|
|
|
150
150
|
// structurally impossible — the reader only ever sees it already at the
|
|
151
151
|
// bottom. An empty timeline reveals immediately (there is nothing to anchor).
|
|
152
152
|
const [revealed, setRevealed] = useState(false);
|
|
153
|
-
// Our own scrollTop assignments echo back as scroll events
|
|
154
|
-
//
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
|
|
153
|
+
// Our own scrollTop assignments echo back as delayed scroll events. Keep the
|
|
154
|
+
// ordered, clamped targets rather than only the latest one: successive
|
|
155
|
+
// ResizeObserver corrections can queue several echoes before the browser
|
|
156
|
+
// dispatches them. If reader input lands at a different position, its value
|
|
157
|
+
// wins and clears the pending echoes. A matching echo must not recapture the
|
|
158
|
+
// visual anchor while progressive prepend is still mounting older rows.
|
|
159
|
+
const programmaticScrollTargetsRef = useRef<number[]>([]);
|
|
159
160
|
const assignScrollTop = useCallback((node: HTMLElement, value: number) => {
|
|
160
161
|
const previous = node.scrollTop;
|
|
161
|
-
programmaticScrollRef.current = true;
|
|
162
162
|
node.scrollTop = value;
|
|
163
163
|
if (node.scrollTop === previous) {
|
|
164
|
-
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const pending = programmaticScrollTargetsRef.current;
|
|
167
|
+
pending.push(node.scrollTop);
|
|
168
|
+
if (pending.length > 32) {
|
|
169
|
+
pending.splice(0, pending.length - 32);
|
|
165
170
|
}
|
|
166
171
|
}, []);
|
|
167
172
|
// Mirror `pinned` into a ref so the ResizeObserver callback (a stable closure)
|
|
@@ -187,9 +192,12 @@ export function MessageTimeline({
|
|
|
187
192
|
allGroups.length > 0 && (bulkActive || firstKeyChangedForBulk || mountingOlderGroups);
|
|
188
193
|
|
|
189
194
|
// Snapshot the topmost visible element and where it sits in the viewport, so a
|
|
190
|
-
// later reflow can restore it to the same spot.
|
|
191
|
-
//
|
|
192
|
-
//
|
|
195
|
+
// later reflow can restore it to the same spot. Prefer durable row wrappers
|
|
196
|
+
// over their containing group: an older activity item can merge into the
|
|
197
|
+
// existing group, leaving that outer group in place while shifting every row
|
|
198
|
+
// after the insertion. Transient chrome (the backfill sentinel and shimmer) is
|
|
199
|
+
// skipped — anchoring to a row that unmounts when the older window lands would
|
|
200
|
+
// drop the correction mid-prepend.
|
|
193
201
|
const captureAnchor = useCallback(() => {
|
|
194
202
|
const node = scrollRef.current;
|
|
195
203
|
const inner = node?.firstElementChild;
|
|
@@ -198,31 +206,63 @@ export function MessageTimeline({
|
|
|
198
206
|
return;
|
|
199
207
|
}
|
|
200
208
|
const containerTop = node.getBoundingClientRect().top;
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
+
const findVisible = (selector: string): Element | null => {
|
|
210
|
+
for (const candidate of inner.querySelectorAll(selector)) {
|
|
211
|
+
if (candidate instanceof HTMLElement && candidate.dataset.ogTimelineChrome !== undefined) {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
const rect = candidate.getBoundingClientRect();
|
|
215
|
+
if (rect.bottom > containerTop + 1 && rect.height > 0) {
|
|
216
|
+
return candidate;
|
|
217
|
+
}
|
|
209
218
|
}
|
|
219
|
+
return null;
|
|
220
|
+
};
|
|
221
|
+
const anchor =
|
|
222
|
+
findVisible("[data-og-timeline-row-anchor]") ??
|
|
223
|
+
findVisible("[data-og-timeline-group-anchor]");
|
|
224
|
+
if (anchor) {
|
|
225
|
+
const rect = anchor.getBoundingClientRect();
|
|
226
|
+
anchorRef.current = { el: anchor, top: rect.top - containerTop };
|
|
227
|
+
return;
|
|
210
228
|
}
|
|
211
229
|
anchorRef.current = null;
|
|
212
230
|
}, []);
|
|
213
231
|
|
|
214
|
-
//
|
|
232
|
+
// Restore the user-owned anchor synchronously after React commits a frame.
|
|
233
|
+
// ResizeObserver remains a late-measurement fallback, but progressive history
|
|
234
|
+
// frames must be corrected before the browser can paint an intermediate jump.
|
|
235
|
+
const restoreAnchor = useCallback(
|
|
236
|
+
(node: HTMLElement) => {
|
|
237
|
+
if (autoFollow && pinnedRef.current) {
|
|
238
|
+
assignScrollTop(node, node.scrollHeight);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const anchor = anchorRef.current;
|
|
242
|
+
if (!anchor || !anchor.el.isConnected) {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
const containerTop = node.getBoundingClientRect().top;
|
|
246
|
+
const now = anchor.el.getBoundingClientRect().top - containerTop;
|
|
247
|
+
const diff = now - anchor.top;
|
|
248
|
+
if (diff !== 0) {
|
|
249
|
+
assignScrollTop(node, node.scrollTop + diff);
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
[assignScrollTop, autoFollow],
|
|
253
|
+
);
|
|
254
|
+
|
|
215
255
|
// A LAYOUT effect so the very first paint of a freshly loaded session is
|
|
216
256
|
// already anchored at the bottom — no visible traversal down the history.
|
|
217
257
|
useLayoutEffect(() => {
|
|
218
258
|
const node = scrollRef.current;
|
|
219
|
-
if (node
|
|
220
|
-
|
|
259
|
+
if (node) {
|
|
260
|
+
restoreAnchor(node);
|
|
221
261
|
}
|
|
222
262
|
if (!revealed && groups.length > 0) {
|
|
223
263
|
setRevealed(true);
|
|
224
264
|
}
|
|
225
|
-
}, [resolvedItems, working,
|
|
265
|
+
}, [resolvedItems, working, revealed, groups.length, restoreAnchor]);
|
|
226
266
|
|
|
227
267
|
// A cleared timeline (stream identity change) re-arms the reveal so the next
|
|
228
268
|
// session also first paints at its bottom.
|
|
@@ -297,32 +337,26 @@ export function MessageTimeline({
|
|
|
297
337
|
if (!current) {
|
|
298
338
|
return;
|
|
299
339
|
}
|
|
300
|
-
|
|
301
|
-
assignScrollTop(current, current.scrollHeight);
|
|
302
|
-
} else {
|
|
303
|
-
const anchor = anchorRef.current;
|
|
304
|
-
if (anchor && anchor.el.isConnected) {
|
|
305
|
-
const containerTop = current.getBoundingClientRect().top;
|
|
306
|
-
const now = anchor.el.getBoundingClientRect().top - containerTop;
|
|
307
|
-
const diff = now - anchor.top;
|
|
308
|
-
if (diff !== 0) {
|
|
309
|
-
assignScrollTop(current, current.scrollTop + diff);
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
captureAnchor();
|
|
340
|
+
restoreAnchor(current);
|
|
314
341
|
});
|
|
315
342
|
observer.observe(inner);
|
|
316
343
|
return () => observer.disconnect();
|
|
317
|
-
}, [
|
|
344
|
+
}, [restoreAnchor]);
|
|
318
345
|
|
|
319
346
|
const onScroll = () => {
|
|
320
347
|
const node = scrollRef.current;
|
|
321
348
|
if (!node) {
|
|
322
349
|
return;
|
|
323
350
|
}
|
|
324
|
-
const
|
|
325
|
-
|
|
351
|
+
const pending = programmaticScrollTargetsRef.current;
|
|
352
|
+
const programmaticIndex = pending.findIndex((target) => Math.abs(node.scrollTop - target) <= 1);
|
|
353
|
+
const programmatic = programmaticIndex >= 0;
|
|
354
|
+
if (programmatic) {
|
|
355
|
+
pending.splice(0, programmaticIndex + 1);
|
|
356
|
+
} else {
|
|
357
|
+
// A real reader scroll supersedes every delayed programmatic echo.
|
|
358
|
+
pending.length = 0;
|
|
359
|
+
}
|
|
326
360
|
const nextPinned = node.scrollHeight - node.scrollTop - node.clientHeight < 48;
|
|
327
361
|
// Echoes of our own assignments may PIN but never UNPIN — only the reader
|
|
328
362
|
// scrolling away releases the bottom-follow.
|
|
@@ -330,7 +364,12 @@ export function MessageTimeline({
|
|
|
330
364
|
pinnedRef.current = nextPinned;
|
|
331
365
|
setPinned(nextPinned);
|
|
332
366
|
}
|
|
333
|
-
|
|
367
|
+
// The assignment site already captured the corrected anchor. Recapturing
|
|
368
|
+
// it from a delayed echo can observe the next prepended row before that
|
|
369
|
+
// row's ResizeObserver correction and make the viewport walk up the page.
|
|
370
|
+
if (!programmatic) {
|
|
371
|
+
captureAnchor();
|
|
372
|
+
}
|
|
334
373
|
};
|
|
335
374
|
|
|
336
375
|
return (
|
|
@@ -366,17 +405,18 @@ export function MessageTimeline({
|
|
|
366
405
|
</div>
|
|
367
406
|
) : null}
|
|
368
407
|
{groups.map(({ group, key }, index) => (
|
|
369
|
-
<
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
408
|
+
<div key={key} data-og-timeline-group-anchor="">
|
|
409
|
+
<TimelineGroupView
|
|
410
|
+
group={group}
|
|
411
|
+
renderMessageText={renderMessageText}
|
|
412
|
+
onOpenSession={onOpenSession}
|
|
413
|
+
onMemoryClick={onMemoryClick}
|
|
414
|
+
onReconnect={onReconnect}
|
|
415
|
+
resolveProviderLogo={resolveProviderLogo}
|
|
416
|
+
toolRegistry={toolRegistry}
|
|
417
|
+
foldLiveCluster={isAgentProgress(groups[index + 1]?.group)}
|
|
418
|
+
/>
|
|
419
|
+
</div>
|
|
380
420
|
))}
|
|
381
421
|
{working ? (
|
|
382
422
|
<div className="flex items-center gap-2 text-sm">
|
|
@@ -731,8 +771,8 @@ function clusterIsSettled(group: Extract<TimelineGroup, { kind: "activity" }>):
|
|
|
731
771
|
if (item.kind === "reasoning") {
|
|
732
772
|
return !item.streaming;
|
|
733
773
|
}
|
|
734
|
-
//
|
|
735
|
-
if (item.kind === "memory") {
|
|
774
|
+
// Memory writes and fleet observations are discrete, already-settled events.
|
|
775
|
+
if (item.kind === "memory" || item.kind === "fleet-decision") {
|
|
736
776
|
return true;
|
|
737
777
|
}
|
|
738
778
|
return item.status !== "running";
|