qontinui-types 0.6.0

Canonical DTO types for Qontinui. Rust is the source of truth; TypeScript and Python are generated from JSON Schema emitted by schemars.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
//! UI Bridge element-state DTOs.
//!
//! Wire-format types for the React UI Bridge registry: the element state,
//! bounding rectangle, element identifier, the registered-element and
//! registered-component shapes, action requests/responses, discovery
//! request/response, and the full snapshot envelope.
//!
//! These are ports of the shape-bearing portion of
//! `qontinui-runner/src-tauri/src/commands/ui_bridge.rs`. Runtime state
//! (Tauri `AppHandle`, IPC event listeners, `CommandResponse` wrappers,
//! DOM capture engines, WebView handles) stays in the runner. This module
//! is data-only.
//!
//! ## Wire-format notes
//!
//! - All structs serialize with `camelCase` field names to match the
//!   JavaScript/TypeScript wire contract consumed by `qontinui-web` and
//!   the Python SDK.
//! - `ElementRect` intentionally carries both `x`/`y`/`width`/`height`
//!   **and** `top`/`right`/`bottom`/`left`; the React `getBoundingClientRect`
//!   shape includes all eight.
//! - Dates/times are ISO 8601 strings or Unix-epoch millisecond `i64`s
//!   (see crate-level docs).

use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

// ============================================================================
// Element geometry
// ============================================================================

/// Viewport-relative bounding box in CSS pixels.
///
/// This is the live on-screen geometry captured at snapshot time via
/// `Element.getBoundingClientRect()`. Present only when the SDK has a
/// live DOM ref for the element; absent when the element is registered
/// without a ref or when the snapshot is served from the DOM-fallback
/// scanner.
///
/// Click target for a hit is `(x + width/2, y + height/2)`.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[schemars(deny_unknown_fields)]
pub struct ElementBbox {
    /// X coordinate of the bbox origin in viewport CSS pixels.
    pub x: f64,
    /// Y coordinate of the bbox origin in viewport CSS pixels.
    pub y: f64,
    /// Width of the bbox in CSS pixels.
    pub width: f64,
    /// Height of the bbox in CSS pixels.
    pub height: f64,
}

/// Bounding rectangle of a DOM element, mirroring the output of
/// `Element.getBoundingClientRect()`.
///
/// Contains both the origin+size pair (`x`, `y`, `width`, `height`) and the
/// edge offsets (`top`, `right`, `bottom`, `left`) for consumer convenience.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[schemars(deny_unknown_fields)]
pub struct ElementRect {
    /// X coordinate of the element's origin (same as `left`).
    pub x: f64,
    /// Y coordinate of the element's origin (same as `top`).
    pub y: f64,
    /// Width of the element in CSS pixels.
    pub width: f64,
    /// Height of the element in CSS pixels.
    pub height: f64,
    /// Distance from the top of the viewport.
    pub top: f64,
    /// Distance from the left of the viewport plus `width`.
    pub right: f64,
    /// Distance from the top of the viewport plus `height`.
    pub bottom: f64,
    /// Distance from the left of the viewport.
    pub left: f64,
}

// ============================================================================
// Element state
// ============================================================================

/// Observable state of a UI Bridge element as returned from the React
/// registry.
///
/// Every element returned by the bridge includes a snapshot of its current
/// visibility, interactivity, and form-control value.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(deny_unknown_fields)]
pub struct ElementState {
    /// Whether the element is currently visible in the viewport.
    pub visible: bool,
    /// Whether the element is enabled (not disabled).
    pub enabled: bool,
    /// Whether the element currently has keyboard focus.
    pub focused: bool,
    /// Bounding rectangle of the element.
    pub rect: ElementRect,
    /// Current value for input/textarea elements.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub value: Option<String>,
    /// Current checked state for checkbox/radio elements.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub checked: Option<bool>,
    /// Currently selected options for `<select>` elements.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub selected_options: Option<Vec<String>>,
    /// Text content of the element (innerText).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub text_content: Option<String>,
}

// ============================================================================
// Element identifier
// ============================================================================

