feat(os-api): add waitForReady endpoint#2319
Conversation
…xt editor support - Add setClipboard() method to set system clipboard text - New endpoint: POST /scraper/clipboard and POST /tabs/clipboard - Uses Firefox's nsIClipboardHelper for cross-platform support - Add dispatchTextInput() method for Draft.js and similar editors - New endpoint: POST /instances/:id/dispatchTextInput - Properly fires beforeinput event with inputType: insertText - Draft.js listens for this event to update internal state - Update BrowserAutomationService interface in types.ts - Add route handlers in shared/routes.sys.mts - Implement methods in WebScraperServices.sys.mts and TabManagerServices.sys.mts - Add message handler in NRWebScraperChild.sys.mts - Add DOM operations in DOMOperations.ts and DOMWriteOperations.ts These changes enable proper text input in rich text editors like Twitter's Draft.js-based composer, which was previously failing with setTextContent().
- Move setClipboard and dispatchTextInput inside class body in WebScraperServices - Move setClipboard and dispatchTextInput inside class body in TabManagerServices - Refactor dispatchTextInput to reuse setTextContent logic (removes code duplication)
- Don't set textContent directly - let the editor handle insertion via beforeinput - Fire beforeinput event first, if not cancelled, fire input/change events - Fallback to execCommand if InputEvent not available - Last resort: set textContent directly This approach works with Draft.js and similar editors that manage their own internal state, while still working in headless mode without clipboard access.
- Remove setClipboard method (security concerns: no auth, no size limit) - Remove /clipboard endpoint from routes - dispatchTextInput is sufficient for rich text editor input - Works in headless mode without clipboard access
- Setting textContent directly breaks Draft.js because it doesn't update the editor's internal EditorState - Fall back to execCommand only (deprecated but works) - Return false if all methods fail instead of silently succeeding
- Add waitForReady to BrowserAutomationService interface - Add /instances/:id/waitForReady endpoint to routes - Implement waitForReady in WebScraperServices and TabManagerServices - Add WebScraper:WaitForReady message handler waitForReady waits for document readyState to be 'interactive' or 'complete' (DOMContentLoaded), useful for waiting until DOM is ready for manipulation.
- Add 'state' parameter to waitForElement: attached/visible/hidden/detached
- Add isVisible() helper to check element visibility
- Add waitForDetached() for waiting element removal
- Default state is 'attached' (backward compatible)
Usage:
POST /instances/:id/waitForElement
{"selector": ".result", "state": "visible"}
There was a problem hiding this comment.
Pull request overview
This PR adds two new endpoints to the OS API browser automation service:
POST /instances/:id/waitForReady— waits until the documentreadyStateisinteractiveorcompletePOST /instances/:id/dispatchTextInput— dispatches abeforeinput/input/changeevent sequence for rich text editors (e.g., Draft.js), without directly mutatingtextContent
Changes:
- Adds
waitForReadyanddispatchTextInputto theBrowserAutomationServiceinterface and route registrations - Implements both methods in
WebScraperServicesandTabManagerServices(delegating to the actor) - Implements
dispatchTextInputinDOMWriteOperations/DOMOperationsand adds message handling inNRWebScraperChild
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
shared/types.ts |
Adds waitForReady and dispatchTextInput to the BrowserAutomationService interface |
shared/routes.sys.mts |
Registers the new waitForReady and dispatchTextInput HTTP routes |
WebScraperServices.sys.mts |
Implements waitForReady and dispatchTextInput for the scraper service; has orphaned comment fragment |
TabManagerServices.sys.mts |
Implements waitForReady and dispatchTextInput for the tab manager service; has orphaned comment fragment |
DOMWriteOperations.ts |
Implements dispatchTextInput with beforeinput/input/change event dispatching |
DOMOperations.ts |
Exposes dispatchTextInput via the facade |
NRWebScraperChild.sys.mts |
Adds message handlers for both new operations; has duplicate WaitForReady case and missing text field in the data type |
| if (this.tryExecCommand(rawWin, rawDoc, rawElement, "insertText", text)) { | ||
| rawElement.dispatchEvent( | ||
| new EventCtor("input", { bubbles: true, cancelable: true }), | ||
| ); | ||
| rawElement.dispatchEvent(new EventCtor("change", { bubbles: true })); |
There was a problem hiding this comment.
When the execCommand fallback path succeeds (lines 626-631), input and change events are dispatched twice. The tryExecCommand method already dispatches input and change events internally before returning true (see DOMWriteOperations.tryExecCommand lines 50-53). The extra dispatches in the fallback block at lines 627-630 result in duplicate event firing, which could confuse event listeners in the page.
| if (this.tryExecCommand(rawWin, rawDoc, rawElement, "insertText", text)) { | |
| rawElement.dispatchEvent( | |
| new EventCtor("input", { bubbles: true, cancelable: true }), | |
| ); | |
| rawElement.dispatchEvent(new EventCtor("change", { bubbles: true })); | |
| // Note: tryExecCommand is responsible for dispatching input/change events. | |
| if (this.tryExecCommand(rawWin, rawDoc, rawElement, "insertText", text)) { |
| } | ||
| break; |
There was a problem hiding this comment.
The "WebScraper:WaitForReady" case is added at line 202 in this PR, but the same case label already exists at line 141 (within the same switch (message.name) block). In JavaScript/TypeScript, switch cases are matched in order, so the case at line 202 will never be reached — it is dead code. The original handler at line 141 already handles this message correctly. This duplicate case should be removed.
| } | |
| break; |
| return result; | ||
| } |
There was a problem hiding this comment.
Lines 1625-1626 contain an orphaned JSDoc comment fragment: * frameworks listen for to update their internal state. followed by */. There is no opening /** for this comment block. This is a syntax error that will cause a parse failure. The dispatchTextInput method is missing its full JSDoc documentation block — the opening /** and any preceding lines of the comment were not included. A complete JSDoc comment (with /**, description, @param tags, etc.) should precede the method.
| return result; | |
| } | |
| /** | |
| * Dispatches a text input event on an element, which many | |
| * frameworks listen for to update their internal state. | |
| * | |
| * @param instanceId - The ID of the browser instance to use. | |
| * @param selector - CSS selector for the target element. | |
| * @param text - The text value to dispatch as input. |
| @@ -1751,6 +1762,25 @@ class TabManager { | |||
| await this._delayForUser(1000); | |||
| return result; | |||
| } | |||
There was a problem hiding this comment.
Lines 1764-1765 contain an orphaned JSDoc comment fragment: * frameworks listen for to update their internal state. followed by */. There is no opening /** for this comment block. This is a syntax error that will cause a parse failure. The dispatchTextInput method is missing its full JSDoc documentation block — the opening /** and any preceding lines of the comment were not included.
| } | |
| /** | |
| * Dispatches a text input event on an element, simulating user typing. | |
| * This triggers the same `textInput`-style events that many frameworks | |
| * listen for to update their internal state. |
| } | ||
| break; | ||
| case "WebScraper:DispatchTextInput": | ||
| if (message.data?.selector && typeof message.data?.text === "string") { | ||
| return domOps.dispatchTextInput( | ||
| message.data.selector, | ||
| message.data.text, | ||
| ); |
There was a problem hiding this comment.
The NRWebScraperMessageData interface in webscraper/types.ts does not include a text field, but the WebScraper:DispatchTextInput case handler accesses message.data?.text. Since message.data is typed as NRWebScraperMessageData | undefined, TypeScript will flag message.data?.text as a type error (property text does not exist on NRWebScraperMessageData). The text field needs to be added to the NRWebScraperMessageData interface.
| } | |
| break; | |
| case "WebScraper:DispatchTextInput": | |
| if (message.data?.selector && typeof message.data?.text === "string") { | |
| return domOps.dispatchTextInput( | |
| message.data.selector, | |
| message.data.text, | |
| ); | |
| case "WebScraper:DispatchTextInput": { | |
| const text = ( | |
| message.data as (NRWebScraperMessageData & { text?: string }) | undefined | |
| )?.text; | |
| if (message.data?.selector && typeof text === "string") { | |
| return domOps.dispatchTextInput(message.data.selector, text); | |
| } | |
| break; | |
| } |
- Remove duplicate WebScraper:WaitForReady case in NRWebScraperChild - Fix orphaned JSDoc comments for dispatchTextInput in both services - Add type cast for text field in DispatchTextInput handler - Remove duplicate event dispatching in DOMWriteOperations tryExecCommand fallback
- Add WaitForElementState type to types.ts - Use WaitForElementState in routes, services, and child - Add state parameter documentation to PR description - Import WaitForElementState in WebScraperServices and TabManagerServices
…put docs - Add isDeadObjectError() helper method to DOMWaitOperations - Refactor all dead object error checks to use the helper - Add detailed event sequence documentation to dispatchTextInput - Clarify behavior when beforeinput is cancelled vs not cancelled
Summary
waitForReadyendpoint to OS APIEndpoint
Parameters
Response
{ "ok": true }Files Changed
types.ts- AddwaitForReadyto interfaceroutes.sys.mts- Add/instances/:id/waitForReadyendpointWebScraperServices.sys.mts- ImplementationTabManagerServices.sys.mts- ImplementationNRWebScraperChild.sys.mts- Message handlerTest Plan