@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.
Files changed (101) hide show
  1. package/README.md +110 -10
  2. package/dist/{chunk-TOJR776I.js → chunk-6BXBGJ3B.js} +45 -369
  3. package/dist/chunk-6BXBGJ3B.js.map +1 -0
  4. package/dist/chunk-6UT5OC3P.js +2133 -0
  5. package/dist/chunk-6UT5OC3P.js.map +1 -0
  6. package/dist/chunk-EBTT3MYN.js +92 -0
  7. package/dist/chunk-EBTT3MYN.js.map +1 -0
  8. package/dist/chunk-HHFA4AFX.js +425 -0
  9. package/dist/chunk-HHFA4AFX.js.map +1 -0
  10. package/dist/chunk-LAZ2A743.js +2393 -0
  11. package/dist/chunk-LAZ2A743.js.map +1 -0
  12. package/dist/chunk-MRSECXRP.js +563 -0
  13. package/dist/chunk-MRSECXRP.js.map +1 -0
  14. package/dist/chunk-TK7G6XLT.js +18 -0
  15. package/dist/chunk-TK7G6XLT.js.map +1 -0
  16. package/dist/composer-DvUaa5ki.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 +7015 -8806
  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-NQMV2ML3.js +890 -0
  30. package/dist/queue-surface-implementation-NQMV2ML3.js.map +1 -0
  31. package/dist/session-Gd4iVPYw.d.ts +425 -0
  32. package/dist/session-context-DIFFlXKc.d.ts +52 -0
  33. package/dist/session.d.ts +4 -0
  34. package/dist/session.js +50 -0
  35. package/dist/session.js.map +1 -0
  36. package/dist/use-composer-BCfm4mzX.d.ts +112 -0
  37. package/package.json +20 -4
  38. package/src/approvals.ts +5 -3
  39. package/src/client.ts +40 -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 +15 -7
  53. package/src/components/model-picker.tsx +12 -3
  54. package/src/components/pierre-diff.tsx +32 -6
  55. package/src/components/queue-surface-implementation.tsx +1041 -0
  56. package/src/components/queue-surface-state.tsx +94 -0
  57. package/src/components/queue-surface.tsx +41 -559
  58. package/src/components/sandbox-files.tsx +138 -14
  59. package/src/components/sandbox-terminal.tsx +8 -2
  60. package/src/components/sandbox-workspace.tsx +339 -119
  61. package/src/components/workbench-changes.tsx +84 -33
  62. package/src/components/workspace-dock.tsx +188 -18
  63. package/src/composer.ts +60 -0
  64. package/src/hooks/internal.ts +115 -34
  65. package/src/hooks/use-composer.ts +335 -104
  66. package/src/hooks/use-human-input.ts +131 -0
  67. package/src/hooks/use-machine-chip.ts +2 -2
  68. package/src/hooks/use-machines.ts +27 -11
  69. package/src/hooks/use-sandbox-files.ts +310 -62
  70. package/src/hooks/use-sandbox-git.ts +154 -40
  71. package/src/hooks/use-sandbox-terminal.ts +28 -6
  72. package/src/hooks/use-session-capabilities.ts +38 -9
  73. package/src/hooks/use-session-control.ts +37 -8
  74. package/src/hooks/use-session-events.ts +522 -48
  75. package/src/hooks/use-slash-commands.ts +38 -38
  76. package/src/hooks/use-transcription.ts +757 -0
  77. package/src/hooks/use-turn-queue.ts +134 -66
  78. package/src/hooks/use-windowed-sections.ts +2 -2
  79. package/src/hooks/use-workspace-capture.ts +146 -40
  80. package/src/hooks/use-workspace-edit.ts +50 -14
  81. package/src/hooks/use-workspace-sessions.ts +3 -0
  82. package/src/human-input.ts +72 -0
  83. package/src/index.ts +76 -6
  84. package/src/lib/noto-sans-arabic-loader.ts +21 -0
  85. package/src/lib/noto-sans-jp-loader.generated.ts +295 -0
  86. package/src/lib/use-portal-token-style.ts +51 -0
  87. package/src/lib/use-unicode-fonts.ts +57 -0
  88. package/src/provider.tsx +4 -69
  89. package/src/session-context.ts +135 -0
  90. package/src/session.ts +79 -0
  91. package/src/timeline/parsers.ts +35 -0
  92. package/src/timeline/projection.ts +12 -4
  93. package/src/timeline/tool-renderers.tsx +41 -0
  94. package/src/timeline/turn-summary.tsx +2 -2
  95. package/src/timeline/types.ts +16 -8
  96. package/styles/index.css +56 -0
  97. package/styles/index.d.ts +2 -0
  98. package/styles/tokens.css +11 -6
  99. package/styles/tokens.d.ts +2 -0
  100. package/dist/chunk-TOJR776I.js.map +0 -1
  101. package/dist/machines-BpdwuQcD.d.ts +0 -449
@@ -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.15.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": "./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.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
- 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,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
+ }