@opengeni/react 0.15.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +110 -10
- package/dist/{chunk-TOJR776I.js → chunk-6BXBGJ3B.js} +45 -369
- package/dist/chunk-6BXBGJ3B.js.map +1 -0
- package/dist/chunk-6UT5OC3P.js +2133 -0
- package/dist/chunk-6UT5OC3P.js.map +1 -0
- package/dist/chunk-EBTT3MYN.js +92 -0
- package/dist/chunk-EBTT3MYN.js.map +1 -0
- package/dist/chunk-HHFA4AFX.js +425 -0
- package/dist/chunk-HHFA4AFX.js.map +1 -0
- package/dist/chunk-LAZ2A743.js +2393 -0
- package/dist/chunk-LAZ2A743.js.map +1 -0
- package/dist/chunk-MRSECXRP.js +563 -0
- package/dist/chunk-MRSECXRP.js.map +1 -0
- package/dist/chunk-TK7G6XLT.js +18 -0
- package/dist/chunk-TK7G6XLT.js.map +1 -0
- package/dist/composer-DvUaa5ki.d.ts +585 -0
- package/dist/composer.d.ts +6 -0
- package/dist/composer.js +56 -0
- package/dist/composer.js.map +1 -0
- package/dist/index.d.ts +1288 -1785
- package/dist/index.js +7015 -8806
- package/dist/index.js.map +1 -1
- package/dist/machines.d.ts +412 -3
- package/dist/machines.js +3 -1
- package/dist/noto-sans-arabic-loader-IA2T4X2A.js +21 -0
- package/dist/noto-sans-arabic-loader-IA2T4X2A.js.map +1 -0
- package/dist/noto-sans-jp-loader.generated-HFGLYBR3.js +292 -0
- package/dist/noto-sans-jp-loader.generated-HFGLYBR3.js.map +1 -0
- package/dist/queue-surface-implementation-NQMV2ML3.js +890 -0
- package/dist/queue-surface-implementation-NQMV2ML3.js.map +1 -0
- package/dist/session-Gd4iVPYw.d.ts +425 -0
- package/dist/session-context-DIFFlXKc.d.ts +52 -0
- package/dist/session.d.ts +4 -0
- package/dist/session.js +50 -0
- package/dist/session.js.map +1 -0
- package/dist/use-composer-BCfm4mzX.d.ts +112 -0
- package/package.json +20 -4
- package/src/approvals.ts +5 -3
- package/src/client.ts +40 -0
- package/src/components/approval-surface.tsx +198 -0
- package/src/components/chat-composer.tsx +94 -993
- package/src/components/code-editor.tsx +104 -26
- package/src/components/command-palette.tsx +8 -2
- package/src/components/composer-transcription-control.tsx +211 -0
- package/src/components/composer.tsx +1536 -0
- package/src/components/desktop-viewer.tsx +3 -3
- package/src/components/enrollment-consent.tsx +2 -2
- package/src/components/file-browser.tsx +367 -29
- package/src/components/human-input-form.tsx +417 -0
- package/src/components/machine-card.tsx +1 -1
- package/src/components/machines-dashboard.tsx +3 -3
- package/src/components/message-timeline.tsx +15 -7
- package/src/components/model-picker.tsx +12 -3
- package/src/components/pierre-diff.tsx +32 -6
- package/src/components/queue-surface-implementation.tsx +1041 -0
- package/src/components/queue-surface-state.tsx +94 -0
- package/src/components/queue-surface.tsx +41 -559
- package/src/components/sandbox-files.tsx +138 -14
- package/src/components/sandbox-terminal.tsx +8 -2
- package/src/components/sandbox-workspace.tsx +339 -119
- package/src/components/workbench-changes.tsx +84 -33
- package/src/components/workspace-dock.tsx +188 -18
- package/src/composer.ts +60 -0
- package/src/hooks/internal.ts +115 -34
- package/src/hooks/use-composer.ts +335 -104
- package/src/hooks/use-human-input.ts +131 -0
- package/src/hooks/use-machine-chip.ts +2 -2
- package/src/hooks/use-machines.ts +27 -11
- package/src/hooks/use-sandbox-files.ts +310 -62
- package/src/hooks/use-sandbox-git.ts +154 -40
- package/src/hooks/use-sandbox-terminal.ts +28 -6
- package/src/hooks/use-session-capabilities.ts +38 -9
- package/src/hooks/use-session-control.ts +37 -8
- package/src/hooks/use-session-events.ts +522 -48
- package/src/hooks/use-slash-commands.ts +38 -38
- package/src/hooks/use-transcription.ts +757 -0
- package/src/hooks/use-turn-queue.ts +134 -66
- package/src/hooks/use-windowed-sections.ts +2 -2
- package/src/hooks/use-workspace-capture.ts +146 -40
- package/src/hooks/use-workspace-edit.ts +50 -14
- package/src/hooks/use-workspace-sessions.ts +3 -0
- package/src/human-input.ts +72 -0
- package/src/index.ts +76 -6
- package/src/lib/noto-sans-arabic-loader.ts +21 -0
- package/src/lib/noto-sans-jp-loader.generated.ts +295 -0
- package/src/lib/use-portal-token-style.ts +51 -0
- package/src/lib/use-unicode-fonts.ts +57 -0
- package/src/provider.tsx +4 -69
- package/src/session-context.ts +135 -0
- package/src/session.ts +79 -0
- package/src/timeline/parsers.ts +35 -0
- package/src/timeline/projection.ts +12 -4
- package/src/timeline/tool-renderers.tsx +41 -0
- package/src/timeline/turn-summary.tsx +2 -2
- package/src/timeline/types.ts +16 -8
- package/styles/index.css +56 -0
- package/styles/index.d.ts +2 -0
- package/styles/tokens.css +11 -6
- package/styles/tokens.d.ts +2 -0
- package/dist/chunk-TOJR776I.js.map +0 -1
- package/dist/machines-BpdwuQcD.d.ts +0 -449
package/dist/session.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import {
|
|
2
|
+
approvalsFromRequiresAction,
|
|
3
|
+
buildTimeline,
|
|
4
|
+
creditExhaustedFromEvents,
|
|
5
|
+
extractSessionRef,
|
|
6
|
+
groupTimeline,
|
|
7
|
+
humanInputRequestFromEvent,
|
|
8
|
+
isHumanInputEvent,
|
|
9
|
+
isTurnQueueEvent,
|
|
10
|
+
projectPendingApprovals,
|
|
11
|
+
projectPendingHumanInputRequests,
|
|
12
|
+
sessionStatusFromEvents,
|
|
13
|
+
toolDisplayName,
|
|
14
|
+
useHumanInputRequests,
|
|
15
|
+
useSessionControl,
|
|
16
|
+
useSessionEvents,
|
|
17
|
+
useTurnQueue
|
|
18
|
+
} from "./chunk-6UT5OC3P.js";
|
|
19
|
+
import {
|
|
20
|
+
FILE_ONLY_MESSAGE_TEXT,
|
|
21
|
+
composeSendInput,
|
|
22
|
+
shouldSteerOnKey,
|
|
23
|
+
shouldSubmitOnKey,
|
|
24
|
+
useComposer
|
|
25
|
+
} from "./chunk-MRSECXRP.js";
|
|
26
|
+
import "./chunk-HHFA4AFX.js";
|
|
27
|
+
export {
|
|
28
|
+
FILE_ONLY_MESSAGE_TEXT,
|
|
29
|
+
approvalsFromRequiresAction,
|
|
30
|
+
buildTimeline,
|
|
31
|
+
composeSendInput,
|
|
32
|
+
creditExhaustedFromEvents,
|
|
33
|
+
extractSessionRef,
|
|
34
|
+
groupTimeline,
|
|
35
|
+
humanInputRequestFromEvent,
|
|
36
|
+
isHumanInputEvent,
|
|
37
|
+
isTurnQueueEvent,
|
|
38
|
+
projectPendingApprovals,
|
|
39
|
+
projectPendingHumanInputRequests,
|
|
40
|
+
sessionStatusFromEvents,
|
|
41
|
+
shouldSteerOnKey,
|
|
42
|
+
shouldSubmitOnKey,
|
|
43
|
+
toolDisplayName,
|
|
44
|
+
useComposer,
|
|
45
|
+
useHumanInputRequests,
|
|
46
|
+
useSessionControl,
|
|
47
|
+
useSessionEvents,
|
|
48
|
+
useTurnQueue
|
|
49
|
+
};
|
|
50
|
+
//# sourceMappingURL=session.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { SessionEvent, EffectiveControlResumeOption, ComposerDraft, ResourceRef, SendMessageInput, EffectiveSessionControl } from '@opengeni/sdk';
|
|
2
|
+
import { E as EmbeddedSessionClientOverride } from './session-context-DIFFlXKc.js';
|
|
3
|
+
|
|
4
|
+
type SessionEventFeedOptions = {
|
|
5
|
+
/**
|
|
6
|
+
* Share an existing event log (from `useSessionEvents`) instead of opening
|
|
7
|
+
* a second stream. When omitted the hook tails the session's event stream
|
|
8
|
+
* itself, starting at the current `lastSequence`.
|
|
9
|
+
*/
|
|
10
|
+
events?: SessionEvent[] | undefined;
|
|
11
|
+
enabled?: boolean | undefined;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type ComposerSendExtras = Omit<SendMessageInput, "text" | "clientEventId">;
|
|
15
|
+
type UseComposerOptions = EmbeddedSessionClientOverride & SessionEventFeedOptions & {
|
|
16
|
+
/** Called with the accepted text after a successful send. */
|
|
17
|
+
onSent?: ((text: string) => void) | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* Extra message fields (resources, tools, model, reasoningEffort) merged
|
|
20
|
+
* into every send. A function is evaluated at send time so it can read the
|
|
21
|
+
* surrounding UI state (attachment pickers, model selectors, ...).
|
|
22
|
+
*/
|
|
23
|
+
sendExtras?: ComposerSendExtras | (() => ComposerSendExtras) | undefined;
|
|
24
|
+
/** Latest server-derived workstream control; bound into Send/Steer OCC. */
|
|
25
|
+
effectiveControl?: EffectiveSessionControl | null | undefined;
|
|
26
|
+
/** Apply durable model/tool/reasoning settings in the host's controlled UI. */
|
|
27
|
+
onDraftApplied?: ((draft: ComposerDraft) => void) | undefined;
|
|
28
|
+
/**
|
|
29
|
+
* Opt out of OpenGeni's remote composer-draft reads and writes. Embedded
|
|
30
|
+
* hosts can use this when their human lane permits messages and controls but
|
|
31
|
+
* deliberately withholds draft mutation because model/tool policy is
|
|
32
|
+
* server-owned. The unsent value remains local React state and send/steer
|
|
33
|
+
* continue to work without a draft revision fence. Queue checkout is hidden
|
|
34
|
+
* because the current queue-edit contract necessarily creates a durable
|
|
35
|
+
* composer draft.
|
|
36
|
+
*
|
|
37
|
+
* @default "durable"
|
|
38
|
+
*/
|
|
39
|
+
draftPersistence?: "durable" | "disabled" | undefined;
|
|
40
|
+
};
|
|
41
|
+
type ComposerState = {
|
|
42
|
+
value: string;
|
|
43
|
+
setValue: (value: string) => void;
|
|
44
|
+
/** Append the draft behind prompts already visible in the queue. */
|
|
45
|
+
send: (text?: string) => Promise<boolean>;
|
|
46
|
+
/** Supersede current direction with the draft. */
|
|
47
|
+
steer: (text?: string) => Promise<boolean>;
|
|
48
|
+
sending: boolean;
|
|
49
|
+
canSend: boolean;
|
|
50
|
+
/** Pause the session without deleting its prompt queue. */
|
|
51
|
+
pause: (reason?: string) => Promise<void>;
|
|
52
|
+
pausing: boolean;
|
|
53
|
+
resume: (reason?: string) => Promise<void>;
|
|
54
|
+
resumeScope: (option: EffectiveControlResumeOption) => Promise<void>;
|
|
55
|
+
resuming: boolean;
|
|
56
|
+
draft: ComposerDraft | null;
|
|
57
|
+
draftRevision: number;
|
|
58
|
+
draftLoading: boolean;
|
|
59
|
+
draftSaving: boolean;
|
|
60
|
+
draftConflict: Error | null;
|
|
61
|
+
/**
|
|
62
|
+
* Whether this controller owns a durable server-side draft. Custom
|
|
63
|
+
* controllers that omit the field retain the historical durable behavior.
|
|
64
|
+
* Queue checkout is unavailable when persistence is disabled because the
|
|
65
|
+
* current queue-edit contract atomically moves the turn into that draft.
|
|
66
|
+
*/
|
|
67
|
+
draftPersistence?: "durable" | "disabled" | undefined;
|
|
68
|
+
/** Apply an atomic queue Edit checkout without a second read. */
|
|
69
|
+
applyDraft: (draft: ComposerDraft) => void;
|
|
70
|
+
reloadDraft: () => Promise<void>;
|
|
71
|
+
resolveDraftConflict: (choice: "keep_mine" | "use_remote") => Promise<void>;
|
|
72
|
+
restoredResources: ResourceRef[];
|
|
73
|
+
removeRestoredResource: (index: number) => void;
|
|
74
|
+
error: Error | null;
|
|
75
|
+
clearError: () => void;
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Draft + send + Pause/Resume state for the chat composer — the only
|
|
79
|
+
* human-to-agent input surface. The draft survives a failed send (nothing is
|
|
80
|
+
* more hostile than losing a typed message); each send carries a generated
|
|
81
|
+
* `clientEventId` so retries stay idempotent server-side.
|
|
82
|
+
*/
|
|
83
|
+
declare function useComposer(sessionId: string | null | undefined, options?: UseComposerOptions): ComposerState;
|
|
84
|
+
/**
|
|
85
|
+
* Default text for a file-only message (attachment(s) present, no typed draft).
|
|
86
|
+
* Kept non-empty so the wire contract (`text: z.string().min(1)`) and the
|
|
87
|
+
* worker's non-whitespace guard accept it; the attached files still ride in
|
|
88
|
+
* `resources`. Exported for tests.
|
|
89
|
+
*/
|
|
90
|
+
declare const FILE_ONLY_MESSAGE_TEXT = "(see attached files)";
|
|
91
|
+
/**
|
|
92
|
+
* Merge the draft text + idempotency key with caller-provided extras. The
|
|
93
|
+
* text and clientEventId always win over extras. Exported for tests.
|
|
94
|
+
*/
|
|
95
|
+
declare function composeSendInput(text: string, clientEventId: string, extras: ComposerSendExtras | (() => ComposerSendExtras) | undefined, bound?: Partial<SendMessageInput>): SendMessageInput;
|
|
96
|
+
/** Submit on plain Enter; Shift+Enter inserts a newline. Exported for tests. */
|
|
97
|
+
declare function shouldSubmitOnKey(event: {
|
|
98
|
+
key: string;
|
|
99
|
+
shiftKey: boolean;
|
|
100
|
+
metaKey?: boolean;
|
|
101
|
+
ctrlKey?: boolean;
|
|
102
|
+
nativeEvent?: {
|
|
103
|
+
isComposing?: boolean;
|
|
104
|
+
};
|
|
105
|
+
}): boolean;
|
|
106
|
+
/** Cmd/Ctrl+Enter steers; ordinary Enter appends to the queue. */
|
|
107
|
+
declare function shouldSteerOnKey(event: {
|
|
108
|
+
metaKey?: boolean;
|
|
109
|
+
ctrlKey?: boolean;
|
|
110
|
+
}): boolean;
|
|
111
|
+
|
|
112
|
+
export { type ComposerState as C, FILE_ONLY_MESSAGE_TEXT as F, type SessionEventFeedOptions as S, type UseComposerOptions as U, type ComposerSendExtras as a, shouldSubmitOnKey as b, composeSendInput as c, shouldSteerOnKey as s, useComposer as u };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengeni/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.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": {
|
|
@@ -23,12 +23,26 @@
|
|
|
23
23
|
"types": "./dist/index.d.ts",
|
|
24
24
|
"import": "./dist/index.js"
|
|
25
25
|
},
|
|
26
|
+
"./composer": {
|
|
27
|
+
"types": "./dist/composer.d.ts",
|
|
28
|
+
"import": "./dist/composer.js"
|
|
29
|
+
},
|
|
30
|
+
"./session": {
|
|
31
|
+
"types": "./dist/session.d.ts",
|
|
32
|
+
"import": "./dist/session.js"
|
|
33
|
+
},
|
|
26
34
|
"./machines": {
|
|
27
35
|
"types": "./dist/machines.d.ts",
|
|
28
36
|
"import": "./dist/machines.js"
|
|
29
37
|
},
|
|
30
|
-
"./styles.css":
|
|
31
|
-
|
|
38
|
+
"./styles.css": {
|
|
39
|
+
"types": "./styles/index.d.ts",
|
|
40
|
+
"import": "./styles/index.css"
|
|
41
|
+
},
|
|
42
|
+
"./tokens.css": {
|
|
43
|
+
"types": "./styles/tokens.d.ts",
|
|
44
|
+
"import": "./styles/tokens.css"
|
|
45
|
+
}
|
|
32
46
|
},
|
|
33
47
|
"publishConfig": {
|
|
34
48
|
"access": "public",
|
|
@@ -46,7 +60,9 @@
|
|
|
46
60
|
"@dnd-kit/core": "6.3.1",
|
|
47
61
|
"@dnd-kit/sortable": "10.0.0",
|
|
48
62
|
"@dnd-kit/utilities": "3.2.2",
|
|
49
|
-
"@
|
|
63
|
+
"@fontsource-variable/noto-sans-arabic": "5.2.10",
|
|
64
|
+
"@fontsource-variable/noto-sans-jp": "5.2.10",
|
|
65
|
+
"@opengeni/sdk": "^0.20.0",
|
|
50
66
|
"clsx": "^2.1.1",
|
|
51
67
|
"lucide-react": "^1.8.0",
|
|
52
68
|
"motion": "^12.0.0",
|
package/src/approvals.ts
CHANGED
|
@@ -50,9 +50,11 @@ export function approvalsFromRequiresAction(payload: unknown): PendingApproval[]
|
|
|
50
50
|
? (raw.rawItem as Record<string, unknown>)
|
|
51
51
|
: {};
|
|
52
52
|
return {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
// Mirror the runtime's canonical interruption identity before retaining
|
|
54
|
+
// the legacy top-level callId fallback for already-normalized payloads.
|
|
55
|
+
id: String(rawItem.callId ?? rawItem.id ?? raw.id ?? raw.callId ?? raw.name ?? index),
|
|
56
|
+
name: String(raw.name ?? raw.toolName ?? rawItem.name ?? "approval"),
|
|
57
|
+
arguments: raw.arguments ?? rawItem.arguments,
|
|
56
58
|
raw: approval,
|
|
57
59
|
};
|
|
58
60
|
});
|
package/src/client.ts
CHANGED
|
@@ -21,6 +21,9 @@ export type SessionClientLike = Pick<
|
|
|
21
21
|
| "pauseSession"
|
|
22
22
|
| "resumeSession"
|
|
23
23
|
| "sendApprovalDecision"
|
|
24
|
+
| "listHumanInputRequests"
|
|
25
|
+
| "getHumanInputRequest"
|
|
26
|
+
| "submitHumanInputResponse"
|
|
24
27
|
| "listEvents"
|
|
25
28
|
| "streamEvents"
|
|
26
29
|
// Turn queue
|
|
@@ -111,3 +114,40 @@ export type SessionClientLike = Pick<
|
|
|
111
114
|
| "terminalPtyResize"
|
|
112
115
|
| "terminalPtyClose"
|
|
113
116
|
>;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Tenant-safe client surface required by the session-only React entry.
|
|
120
|
+
*
|
|
121
|
+
* A host proxy can implement only these session-scoped operations instead of
|
|
122
|
+
* stubbing OpenGeni's workbench, billing, rig, file-system, and workspace
|
|
123
|
+
* administration APIs. Workspace-level resume is deliberately optional: a
|
|
124
|
+
* host that does not expose that authority still supports every session-local
|
|
125
|
+
* composer/control path.
|
|
126
|
+
*/
|
|
127
|
+
export type EmbeddedSessionClientLike = Pick<
|
|
128
|
+
OpenGeniClient,
|
|
129
|
+
| "getSession"
|
|
130
|
+
| "listEvents"
|
|
131
|
+
| "streamEvents"
|
|
132
|
+
| "getComposerDraft"
|
|
133
|
+
| "saveComposerDraft"
|
|
134
|
+
| "sendMessage"
|
|
135
|
+
| "steerMessage"
|
|
136
|
+
| "getQueue"
|
|
137
|
+
| "moveQueueItem"
|
|
138
|
+
| "editQueueItem"
|
|
139
|
+
| "steerQueueItem"
|
|
140
|
+
| "deleteQueueItem"
|
|
141
|
+
| "pauseSession"
|
|
142
|
+
| "resumeSession"
|
|
143
|
+
| "sendApprovalDecision"
|
|
144
|
+
> & {
|
|
145
|
+
setWorkspaceInferenceState?: OpenGeniClient["setWorkspaceInferenceState"] | undefined;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
/** Session client refinement required only by structured human-input hooks. */
|
|
149
|
+
export type EmbeddedHumanInputSessionClientLike = EmbeddedSessionClientLike &
|
|
150
|
+
Pick<
|
|
151
|
+
OpenGeniClient,
|
|
152
|
+
"listHumanInputRequests" | "getHumanInputRequest" | "submitHumanInputResponse"
|
|
153
|
+
>;
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { CheckIcon, ShieldCheckIcon, XIcon } from "lucide-react";
|
|
2
|
+
import { useEffect, useId, useRef, useState, type ReactNode } from "react";
|
|
3
|
+
import type { PendingApproval } from "../approvals";
|
|
4
|
+
import { cn } from "../lib/cn";
|
|
5
|
+
|
|
6
|
+
export type ApprovalSurfaceMessages = {
|
|
7
|
+
title: string;
|
|
8
|
+
description: string;
|
|
9
|
+
approve: string;
|
|
10
|
+
reject: string;
|
|
11
|
+
approving: string;
|
|
12
|
+
rejecting: string;
|
|
13
|
+
formatToolName: (name: string) => string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const defaultApprovalSurfaceMessages: ApprovalSurfaceMessages = {
|
|
17
|
+
title: "Approval required",
|
|
18
|
+
description: "Review the requested action before the agent continues.",
|
|
19
|
+
approve: "Approve",
|
|
20
|
+
reject: "Reject",
|
|
21
|
+
approving: "Approving…",
|
|
22
|
+
rejecting: "Rejecting…",
|
|
23
|
+
formatToolName: (name) => name.replaceAll("_", " ").replaceAll(".", " › "),
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type ApprovalSurfaceProps = {
|
|
27
|
+
approvals: PendingApproval[];
|
|
28
|
+
onApprove: (approval: PendingApproval) => void | Promise<void>;
|
|
29
|
+
onReject: (approval: PendingApproval) => void | Promise<void>;
|
|
30
|
+
responding?: boolean | undefined;
|
|
31
|
+
error?: string | Error | null | undefined;
|
|
32
|
+
messages?: Partial<ApprovalSurfaceMessages> | undefined;
|
|
33
|
+
renderApproval?: ((approval: PendingApproval) => ReactNode) | undefined;
|
|
34
|
+
className?: string | undefined;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const APPROVAL_ARGUMENT_PREVIEW_CHARACTERS = 4_000;
|
|
38
|
+
|
|
39
|
+
function approvalArgumentsPreview(value: unknown): string | null {
|
|
40
|
+
if (value === undefined) return null;
|
|
41
|
+
let serialized: string;
|
|
42
|
+
try {
|
|
43
|
+
serialized =
|
|
44
|
+
typeof value === "string" ? value : (JSON.stringify(value, null, 2) ?? String(value));
|
|
45
|
+
} catch {
|
|
46
|
+
serialized = "[Arguments unavailable]";
|
|
47
|
+
}
|
|
48
|
+
const characters = Array.from(serialized);
|
|
49
|
+
if (characters.length <= APPROVAL_ARGUMENT_PREVIEW_CHARACTERS) return serialized;
|
|
50
|
+
return `${characters.slice(0, APPROVAL_ARGUMENT_PREVIEW_CHARACTERS).join("")}\n… ${characters.length - APPROVAL_ARGUMENT_PREVIEW_CHARACTERS} characters omitted`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function approvalOwnershipKey(approval: PendingApproval): string {
|
|
54
|
+
return `${approval.id}\u0000${approval.name}\u0000${approvalArgumentsPreview(approval.arguments) ?? ""}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Host-neutral approval presentation backed by the native pending-approval
|
|
59
|
+
* projection and control callbacks. It owns only presentation and duplicate
|
|
60
|
+
* click fencing; OpenGeni remains the authority for approval state.
|
|
61
|
+
*/
|
|
62
|
+
export function ApprovalSurface({
|
|
63
|
+
approvals,
|
|
64
|
+
onApprove,
|
|
65
|
+
onReject,
|
|
66
|
+
responding = false,
|
|
67
|
+
error,
|
|
68
|
+
messages: overrides,
|
|
69
|
+
renderApproval,
|
|
70
|
+
className,
|
|
71
|
+
}: ApprovalSurfaceProps) {
|
|
72
|
+
const titleId = useId();
|
|
73
|
+
const messages = { ...defaultApprovalSurfaceMessages, ...overrides };
|
|
74
|
+
const [pending, setPending] = useState<{
|
|
75
|
+
approvalKey: string;
|
|
76
|
+
decision: "approve" | "reject";
|
|
77
|
+
token: symbol;
|
|
78
|
+
} | null>(null);
|
|
79
|
+
const pendingRef = useRef<{ approvalKey: string; token: symbol } | null>(null);
|
|
80
|
+
const [decisionError, setDecisionError] = useState<Error | null>(null);
|
|
81
|
+
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (
|
|
84
|
+
pending &&
|
|
85
|
+
!approvals.some((approval) => approvalOwnershipKey(approval) === pending.approvalKey)
|
|
86
|
+
) {
|
|
87
|
+
if (pendingRef.current?.token === pending.token) {
|
|
88
|
+
pendingRef.current = null;
|
|
89
|
+
}
|
|
90
|
+
setPending((current) => (current?.token === pending.token ? null : current));
|
|
91
|
+
}
|
|
92
|
+
}, [approvals, pending]);
|
|
93
|
+
|
|
94
|
+
if (approvals.length === 0) return null;
|
|
95
|
+
const busy = responding || pendingRef.current !== null;
|
|
96
|
+
const decide = async (
|
|
97
|
+
approval: PendingApproval,
|
|
98
|
+
decision: "approve" | "reject",
|
|
99
|
+
): Promise<void> => {
|
|
100
|
+
if (responding || pendingRef.current !== null) return;
|
|
101
|
+
const approvalKey = approvalOwnershipKey(approval);
|
|
102
|
+
const token = Symbol(approvalKey);
|
|
103
|
+
pendingRef.current = { approvalKey, token };
|
|
104
|
+
setPending({ approvalKey, decision, token });
|
|
105
|
+
setDecisionError(null);
|
|
106
|
+
try {
|
|
107
|
+
await (decision === "approve" ? onApprove(approval) : onReject(approval));
|
|
108
|
+
// Keep the decision fenced until the authoritative projection removes or
|
|
109
|
+
// replaces this exact approval. A successful callback is transport
|
|
110
|
+
// acceptance, not settlement.
|
|
111
|
+
} catch (cause) {
|
|
112
|
+
if (pendingRef.current?.token === token) {
|
|
113
|
+
pendingRef.current = null;
|
|
114
|
+
setPending((current) => (current?.token === token ? null : current));
|
|
115
|
+
setDecisionError(cause instanceof Error ? cause : new Error(String(cause)));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
const errorMessage = decisionError?.message ?? (error instanceof Error ? error.message : error);
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<section
|
|
123
|
+
className={cn(
|
|
124
|
+
"og-root flex w-full flex-col gap-3 rounded-og-lg border border-og-status-waiting/35 bg-og-status-waiting/5 p-4 shadow-og-sm",
|
|
125
|
+
className,
|
|
126
|
+
)}
|
|
127
|
+
aria-labelledby={titleId}
|
|
128
|
+
>
|
|
129
|
+
<header className="flex items-start gap-3">
|
|
130
|
+
<span className="mt-0.5 inline-flex size-8 shrink-0 items-center justify-center rounded-og-md bg-og-status-waiting/12 text-og-status-waiting">
|
|
131
|
+
<ShieldCheckIcon aria-hidden="true" className="size-4" />
|
|
132
|
+
</span>
|
|
133
|
+
<div className="min-w-0">
|
|
134
|
+
<h2 id={titleId} className="text-og-md font-semibold text-og-fg">
|
|
135
|
+
{messages.title}
|
|
136
|
+
</h2>
|
|
137
|
+
<p className="mt-0.5 text-og-sm text-og-fg-muted">{messages.description}</p>
|
|
138
|
+
</div>
|
|
139
|
+
</header>
|
|
140
|
+
|
|
141
|
+
<div className="flex flex-col gap-2">
|
|
142
|
+
{approvals.map((approval) => {
|
|
143
|
+
const approvalKey = approvalOwnershipKey(approval);
|
|
144
|
+
const active = pending?.approvalKey === approvalKey ? pending.decision : null;
|
|
145
|
+
const argumentsPreview = approvalArgumentsPreview(approval.arguments);
|
|
146
|
+
return (
|
|
147
|
+
<article
|
|
148
|
+
key={approval.id}
|
|
149
|
+
data-approval-id={approval.id}
|
|
150
|
+
className="rounded-og-md border border-og-border bg-og-surface-1 p-3"
|
|
151
|
+
>
|
|
152
|
+
{renderApproval ? (
|
|
153
|
+
renderApproval(approval)
|
|
154
|
+
) : (
|
|
155
|
+
<>
|
|
156
|
+
<p className="text-og-sm font-medium text-og-fg">
|
|
157
|
+
{messages.formatToolName(approval.name)}
|
|
158
|
+
</p>
|
|
159
|
+
{argumentsPreview !== null ? (
|
|
160
|
+
<pre className="mt-2 max-h-48 overflow-auto whitespace-pre-wrap break-words rounded-og-sm bg-og-surface-2 p-2 font-mono text-og-xs text-og-fg-muted">
|
|
161
|
+
{argumentsPreview}
|
|
162
|
+
</pre>
|
|
163
|
+
) : null}
|
|
164
|
+
</>
|
|
165
|
+
)}
|
|
166
|
+
<div className="mt-3 flex flex-wrap justify-end gap-2">
|
|
167
|
+
<button
|
|
168
|
+
type="button"
|
|
169
|
+
disabled={busy}
|
|
170
|
+
onClick={() => void decide(approval, "reject")}
|
|
171
|
+
className="inline-flex min-h-9 items-center gap-1.5 rounded-og-md border border-og-border px-3 py-1.5 text-og-sm font-medium text-og-fg-muted transition-colors hover:bg-og-surface-2 hover:text-og-fg disabled:opacity-50"
|
|
172
|
+
>
|
|
173
|
+
<XIcon aria-hidden="true" className="size-3.5" />
|
|
174
|
+
{active === "reject" ? messages.rejecting : messages.reject}
|
|
175
|
+
</button>
|
|
176
|
+
<button
|
|
177
|
+
type="button"
|
|
178
|
+
disabled={busy}
|
|
179
|
+
onClick={() => void decide(approval, "approve")}
|
|
180
|
+
className="inline-flex min-h-9 items-center gap-1.5 rounded-og-md bg-og-accent px-3 py-1.5 text-og-sm font-medium text-og-accent-fg transition-colors hover:bg-og-accent-strong disabled:opacity-50"
|
|
181
|
+
>
|
|
182
|
+
<CheckIcon aria-hidden="true" className="size-3.5" />
|
|
183
|
+
{active === "approve" ? messages.approving : messages.approve}
|
|
184
|
+
</button>
|
|
185
|
+
</div>
|
|
186
|
+
</article>
|
|
187
|
+
);
|
|
188
|
+
})}
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
{errorMessage ? (
|
|
192
|
+
<p role="alert" className="text-og-sm text-og-status-failed">
|
|
193
|
+
{errorMessage}
|
|
194
|
+
</p>
|
|
195
|
+
) : null}
|
|
196
|
+
</section>
|
|
197
|
+
);
|
|
198
|
+
}
|