@opengeni/react 0.15.0 → 0.23.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.
Files changed (103) hide show
  1. package/README.md +110 -10
  2. package/dist/{chunk-TOJR776I.js → chunk-6XUS5VFM.js} +45 -369
  3. package/dist/chunk-6XUS5VFM.js.map +1 -0
  4. package/dist/chunk-CATATY77.js +2393 -0
  5. package/dist/chunk-CATATY77.js.map +1 -0
  6. package/dist/chunk-I3BJZIG5.js +439 -0
  7. package/dist/chunk-I3BJZIG5.js.map +1 -0
  8. package/dist/chunk-TK7G6XLT.js +18 -0
  9. package/dist/chunk-TK7G6XLT.js.map +1 -0
  10. package/dist/chunk-UAHPKLB6.js +2237 -0
  11. package/dist/chunk-UAHPKLB6.js.map +1 -0
  12. package/dist/chunk-UQS7OQDE.js +591 -0
  13. package/dist/chunk-UQS7OQDE.js.map +1 -0
  14. package/dist/chunk-YDNWMKXO.js +102 -0
  15. package/dist/chunk-YDNWMKXO.js.map +1 -0
  16. package/dist/composer-CVm5uoUI.d.ts +585 -0
  17. package/dist/composer.d.ts +6 -0
  18. package/dist/composer.js +56 -0
  19. package/dist/composer.js.map +1 -0
  20. package/dist/index.d.ts +1288 -1785
  21. package/dist/index.js +7140 -8817
  22. package/dist/index.js.map +1 -1
  23. package/dist/machines.d.ts +412 -3
  24. package/dist/machines.js +3 -1
  25. package/dist/noto-sans-arabic-loader-IA2T4X2A.js +21 -0
  26. package/dist/noto-sans-arabic-loader-IA2T4X2A.js.map +1 -0
  27. package/dist/noto-sans-jp-loader.generated-HFGLYBR3.js +292 -0
  28. package/dist/noto-sans-jp-loader.generated-HFGLYBR3.js.map +1 -0
  29. package/dist/queue-surface-implementation-3UW3MIIR.js +890 -0
  30. package/dist/queue-surface-implementation-3UW3MIIR.js.map +1 -0
  31. package/dist/session-Db00TRwl.d.ts +446 -0
  32. package/dist/session-context-rgrfElMl.d.ts +58 -0
  33. package/dist/session.d.ts +4 -0
  34. package/dist/session.js +54 -0
  35. package/dist/session.js.map +1 -0
  36. package/dist/use-composer-By1wn3MM.d.ts +99 -0
  37. package/package.json +20 -4
  38. package/src/approvals.ts +5 -3
  39. package/src/client.ts +44 -0
  40. package/src/components/approval-surface.tsx +198 -0
  41. package/src/components/chat-composer.tsx +94 -993
  42. package/src/components/code-editor.tsx +104 -26
  43. package/src/components/command-palette.tsx +8 -2
  44. package/src/components/composer-transcription-control.tsx +211 -0
  45. package/src/components/composer.tsx +1536 -0
  46. package/src/components/desktop-viewer.tsx +3 -3
  47. package/src/components/enrollment-consent.tsx +2 -2
  48. package/src/components/file-browser.tsx +367 -29
  49. package/src/components/human-input-form.tsx +417 -0
  50. package/src/components/machine-card.tsx +1 -1
  51. package/src/components/machines-dashboard.tsx +3 -3
  52. package/src/components/message-timeline.tsx +205 -25
  53. package/src/components/model-picker.tsx +12 -3
  54. package/src/components/pierre-diff.tsx +32 -6
  55. package/src/components/queue-draft-policy.ts +18 -0
  56. package/src/components/queue-surface-implementation.tsx +1037 -0
  57. package/src/components/queue-surface-state.tsx +94 -0
  58. package/src/components/queue-surface.tsx +42 -559
  59. package/src/components/sandbox-files.tsx +138 -14
  60. package/src/components/sandbox-terminal.tsx +8 -2
  61. package/src/components/sandbox-workspace.tsx +339 -119
  62. package/src/components/workbench-changes.tsx +84 -33
  63. package/src/components/workspace-dock.tsx +188 -18
  64. package/src/composer.ts +60 -0
  65. package/src/hooks/internal.ts +115 -34
  66. package/src/hooks/use-composer.ts +385 -104
  67. package/src/hooks/use-human-input.ts +131 -0
  68. package/src/hooks/use-machine-chip.ts +2 -2
  69. package/src/hooks/use-machines.ts +27 -11
  70. package/src/hooks/use-sandbox-files.ts +310 -62
  71. package/src/hooks/use-sandbox-git.ts +154 -40
  72. package/src/hooks/use-sandbox-terminal.ts +28 -6
  73. package/src/hooks/use-session-capabilities.ts +38 -9
  74. package/src/hooks/use-session-control.ts +37 -8
  75. package/src/hooks/use-session-events.ts +522 -48
  76. package/src/hooks/use-session-mcp-approval-policy.ts +161 -0
  77. package/src/hooks/use-slash-commands.ts +38 -38
  78. package/src/hooks/use-transcription.ts +757 -0
  79. package/src/hooks/use-turn-queue.ts +134 -66
  80. package/src/hooks/use-windowed-sections.ts +2 -2
  81. package/src/hooks/use-workspace-capture.ts +146 -40
  82. package/src/hooks/use-workspace-edit.ts +50 -14
  83. package/src/hooks/use-workspace-sessions.ts +3 -0
  84. package/src/human-input.ts +72 -0
  85. package/src/index.ts +85 -6
  86. package/src/lib/noto-sans-arabic-loader.ts +21 -0
  87. package/src/lib/noto-sans-jp-loader.generated.ts +295 -0
  88. package/src/lib/use-portal-token-style.ts +51 -0
  89. package/src/lib/use-unicode-fonts.ts +57 -0
  90. package/src/provider.tsx +4 -69
  91. package/src/session-context.ts +160 -0
  92. package/src/session.ts +89 -0
  93. package/src/timeline/parsers.ts +35 -0
  94. package/src/timeline/projection.ts +12 -4
  95. package/src/timeline/tool-renderers.tsx +41 -0
  96. package/src/timeline/turn-summary.tsx +2 -2
  97. package/src/timeline/types.ts +10 -9
  98. package/styles/index.css +56 -0
  99. package/styles/index.d.ts +2 -0
  100. package/styles/tokens.css +11 -6
  101. package/styles/tokens.d.ts +2 -0
  102. package/dist/chunk-TOJR776I.js.map +0 -1
  103. package/dist/machines-BpdwuQcD.d.ts +0 -449
