@opengeni/react 7.6.1 → 7.7.0-canary.37111190284001

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 (145) hide show
  1. package/README.md +47 -2
  2. package/dist/{browser-viewer-5X47I6T2.js → browser-viewer-63TXMINB.js} +6 -5
  3. package/dist/{chunk-5EXGU7UF.js → chunk-55DDDVKU.js} +3 -3
  4. package/dist/{chunk-UINDK6C3.js → chunk-7CYYXWHU.js} +5 -8
  5. package/dist/chunk-7CYYXWHU.js.map +1 -0
  6. package/dist/{chunk-LZ3KHA55.js → chunk-A2L5N3EI.js} +332 -339
  7. package/dist/chunk-A2L5N3EI.js.map +1 -0
  8. package/dist/{chunk-KGENSZKS.js → chunk-AAG4R3QN.js} +4 -3
  9. package/dist/chunk-AAG4R3QN.js.map +1 -0
  10. package/dist/{chunk-Z3DISEQN.js → chunk-CWWKTNKK.js} +35 -18
  11. package/dist/chunk-CWWKTNKK.js.map +1 -0
  12. package/dist/{chunk-WZ55DVGK.js → chunk-ELCE5SXV.js} +8 -4
  13. package/dist/chunk-ELCE5SXV.js.map +1 -0
  14. package/dist/{chunk-YSMMDOOE.js → chunk-GISEVGS6.js} +10 -2
  15. package/dist/chunk-GISEVGS6.js.map +1 -0
  16. package/dist/{chunk-BI57CQUV.js → chunk-I7IWRQLN.js} +2 -2
  17. package/dist/{chunk-XA6R7DJQ.js → chunk-IQUCRAT7.js} +3 -3
  18. package/dist/{chunk-CHPKSLPN.js → chunk-NBZ64YY7.js} +4 -2
  19. package/dist/chunk-NBZ64YY7.js.map +1 -0
  20. package/dist/{chunk-DV2DBNPQ.js → chunk-Q5QBEZIQ.js} +48 -29
  21. package/dist/chunk-Q5QBEZIQ.js.map +1 -0
  22. package/dist/{chunk-FGBCYQOT.js → chunk-RDKBXMWX.js} +120 -74
  23. package/dist/chunk-RDKBXMWX.js.map +1 -0
  24. package/dist/{chunk-45GE35O7.js → chunk-TKB4SB44.js} +10 -2
  25. package/dist/chunk-TKB4SB44.js.map +1 -0
  26. package/dist/{chunk-YCGBREUV.js → chunk-TOAH2BKA.js} +6 -2
  27. package/dist/{chunk-YCGBREUV.js.map → chunk-TOAH2BKA.js.map} +1 -1
  28. package/dist/chunk-UGLEYEMP.js +33 -0
  29. package/dist/chunk-UGLEYEMP.js.map +1 -0
  30. package/dist/{chunk-UJRYEWZI.js → chunk-UTRTG6LH.js} +48 -8
  31. package/dist/chunk-UTRTG6LH.js.map +1 -0
  32. package/dist/{chunk-YRYF2GLG.js → chunk-WYVLAAJA.js} +39 -31
  33. package/dist/chunk-WYVLAAJA.js.map +1 -0
  34. package/dist/{chunk-VGIKFIO2.js → chunk-XOAMRDDK.js} +6 -3
  35. package/dist/chunk-XOAMRDDK.js.map +1 -0
  36. package/dist/{chunk-6NEZPF53.js → chunk-YN7I3STW.js} +153 -32
  37. package/dist/chunk-YN7I3STW.js.map +1 -0
  38. package/dist/{chunk-KZ73RL76.js → chunk-YQEAA6LT.js} +7 -3
  39. package/dist/chunk-YQEAA6LT.js.map +1 -0
  40. package/dist/client.d.ts +4 -1
  41. package/dist/commands/types.d.ts +2 -0
  42. package/dist/components/approval-surface.d.ts +15 -0
  43. package/dist/components/session-conversation.d.ts +3 -0
  44. package/dist/composer.js +8 -7
  45. package/dist/{computer-viewer-7GF35SNY.js → computer-viewer-X2MBRZY4.js} +5 -4
  46. package/dist/connect.js +3 -3
  47. package/dist/hooks/use-browser-session.d.ts +4 -0
  48. package/dist/hooks/use-composer.d.ts +2 -0
  49. package/dist/hooks/use-computer-session.d.ts +2 -0
  50. package/dist/hooks/use-file-attachments.d.ts +2 -0
  51. package/dist/hooks/use-session-events.d.ts +1 -1
  52. package/dist/hooks/use-workspace-sessions.d.ts +15 -5
  53. package/dist/index.d.ts +2 -2
  54. package/dist/index.js +28 -21
  55. package/dist/index.js.map +1 -1
  56. package/dist/interaction.d.ts +1 -1
  57. package/dist/interaction.js +6 -5
  58. package/dist/interaction.js.map +1 -1
  59. package/dist/{latest-question-3BJGKQQ6.js → latest-question-CNEZ5KAN.js} +3 -3
  60. package/dist/lib/error-message.d.ts +7 -0
  61. package/dist/lib/format.d.ts +27 -3
  62. package/dist/lib/interaction-errors.d.ts +3 -0
  63. package/dist/machines.js +5 -4
  64. package/dist/{model-policy-picker-menu-PUTA5JOB.js → model-policy-picker-menu-GN2LTB7Z.js} +4 -4
  65. package/dist/model-policy.js +1 -1
  66. package/dist/provider.d.ts +5 -1
  67. package/dist/{queue-surface-implementation-2CAQAIHM.js → queue-surface-implementation-HENWYFWA.js} +3 -2
  68. package/dist/{queue-surface-implementation-2CAQAIHM.js.map → queue-surface-implementation-HENWYFWA.js.map} +1 -1
  69. package/dist/realtime.js +2 -2
  70. package/dist/{sandbox-files-AEJTWGT4.js → sandbox-files-BBC47SLD.js} +4 -3
  71. package/dist/session-ui.d.ts +1 -1
  72. package/dist/session-ui.js +13 -12
  73. package/dist/session-ui.js.map +1 -1
  74. package/dist/session.js +7 -6
  75. package/dist/usage.js +2 -1
  76. package/dist/usage.js.map +1 -1
  77. package/package.json +3 -3
  78. package/src/client.ts +5 -1
  79. package/src/commands/registry.ts +18 -15
  80. package/src/commands/types.ts +2 -0
  81. package/src/components/approval-surface.tsx +166 -42
  82. package/src/components/browser-viewer.tsx +38 -9
  83. package/src/components/composer.tsx +9 -2
  84. package/src/components/computer-viewer.tsx +160 -30
  85. package/src/components/generated-video-player.tsx +4 -2
  86. package/src/components/human-input-form.tsx +18 -5
  87. package/src/components/message-timeline.tsx +120 -168
  88. package/src/components/model-policy-picker.tsx +2 -15
  89. package/src/components/open-geni-chat.tsx +11 -4
  90. package/src/components/queue-surface-state.tsx +3 -1
  91. package/src/components/session-chrome.tsx +5 -2
  92. package/src/components/session-commands-panel.tsx +3 -1
  93. package/src/components/session-conversation.tsx +10 -3
  94. package/src/components/session-list.tsx +7 -5
  95. package/src/components/session-mcp-capability-card.tsx +11 -1
  96. package/src/hooks/internal.ts +3 -2
  97. package/src/hooks/use-browser-session.ts +115 -79
  98. package/src/hooks/use-composer.ts +70 -31
  99. package/src/hooks/use-computer-session.ts +57 -6
  100. package/src/hooks/use-file-attachments.ts +16 -3
  101. package/src/hooks/use-goal.ts +2 -1
  102. package/src/hooks/use-session-capabilities.ts +3 -2
  103. package/src/hooks/use-session-events.ts +15 -8
  104. package/src/hooks/use-slash-commands.ts +6 -11
  105. package/src/hooks/use-turn-queue.ts +2 -1
  106. package/src/hooks/use-workspace-sessions.ts +40 -7
  107. package/src/index.ts +7 -2
  108. package/src/interaction.ts +1 -0
  109. package/src/lib/error-message.ts +37 -0
  110. package/src/lib/format.ts +73 -9
  111. package/src/lib/interaction-errors.ts +15 -0
  112. package/src/model-policy.ts +6 -1
  113. package/src/provider.tsx +12 -4
  114. package/src/session-ui.ts +1 -1
  115. package/src/timeline/projection.ts +12 -1
  116. package/styles/compiled.css +14 -30
  117. package/dist/chunk-45GE35O7.js.map +0 -1
  118. package/dist/chunk-6NEZPF53.js.map +0 -1
  119. package/dist/chunk-CHPKSLPN.js.map +0 -1
  120. package/dist/chunk-DV2DBNPQ.js.map +0 -1
  121. package/dist/chunk-FGBCYQOT.js.map +0 -1
  122. package/dist/chunk-KGENSZKS.js.map +0 -1
  123. package/dist/chunk-KZ73RL76.js.map +0 -1
  124. package/dist/chunk-LZ3KHA55.js.map +0 -1
  125. package/dist/chunk-UINDK6C3.js.map +0 -1
  126. package/dist/chunk-UJRYEWZI.js.map +0 -1
  127. package/dist/chunk-VGIKFIO2.js.map +0 -1
  128. package/dist/chunk-WZ55DVGK.js.map +0 -1
  129. package/dist/chunk-YRYF2GLG.js.map +0 -1
  130. package/dist/chunk-YSMMDOOE.js.map +0 -1
  131. package/dist/chunk-Z3DISEQN.js.map +0 -1
  132. package/dist/components/timeline-pill-geometry.d.ts +0 -8
  133. package/dist/components/timeline-pill-placement.d.ts +0 -24
  134. package/dist/timeline-pill-geometry-Z23V243M.js +0 -110
  135. package/dist/timeline-pill-geometry-Z23V243M.js.map +0 -1
  136. package/src/components/timeline-pill-geometry.ts +0 -169
  137. package/src/components/timeline-pill-placement.ts +0 -99
  138. /package/dist/{browser-viewer-5X47I6T2.js.map → browser-viewer-63TXMINB.js.map} +0 -0
  139. /package/dist/{chunk-5EXGU7UF.js.map → chunk-55DDDVKU.js.map} +0 -0
  140. /package/dist/{chunk-BI57CQUV.js.map → chunk-I7IWRQLN.js.map} +0 -0
  141. /package/dist/{chunk-XA6R7DJQ.js.map → chunk-IQUCRAT7.js.map} +0 -0
  142. /package/dist/{computer-viewer-7GF35SNY.js.map → computer-viewer-X2MBRZY4.js.map} +0 -0
  143. /package/dist/{latest-question-3BJGKQQ6.js.map → latest-question-CNEZ5KAN.js.map} +0 -0
  144. /package/dist/{model-policy-picker-menu-PUTA5JOB.js.map → model-policy-picker-menu-GN2LTB7Z.js.map} +0 -0
  145. /package/dist/{sandbox-files-AEJTWGT4.js.map → sandbox-files-BBC47SLD.js.map} +0 -0