/// Identifier bundle for locating a UI Bridge element.
///
/// Elements can be addressed by any combination of UI-Bridge ID, test ID,
/// AWAS ID, HTML ID, XPath, or CSS selector. The `xpath` and `selector`
/// fields are always present; the named IDs are optional.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(deny_unknown_fields)]
pub struct ElementIdentifier {
    /// Application-assigned UI Bridge ID.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub ui_id: Option<String>,
    /// `data-testid` attribute value.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub test_id: Option<String>,
    /// AWAS-assigned action identifier.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub awas_id: Option<String>,
    /// Native HTML `id` attribute.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub html_id: Option<String>,
    /// Full XPath to the element.
    pub xpath: String,
    /// CSS selector that uniquely identifies the element.
    pub selector: String,
}

// ============================================================================
// Registered element / component
// ============================================================================

/// A registered element in the UI Bridge registry.
///
/// This is the serializable subset of the React `RegisteredElement`; it
/// includes identity, available actions, current state, and lifecycle info.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(deny_unknown_fields)]
pub struct UIBridgeElement {
    /// Unique element ID within the registry.
    pub id: String,
    /// Element type (e.g. `"button"`, `"input"`, `"select"`).
    #[serde(rename = "type")]
    pub element_type: String,
    /// Human-readable label for the element.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub label: Option<String>,
    /// Standard actions available on this element.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub actions: Vec<String>,
    /// Custom (application-defined) actions.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub custom_actions: Option<Vec<String>>,
    /// Identifier bundle for locating the element.
    pub identifier: ElementIdentifier,
    /// Current observable state.
    pub state: ElementState,
    /// Unix-epoch millisecond timestamp when the element was registered.
    pub registered_at: i64,
    /// Whether the element's React component is currently mounted.
    pub mounted: bool,
    /// Viewport-relative bounding box in CSS pixels, when the SDK has a
    /// live DOM ref. Absent for elements registered without a ref or when
    /// the snapshot is served from the DOM-fallback scanner.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub bbox: Option<ElementBbox>,
    /// Cheap viewport-visibility signal derived by the SDK as
    /// `bbox.width > 0 && bbox.height > 0`. Use `state.visible` for the
    /// richer occlusion check.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub visible: Option<bool>,

    /// ARIA role of the element (explicit `role=` or implicit per W3C ARIA-in-HTML).
    /// Populated by the SDK's element walker. Source of truth for IrElementCriteria.role.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub role: Option<String>,

    /// HTML tag name in lowercase. Source of truth for IrElementCriteria.tag_name.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub tag_name: Option<String>,

    /// Computed aria-label (explicit attribute, falling back to aria-labelledby
    /// reference resolution). Source of truth for IrElementCriteria.aria_label.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub aria_label: Option<String>,

    /// W3C "accessible name" per the accessible-name algorithm. Distinct from
    /// aria_label because the algorithm may consult aria-labelledby, associated
    /// label elements, title, or visible content. Source of truth for
    /// IrElementCriteria.accessible_name.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub accessible_name: Option<String>,

    /// Visible text content with whitespace collapsed (DOM innerText-equivalent
    /// on web; accessibilityLabel/text equivalent on native). Source of truth for
    /// IrElementCriteria.text and text_contains. Distinct from state.text_content
    /// which is a snapshot of the form-control value.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub text: Option<String>,
}

/// Information about a single action exposed by a UI Bridge component.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(deny_unknown_fields)]
pub struct ComponentActionInfo {
    /// Unique action identifier within the component.
    pub id: String,
    /// Human-readable label.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub label: Option<String>,
    /// Longer description of what the action does.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,
}

/// A registered component in the UI Bridge registry.
///
/// Components group related elements and expose higher-level actions
/// (e.g. "submit form", "reset filters").
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(deny_unknown_fields)]
pub struct UIBridgeComponent {
    /// Unique component ID within the registry.
    pub id: String,
    /// Component name.
    pub name: String,
    /// Human-readable description.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,
    /// Actions exposed by this component.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub actions: Vec<ComponentActionInfo>,
    /// IDs of elements that belong to this component.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub element_ids: Option<Vec<String>>,
    /// Unix-epoch millisecond timestamp when the component was registered.
    pub registered_at: i64,
    /// Whether the component's React component is currently mounted.
    pub mounted: bool,
}

// ============================================================================
// Action requests / responses
// ============================================================================