@@ -0,0 +1,58 @@
1
+ import { OpenGeniClient, WorkspaceControlEvent, StreamConnectionState } from '@opengeni/sdk';
2
+
3
+ /**
4
+ * The slice of `OpenGeniClient` the hooks depend on. Structural, so apps can
5
+ * pass the real SDK client, a proxy-backed client that routes through their
6
+ * own API, or a scripted client in tests/demos.
7
+ */
8
+ type SessionClientLike = Pick<OpenGeniClient, "getClientConfig" | "getSession" | "getSessionLineage" | "updateSession" | "updateSessionPin" | "listSessions" | "listSessionPage" | "sendMessage" | "steerMessage" | "pauseSession" | "resumeSession" | "sendApprovalDecision" | "listHumanInputRequests" | "getHumanInputRequest" | "submitHumanInputResponse" | "listEvents" | "streamEvents" | "getQueue" | "moveQueueItem" | "editQueueItem" | "steerQueueItem" | "deleteQueueItem" | "getComposerDraft" | "saveComposerDraft" | "listTurns" | "getGoal" | "updateGoal" | "deleteGoal" | "clearSessionContext" | "compactSessionContext" | "listScheduledTasks" | "uploadFile" | "getFile" | "createFileDownloadUrl" | "listVariableSets" | "createVariableSet" | "updateVariableSet" | "deleteVariableSet" | "setVariableSetVariable" | "deleteVariableSetVariable" | "listEnvironments" | "createEnvironment" | "updateEnvironment" | "deleteEnvironment" | "setEnvironmentVariable" | "deleteEnvironmentVariable" | "listRigs" | "createRig" | "getRig" | "updateRig" | "deleteRig" | "listRigVersions" | "activateRigVersion" | "listRigChanges" | "proposeRigChange" | "getRigChange" | "verifyRigChange" | "promoteRigChange" | "verifyRig" | "listPacks" | "registerPack" | "enablePack" | "deletePack" | "listWorkspaces" | "createWorkspace" | "updateWorkspace" | "setWorkspaceInferenceState" | "getWorkspace" | "listWorkspaceControlEvents" | "streamWorkspaceControlEvents" | "getBillingUsage" | "getClientConfig" | "getStreamCapabilities" | "acknowledgeStream" | "attachViewer" | "heartbeatViewer" | "detachViewer" | "fsList" | "fsRead" | "fsWrite" | "fsDelete" | "fsMove" | "fsMkdir" | "gitStatus" | "gitDiff" | "getWorkspaceCapture" | "getWorkspaceCaptureFile" | "terminalExec" | "terminalPtyOpen" | "terminalPtyWrite" | "terminalPtyResize" | "terminalPtyClose">;
9
+ /**
10
+ * Tenant-safe client surface required by the session-only React entry.
11
+ *
12
+ * A host proxy can implement only these session-scoped operations instead of
13
+ * stubbing OpenGeni's workbench, billing, rig, file-system, and workspace
14
+ * administration APIs. Workspace-level resume is deliberately optional: a
15
+ * host that does not expose that authority still supports every session-local
16
+ * composer/control path.
17
+ */
18
+ type EmbeddedSessionClientLike = Pick<OpenGeniClient, "getSession" | "listEvents" | "streamEvents" | "getComposerDraft" | "saveComposerDraft" | "sendMessage" | "steerMessage" | "getQueue" | "moveQueueItem" | "editQueueItem" | "steerQueueItem" | "deleteQueueItem" | "pauseSession" | "resumeSession" | "sendApprovalDecision"> & {
19
+ setWorkspaceInferenceState?: OpenGeniClient["setWorkspaceInferenceState"] | undefined;
20
+ };
21
+ /** Session client refinement required only by structured human-input hooks. */
22
+ type EmbeddedHumanInputSessionClientLike = EmbeddedSessionClientLike & Pick<OpenGeniClient, "listHumanInputRequests" | "getHumanInputRequest" | "submitHumanInputResponse">;
23
+ /** Session client refinement required only by MCP approval-policy hooks. */
24
+ type EmbeddedSessionMcpApprovalPolicyClientLike = EmbeddedSessionClientLike & Pick<OpenGeniClient, "updateSessionMcpApprovalPolicy">;
25
+
26
+ type OpenGeniContextValue = {
27
+ client: SessionClientLike;
28
+ workspaceId: string;
29
+ workspaceControlEvent: WorkspaceControlEvent | null;
30
+ workspaceControlConnectionState: StreamConnectionState | "idle" | "error";
31
+ registerSessionReconciler: (sessionId: string, key: string, reconcile: () => Promise<void>) => () => void;
32
+ reconcileSession: (sessionId: string) => Promise<void>;
33
+ };
34
+ type ClientOverride = {
35
+ client?: SessionClientLike | undefined;
36
+ workspaceId?: string | undefined;
37
+ };
38
+ type EmbeddedSessionClientOverride = {
39
+ client?: EmbeddedSessionClientLike | undefined;
40
+ workspaceId?: string | undefined;
41
+ };
42
+ type EmbeddedHumanInputClientOverride = {
43
+ client?: EmbeddedHumanInputSessionClientLike | undefined;
44
+ workspaceId?: string | undefined;
45
+ };
46
+ type EmbeddedSessionMcpApprovalPolicyClientOverride = {
47
+ client?: EmbeddedSessionMcpApprovalPolicyClientLike | undefined;
48
+ workspaceId?: string | undefined;
49
+ };
50
+ /** Resolve client + workspace from explicit overrides or the provider. */
51
+ declare function useOpenGeni(override?: ClientOverride): OpenGeniContextValue;
52
+ /**
53
+ * Resolve the client only — for hooks that are not workspace-scoped
54
+ * (`useWorkspaces`, `useBillingUsage`).
55
+ */
56
+ declare function useOpenGeniClient(override?: Pick<ClientOverride, "client">): SessionClientLike;
57
+
58
+ export { type ClientOverride as C, type EmbeddedSessionClientOverride as E, type OpenGeniContextValue as O, type SessionClientLike as S, type EmbeddedHumanInputSessionClientLike as a, type EmbeddedSessionClientLike as b, type EmbeddedSessionMcpApprovalPolicyClientLike as c, useOpenGeniClient as d, type EmbeddedSessionMcpApprovalPolicyClientOverride as e, type EmbeddedHumanInputClientOverride as f, useOpenGeni as u };
@@ -0,0 +1,4 @@
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-Db00TRwl.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-By1wn3MM.js';
4
+ import '@opengeni/sdk';
@@ -0,0 +1,54 @@
1
+ import {
2
+ approvalsFromRequiresAction,
3
+ buildTimeline,
4
+ creditExhaustedFromEvents,
5
+ extractSessionRef,
6
+ groupTimeline,
7
+ humanInputRequestFromEvent,
8
+ isHumanInputEvent,
9
+ isSessionMcpApprovalPolicyEvent,
10
+ isTurnQueueEvent,
11
+ projectPendingApprovals,
12
+ projectPendingHumanInputRequests,
13
+ sessionStatusFromEvents,
14
+ toolDisplayName,
15
+ useHumanInputRequests,
16
+ useSessionControl,
17
+ useSessionEvents,
18
+ useSessionMcpApprovalPolicy,
19
+ useTurnQueue
20
+ } from "./chunk-UAHPKLB6.js";
21
+ import {
22
+ FILE_ONLY_MESSAGE_TEXT,
23
+ composeSendInput,
24
+ shouldSteerOnKey,
25
+ shouldSubmitOnKey,
26
+ useComposer
27
+ } from "./chunk-UQS7OQDE.js";
28
+ import "./chunk-I3BJZIG5.js";
29
+ export {
30
+ FILE_ONLY_MESSAGE_TEXT,
31
+ approvalsFromRequiresAction,
32
+ buildTimeline,
33
+ composeSendInput,
34
+ creditExhaustedFromEvents,
35
+ extractSessionRef,
36
+ groupTimeline,
37
+ humanInputRequestFromEvent,
38
+ isHumanInputEvent,
39
+ isSessionMcpApprovalPolicyEvent,
40
+ isTurnQueueEvent,
41
+ projectPendingApprovals,
42
+ projectPendingHumanInputRequests,
43
+ sessionStatusFromEvents,
44
+ shouldSteerOnKey,
45
+ shouldSubmitOnKey,
46
+ toolDisplayName,
47
+ useComposer,
48
+ useHumanInputRequests,
49
+ useSessionControl,
50
+ useSessionEvents,
51
+ useSessionMcpApprovalPolicy,
52
+ useTurnQueue
53
+ };
54
+ //# sourceMappingURL=session.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,99 @@
1
+ import { SessionEvent, EffectiveControlResumeOption, ComposerDraft, ResourceRef, SendMessageInput, EffectiveSessionControl } from '@opengeni/sdk';
2
+ import { E as EmbeddedSessionClientOverride } from './session-context-rgrfElMl.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
+ /** Disable remote composer-draft reads and writes for embedded hosts. */
29
+ draftPersistence?: "durable" | "disabled" | undefined;
30
+ };
31
+ type ComposerState = {
32
+ value: string;
33
+ setValue: (value: string) => void;
34
+ /** Read the current draft synchronously before a destructive replacement. */
35
+ hasDraftContent: () => boolean;
36
+ /** Append the draft behind prompts already visible in the queue. */
37
+ send: (text?: string) => Promise<boolean>;
38
+ /** Supersede current direction with the draft. */
39
+ steer: (text?: string) => Promise<boolean>;
40
+ sending: boolean;
41
+ canSend: boolean;
42
+ /** Pause the session without deleting its prompt queue. */
43
+ pause: (reason?: string) => Promise<void>;
44
+ pausing: boolean;
45
+ resume: (reason?: string) => Promise<void>;
46
+ resumeScope: (option: EffectiveControlResumeOption) => Promise<void>;
47
+ resuming: boolean;
48
+ draft: ComposerDraft | null;
49
+ draftRevision: number;
50
+ draftLoading: boolean;
51
+ draftSaving: boolean;
52
+ draftConflict: Error | null;
53
+ /** Whether this controller owns a durable server-side draft. */
54
+ draftPersistence?: "durable" | "disabled" | undefined;
55
+ /** Apply an atomic queue Edit checkout without a second read. */
56
+ applyDraft: (draft: ComposerDraft) => void;
57
+ reloadDraft: () => Promise<void>;
58
+ resolveDraftConflict: (choice: "keep_mine" | "use_remote") => Promise<void>;
59
+ restoredResources: ResourceRef[];
60
+ removeRestoredResource: (index: number) => void;
61
+ error: Error | null;
62
+ clearError: () => void;
63
+ };
64
+ /**
65
+ * Draft + send + Pause/Resume state for the chat composer — the only
66
+ * human-to-agent input surface. The draft survives a failed send (nothing is
67
+ * more hostile than losing a typed message); each send carries a generated
68
+ * `clientEventId` so retries stay idempotent server-side.
69
+ */
70
+ declare function useComposer(sessionId: string | null | undefined, options?: UseComposerOptions): ComposerState;
71
+ /**
72
+ * Default text for a file-only message (attachment(s) present, no typed draft).
73
+ * Kept non-empty so the wire contract (`text: z.string().min(1)`) and the
74
+ * worker's non-whitespace guard accept it; the attached files still ride in
75
+ * `resources`. Exported for tests.
76
+ */
77
+ declare const FILE_ONLY_MESSAGE_TEXT = "(see attached files)";
78
+ /**
79
+ * Merge the draft text + idempotency key with caller-provided extras. The
80
+ * text and clientEventId always win over extras. Exported for tests.
81
+ */
82
+ declare function composeSendInput(text: string, clientEventId: string, extras: ComposerSendExtras | (() => ComposerSendExtras) | undefined, bound?: Partial<SendMessageInput>): SendMessageInput;
83
+ /** Submit on plain Enter; Shift+Enter inserts a newline. Exported for tests. */
84
+ declare function shouldSubmitOnKey(event: {
85
+ key: string;
86
+ shiftKey: boolean;
87
+ metaKey?: boolean;
88
+ ctrlKey?: boolean;
89
+ nativeEvent?: {
90
+ isComposing?: boolean;
91
+ };
92
+ }): boolean;
93
+ /** Cmd/Ctrl+Enter steers; ordinary Enter appends to the queue. */
94
+ declare function shouldSteerOnKey(event: {
95
+ metaKey?: boolean;
96
+ ctrlKey?: boolean;
97
+ }): boolean;
98
+
99
+ 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.15.0",
3
+ "version": "0.23.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": "./styles/index.css",
31
- "./tokens.css": "./styles/tokens.css"
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
- "@opengeni/sdk": "^0.15.0",
63
+ "@fontsource-variable/noto-sans-arabic": "5.2.10",
64
+ "@fontsource-variable/noto-sans-jp": "5.2.10",
65
+ "@opengeni/sdk": "^0.23.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
- id: String(raw.id ?? raw.callId ?? rawItem.callId ?? index),
54
- name: String(raw.name ?? "approval"),
55
- arguments: raw.arguments,
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,44 @@ 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
+ >;
154
+
155
+ /** Session client refinement required only by MCP approval-policy hooks. */
156
+ export type EmbeddedSessionMcpApprovalPolicyClientLike = EmbeddedSessionClientLike &
157
+ Pick<OpenGeniClient, "updateSessionMcpApprovalPolicy">;
@@ -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
+ }