Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/validation/code-oss-web-api-ledger.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Static matches establish source reachability only. Runtime claims require the ev
| `broadcast-channel` | yes | blocked | linux-x64, macos-arm64, windows-x64 | WebScene#81 | 0 | Cross-realm ordering, clone, teardown, queue, and product evidence are incomplete. |
| `cache-storage` | yes | blocked | linux-x64, macos-arm64, windows-x64 | WebScene#266 | 0 | CacheStorage is required by webview resources and remains scheduled in the webview stack. |
| `canvas-2d` | yes | blocked | linux-x64, macos-arm64, windows-x64 | WebScene#227 | 0 | Broad engine coverage exists, but the accepted Code OSS visual scenarios have not yet supplied revision-pinned product evidence here. |
| `clipboard` | yes | blocked | linux-x64, macos-arm64, windows-x64 | WebScene#227 | 0 | The host ABI exists; editor, terminal, webview, denial, focus, and large-payload product qualification is incomplete. |
| `clipboard` | yes | blocked | linux-x64, macos-arm64, windows-x64 | WebScene#227, WebScene#657 | 0 | The bounded host ABI and focused input/textarea PRIMARY publication source contract exist. AppScene#275 integration plus editor, terminal, webview, denial, focus, large-payload and packaged-product qualification remain incomplete. |
| `crypto` | yes | blocked | linux-x64, macos-arm64, windows-x64 | WebScene#102 | 0 | The vetted-provider runtime surface is complete; configured unchanged-package MCP secret-input and browser connection-secret persistence/reopen evidence remains missing across the release RIDs. Stock Code OSS omits proprietary VSDA assets, and bundled Copilot HMAC executes in its Node extension host. |
| `dom-core` | yes | blocked | linux-x64, macos-arm64, windows-x64 | WebScene#227, WebScene#260 | 0 | DOM coverage is extensive but no exhaustive reachable-surface and accepted-scenario reconciliation has been promoted. |
| `drag-drop` | yes | blocked | linux-x64, macos-arm64, windows-x64 | WebScene#260 | 0 | Explorer/editor drag data, cancellation, focus, native input, and teardown evidence is incomplete. |
Expand Down
75 changes: 75 additions & 0 deletions docs/validation/primary-selection-publication-657.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Bounded PRIMARY selected-text publication source contract

WebScene issue #657 adds the provider half of automatic Linux PRIMARY
publication. The exact baseline is WebScene
`dabd1435c8e6415163067ad7ad5befb498a77126`; AppScene #275 is the dependent
native consumer. The provider is a product-neutral native-host signal and has
no X11, browser-process, Electron, CEF, WebView or VS Code dependency.

## Provider boundary

A settled nonempty selection in the currently focused, connected, writable
`input` or `textarea` emits the existing `webscene_host_request_v1` with this
exact shape:

- kind `WEBSCENE_HOST_REQUEST_CLIPBOARD_WRITE_V1`;
- request ID and target node ID zero;
- flags `WEBSCENE_HOST_REQUEST_CLIPBOARD_REPLACE_V1 |
WEBSCENE_HOST_REQUEST_CLIPBOARD_PRIMARY_V1`;
- content type `text/plain`, no URL, and valid UTF-8 selected bytes;
- a maximum payload of 64 KiB, with oversize selections refused rather than
truncated.

The zero request ID makes the update one-way. It creates no JavaScript API,
Promise, completion binding or user-activation requirement. Ordinary Clipboard
API and shortcut reads/writes retain their nonzero request IDs, existing flags,
16-operation completion cap and CLIPBOARD meaning.

Password, readonly, disabled, inert, disconnected and unfocused controls are
ineligible. A collapsed or empty selection, an invalid UTF-8 slice, a UTF-16
boundary inside a surrogate pair, and an oversize slice emit nothing. These
cases do not clear a previously published or pending PRIMARY value.

## Coalescing and lifecycle

Selection and value mutations mark only the active text control dirty. The
runtime extracts its final selected slice after the current script, event and
microtask work reaches a stable checkpoint. Extraction checks byte offsets and
the 64 KiB cap before allocating the request payload.

The mutex-protected typed-host FIFO recognizes only the exact zero-ID PRIMARY
publication shape. If one is already pending, the runtime replaces that lease
in place with the newest eligible payload. Otherwise it appends one only while
the aggregate 1,024-request queue has capacity. Publication therefore uses at
most one pending slot and cannot evict or consume capacity reserved by an
existing completion-bearing request. Saturation drops the update without
changing clipboard state or exposing selected bytes in diagnostics.