@@ -1,21 +1,26 @@
1
1
  import { CheckIcon, ShieldCheckIcon, XIcon } from "lucide-react";
2
- import { useEffect, useId, useRef, useState, type ReactNode } from "react";
2
+ import { Fragment, useEffect, useId, useRef, useState, type ReactNode } from "react";
3
3
  import type { PendingApproval } from "../approvals";
4
4
  import { cn } from "../lib/cn";
5
+ import { useErrorMessage } from "../lib/error-message";
5
6
  import { toolDisplayName } from "../timeline/tool-display-name";
6
7
 
7
8
  export type ApprovalSurfaceMessages = {
8
9
  title: string;
10
+ /** Screen-reader context for the decision; not shown visually. */
9
11
  description: string;
10
12
  approve: string;
11
13
  reject: string;
12
14
  approving: string;
13
15
  rejecting: string;
14
16
  formatToolName: (name: string) => string;
17
+ /** Toggle for the exact arguments when they are shown as readable fields. */
18
+ showDetails?: string | undefined;
19
+ hideDetails?: string | undefined;
15
20
  };
16
21
 
17
22
  export const defaultApprovalSurfaceMessages: ApprovalSurfaceMessages = {
18
- title: "Approval required",
23
+ title: "Approval needed",
19
24
  description: "Review the requested action before the agent continues.",
20
25
  approve: "Approve",
21
26
  reject: "Reject",
@@ -25,6 +30,8 @@ export const defaultApprovalSurfaceMessages: ApprovalSurfaceMessages = {
25
30
  name.includes("__") || /^[a-f0-9]{64}$/.test(name)
26
31
  ? toolDisplayName(name)
27
32
  : name.replaceAll("_", " ").replaceAll(".", " › "),
33
+ showDetails: "Show exact arguments",
34
+ hideDetails: "Hide exact arguments",
28
35
  };
29
36
 
30
37
  export type ApprovalSurfaceProps = {
@@ -39,6 +46,8 @@ export type ApprovalSurfaceProps = {
39
46
  };
40
47
 
41
48
  const APPROVAL_ARGUMENT_PREVIEW_CHARACTERS = 4_000;
49
+ const APPROVAL_FIELD_LIMIT = 12;
50
+ const APPROVAL_LIST_LIMIT = 10;
42
51
 
43
52
  function approvalArgumentsPreview(value: unknown): string | null {
44
53
  if (value === undefined) return null;
@@ -54,6 +63,68 @@ function approvalArgumentsPreview(value: unknown): string | null {
54
63
  return `${characters.slice(0, APPROVAL_ARGUMENT_PREVIEW_CHARACTERS).join("")}\n… ${characters.length - APPROVAL_ARGUMENT_PREVIEW_CHARACTERS} characters omitted`;
55
64
  }
56
65
 
66
+ export type ApprovalField = { key: string; label: string; value: string };
67
+
68
+ const ACRONYMS = new Set(["id", "ids", "url", "uri", "api", "ip", "sku", "utc", "iban", "vat"]);
69
+
70
+ /** "paymentId" / "payment_id" → "Payment ID". */
71
+ function fieldLabel(key: string): string {
72
+ const words = key
73
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
74
+ .replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2")
75
+ .split(/[\s_.-]+/)
76
+ .filter(Boolean)
77
+ .map((word) => (ACRONYMS.has(word.toLowerCase()) ? word.toUpperCase() : word.toLowerCase()));
78
+ const label = words.join(" ");
79
+ return label ? label.charAt(0).toUpperCase() + label.slice(1) : key;
80
+ }
81
+
82
+ function scalarText(value: unknown): string | null {
83
+ if (typeof value === "string") return value;
84
+ if (typeof value === "number" || typeof value === "bigint") return String(value);
85
+ if (typeof value === "boolean") return value ? "Yes" : "No";
86
+ if (value === null) return "None";
87
+ return null;
88
+ }
89
+
90
+ /**
91
+ * Readable fields for a flat argument object (scalars and short scalar lists).
92
+ * Returns null when the arguments are nested, large, or not an object, so the
93
+ * caller shows the exact JSON instead of a lossy summary.
94
+ */
95
+ export function approvalArgumentFields(value: unknown): ApprovalField[] | null {
96
+ let record = value;
97
+ if (typeof record === "string") {
98
+ try {
99
+ record = JSON.parse(record) as unknown;
100
+ } catch {
101
+ return null;
102
+ }
103
+ }
104
+ if (!record || typeof record !== "object" || Array.isArray(record)) return null;
105
+ const entries = Object.entries(record as Record<string, unknown>);
106
+ if (entries.length === 0 || entries.length > APPROVAL_FIELD_LIMIT) return null;
107
+ const fields: ApprovalField[] = [];
108
+ for (const [key, entry] of entries) {
109
+ let text = scalarText(entry);
110
+ if (text === null && Array.isArray(entry) && entry.length <= APPROVAL_LIST_LIMIT) {
111
+ const items = entry.map(scalarText);
112
+ if (items.every((item): item is string => item !== null)) text = items.join(", ");
113
+ }
114
+ if (text === null || text.length > APPROVAL_ARGUMENT_PREVIEW_CHARACTERS) return null;
115
+ fields.push({ key, label: fieldLabel(key), value: text });
116
+ }
117
+ return fields;
118
+ }
119
+
120
+ function isEmptyArguments(value: unknown): boolean {
121
+ return (
122
+ value === undefined ||
123
+ value === null ||
124
+ (typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 0)
125
+ );
126
+ }
127
+
57
128
  function approvalOwnershipKey(approval: PendingApproval): string {
58
129
  return `${approval.id}\u0000${approval.name}\u0000${approvalArgumentsPreview(approval.arguments) ?? ""}`;
59
130
  }
@@ -74,6 +145,8 @@ export function ApprovalSurface({
74
145
  className,
75
146
  }: ApprovalSurfaceProps) {
76
147
  const titleId = useId();
148
+ const formatError = useErrorMessage();
149
+ const descriptionId = useId();
77
150
  const messages = { ...defaultApprovalSurfaceMessages, ...overrides };
78
151
  const [pending, setPending] = useState<{
79
152
  approvalKey: string;
@@ -81,7 +154,7 @@ export function ApprovalSurface({
81
154
  token: symbol;
82
155
  } | null>(null);
83
156
  const pendingRef = useRef<{ approvalKey: string; token: symbol } | null>(null);
84
- const [decisionError, setDecisionError] = useState<Error | null>(null);
157
+ const [decisionError, setDecisionError] = useState<{ cause: unknown } | null>(null);
85
158
 
86
159
  useEffect(() => {
87
160
  if (
@@ -116,69 +189,58 @@ export function ApprovalSurface({
116
189
  if (pendingRef.current?.token === token) {
117
190
  pendingRef.current = null;
118
191
  setPending((current) => (current?.token === token ? null : current));
119
- setDecisionError(cause instanceof Error ? cause : new Error(String(cause)));
192
+ setDecisionError({ cause });
120
193
  }
121
194
  }
122
195
  };
123
- const errorMessage = decisionError?.message ?? (error instanceof Error ? error.message : error);
196
+ const errorMessage = decisionError
197
+ ? formatError(decisionError.cause)
198
+ : error instanceof Error
199
+ ? formatError(error)
200
+ : error;
124
201
 
125
202
  return (
126
203
  <section
127
204
  className={cn(
128
- "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",
205
+ "og-root box-border flex w-full flex-col rounded-og-lg border border-og-status-waiting/35 bg-og-status-waiting/5 shadow-og-sm",
129
206
  className,
130
207
  )}
131
208
  aria-labelledby={titleId}
209
+ aria-describedby={descriptionId}
210
+ data-og-approval-surface=""
132
211
  >
133
- <header className="flex items-start gap-3">
134
- <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">
135
- <ShieldCheckIcon aria-hidden="true" className="size-4" />
136
- </span>
137
- <div className="min-w-0">
138
- <h2 id={titleId} className="text-og-md font-semibold text-og-fg">
139
- {messages.title}
140
- </h2>
141
- <p className="mt-0.5 text-og-sm text-og-fg-muted">{messages.description}</p>
142
- </div>
143
- </header>
144
-
145
- <div className="flex flex-col gap-2">
212
+ <h2 id={titleId} className="sr-only">
213
+ {messages.title}
214
+ </h2>
215
+ <p id={descriptionId} className="sr-only">
216
+ {messages.description}
217
+ </p>
218
+ <div className="flex flex-col divide-y divide-og-status-waiting/20">
146
219
  {approvals.map((approval) => {
147
220
  const approvalKey = approvalOwnershipKey(approval);
148
221
  const active = pending?.approvalKey === approvalKey ? pending.decision : null;
149
- const argumentsPreview = approvalArgumentsPreview(approval.arguments);
150
222
  return (
151
223
  <article
152
224
  key={approval.id}
153
225
  data-approval-id={approval.id}
154
- className="rounded-og-md border border-og-border bg-og-surface-1 p-3"
226
+ className="flex flex-col gap-3 p-4"
155
227
  >
156
228
  {renderApproval ? (
157
- renderApproval(approval)
158
- ) : (
159
- <>
160
- <p className="text-og-sm font-medium text-og-fg">
161
- {approval.display?.title ??
162
- messages.formatToolName(approval.display?.toolName ?? approval.name)}
229
+ <div className="min-w-0">
230
+ <p className="mb-1 text-og-xs font-medium text-og-status-waiting" aria-hidden>
231
+ {messages.title}
163
232
  </p>
164
- {approval.display?.accountLabel ? (
165
- <p className="mt-1 break-words text-og-xs text-og-fg-muted">
166
- {approval.display.accountLabel}
167
- </p>
168
- ) : null}
169
- {argumentsPreview !== null ? (
170
- <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">
171
- {argumentsPreview}
172
- </pre>
173
- ) : null}
174
- </>
233
+ {renderApproval(approval)}
234
+ </div>
235
+ ) : (
236
+ <ApprovalDetails approval={approval} messages={messages} />
175
237
  )}
176
- <div className="mt-3 flex flex-wrap justify-end gap-2">
238
+ <div className="flex flex-wrap items-center justify-end gap-2">
177
239
  <button
178
240
  type="button"
179
241
  disabled={busy}
180
242
  onClick={() => void decide(approval, "reject")}
181
- 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"
243
+ className="inline-flex min-h-8 items-center gap-1.5 rounded-og-md border border-og-border bg-og-surface-1 px-3 py-1 text-og-sm font-medium text-og-fg transition-colors hover:border-og-border-strong disabled:opacity-50"
182
244
  >
183
245
  <XIcon aria-hidden="true" className="size-3.5" />
184
246
  {active === "reject" ? messages.rejecting : messages.reject}
@@ -187,7 +249,7 @@ export function ApprovalSurface({
187
249
  type="button"
188
250
  disabled={busy}
189
251
  onClick={() => void decide(approval, "approve")}
190
- className="inline-flex min-h-9 items-center gap-1.5 rounded-og-md border border-og-primary-border bg-og-primary text-og-primary-fg px-3 py-1.5 text-og-sm font-medium transition-colors hover:bg-og-primary-hover disabled:opacity-50"
252
+ className="inline-flex min-h-8 items-center gap-1.5 rounded-og-md border border-og-primary-border bg-og-primary px-3 py-1 text-og-sm font-medium text-og-primary-fg transition-colors hover:bg-og-primary-hover disabled:opacity-50"
191
253
  >
192
254
  <CheckIcon aria-hidden="true" className="size-3.5" />
193
255
  {active === "approve" ? messages.approving : messages.approve}
@@ -199,10 +261,72 @@ export function ApprovalSurface({
199
261
  </div>
200
262
 
201
263
  {errorMessage ? (
202
- <p role="alert" className="text-og-sm text-og-status-failed">
264
+ <p role="alert" className="px-4 pb-3 text-og-sm text-og-status-failed">
203
265
  {errorMessage}
204
266
  </p>
205
267
  ) : null}
206
268
  </section>
207
269
  );
208
270
  }
271
+
272
+ function ApprovalDetails({
273
+ approval,
274
+ messages,
275
+ }: {
276
+ approval: PendingApproval;
277
+ messages: ApprovalSurfaceMessages;
278
+ }) {
279
+ const [showExact, setShowExact] = useState(false);
280
+ const fields = approvalArgumentFields(approval.arguments);
281
+ const exact = isEmptyArguments(approval.arguments)
282
+ ? null
283
+ : approvalArgumentsPreview(approval.arguments);
284
+ return (
285
+ <div className="flex min-w-0 items-start gap-3">
286
+ <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">
287
+ <ShieldCheckIcon aria-hidden="true" className="size-4" />
288
+ </span>
289
+ <div className="min-w-0 flex-1">
290
+ <p className="text-og-xs font-medium text-og-status-waiting">{messages.title}</p>
291
+ <p className="mt-0.5 break-words text-og-md font-semibold text-og-fg">
292
+ {approval.display?.title ??
293
+ messages.formatToolName(approval.display?.toolName ?? approval.name)}
294
+ </p>
295
+ {approval.display?.accountLabel ? (
296
+ <p className="mt-0.5 break-words text-og-xs text-og-fg-muted">
297
+ {approval.display.accountLabel}
298
+ </p>
299
+ ) : null}
300
+ {fields ? (
301
+ <dl className="m-0 mt-3 grid grid-cols-[minmax(5.5rem,max-content)_minmax(0,1fr)] gap-x-5 gap-y-1.5 text-og-sm">
302
+ {fields.map((field) => (
303
+ <Fragment key={field.key}>
304
+ <dt className="m-0 text-og-fg-muted">{field.label}</dt>
305
+ <dd className="m-0 max-h-32 min-w-0 overflow-auto whitespace-pre-wrap break-words text-og-fg">
306
+ {field.value}
307
+ </dd>
308
+ </Fragment>
309
+ ))}
310
+ </dl>
311
+ ) : null}
312
+ {exact !== null && (!fields || showExact) ? (
313
+ <pre className="mt-3 max-h-48 overflow-auto whitespace-pre-wrap break-words rounded-og-sm bg-og-fg/5 p-2.5 font-mono text-og-xs text-og-fg-muted">
314
+ {exact}
315
+ </pre>
316
+ ) : null}
317
+ {fields && exact !== null ? (
318
+ <button
319
+ type="button"
320
+ aria-expanded={showExact}
321
+ onClick={() => setShowExact((value) => !value)}
322
+ className="mt-2 text-og-xs text-og-fg-muted underline-offset-2 hover:text-og-fg hover:underline"
323
+ >
324
+ {showExact
325
+ ? (messages.hideDetails ?? defaultApprovalSurfaceMessages.hideDetails)
326
+ : (messages.showDetails ?? defaultApprovalSurfaceMessages.showDetails)}
327
+ </button>
328
+ ) : null}
329
+ </div>
330
+ </div>
331
+ );
332
+ }
@@ -20,7 +20,6 @@ import type {
20
20
  SiteAuthConnection,
21
21
  } from "@opengeni/sdk/interaction";
22
22
  import { interactionControlFailureFromError } from "@opengeni/sdk/interaction";
23
- import { OpenGeniApiError } from "@opengeni/sdk";
24
23
  import {
25
24
  BugIcon,
26
25
  ArchiveIcon,
@@ -71,7 +70,10 @@ import { useSiteAuthConnections } from "../hooks/use-site-auth-connections";
71
70
  import { cn } from "../lib/cn";
72
71
  import { copyTextToClipboard } from "../lib/clipboard";
73
72
  import { formatBytes } from "../lib/format";
74
- import { isSourcePlacementChangedError } from "../lib/interaction-errors";
73
+ import {
74
+ isInteractionControlUnavailable,
75
+ isSourcePlacementChangedError,
76
+ } from "../lib/interaction-errors";
75
77
  import type { EmbeddedBrowserInteractionClientOverride } from "../session-context";
76
78
  import { browserKey, HUMAN_BROWSER_HOME_URL, normalizeBrowserAddress } from "./browser-input";
77
79
  import { InteractionInterventionBanner } from "./interaction-intervention-banner";
@@ -434,7 +436,7 @@ export function BrowserViewer({
434
436
  }, [frameIsLive]);
435
437
  const supportsLiveFrames =
436
438
  (browser.session ?? selectedRegistrySession)?.capabilities.liveFrames === true;
437
- const controlUnavailable = isBrowserControlUnavailable(browser.error);
439
+ const controlUnavailable = isInteractionControlUnavailable(browser.error);
438
440
  const connectionError = controlUnavailable ? browser.error : (frames.error ?? browser.error);
439
441
  // A managed controller can reject a stale attachment while the same browser
440
442
  // remains healthy. Only extension-attached Chrome requires a new browser on
@@ -919,6 +921,32 @@ export function BrowserViewer({
919
921
  .catch((cause) => notifyError(cause, "Could not reload."));
920
922
  }}
921
923
  />
924
+ {browser.inputFailure ? (
925
+ <div
926
+ role="alert"
927
+ aria-label="Browser input status"
928
+ className="flex shrink-0 items-center gap-2 border-t border-og-border bg-og-surface-1 px-3 py-2 text-og-xs"
929
+ >
930
+ <CircleAlertIcon className="size-4 shrink-0 text-og-status-failed" />
931
+ <div className="min-w-0 flex-1">
932
+ <span className="font-medium">
933
+ {browser.inputFailure.state === "failed"
934
+ ? "Browser input failed"
935
+ : "Input result unknown"}
936
+ </span>
937
+ <p className="text-og-fg-subtle">
938
+ {browser.inputFailure.error?.message ?? "Inspect the page before continuing."}
939
+ </p>
940
+ </div>
941
+ <button
942
+ type="button"
943
+ onClick={() => void browser.refresh()}
944
+ className="shrink-0 rounded border border-og-border px-2 py-1 transition hover:bg-og-surface-2"
945
+ >
946
+ Check browser
947
+ </button>
948
+ </div>
949
+ ) : null}
922
950
  <div className="relative flex min-h-0 min-w-0 flex-1 overflow-hidden">
923
951
  <BrowserViewport
924
952
  // A new control scope must discard every buffered gesture and queued action.
@@ -1022,6 +1050,7 @@ export function BrowserViewer({
1022
1050
  profile={selectedProfile}
1023
1051
  target={browser.selectedTarget}
1024
1052
  observation={browser.observation}
1053
+ inputFailure={browser.inputFailure}
1025
1054
  connectionState={displayConnectionState}
1026
1055
  refreshing={registry.refreshing}
1027
1056
  diagnosticsOpen={diagnosticsView !== null}
@@ -2640,6 +2669,7 @@ function BrowserStatusBar(props: {
2640
2669
  profile: BrowserIdentity | null;
2641
2670
  target: BrowserTarget | null;
2642
2671
  observation: ReturnType<typeof useBrowserSession>["observation"];
2672
+ inputFailure: ReturnType<typeof useBrowserSession>["inputFailure"];
2643
2673
  connectionState: string;
2644
2674
  refreshing: boolean;
2645
2675
  diagnosticsOpen: boolean;
@@ -2667,6 +2697,11 @@ function BrowserStatusBar(props: {
2667
2697
  ? "Semantic"
2668
2698
  : browserConnectionLabel(props.connectionState)}
2669
2699
  </span>
2700
+ {props.inputFailure ? (
2701
+ <span className="text-og-status-failed">
2702
+ {props.inputFailure.state === "failed" ? "Input failed" : "Input result unknown"}
2703
+ </span>
2704
+ ) : null}
2670
2705
  <span>{props.profile?.name ?? "Temporary browser"}</span>
2671
2706
  <span className="min-w-0 flex-1 truncate">{props.target?.title}</span>
2672
2707
  {props.refreshing ? <LoaderCircleIcon className="size-3 animate-spin" /> : null}
@@ -3280,12 +3315,6 @@ function attachedChromeGenerationLoss(
3280
3315
  return lost?.placement.kind === "attached_device" ? { deviceId: lost.placement.deviceId } : null;
3281
3316
  }
3282
3317
 
3283
- function isBrowserControlUnavailable(error: Error | null): boolean {
3284
- // Receiving pixels proves only the media channel. A failed control request
3285
- // must not leave a frozen screenshot presented as an interactive live page.
3286
- return error instanceof OpenGeniApiError && (error.status >= 500 || error.status === 0);
3287
- }
3288
-
3289
3318
  function isAttachedChromeGenerationLossError(error: Error | null): boolean {
3290
3319
  if (!error) return false;
3291
3320
  return /placement instance changed|controller authority changed/i.test(error.message);
@@ -36,6 +36,7 @@ import {
36
36
  type RefObject,
37
37
  } from "react";
38
38
  import { argHint, defaultCommands } from "../commands/registry";
39
+ import { useErrorMessage } from "../lib/error-message";
39
40
  import type { Notice, SlashCommand } from "../commands/types";
40
41
  import type { ComposerState } from "../hooks/use-composer";
41
42
  import { shouldSteerOnKey, shouldSubmitOnKey } from "../hooks/use-composer";
@@ -175,7 +176,9 @@ export const defaultChatComposerMessages: ChatComposerMessages = {
175
176
  messagePlaceholder: "Message the agent…",
176
177
  pausedPlaceholder: "Message the agent — it will wait in the queue…",
177
178
  inputLabel: "Message the agent",
178
- keyboardHint: "Enter to queue · Cmd/Ctrl+Enter to steer · Shift+Enter for a new line",
179
+ // Shortcuts live in the send button's title; the footer stays quiet unless
180
+ // the host supplies its own hint.
181
+ keyboardHint: "",
179
182
  slashCommandBlocked:
180
183
  "That's a slash command — press Enter in the command list to run it, or edit the line to send a message.",
181
184
  controlChangedError:
@@ -374,6 +377,7 @@ export function useChatComposerController({
374
377
  onPaste,
375
378
  messages: messageOverrides,
376
379
  }: UseChatComposerControllerOptions) {
380
+ const formatError = useErrorMessage();
377
381
  const messages = useMemo(
378
382
  () => ({ ...defaultChatComposerMessages, ...messageOverrides }),
379
383
  [messageOverrides],
@@ -663,7 +667,10 @@ export function useChatComposerController({
663
667
  tone: "error" as const,
664
668
  message: /control changed|paused while/i.test(delivery.error.message)
665
669
  ? messages.controlChangedError
666
- : composerSubmissionErrorMessage(delivery.error) || messages.sendFailedError,
670
+ : formatError(
671
+ delivery.error,
672
+ composerSubmissionErrorMessage(delivery.error) || messages.sendFailedError,
673
+ ),
667
674
  }
668
675
  : null);
669
676
  useEffect(() => {