/// Wait-condition options attached to an element action request.
///
/// Before executing the action the bridge can optionally wait until the
/// target element reaches a specified visibility/enabled/focused state.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(deny_unknown_fields)]
pub struct WaitOptions {
    /// Wait until the element is visible (or hidden if `false`).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub visible: Option<bool>,
    /// Wait until the element is enabled (or disabled if `false`).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub enabled: Option<bool>,
    /// Wait until the element has focus (or loses focus if `false`).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub focused: Option<bool>,
    /// Maximum time to wait in milliseconds before timing out.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub timeout: Option<u32>,
    /// Polling interval in milliseconds for condition checks.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub interval: Option<u32>,
}

/// Request to execute an action on a UI Bridge element.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(deny_unknown_fields)]
pub struct ElementActionRequest {
    /// Action name (e.g. `"click"`, `"type"`, `"select"`).
    pub action: String,
    /// Optional action-specific parameters.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub params: Option<serde_json::Value>,
    /// Optional wait conditions to satisfy before executing.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub wait_options: Option<WaitOptions>,
}

/// Request to execute an action on a UI Bridge component.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(deny_unknown_fields)]
pub struct ComponentActionRequest {
    /// Action name.
    pub action: String,
    /// Optional action-specific parameters.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub params: Option<serde_json::Value>,
}

/// Response from executing an action on an element or component.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(deny_unknown_fields)]
pub struct ActionResponse {
    /// Whether the action completed successfully.
    pub success: bool,
    /// Updated element state after the action (if applicable).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub element_state: Option<ElementState>,
    /// Action-specific return value.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub result: Option<serde_json::Value>,
    /// Error message if the action failed.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub error: Option<String>,
    /// Stack trace if the action threw an exception.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub stack: Option<String>,
    /// Time taken to execute the action in milliseconds.
    pub duration_ms: u64,
    /// Unix-epoch millisecond timestamp when the action completed.
    pub timestamp: i64,
    /// D3 effect-calculus verification: the predicted-vs-observed outcome for
    /// this action, present only when a handler effect signature resolved for
    /// the `(action, element)` (opt-in; absent otherwise).
    ///
    /// Carried as an opaque JSON object on the wire, deliberately matching the
    /// established lean-wire / rich-SDK split (the SDK's `EffectVerification`
    /// has a richer shape than any consumer needs to type). The runner
    /// deserializes the sub-shape it asserts on (`outcome` / `cause` /
    /// `containment` / `durationMs`) with its own local struct in the
    /// `effect_check` step handler, and relays the rest into `result_json`. See
    /// `ui-bridge/.../control/effect-types.ts` for the SDK-side producer. Kept
    /// opaque here so the nested effect types need no top-level codegen
    /// registration (which would couple this crate's bindings to a runner
    /// `schema_export.rs` change).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub effect_verification: Option<serde_json::Value>,
}

// ============================================================================
// Discovery
// ============================================================================

/// Options for a UI Bridge element-discovery scan.
///
/// Discovery crawls the live DOM and returns elements that match the
/// provided filters, regardless of whether they are registered in the
/// bridge registry.
#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(deny_unknown_fields)]
pub struct DiscoveryRequest {
    /// CSS selector for the root element to start scanning from.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub root: Option<String>,
    /// If `true`, only return interactive elements (buttons, inputs, etc.).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub interactive_only: Option<bool>,
    /// If `true`, include hidden/off-screen elements.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub include_hidden: Option<bool>,
    /// Maximum number of elements to return.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub limit: Option<u32>,
    /// Filter by element types (e.g. `["button", "input"]`).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub types: Option<Vec<String>>,
    /// CSS selector filter (only elements matching this selector).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub selector: Option<String>,
}

/// An element found during a discovery scan.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(deny_unknown_fields)]
pub struct DiscoveredElement {
    /// Unique element ID.
    pub id: String,
    /// Element type (e.g. `"button"`, `"input"`).
    #[serde(rename = "type")]
    pub element_type: String,
    /// Human-readable label.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub label: Option<String>,
    /// HTML tag name (e.g. `"BUTTON"`, `"INPUT"`).
    pub tag_name: String,
    /// ARIA role attribute.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub role: Option<String>,
    /// Computed accessible name.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub accessible_name: Option<String>,
    /// Available actions for this element.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub actions: Vec<String>,
    /// Current observable state.
    pub state: ElementState,
    /// Whether the element is already registered in the bridge registry.
    pub registered: bool,
}