Top-level navigation discards every publication still in WebScene's queue and
clears its dirty observation. A queued child-frame publication carries a
private frame-owner identity, outside the public ABI, so frame navigation or
detach can remove it before retiring that realm. Engine teardown destroys the
same bounded state. Once a host has taken a stable one-way lease, it represents
the prior selection and may remain PRIMARY across document navigation,
matching desktop selection ownership. Collapse, blur and teardown never send
a clearing write.

## Authored contracts and remaining acceptance

`test_primary_selection_publication` covers the exact ABI, UTF-8 text,
same-task and pending-request coalescing, retention on collapse, ineligible
controls, detach, refusal above 64 KiB and admission at the cap. Existing
clipboard contracts continue to cover ordinary Clipboard API flags, request
IDs, completion and the PRIMARY middle-paste read path.

Unchanged Code OSS mirrors short Monaco selections into its writable hidden
textarea, so those exact selections reach this provider without a source
patch. Monaco can elide large or multipage screen-reader textarea content and
does not represent all multicursor model selections there. Exact large,
multipage and multicursor Monaco publication, readonly controls,
contenteditable and document `Selection` remain outside #657.

Only `git diff --check` is run for this fast implementation slice. The authored
native contract, compiler/build, browser-oracle comparison, 10,000-change CPU
and allocation measurements, AppScene #275 integration, physical X11 clients,
packaged unchanged Code OSS, VM and computer-use gates remain unexecuted.
Original file line number Diff line number Diff line change
Expand Up @@ -2689,8 +2689,9 @@ WEBSCENE_API void webscene_file_grant_release_request_release_v2(
const webscene_file_grant_release_request_v2* request);

