@opengeni/react 0.23.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-UQS7OQDE.js → chunk-AVPU5PMC.js} +6 -4
- package/dist/chunk-AVPU5PMC.js.map +1 -0
- package/dist/{chunk-CATATY77.js → chunk-RDPDU4TA.js} +7 -6
- package/dist/chunk-RDPDU4TA.js.map +1 -0
- package/dist/{chunk-UAHPKLB6.js → chunk-SHOFILHJ.js} +18 -8
- package/dist/chunk-SHOFILHJ.js.map +1 -0
- package/dist/{composer-CVm5uoUI.d.ts → composer-BXb0Q1HF.d.ts} +18 -4
- package/dist/composer.d.ts +2 -2
- package/dist/composer.js +2 -2
- 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 +12 -9
- package/dist/index.js +332 -224
- package/dist/index.js.map +1 -1
- package/dist/{session-Db00TRwl.d.ts → session-BEvtFWhe.d.ts} +40 -14
- package/dist/session.d.ts +2 -2
- package/dist/session.js +2 -2
- package/dist/{use-composer-By1wn3MM.d.ts → use-composer-BnEzheSk.d.ts} +8 -2
- package/package.json +2 -2
- package/src/components/composer.tsx +7 -6
- package/src/components/message-timeline.tsx +2 -2
- package/src/hooks/use-composer.ts +15 -5
- package/src/hooks/use-file-attachments.ts +95 -2
- package/src/hooks/use-goal.ts +22 -4
- package/src/hooks/use-workspace-sessions.ts +30 -5
- package/src/timeline/activity-rail.tsx +17 -1
- 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-UAHPKLB6.js.map +0 -1
- package/dist/chunk-UQS7OQDE.js.map +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { E as EmbeddedSessionClientOverride, e as EmbeddedSessionMcpApprovalPolicyClientOverride, f as EmbeddedHumanInputClientOverride } from './session-context-rgrfElMl.js';
|
|
2
2
|
import { ToolAuthNeededPayload, ResourceRef, ToolRef, SessionStatus, SessionEvent, StreamConnectionState, SessionQueueSnapshot, SessionTurn, EffectiveSessionControl, ComposerDraft, SessionControlResponse, SessionMcpServerMetadata, SessionMcpApprovalPolicy, UpdateSessionMcpApprovalPolicyResponse, HumanInputQuestion, SessionHumanInputRequest, SubmitHumanInputResponseRequest } from '@opengeni/sdk';
|
|
3
|
-
import { S as SessionEventFeedOptions } from './use-composer-
|
|
3
|
+
import { S as SessionEventFeedOptions } from './use-composer-BnEzheSk.js';
|
|
4
4
|
|
|
5
5
|
type UserMessageItem = {
|
|
6
6
|
kind: "user-message";
|
|
@@ -125,6 +125,41 @@ type MemoryItem = {
|
|
|
125
125
|
replacementMemoryId?: string;
|
|
126
126
|
occurredAt: string;
|
|
127
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
|
+
};
|
|
128
163
|
type SessionStatusItem = {
|
|
129
164
|
kind: "session-status";
|
|
130
165
|
id: string;
|
|
@@ -192,9 +227,9 @@ type TurnEndItem = {
|
|
|
192
227
|
failureText: string | null;
|
|
193
228
|
occurredAt: string;
|
|
194
229
|
};
|
|
195
|
-
type TimelineItem = UserMessageItem | AgentMessageItem | ReasoningItem | ToolCallItem | WorkerItem | WorkerCompletionItem | SandboxItem | SessionStatusItem | GoalItem | NoticeItem | AuthNeededItem | MemoryItem | TurnEndItem;
|
|
230
|
+
type TimelineItem = UserMessageItem | AgentMessageItem | ReasoningItem | ToolCallItem | WorkerItem | WorkerCompletionItem | SandboxItem | SessionStatusItem | GoalItem | NoticeItem | AuthNeededItem | MemoryItem | FleetDecisionItem | TurnEndItem;
|
|
196
231
|
/** Activity items cluster between chat messages (reasoning, tools, workers, sandbox, memory). */
|
|
197
|
-
type ActivityItem = ReasoningItem | ToolCallItem | WorkerItem | SandboxItem | MemoryItem;
|
|
232
|
+
type ActivityItem = ReasoningItem | ToolCallItem | WorkerItem | SandboxItem | MemoryItem | FleetDecisionItem;
|
|
198
233
|
type TimelineGroup = {
|
|
199
234
|
kind: "item";
|
|
200
235
|
item: TimelineItem;
|
|
@@ -414,6 +449,8 @@ type UseHumanInputRequestsResult = {
|
|
|
414
449
|
*/
|
|
415
450
|
declare function useHumanInputRequests(sessionId: string | null | undefined, options?: UseHumanInputRequestsOptions): UseHumanInputRequestsResult;
|
|
416
451
|
|
|
452
|
+
/** Readable label for a tool call, without leaking an MCP server prefix. */
|
|
453
|
+
declare function toolDisplayName(name: string): string;
|
|
417
454
|
declare function buildTimeline(events: SessionEvent[]): TimelineItem[];
|
|
418
455
|
/**
|
|
419
456
|
* Whether the session's most recent turn ended in credit exhaustion — the
|
|
@@ -431,16 +468,5 @@ declare function groupTimeline(items: TimelineItem[]): TimelineGroup[];
|
|
|
431
468
|
* text: "{...}" }], structuredContent? }`).
|
|
432
469
|
*/
|
|
433
470
|
declare function extractSessionRef(value: unknown, depth?: number): string | null;
|
|
434
|
-
/**
|
|
435
|
-
* Readable label for a tool call ("session_create" -> "session create").
|
|
436
|
-
*
|
|
437
|
-
* MCP tools are namespaced `<serverId>__<toolName>` (see prefixedMcpToolName),
|
|
438
|
-
* and for catalog-imported servers that serverId is an opaque slug+hash
|
|
439
|
-
* ("mcp-integrations-sh-supabase-com-34ed9dcf1390-0i6tcf8"). De-slugging the
|
|
440
|
-
* whole thing leaked that id into the timeline; strip the server prefix and show
|
|
441
|
-
* just the tool ("list organizations"). Names without the `__` boundary (plain
|
|
442
|
-
* built-ins like "session_create") are unaffected.
|
|
443
|
-
*/
|
|
444
|
-
declare function toolDisplayName(name: string): string;
|
|
445
471
|
|
|
446
472
|
export { useTurnQueue as $, type ActivityItem as A, type BrowserSessionEventWindow as B, buildTimeline as C, creditExhaustedFromEvents as D, extractSessionRef as E, groupTimeline as F, type GoalItem as G, humanInputRequestFromEvent as H, isHumanInputEvent as I, isSessionMcpApprovalPolicyEvent as J, isTurnQueueEvent as K, projectPendingApprovals as L, projectPendingHumanInputRequests as M, type NoticeItem as N, sessionStatusFromEvents as O, type PendingApproval as P, type QueueMutationKind as Q, type ReasoningItem as R, SESSION_EVENT_BROWSER_MAX_BYTES as S, type ToolCallItem as T, type UseTurnQueueResult as U, toolDisplayName as V, type WorkerItem as W, useHumanInputRequests as X, useSessionControl as Y, useSessionEvents as Z, useSessionMcpApprovalPolicy as _, type TurnOutcome as a, type MemoryItem as a0, type WorkerCompletionItem as a1, type TimelineItem as b, type AgentMessageItem as c, type UserMessageItem as d, type AuthNeededItem as e, type PendingHumanInputRequest as f, SESSION_EVENT_BROWSER_MAX_COUNT as g, SESSION_EVENT_BROWSER_PENDING_MAX_BYTES as h, SESSION_EVENT_BROWSER_PENDING_MAX_COUNT as i, SESSION_EVENT_BROWSER_SINGLE_EVENT_MAX_BYTES as j, type SandboxItem as k, type SessionEventsConnectionState as l, type SessionStatusItem as m, type TimelineGroup as n, type TurnEndItem as o, type UseHumanInputRequestsOptions as p, type UseHumanInputRequestsResult as q, type UseSessionControlOptions as r, type UseSessionControlResult as s, type UseSessionEventsOptions as t, type UseSessionEventsResult as u, type UseSessionMcpApprovalPolicyOptions as v, type UseSessionMcpApprovalPolicyResult as w, type UseTurnQueueOptions as x, approvalsFromRequiresAction as y, boundBrowserSessionEventWindow as z };
|
package/dist/session.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { E as ClientOverride, f as HumanInputClientOverride, a as HumanInputSessionClientLike, b as SessionClientLike, c as SessionMcpApprovalPolicyClientLike, e as SessionMcpApprovalPolicyClientOverride } from './session-context-rgrfElMl.js';
|
|
2
|
-
export { A as ActivityItem, c as AgentMessageItem, e as AuthNeededItem, G as GoalItem, a0 as MemoryItem, N as NoticeItem, P as PendingApproval, f as PendingHumanInputRequest, Q as QueueMutationKind, R as ReasoningItem, k as SandboxItem, l as SessionEventsConnectionState, m as SessionStatusItem, n as TimelineGroup, b as TimelineItem, T as ToolCallItem, o as TurnEndItem, a as TurnOutcome, p as UseHumanInputRequestsOptions, q as UseHumanInputRequestsResult, r as UseSessionControlOptions, s as UseSessionControlResult, t as UseSessionEventsOptions, u as UseSessionEventsResult, v as UseSessionMcpApprovalPolicyOptions, w as UseSessionMcpApprovalPolicyResult, x as UseTurnQueueOptions, U as UseTurnQueueResult, d as UserMessageItem, a1 as WorkerCompletionItem, W as WorkerItem, y as approvalsFromRequiresAction, C as buildTimeline, D as creditExhaustedFromEvents, E as extractSessionRef, F as groupTimeline, H as humanInputRequestFromEvent, I as isHumanInputEvent, J as isSessionMcpApprovalPolicyEvent, K as isTurnQueueEvent, L as projectPendingApprovals, M as projectPendingHumanInputRequests, O as sessionStatusFromEvents, V as toolDisplayName, X as useHumanInputRequests, Y as useSessionControl, Z as useSessionEvents, _ as useSessionMcpApprovalPolicy, $ as useTurnQueue } from './session-
|
|
3
|
-
export { a as ComposerSendExtras, C as ComposerState, F as FILE_ONLY_MESSAGE_TEXT, U as UseComposerOptions, c as composeSendInput, s as shouldSteerOnKey, b as shouldSubmitOnKey, u as useComposer } from './use-composer-
|
|
2
|
+
export { A as ActivityItem, c as AgentMessageItem, e as AuthNeededItem, G as GoalItem, a0 as MemoryItem, N as NoticeItem, P as PendingApproval, f as PendingHumanInputRequest, Q as QueueMutationKind, R as ReasoningItem, k as SandboxItem, l as SessionEventsConnectionState, m as SessionStatusItem, n as TimelineGroup, b as TimelineItem, T as ToolCallItem, o as TurnEndItem, a as TurnOutcome, p as UseHumanInputRequestsOptions, q as UseHumanInputRequestsResult, r as UseSessionControlOptions, s as UseSessionControlResult, t as UseSessionEventsOptions, u as UseSessionEventsResult, v as UseSessionMcpApprovalPolicyOptions, w as UseSessionMcpApprovalPolicyResult, x as UseTurnQueueOptions, U as UseTurnQueueResult, d as UserMessageItem, a1 as WorkerCompletionItem, W as WorkerItem, y as approvalsFromRequiresAction, C as buildTimeline, D as creditExhaustedFromEvents, E as extractSessionRef, F as groupTimeline, H as humanInputRequestFromEvent, I as isHumanInputEvent, J as isSessionMcpApprovalPolicyEvent, K as isTurnQueueEvent, L as projectPendingApprovals, M as projectPendingHumanInputRequests, O as sessionStatusFromEvents, V as toolDisplayName, X as useHumanInputRequests, Y as useSessionControl, Z as useSessionEvents, _ as useSessionMcpApprovalPolicy, $ as useTurnQueue } from './session-BEvtFWhe.js';
|
|
3
|
+
export { a as ComposerSendExtras, C as ComposerState, F as FILE_ONLY_MESSAGE_TEXT, U as UseComposerOptions, c as composeSendInput, s as shouldSteerOnKey, b as shouldSubmitOnKey, u as useComposer } from './use-composer-BnEzheSk.js';
|
|
4
4
|
import '@opengeni/sdk';
|
package/dist/session.js
CHANGED
|
@@ -17,14 +17,14 @@ import {
|
|
|
17
17
|
useSessionEvents,
|
|
18
18
|
useSessionMcpApprovalPolicy,
|
|
19
19
|
useTurnQueue
|
|
20
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-SHOFILHJ.js";
|
|
21
21
|
import {
|
|
22
22
|
FILE_ONLY_MESSAGE_TEXT,
|
|
23
23
|
composeSendInput,
|
|
24
24
|
shouldSteerOnKey,
|
|
25
25
|
shouldSubmitOnKey,
|
|
26
26
|
useComposer
|
|
27
|
-
} from "./chunk-
|
|
27
|
+
} from "./chunk-AVPU5PMC.js";
|
|
28
28
|
import "./chunk-I3BJZIG5.js";
|
|
29
29
|
export {
|
|
30
30
|
FILE_ONLY_MESSAGE_TEXT,
|
|
@@ -13,14 +13,20 @@ type SessionEventFeedOptions = {
|
|
|
13
13
|
|
|
14
14
|
type ComposerSendExtras = Omit<SendMessageInput, "text" | "clientEventId">;
|
|
15
15
|
type UseComposerOptions = EmbeddedSessionClientOverride & SessionEventFeedOptions & {
|
|
16
|
-
/** Called with the accepted
|
|
17
|
-
onSent?: ((text: string) => void) | undefined;
|
|
16
|
+
/** Called with the exact accepted wire input after a successful send. */
|
|
17
|
+
onSent?: ((text: string, input: SendMessageInput) => void) | undefined;
|
|
18
18
|
/**
|
|
19
19
|
* Extra message fields (resources, tools, model, reasoningEffort) merged
|
|
20
20
|
* into every send. A function is evaluated at send time so it can read the
|
|
21
21
|
* surrounding UI state (attachment pickers, model selectors, ...).
|
|
22
22
|
*/
|
|
23
23
|
sendExtras?: ComposerSendExtras | (() => ComposerSendExtras) | undefined;
|
|
24
|
+
/**
|
|
25
|
+
* Fail-closed delivery guard evaluated at send time. Attachment hosts use
|
|
26
|
+
* this to preserve unresolved upload cards until the operator waits,
|
|
27
|
+
* retries, or removes them; direct hook callers cannot bypass the UI gate.
|
|
28
|
+
*/
|
|
29
|
+
sendBlocked?: (() => boolean) | undefined;
|
|
24
30
|
/** Latest server-derived workstream control; bound into Send/Steer OCC. */
|
|
25
31
|
effectiveControl?: EffectiveSessionControl | null | undefined;
|
|
26
32
|
/** Apply durable model/tool/reasoning settings in the host's controlled UI. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengeni/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0",
|
|
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": {
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"@dnd-kit/utilities": "3.2.2",
|
|
63
63
|
"@fontsource-variable/noto-sans-arabic": "5.2.10",
|
|
64
64
|
"@fontsource-variable/noto-sans-jp": "5.2.10",
|
|
65
|
-
"@opengeni/sdk": "^0.
|
|
65
|
+
"@opengeni/sdk": "^0.25.0",
|
|
66
66
|
"clsx": "^2.1.1",
|
|
67
67
|
"lucide-react": "^1.8.0",
|
|
68
68
|
"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,
|
|
@@ -731,8 +731,8 @@ function clusterIsSettled(group: Extract<TimelineGroup, { kind: "activity" }>):
|
|
|
731
731
|
if (item.kind === "reasoning") {
|
|
732
732
|
return !item.streaming;
|
|
733
733
|
}
|
|
734
|
-
//
|
|
735
|
-
if (item.kind === "memory") {
|
|
734
|
+
// Memory writes and fleet observations are discrete, already-settled events.
|
|
735
|
+
if (item.kind === "memory" || item.kind === "fleet-decision") {
|
|
736
736
|
return true;
|
|
737
737
|
}
|
|
738
738
|
return item.status !== "running";
|
|
@@ -15,14 +15,20 @@ export type ComposerSendExtras = Omit<SendMessageInput, "text" | "clientEventId"
|
|
|
15
15
|
|
|
16
16
|
export type UseComposerOptions = EmbeddedSessionClientOverride &
|
|
17
17
|
SessionEventFeedOptions & {
|
|
18
|
-
/** Called with the accepted
|
|
19
|
-
onSent?: ((text: string) => void) | undefined;
|
|
18
|
+
/** Called with the exact accepted wire input after a successful send. */
|
|
19
|
+
onSent?: ((text: string, input: SendMessageInput) => void) | undefined;
|
|
20
20
|
/**
|
|
21
21
|
* Extra message fields (resources, tools, model, reasoningEffort) merged
|
|
22
22
|
* into every send. A function is evaluated at send time so it can read the
|
|
23
23
|
* surrounding UI state (attachment pickers, model selectors, ...).
|
|
24
24
|
*/
|
|
25
25
|
sendExtras?: ComposerSendExtras | (() => ComposerSendExtras) | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* Fail-closed delivery guard evaluated at send time. Attachment hosts use
|
|
28
|
+
* this to preserve unresolved upload cards until the operator waits,
|
|
29
|
+
* retries, or removes them; direct hook callers cannot bypass the UI gate.
|
|
30
|
+
*/
|
|
31
|
+
sendBlocked?: (() => boolean) | undefined;
|
|
26
32
|
/** Latest server-derived workstream control; bound into Send/Steer OCC. */
|
|
27
33
|
effectiveControl?: EffectiveSessionControl | null | undefined;
|
|
28
34
|
/** Apply durable model/tool/reasoning settings in the host's controlled UI. */
|
|
@@ -106,6 +112,8 @@ export function useComposer(
|
|
|
106
112
|
// callers passing inline functions) does not invalidate `send`.
|
|
107
113
|
const sendExtrasRef = useRef(options.sendExtras);
|
|
108
114
|
sendExtrasRef.current = options.sendExtras;
|
|
115
|
+
const sendBlockedRef = useRef(options.sendBlocked);
|
|
116
|
+
sendBlockedRef.current = options.sendBlocked;
|
|
109
117
|
const liveExtrasVersion = JSON.stringify(resolveSendExtras(options.sendExtras));
|
|
110
118
|
|
|
111
119
|
// A composer is bound to one session: switching targets must not leak the
|
|
@@ -378,6 +386,7 @@ export function useComposer(
|
|
|
378
386
|
(!hasText && !hasResources) ||
|
|
379
387
|
!sessionId ||
|
|
380
388
|
sending ||
|
|
389
|
+
sendBlockedRef.current?.() === true ||
|
|
381
390
|
targetKeyRef.current !== ownedTargetKey
|
|
382
391
|
) {
|
|
383
392
|
return false;
|
|
@@ -457,7 +466,7 @@ export function useComposer(
|
|
|
457
466
|
setValue("");
|
|
458
467
|
}
|
|
459
468
|
}
|
|
460
|
-
onSent?.(sendText);
|
|
469
|
+
onSent?.(sendText, input);
|
|
461
470
|
return true;
|
|
462
471
|
} catch (cause) {
|
|
463
472
|
if (
|
|
@@ -498,8 +507,8 @@ export function useComposer(
|
|
|
498
507
|
// A send is possible with non-empty text OR with ≥1 attached resource (a
|
|
499
508
|
// file-only message). Resources ride in `sendExtras`, so we resolve them here
|
|
500
509
|
// — keeping useComposer attachment-agnostic while still lighting up the send
|
|
501
|
-
// affordance the moment a file is ready.
|
|
502
|
-
//
|
|
510
|
+
// affordance the moment a file is ready. Attachment hosts bind `sendBlocked`
|
|
511
|
+
// to unresolved uploads so direct send()/steer() calls fail closed too.
|
|
503
512
|
const hasReadyResources =
|
|
504
513
|
restoredResources.length > 0 ||
|
|
505
514
|
(resolveSendExtras(sendExtrasRef.current).resources?.length ?? 0) > 0;
|
|
@@ -717,6 +726,7 @@ export function useComposer(
|
|
|
717
726
|
identityMatches &&
|
|
718
727
|
Boolean(sessionId) &&
|
|
719
728
|
!sending &&
|
|
729
|
+
sendBlockedRef.current?.() !== true &&
|
|
720
730
|
(value.trim().length > 0 || hasReadyResources),
|
|
721
731
|
pause,
|
|
722
732
|
pausing: identityMatches ? pausing : false,
|
|
@@ -33,12 +33,20 @@ export type UseFileAttachmentsResult = {
|
|
|
33
33
|
* straight into `useComposer`'s `sendExtras.resources`.
|
|
34
34
|
*/
|
|
35
35
|
readyResources: FileResourceRef[];
|
|
36
|
-
/** True while any attachment is still uploading (drives
|
|
36
|
+
/** True while any attachment is still uploading (drives progress UI). */
|
|
37
37
|
uploading: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* True while any attachment still needs an explicit outcome: wait for an
|
|
40
|
+
* upload, retry a failure, or remove it. This is the loss-prevention send
|
|
41
|
+
* gate; failed cards must never be silently omitted from a message.
|
|
42
|
+
*/
|
|
43
|
+
hasUnresolved: boolean;
|
|
38
44
|
/** Explicit picker / drop path — uploads every file, no filter. */
|
|
39
45
|
addFiles: (files: Iterable<File>) => void;
|
|
40
46
|
/** Clipboard path — applies `pasteFilter` (default `image/*`) then uploads. */
|
|
41
47
|
addFromPaste: (event: { clipboardData: DataTransfer | null }) => void;
|
|
48
|
+
/** Restore already-ready server assets without recreating browser-local bytes. */
|
|
49
|
+
restoreReadyFiles: (files: Iterable<FileAsset>) => void;
|
|
42
50
|
/**
|
|
43
51
|
* Re-run the upload for a `failed` attachment, in place (same id, same
|
|
44
52
|
* source file). No-op for an id that isn't a known failed upload.
|
|
@@ -46,7 +54,13 @@ export type UseFileAttachmentsResult = {
|
|
|
46
54
|
retry: (id: string) => void;
|
|
47
55
|
/** Remove one attachment; revokes its object-URL. */
|
|
48
56
|
remove: (id: string) => void;
|
|
49
|
-
/**
|
|
57
|
+
/**
|
|
58
|
+
* Remove only finalized files whose durable ids were accepted by a send.
|
|
59
|
+
* Attachments added while that request was in flight remain queued for the
|
|
60
|
+
* next message.
|
|
61
|
+
*/
|
|
62
|
+
removeReadyFiles: (fileIds: Iterable<string>) => void;
|
|
63
|
+
/** Remove all attachments and revoke every object-URL. */
|
|
50
64
|
clear: () => void;
|
|
51
65
|
};
|
|
52
66
|
|
|
@@ -83,6 +97,10 @@ export function useFileAttachments(
|
|
|
83
97
|
data: file,
|
|
84
98
|
})
|
|
85
99
|
.then((asset) => {
|
|
100
|
+
// Retry bytes are useful only until durable finalization succeeds.
|
|
101
|
+
// Drop the source File immediately; restored/ready attachments must
|
|
102
|
+
// never retain browser-local byte authority.
|
|
103
|
+
sources.current.delete(id);
|
|
86
104
|
setAttachments((current) =>
|
|
87
105
|
current.map((attachment) =>
|
|
88
106
|
attachment.id === id
|
|
@@ -171,6 +189,60 @@ export function useFileAttachments(
|
|
|
171
189
|
[addFiles, pasteFilter],
|
|
172
190
|
);
|
|
173
191
|
|
|
192
|
+
const restoreReadyFiles = useCallback(
|
|
193
|
+
(files: Iterable<FileAsset>) => {
|
|
194
|
+
const incoming = new Map<string, FileAsset>();
|
|
195
|
+
for (const file of files) {
|
|
196
|
+
if (file.status === "ready" && file.workspaceId === workspaceId) {
|
|
197
|
+
incoming.set(file.id, file);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
setAttachments((current) => {
|
|
201
|
+
const unresolved = current.filter((attachment) => attachment.status !== "ready");
|
|
202
|
+
const existingReady = new Map(
|
|
203
|
+
current.flatMap((attachment) =>
|
|
204
|
+
attachment.status === "ready" && attachment.file
|
|
205
|
+
? ([[attachment.file.id, attachment]] as const)
|
|
206
|
+
: [],
|
|
207
|
+
),
|
|
208
|
+
);
|
|
209
|
+
const restored = [...incoming.values()].map((file): FileAttachment => {
|
|
210
|
+
const existing = existingReady.get(file.id);
|
|
211
|
+
return existing
|
|
212
|
+
? {
|
|
213
|
+
...existing,
|
|
214
|
+
name: file.filename,
|
|
215
|
+
contentType: file.contentType,
|
|
216
|
+
sizeBytes: file.sizeBytes,
|
|
217
|
+
status: "ready",
|
|
218
|
+
file,
|
|
219
|
+
error: undefined,
|
|
220
|
+
}
|
|
221
|
+
: {
|
|
222
|
+
id: `restored:${file.id}`,
|
|
223
|
+
name: file.filename,
|
|
224
|
+
contentType: file.contentType,
|
|
225
|
+
sizeBytes: file.sizeBytes,
|
|
226
|
+
status: "ready",
|
|
227
|
+
file,
|
|
228
|
+
// No source File and no object URL: server metadata is the
|
|
229
|
+
// only authority restored across page/device boundaries.
|
|
230
|
+
};
|
|
231
|
+
});
|
|
232
|
+
for (const [fileId, attachment] of existingReady) {
|
|
233
|
+
if (!incoming.has(fileId) && attachment.previewUrl) {
|
|
234
|
+
URL.revokeObjectURL(attachment.previewUrl);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
// A server restoration is authoritative for finalized assets, but an
|
|
238
|
+
// upload that has not finalized still belongs to the local actor. Keep
|
|
239
|
+
// those unresolved entries while replacing the ready set exactly.
|
|
240
|
+
return [...unresolved, ...restored];
|
|
241
|
+
});
|
|
242
|
+
},
|
|
243
|
+
[workspaceId],
|
|
244
|
+
);
|
|
245
|
+
|
|
174
246
|
const remove = useCallback((id: string) => {
|
|
175
247
|
sources.current.delete(id);
|
|
176
248
|
setAttachments((current) => {
|
|
@@ -182,6 +254,24 @@ export function useFileAttachments(
|
|
|
182
254
|
});
|
|
183
255
|
}, []);
|
|
184
256
|
|
|
257
|
+
const removeReadyFiles = useCallback((fileIds: Iterable<string>) => {
|
|
258
|
+
const accepted = new Set(fileIds);
|
|
259
|
+
if (accepted.size === 0) return;
|
|
260
|
+
setAttachments((current) =>
|
|
261
|
+
current.filter((attachment) => {
|
|
262
|
+
const removeAccepted =
|
|
263
|
+
attachment.status === "ready" &&
|
|
264
|
+
attachment.file !== undefined &&
|
|
265
|
+
accepted.has(attachment.file.id);
|
|
266
|
+
if (removeAccepted) {
|
|
267
|
+
sources.current.delete(attachment.id);
|
|
268
|
+
if (attachment.previewUrl) URL.revokeObjectURL(attachment.previewUrl);
|
|
269
|
+
}
|
|
270
|
+
return !removeAccepted;
|
|
271
|
+
}),
|
|
272
|
+
);
|
|
273
|
+
}, []);
|
|
274
|
+
|
|
185
275
|
const clear = useCallback(() => {
|
|
186
276
|
sources.current.clear();
|
|
187
277
|
setAttachments((current) => {
|
|
@@ -202,10 +292,13 @@ export function useFileAttachments(
|
|
|
202
292
|
: [],
|
|
203
293
|
),
|
|
204
294
|
uploading: attachments.some((attachment) => attachment.status === "uploading"),
|
|
295
|
+
hasUnresolved: attachments.some((attachment) => attachment.status !== "ready"),
|
|
205
296
|
addFiles,
|
|
206
297
|
addFromPaste,
|
|
298
|
+
restoreReadyFiles,
|
|
207
299
|
retry,
|
|
208
300
|
remove,
|
|
301
|
+
removeReadyFiles,
|
|
209
302
|
clear,
|
|
210
303
|
};
|
|
211
304
|
}
|
package/src/hooks/use-goal.ts
CHANGED
|
@@ -13,9 +13,26 @@ export function isGoalEvent(event: Pick<SessionEvent, "type">): boolean {
|
|
|
13
13
|
return event.type.startsWith("goal.");
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Events that can change the server-authoritative continuation projection.
|
|
18
|
+
* The projection is derived from durable turn, session, control, and system
|
|
19
|
+
* update state, so those events refresh the goal without model polling.
|
|
20
|
+
*/
|
|
21
|
+
export function isGoalRefreshEvent(event: Pick<SessionEvent, "type">): boolean {
|
|
22
|
+
const type = event.type;
|
|
23
|
+
return (
|
|
24
|
+
isGoalEvent(event) ||
|
|
25
|
+
type.startsWith("turn.") ||
|
|
26
|
+
type.startsWith("session.") ||
|
|
27
|
+
type.startsWith("system.update.") ||
|
|
28
|
+
type.startsWith("workspace.inference.") ||
|
|
29
|
+
type.startsWith("user.")
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
16
33
|
export type UseGoalOptions = ClientOverride &
|
|
17
34
|
SessionEventFeedOptions & {
|
|
18
|
-
/** Optional safety-net polling (ms). Off by default —
|
|
35
|
+
/** Optional safety-net polling (ms). Off by default — event refreshes drive updates. */
|
|
19
36
|
pollIntervalMs?: number | undefined;
|
|
20
37
|
};
|
|
21
38
|
|
|
@@ -46,8 +63,9 @@ export type UseGoalResult = {
|
|
|
46
63
|
/**
|
|
47
64
|
* The session's goal: state, the autonomy counters (`autoContinuations`,
|
|
48
65
|
* `noProgressStreak`), and pause/resume control. A goal-less session yields
|
|
49
|
-
* `goal: null` (the 404 is absorbed). Live-updates on
|
|
50
|
-
* pass `options.events` from
|
|
66
|
+
* `goal: null` (the 404 is absorbed). Live-updates on goal, turn, session,
|
|
67
|
+
* control, and system-update events — pass `options.events` from
|
|
68
|
+
* `useSessionEvents` to reuse its stream.
|
|
51
69
|
*/
|
|
52
70
|
export function useGoal(
|
|
53
71
|
sessionId: string | null | undefined,
|
|
@@ -124,7 +142,7 @@ export function useGoal(
|
|
|
124
142
|
}, [load, enabled, workspaceId, sessionId, options.pollIntervalMs, sharedFeed]);
|
|
125
143
|
|
|
126
144
|
const scheduleRefresh = useDebouncedCallback(() => void load());
|
|
127
|
-
useSessionEventTrigger(client, workspaceId, sessionId,
|
|
145
|
+
useSessionEventTrigger(client, workspaceId, sessionId, isGoalRefreshEvent, scheduleRefresh, {
|
|
128
146
|
enabled,
|
|
129
147
|
...(sharedEvents !== undefined ? { events: sharedEvents } : {}),
|
|
130
148
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Session } from "@opengeni/sdk";
|
|
2
|
-
import { useCallback } from "react";
|
|
2
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
3
3
|
import { useOpenGeni, type ClientOverride } from "../provider";
|
|
4
4
|
import { usePolledValue } from "./internal";
|
|
5
5
|
|
|
@@ -8,6 +8,8 @@ export type UseWorkspaceSessionsOptions = ClientOverride & {
|
|
|
8
8
|
parentSessionId?: string | null | undefined;
|
|
9
9
|
cursor?: string | undefined;
|
|
10
10
|
search?: string | undefined;
|
|
11
|
+
/** Return only the complete personal pinned projection. */
|
|
12
|
+
pinsOnly?: boolean | undefined;
|
|
11
13
|
/** Refresh interval (ms) for fleet/manager views. Off by default. */
|
|
12
14
|
pollIntervalMs?: number | undefined;
|
|
13
15
|
enabled?: boolean | undefined;
|
|
@@ -38,7 +40,21 @@ export function useWorkspaceSessions(
|
|
|
38
40
|
const parentSessionId = options.parentSessionId;
|
|
39
41
|
const cursor = options.cursor;
|
|
40
42
|
const search = options.search;
|
|
41
|
-
const
|
|
43
|
+
const pinsOnly = options.pinsOnly;
|
|
44
|
+
const enabled = options.enabled ?? true;
|
|
45
|
+
const queryKey = [
|
|
46
|
+
workspaceId,
|
|
47
|
+
limit ?? "",
|
|
48
|
+
parentSessionId === null ? "null" : (parentSessionId ?? ""),
|
|
49
|
+
cursor ?? "",
|
|
50
|
+
search ?? "",
|
|
51
|
+
pinsOnly ? "1" : "",
|
|
52
|
+
].join("\u0000");
|
|
53
|
+
const previousQueryKey = useRef(queryKey);
|
|
54
|
+
const queryKeyTransition = previousQueryKey.current !== queryKey;
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
previousQueryKey.current = queryKey;
|
|
57
|
+
}, [queryKey]);
|
|
42
58
|
const load = useCallback(
|
|
43
59
|
async () => ({
|
|
44
60
|
queryKey,
|
|
@@ -47,13 +63,14 @@ export function useWorkspaceSessions(
|
|
|
47
63
|
...(parentSessionId !== undefined ? { parentSessionId } : {}),
|
|
48
64
|
...(cursor !== undefined ? { cursor } : {}),
|
|
49
65
|
...(search !== undefined ? { search } : {}),
|
|
66
|
+
...(pinsOnly ? { pinsOnly: true } : {}),
|
|
50
67
|
}),
|
|
51
68
|
}),
|
|
52
|
-
[client, workspaceId, limit, parentSessionId, cursor, search, queryKey],
|
|
69
|
+
[client, workspaceId, limit, parentSessionId, cursor, search, pinsOnly, queryKey],
|
|
53
70
|
);
|
|
54
71
|
const state = usePolledValue(load, {
|
|
55
72
|
pollIntervalMs: options.pollIntervalMs,
|
|
56
|
-
enabled
|
|
73
|
+
enabled,
|
|
57
74
|
});
|
|
58
75
|
// usePolledValue drops stale async completions, while the explicit query key
|
|
59
76
|
// also prevents the previous query's cached value from painting for the one
|
|
@@ -69,7 +86,15 @@ export function useWorkspaceSessions(
|
|
|
69
86
|
pinned,
|
|
70
87
|
pinnedTruncated: page?.pinnedTruncated ?? false,
|
|
71
88
|
nextCursor: page?.nextCursor ?? null,
|
|
72
|
-
|
|
89
|
+
// `usePolledValue` clears the old data and starts the new request in an
|
|
90
|
+
// effect. During that query-key transition render, its old loading flag
|
|
91
|
+
// can still be false; expose loading immediately so consumers do not
|
|
92
|
+
// announce a transient false zero-match result.
|
|
93
|
+
loading:
|
|
94
|
+
enabled &&
|
|
95
|
+
(state.loading ||
|
|
96
|
+
queryKeyTransition ||
|
|
97
|
+
(state.data !== null && state.data.queryKey !== queryKey)),
|
|
73
98
|
error: state.error,
|
|
74
99
|
refresh: state.refresh,
|
|
75
100
|
};
|
|
@@ -5,15 +5,19 @@ import {
|
|
|
5
5
|
BrainIcon,
|
|
6
6
|
SquareTerminalIcon,
|
|
7
7
|
} from "lucide-react";
|
|
8
|
+
import { lazy, Suspense } from "react";
|
|
9
|
+
import { jsx as rowJsx, jsxs as rowJsxs } from "react/jsx-runtime";
|
|
8
10
|
import { cn } from "../lib/cn";
|
|
9
11
|
import { truncate } from "../lib/format";
|
|
10
12
|
import { defaultToolRegistry } from "./tool-renderers";
|
|
11
13
|
import { useEntranceAnimation } from "./entrance";
|
|
12
14
|
import type { ToolRegistry } from "./registry";
|
|
13
15
|
import { BodyNote, PayloadBlock, ActivityDisclosure } from "./shared";
|
|
14
|
-
import { toolDisplayName } from "./
|
|
16
|
+
import { toolDisplayName } from "./tool-display-name";
|
|
15
17
|
import type { ActivityItem, MemoryItem, ReasoningItem, SandboxItem, WorkerItem } from "./types";
|
|
16
18
|
|
|
19
|
+
const LazyFleetDecisionRow = lazy(() => import("./fleet-decision-row"));
|
|
20
|
+
|
|
17
21
|
/* ----------------------------------------------------------------------------
|
|
18
22
|
Activity rail
|
|
19
23
|
|
|
@@ -112,6 +116,18 @@ function renderActivity(
|
|
|
112
116
|
return <SandboxRow item={item} />;
|
|
113
117
|
case "memory":
|
|
114
118
|
return <MemoryRow item={item} onMemoryClick={onMemoryClick} />;
|
|
119
|
+
case "fleet-decision":
|
|
120
|
+
return (
|
|
121
|
+
<Suspense fallback={null}>
|
|
122
|
+
<LazyFleetDecisionRow
|
|
123
|
+
item={item}
|
|
124
|
+
d={ActivityDisclosure}
|
|
125
|
+
b={BodyNote}
|
|
126
|
+
j={rowJsx}
|
|
127
|
+
s={rowJsxs}
|
|
128
|
+
/>
|
|
129
|
+
</Suspense>
|
|
130
|
+
);
|
|
115
131
|
default:
|
|
116
132
|
return assertNever(item);
|
|
117
133
|
}
|