/// Response from a UI Bridge discovery scan.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(deny_unknown_fields)]
pub struct DiscoveryResponse {
    /// Discovered elements matching the request filters.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub elements: Vec<DiscoveredElement>,
    /// Total number of elements found (before any limit).
    pub total: usize,
    /// Time taken for the discovery scan in milliseconds.
    pub duration_ms: u64,
    /// Unix-epoch millisecond timestamp of the scan.
    pub timestamp: i64,
}

// ============================================================================
// Snapshot
// ============================================================================

/// Workflow metadata included in a UI Bridge snapshot.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(deny_unknown_fields)]
pub struct WorkflowInfo {
    /// Workflow ID.
    pub id: String,
    /// Workflow name.
    pub name: String,
    /// Optional description.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,
    /// Number of steps in the workflow.
    pub step_count: usize,
}

/// Modal/dialog entry in the active modal stack.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(deny_unknown_fields)]
pub struct UIBridgeModalInfo {
    pub id: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub title: Option<String>,
    /// Modal kind. Web: dialog/alertdialog/modal/drawer/popover/sheet.
    /// Native (RN): modal/sheet/drawer/popover/alertdialog/dialog.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub r#type: Option<String>,
    /// Whether this modal blocks interaction with content behind it.
    pub blocking: bool,
    /// Whether the modal is dismissible (RN-specific). Optional so the
    /// web shape (which doesn't carry this bit) round-trips cleanly.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub dismissible: Option<bool>,
    /// Web-only: computed z-index of the modal.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub z_index: Option<i64>,
    /// Web-only: whether a backdrop/overlay is present.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub has_backdrop: Option<bool>,
    /// Timestamp when the modal was detected (epoch ms).
    pub detected_at: i64,
}

/// Modal stack context attached to a snapshot when the SDK has a
/// `ModalDetector` enricher configured.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(deny_unknown_fields)]
pub struct UIBridgeModalStack {
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub modals: Vec<UIBridgeModalInfo>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub top_modal: Option<UIBridgeModalInfo>,
    pub has_blocking_modal: bool,
    pub count: usize,
}

/// A captured toast/notification entry.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(deny_unknown_fields)]
pub struct UIBridgeCapturedToast {
    pub id: String,
    pub message: String,
    /// Severity level. One of: info|success|warning|error|loading|unknown.
    pub level: String,
    pub appeared_at: i64,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub dismissed_at: Option<i64>,
    pub visible: bool,
    pub duration_ms: i64,
}

/// Toast snapshot context.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(deny_unknown_fields)]
pub struct UIBridgeToastContext {
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub active: Vec<UIBridgeCapturedToast>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub recent: Vec<UIBridgeCapturedToast>,
    pub total_captured: usize,
}

/// Undo/redo availability snapshot context.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(deny_unknown_fields)]
pub struct UIBridgeUndoContext {
    pub can_undo: bool,
    pub can_redo: bool,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub undo_description: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub redo_description: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub undo_depth: Option<usize>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub redo_depth: Option<usize>,
    pub summary: String,
}

/// Full snapshot of the UI Bridge state.
///
/// Captures all registered elements, components, and active workflows
/// at a single point in time. The optional `modalStack` / `toasts` /
/// `undoRedo` fields are populated by the SDK's enricher slot when
/// configured (see `setEnrichers` on web and native registries).
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[schemars(deny_unknown_fields)]
pub struct UIBridgeSnapshot {
    /// Unix-epoch millisecond timestamp of the snapshot.
    pub timestamp: i64,
    /// All registered elements.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub elements: Vec<UIBridgeElement>,
    /// All registered components.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub components: Vec<UIBridgeComponent>,
    /// Active workflows.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub workflows: Vec<WorkflowInfo>,
    /// Modal/sheet stack (populated when ModalDetector enricher is set).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub modal_stack: Option<UIBridgeModalStack>,
    /// Active and recently dismissed toasts (populated by ToastCapture).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub toasts: Option<UIBridgeToastContext>,
    /// Undo/redo availability (populated by UndoTracker).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub undo_redo: Option<UIBridgeUndoContext>,
    /// Native-only: current navigation route (Expo Router pathname).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub current_route: Option<String>,
    /// Native-only: current route segments.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub segments: Vec<String>,
}