/* Typed native desktop request ABI. Request memory is immutable and remains
* valid until release. Byte payloads are capped at 16 MiB, strings are UTF-8,
* and at most 16 completion-bearing operations may be pending per document. */
* valid until release. Byte payloads are capped at 16 MiB, with one-way
* selected-text publication capped at 64 KiB. Strings are UTF-8, and at most
* 16 completion-bearing operations may be pending per document. */
enum {
WEBSCENE_HOST_REQUEST_OPEN_EXTERNAL_URL_V1 = 1,
WEBSCENE_HOST_REQUEST_CLIPBOARD_READ_V1 = 2,
Expand All @@ -2704,9 +2705,11 @@ enum {
};
enum {
WEBSCENE_HOST_REQUEST_CLIPBOARD_REPLACE_V1 = 1U << 0U,
/* Selects the platform's primary/selection clipboard for a text/plain
* READ request. Ordinary Clipboard API and shortcut requests leave this
* clear and continue to address the normal clipboard. */
/* Selects the platform's primary/selection clipboard. It is valid either
* alone for a completion-bearing text/plain READ request or together with
* REPLACE for the engine's zero-ID, one-way selected-text WRITE. Ordinary
* Clipboard API and shortcut requests leave this clear and continue to
* address the normal clipboard. */
WEBSCENE_HOST_REQUEST_CLIPBOARD_PRIMARY_V1 = 1U << 1U,
WEBSCENE_HOST_REQUEST_NAVIGATION_REPLACE_V1 = 1U << 0U,
WEBSCENE_HOST_REQUEST_EXTERNAL_NEW_CONTEXT_V1 = 1U << 0U,
Expand Down
130 changes: 126 additions & 4 deletions experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6526,6 +6526,126 @@ struct v8_dom_runtime::implementation final {
return true;
}

bool enqueue_primary_selection_publication(
std::unique_ptr<native_host_request> request)
{
if (!request) return false;
auto notify = false;
{
std::lock_guard lock(host_request_mutex);
const auto pending = std::find_if(
typed_host_requests.begin(),
typed_host_requests.end(),
[](const auto& candidate) {
return candidate != nullptr
&& candidate->view.request_id == 0U
&& candidate->view.kind
== WEBSCENE_HOST_REQUEST_CLIPBOARD_WRITE_V1
&& candidate->view.flags
== (WEBSCENE_HOST_REQUEST_CLIPBOARD_REPLACE_V1
| WEBSCENE_HOST_REQUEST_CLIPBOARD_PRIMARY_V1);
});
if (pending != typed_host_requests.end()) {
*pending = std::move(request);
return true;
}
if (host_requests.size() + typed_host_requests.size()
+ reserved_host_request_count
>= maximum_host_request_count) return false;
typed_host_requests.push_back(std::move(request));
notify = true;
}
if (notify && host_request_available) host_request_available();
return true;
}

void discard_primary_selection_publication_for_frame(uint32_t frame_id)
{
if (frame_id == 0U) return;
std::lock_guard lock(host_request_mutex);
std::erase_if(typed_host_requests, [&](const auto& request) {
return request != nullptr
&& request->source_frame_owner_id == frame_id
&& request->view.request_id == 0U
&& request->view.kind
== WEBSCENE_HOST_REQUEST_CLIPBOARD_WRITE_V1
&& request->view.flags
== (WEBSCENE_HOST_REQUEST_CLIPBOARD_REPLACE_V1
| WEBSCENE_HOST_REQUEST_CLIPBOARD_PRIMARY_V1);
});
}

void mark_primary_selection_publication_dirty(
const dom_node* candidate) noexcept
{
if (candidate != nullptr && candidate == active_element)
primary_selection_publication_dirty = true;
}

void flush_primary_selection_publication()
{
if (!primary_selection_publication_dirty) return;
primary_selection_publication_dirty = false;
constexpr size_t maximum_primary_selection_bytes = 64U * 1024U;
auto* selected = active_element;
if (!is_writable_text_control(selected)
|| !is_connected(*selected)
|| !selected->form_control().input_focused
|| forms::input_type_is(*selected, "password")
|| focus_document_owner(selected) != focused_document_owner_id) {
return;
}
ensure_form_value(*selected);
const auto& control = selected->form_control();
const auto start = std::min(control.selection_start, control.value.size());
const auto end = std::min(control.selection_end, control.value.size());
if (start >= end || control.selection_start_utf16_suboffset != 0U
|| control.selection_end_utf16_suboffset != 0U
|| end - start > maximum_primary_selection_bytes) {
return;
}
const auto text = std::string_view(control.value).substr(start, end - start);
const auto valid_utf8 = [](std::string_view value) noexcept {
for (size_t offset = 0U; offset < value.size();) {
const auto first = static_cast<unsigned char>(value[offset]);
size_t length{};
uint32_t scalar{};
if (first <= 0x7fU) { length = 1U; scalar = first; }
else if (first >= 0xc2U && first <= 0xdfU) {
length = 2U; scalar = first & 0x1fU;
} else if (first >= 0xe0U && first <= 0xefU) {
length = 3U; scalar = first & 0x0fU;
} else if (first >= 0xf0U && first <= 0xf4U) {
length = 4U; scalar = first & 0x07U;
} else return false;
if (offset + length > value.size()) return false;
for (size_t index = 1U; index < length; ++index) {
const auto continuation =
static_cast<unsigned char>(value[offset + index]);
if ((continuation & 0xc0U) != 0x80U) return false;
scalar = (scalar << 6U) | (continuation & 0x3fU);
}
if ((length == 2U && scalar < 0x80U)
|| (length == 3U && scalar < 0x800U)
|| (length == 4U && scalar < 0x10000U)
|| (scalar >= 0xd800U && scalar <= 0xdfffU)
|| scalar > 0x10ffffU) return false;
offset += length;
}
return true;
};
if (!valid_utf8(text)) return;
auto request = std::make_unique<native_host_request>();
request->view.kind = WEBSCENE_HOST_REQUEST_CLIPBOARD_WRITE_V1;
request->view.flags = WEBSCENE_HOST_REQUEST_CLIPBOARD_REPLACE_V1
| WEBSCENE_HOST_REQUEST_CLIPBOARD_PRIMARY_V1;
request->source_frame_owner_id = focus_document_owner(selected);
request->content_type = "text/plain";
request->bytes.assign(text.begin(), text.end());
static_cast<void>(
enqueue_primary_selection_publication(std::move(request)));
}

bool enqueue_reserved_typed_host_request(
std::unique_ptr<native_host_request> request)
{
Expand Down Expand Up @@ -7481,14 +7601,16 @@ uint64_t v8_dom_runtime::last_resize_observers_nanoseconds() const noexcept

bool v8_dom_runtime::dispatch_input(const webscene_input_event& event, bool defer_cursor_update)
{
return impl_->dispatch_input(event, defer_cursor_update)
&& impl_->promote_pending_promise_error();
const auto dispatched = impl_->dispatch_input(event, defer_cursor_update);
impl_->flush_primary_selection_publication();
return dispatched && impl_->promote_pending_promise_error();
}

bool v8_dom_runtime::dispatch_drag(native_drag_event& event)
{
return impl_->dispatch_drag(event)
&& impl_->promote_pending_promise_error();
const auto dispatched = impl_->dispatch_drag(event);
impl_->flush_primary_selection_publication();
return dispatched && impl_->promote_pending_promise_error();
}

void v8_dom_runtime::refresh_pointer_cursor_after_layout()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ struct native_host_completion {
};
struct native_host_request {
webscene_host_request_v1 view{};
uint32_t source_frame_owner_id{};
std::string content_type;
std::vector<uint8_t> bytes;
std::string url;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2409,6 +2409,8 @@
std::optional<bool> user_validity_interacted = std::nullopt,
std::optional<bool> value_changed_by_user = std::nullopt)
{
if (previous_value != current_value)
mark_primary_selection_publication_dirty(&subject);
document.mark_dirty();
const auto empty_state_changed=previous_value.empty()!=current_value.empty();
const auto previous_range=forms::range_state(subject,previous_value);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2825,6 +2825,7 @@
|| (target != nullptr && subtree_contains(frame, target));
});
}
discard_primary_selection_publication_for_frame(frame.id);
{
std::lock_guard lock(document_generation_mutex);
++frame_navigation_generations.try_emplace(frame.id, 0U)
Expand Down Expand Up @@ -2894,6 +2895,7 @@

void retire_iframe_navigation(uint32_t frame_id)
{
discard_primary_selection_publication_for_frame(frame_id);
{
std::lock_guard lock(document_generation_mutex);
frame_navigation_generations.erase(frame_id);
Expand Down Expand Up @@ -5404,6 +5406,7 @@
control.selection_end_utf16_suboffset = end_offset.utf16_suboffset;
control.selection_direction = text_selection_direction::none;
control.selection_explicitly_set = true;
self->mark_primary_selection_publication_dirty(node);
if(self->is_connected(*node)) {
self->recascade_form_value_selector_transition(
*node,previous_value,control.value,std::nullopt,false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3883,6 +3883,7 @@
v8::Local<v8::Value> value,
const v8::PropertyCallbackInfo<void>& info)
{
auto* self = current(info.GetIsolate());
auto* node = unwrap_node(info.Holder());
if (node == nullptr) return;
const auto index = value->Int32Value(info.GetIsolate()->GetCurrentContext()).FromMaybe(0);
Expand All @@ -3908,6 +3909,7 @@
node->mutable_form_control().selection_end_utf16_suboffset})) {
node->mutable_form_control().selection_direction = text_selection_direction::none;
}
self->mark_primary_selection_publication_dirty(node);
}

static void get_selection_end(
Expand All @@ -3930,6 +3932,7 @@
v8::Local<v8::Value> value,
const v8::PropertyCallbackInfo<void>& info)
{
auto* self = current(info.GetIsolate());
auto* node = unwrap_node(info.Holder());
if (node == nullptr) return;
const auto index = value->Int32Value(info.GetIsolate()->GetCurrentContext()).FromMaybe(0);
Expand All @@ -3955,6 +3958,7 @@
node->mutable_form_control().selection_end_utf16_suboffset})) {
node->mutable_form_control().selection_direction = text_selection_direction::none;
}
self->mark_primary_selection_publication_dirty(node);
}

static void get_selection_direction(
Expand All @@ -3979,6 +3983,7 @@
v8::Local<v8::Value> value,
const v8::PropertyCallbackInfo<void>& info)
{
auto* self = current(info.GetIsolate());
auto* node = unwrap_node(info.Holder());
if (node == nullptr) return;
const auto direction = to_utf8(info.GetIsolate(), value);
Expand All @@ -3995,6 +4000,7 @@
node->mutable_form_control().selection_end_utf16_suboffset})) {
node->mutable_form_control().selection_direction = text_selection_direction::none;
}
self->mark_primary_selection_publication_dirty(node);
}

static void get_boolean_attribute(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,11 @@
fullscreen_element = nullptr;
document_fullscreen = false;
{ std::lock_guard lock(host_request_mutex); host_requests.clear(); }
{ std::lock_guard lock(host_request_mutex); typed_host_requests.clear(); }
{
std::lock_guard lock(host_request_mutex);
typed_host_requests.clear();
primary_selection_publication_dirty = false;
}
{ std::lock_guard lock(host_request_mutex); download_requests.clear(); }
{ std::lock_guard lock(host_request_mutex); outbound_drag_requests.clear(); }
object_url_file_data.clear();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1110,6 +1110,7 @@
node->mutable_form_control().selection_direction =
text_selection_direction::none;
node->mutable_form_control().selection_explicitly_set = true;
mark_primary_selection_publication_dirty(node);
document.mark_dirty();
break;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,7 @@
std::mutex host_request_mutex;
std::deque<std::string> host_requests;
std::deque<std::unique_ptr<native_host_request>> typed_host_requests;
bool primary_selection_publication_dirty{};
std::deque<std::unique_ptr<native_download_request>> download_requests;
std::deque<std::unique_ptr<native_outbound_drag_request>>
outbound_drag_requests;
Expand Down
Loading
Loading