Skip to content

Commit 88797e2

Browse files
Add bounded PRIMARY selection publication (#657) (#658)
1 parent dabd143 commit 88797e2

14 files changed

Lines changed: 357 additions & 11 deletions

‎docs/validation/code-oss-web-api-ledger.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ Static matches establish source reachability only. Runtime claims require the ev
1818
| `broadcast-channel` | yes | blocked | linux-x64, macos-arm64, windows-x64 | WebScene#81 | 0 | Cross-realm ordering, clone, teardown, queue, and product evidence are incomplete. |
1919
| `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. |
2020
| `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. |
21-
| `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. |
21+
| `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. |
2222
| `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. |
2323
| `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. |
2424
| `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. |
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# Bounded PRIMARY selected-text publication source contract
2+
3+
WebScene issue #657 adds the provider half of automatic Linux PRIMARY
4+
publication. The exact baseline is WebScene
5+
`dabd1435c8e6415163067ad7ad5befb498a77126`; AppScene #275 is the dependent
6+
native consumer. The provider is a product-neutral native-host signal and has
7+
no X11, browser-process, Electron, CEF, WebView or VS Code dependency.
8+
9+
## Provider boundary
10+
11+
A settled nonempty selection in the currently focused, connected, writable
12+
`input` or `textarea` emits the existing `webscene_host_request_v1` with this
13+
exact shape:
14+
15+
- kind `WEBSCENE_HOST_REQUEST_CLIPBOARD_WRITE_V1`;
16+
- request ID and target node ID zero;
17+
- flags `WEBSCENE_HOST_REQUEST_CLIPBOARD_REPLACE_V1 |
18+
WEBSCENE_HOST_REQUEST_CLIPBOARD_PRIMARY_V1`;
19+
- content type `text/plain`, no URL, and valid UTF-8 selected bytes;
20+
- a maximum payload of 64 KiB, with oversize selections refused rather than
21+
truncated.
22+
23+
The zero request ID makes the update one-way. It creates no JavaScript API,
24+
Promise, completion binding or user-activation requirement. Ordinary Clipboard
25+
API and shortcut reads/writes retain their nonzero request IDs, existing flags,
26+
16-operation completion cap and CLIPBOARD meaning.
27+
28+
Password, readonly, disabled, inert, disconnected and unfocused controls are
29+
ineligible. A collapsed or empty selection, an invalid UTF-8 slice, a UTF-16
30+
boundary inside a surrogate pair, and an oversize slice emit nothing. These
31+
cases do not clear a previously published or pending PRIMARY value.
32+
33+
## Coalescing and lifecycle
34+
35+
Selection and value mutations mark only the active text control dirty. The
36+
runtime extracts its final selected slice after the current script, event and
37+
microtask work reaches a stable checkpoint. Extraction checks byte offsets and
38+
the 64 KiB cap before allocating the request payload.
39+
40+
The mutex-protected typed-host FIFO recognizes only the exact zero-ID PRIMARY
41+
publication shape. If one is already pending, the runtime replaces that lease
42+
in place with the newest eligible payload. Otherwise it appends one only while
43+
the aggregate 1,024-request queue has capacity. Publication therefore uses at
44+
most one pending slot and cannot evict or consume capacity reserved by an
45+
existing completion-bearing request. Saturation drops the update without
46+
changing clipboard state or exposing selected bytes in diagnostics.
47+
48+
Top-level navigation discards every publication still in WebScene's queue and
49+
clears its dirty observation. A queued child-frame publication carries a
50+
private frame-owner identity, outside the public ABI, so frame navigation or
51+
detach can remove it before retiring that realm. Engine teardown destroys the
52+
same bounded state. Once a host has taken a stable one-way lease, it represents
53+
the prior selection and may remain PRIMARY across document navigation,
54+
matching desktop selection ownership. Collapse, blur and teardown never send
55+
a clearing write.
56+
57+
## Authored contracts and remaining acceptance
58+
59+
`test_primary_selection_publication` covers the exact ABI, UTF-8 text,
60+
same-task and pending-request coalescing, retention on collapse, ineligible
61+
controls, detach, refusal above 64 KiB and admission at the cap. Existing
62+
clipboard contracts continue to cover ordinary Clipboard API flags, request
63+
IDs, completion and the PRIMARY middle-paste read path.
64+
65+
Unchanged Code OSS mirrors short Monaco selections into its writable hidden
66+
textarea, so those exact selections reach this provider without a source
67+
patch. Monaco can elide large or multipage screen-reader textarea content and
68+
does not represent all multicursor model selections there. Exact large,
69+
multipage and multicursor Monaco publication, readonly controls,
70+
contenteditable and document `Selection` remain outside #657.
71+
72+
Only `git diff --check` is run for this fast implementation slice. The authored
73+
native contract, compiler/build, browser-oracle comparison, 10,000-change CPU
74+
and allocation measurements, AppScene #275 integration, physical X11 clients,
75+
packaged unchanged Code OSS, VM and computer-use gates remain unexecuted.

‎experiments/WebScene.NativeEngine.Probe/native/webscene_native_engine.h‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2689,8 +2689,9 @@ WEBSCENE_API void webscene_file_grant_release_request_release_v2(
26892689
const webscene_file_grant_release_request_v2* request);
26902690

26912691
/* Typed native desktop request ABI. Request memory is immutable and remains
2692-
* valid until release. Byte payloads are capped at 16 MiB, strings are UTF-8,
2693-
* and at most 16 completion-bearing operations may be pending per document. */
2692+
* valid until release. Byte payloads are capped at 16 MiB, with one-way
2693+
* selected-text publication capped at 64 KiB. Strings are UTF-8, and at most
2694+
* 16 completion-bearing operations may be pending per document. */
26942695
enum {
26952696
WEBSCENE_HOST_REQUEST_OPEN_EXTERNAL_URL_V1 = 1,
26962697
WEBSCENE_HOST_REQUEST_CLIPBOARD_READ_V1 = 2,
@@ -2704,9 +2705,11 @@ enum {
27042705
};
27052706
enum {
27062707
WEBSCENE_HOST_REQUEST_CLIPBOARD_REPLACE_V1 = 1U << 0U,
2707-
/* Selects the platform's primary/selection clipboard for a text/plain
2708-
* READ request. Ordinary Clipboard API and shortcut requests leave this
2709-
* clear and continue to address the normal clipboard. */
2708+
/* Selects the platform's primary/selection clipboard. It is valid either
2709+
* alone for a completion-bearing text/plain READ request or together with
2710+
* REPLACE for the engine's zero-ID, one-way selected-text WRITE. Ordinary
2711+
* Clipboard API and shortcut requests leave this clear and continue to
2712+
* address the normal clipboard. */
27102713
WEBSCENE_HOST_REQUEST_CLIPBOARD_PRIMARY_V1 = 1U << 1U,
27112714
WEBSCENE_HOST_REQUEST_NAVIGATION_REPLACE_V1 = 1U << 0U,
27122715
WEBSCENE_HOST_REQUEST_EXTERNAL_NEW_CONTEXT_V1 = 1U << 0U,

‎experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime.cpp‎

Lines changed: 126 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6526,6 +6526,126 @@ struct v8_dom_runtime::implementation final {
65266526
return true;
65276527
}
65286528

6529+
bool enqueue_primary_selection_publication(
6530+
std::unique_ptr<native_host_request> request)
6531+
{
6532+
if (!request) return false;
6533+
auto notify = false;
6534+
{
6535+
std::lock_guard lock(host_request_mutex);
6536+
const auto pending = std::find_if(
6537+
typed_host_requests.begin(),
6538+
typed_host_requests.end(),
6539+
[](const auto& candidate) {
6540+
return candidate != nullptr
6541+
&& candidate->view.request_id == 0U
6542+
&& candidate->view.kind
6543+
== WEBSCENE_HOST_REQUEST_CLIPBOARD_WRITE_V1
6544+
&& candidate->view.flags
6545+
== (WEBSCENE_HOST_REQUEST_CLIPBOARD_REPLACE_V1
6546+
| WEBSCENE_HOST_REQUEST_CLIPBOARD_PRIMARY_V1);
6547+
});
6548+
if (pending != typed_host_requests.end()) {
6549+
*pending = std::move(request);
6550+
return true;
6551+
}
6552+
if (host_requests.size() + typed_host_requests.size()
6553+
+ reserved_host_request_count
6554+
>= maximum_host_request_count) return false;
6555+
typed_host_requests.push_back(std::move(request));
6556+
notify = true;
6557+
}
6558+
if (notify && host_request_available) host_request_available();
6559+
return true;
6560+
}
6561+
6562+
void discard_primary_selection_publication_for_frame(uint32_t frame_id)
6563+
{
6564+
if (frame_id == 0U) return;
6565+
std::lock_guard lock(host_request_mutex);
6566+
std::erase_if(typed_host_requests, [&](const auto& request) {
6567+
return request != nullptr
6568+
&& request->source_frame_owner_id == frame_id
6569+
&& request->view.request_id == 0U
6570+
&& request->view.kind
6571+
== WEBSCENE_HOST_REQUEST_CLIPBOARD_WRITE_V1
6572+
&& request->view.flags
6573+
== (WEBSCENE_HOST_REQUEST_CLIPBOARD_REPLACE_V1
6574+
| WEBSCENE_HOST_REQUEST_CLIPBOARD_PRIMARY_V1);
6575+
});
6576+
}
6577+
6578+
void mark_primary_selection_publication_dirty(
6579+
const dom_node* candidate) noexcept
6580+
{
6581+
if (candidate != nullptr && candidate == active_element)
6582+
primary_selection_publication_dirty = true;
6583+
}
6584+
6585+
void flush_primary_selection_publication()
6586+
{
6587+
if (!primary_selection_publication_dirty) return;
6588+
primary_selection_publication_dirty = false;
6589+
constexpr size_t maximum_primary_selection_bytes = 64U * 1024U;
6590+
auto* selected = active_element;
6591+
if (!is_writable_text_control(selected)
6592+
|| !is_connected(*selected)
6593+
|| !selected->form_control().input_focused
6594+
|| forms::input_type_is(*selected, "password")
6595+
|| focus_document_owner(selected) != focused_document_owner_id) {
6596+
return;
6597+
}
6598+
ensure_form_value(*selected);
6599+
const auto& control = selected->form_control();
6600+
const auto start = std::min(control.selection_start, control.value.size());
6601+
const auto end = std::min(control.selection_end, control.value.size());
6602+
if (start >= end || control.selection_start_utf16_suboffset != 0U
6603+
|| control.selection_end_utf16_suboffset != 0U
6604+
|| end - start > maximum_primary_selection_bytes) {
6605+
return;
6606+
}
6607+
const auto text = std::string_view(control.value).substr(start, end - start);
6608+
const auto valid_utf8 = [](std::string_view value) noexcept {
6609+
for (size_t offset = 0U; offset < value.size();) {
6610+
const auto first = static_cast<unsigned char>(value[offset]);
6611+
size_t length{};
6612+
uint32_t scalar{};
6613+
if (first <= 0x7fU) { length = 1U; scalar = first; }
6614+
else if (first >= 0xc2U && first <= 0xdfU) {
6615+
length = 2U; scalar = first & 0x1fU;
6616+
} else if (first >= 0xe0U && first <= 0xefU) {
6617+
length = 3U; scalar = first & 0x0fU;
6618+
} else if (first >= 0xf0U && first <= 0xf4U) {
6619+
length = 4U; scalar = first & 0x07U;
6620+
} else return false;
6621+
if (offset + length > value.size()) return false;
6622+
for (size_t index = 1U; index < length; ++index) {
6623+
const auto continuation =
6624+
static_cast<unsigned char>(value[offset + index]);
6625+
if ((continuation & 0xc0U) != 0x80U) return false;
6626+
scalar = (scalar << 6U) | (continuation & 0x3fU);
6627+
}
6628+
if ((length == 2U && scalar < 0x80U)
6629+
|| (length == 3U && scalar < 0x800U)
6630+
|| (length == 4U && scalar < 0x10000U)
6631+
|| (scalar >= 0xd800U && scalar <= 0xdfffU)
6632+
|| scalar > 0x10ffffU) return false;
6633+
offset += length;
6634+
}
6635+
return true;
6636+
};
6637+
if (!valid_utf8(text)) return;
6638+
auto request = std::make_unique<native_host_request>();
6639+
request->view.kind = WEBSCENE_HOST_REQUEST_CLIPBOARD_WRITE_V1;
6640+
request->view.flags = WEBSCENE_HOST_REQUEST_CLIPBOARD_REPLACE_V1
6641+
| WEBSCENE_HOST_REQUEST_CLIPBOARD_PRIMARY_V1;
6642+
request->source_frame_owner_id = focus_document_owner(selected);
6643+
request->content_type = "text/plain";
6644+
request->bytes.assign(text.begin(), text.end());
6645+
static_cast<void>(
6646+
enqueue_primary_selection_publication(std::move(request)));
6647+
}
6648+
65296649
bool enqueue_reserved_typed_host_request(
65306650
std::unique_ptr<native_host_request> request)
65316651
{
@@ -7481,14 +7601,16 @@ uint64_t v8_dom_runtime::last_resize_observers_nanoseconds() const noexcept
74817601

74827602
bool v8_dom_runtime::dispatch_input(const webscene_input_event& event, bool defer_cursor_update)
74837603
{
7484-
return impl_->dispatch_input(event, defer_cursor_update)
7485-
&& impl_->promote_pending_promise_error();
7604+
const auto dispatched = impl_->dispatch_input(event, defer_cursor_update);
7605+
impl_->flush_primary_selection_publication();
7606+
return dispatched && impl_->promote_pending_promise_error();
74867607
}
74877608

74887609
bool v8_dom_runtime::dispatch_drag(native_drag_event& event)
74897610
{
7490-
return impl_->dispatch_drag(event)
7491-
&& impl_->promote_pending_promise_error();
7611+
const auto dispatched = impl_->dispatch_drag(event);
7612+
impl_->flush_primary_selection_publication();
7613+
return dispatched && impl_->promote_pending_promise_error();
74927614
}
74937615

74947616
void v8_dom_runtime::refresh_pointer_cursor_after_layout()

‎experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ struct native_host_completion {
5555
};
5656
struct native_host_request {
5757
webscene_host_request_v1 view{};
58+
uint32_t source_frame_owner_id{};
5859
std::string content_type;
5960
std::vector<uint8_t> bytes;
6061
std::string url;

‎experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_css_cascade.inc‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2409,6 +2409,8 @@
24092409
std::optional<bool> user_validity_interacted = std::nullopt,
24102410
std::optional<bool> value_changed_by_user = std::nullopt)
24112411
{
2412+
if (previous_value != current_value)
2413+
mark_primary_selection_publication_dirty(&subject);
24122414
document.mark_dirty();
24132415
const auto empty_state_changed=previous_value.empty()!=current_value.empty();
24142416
const auto previous_range=forms::range_state(subject,previous_value);

‎experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_dom_core.inc‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2825,6 +2825,7 @@
28252825
|| (target != nullptr && subtree_contains(frame, target));
28262826
});
28272827
}
2828+
discard_primary_selection_publication_for_frame(frame.id);
28282829
{
28292830
std::lock_guard lock(document_generation_mutex);
28302831
++frame_navigation_generations.try_emplace(frame.id, 0U)
@@ -2894,6 +2895,7 @@
28942895

28952896
void retire_iframe_navigation(uint32_t frame_id)
28962897
{
2898+
discard_primary_selection_publication_for_frame(frame_id);
28972899
{
28982900
std::lock_guard lock(document_generation_mutex);
28992901
frame_navigation_generations.erase(frame_id);
@@ -5404,6 +5406,7 @@
54045406
control.selection_end_utf16_suboffset = end_offset.utf16_suboffset;
54055407
control.selection_direction = text_selection_direction::none;
54065408
control.selection_explicitly_set = true;
5409+
self->mark_primary_selection_publication_dirty(node);
54075410
if(self->is_connected(*node)) {
54085411
self->recascade_form_value_selector_transition(
54095412
*node,previous_value,control.value,std::nullopt,false);

‎experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_dom_properties.inc‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3883,6 +3883,7 @@
38833883
v8::Local<v8::Value> value,
38843884
const v8::PropertyCallbackInfo<void>& info)
38853885
{
3886+
auto* self = current(info.GetIsolate());
38863887
auto* node = unwrap_node(info.Holder());
38873888
if (node == nullptr) return;
38883889
const auto index = value->Int32Value(info.GetIsolate()->GetCurrentContext()).FromMaybe(0);
@@ -3908,6 +3909,7 @@
39083909
node->mutable_form_control().selection_end_utf16_suboffset})) {
39093910
node->mutable_form_control().selection_direction = text_selection_direction::none;
39103911
}
3912+
self->mark_primary_selection_publication_dirty(node);
39113913
}
39123914

39133915
static void get_selection_end(
@@ -3930,6 +3932,7 @@
39303932
v8::Local<v8::Value> value,
39313933
const v8::PropertyCallbackInfo<void>& info)
39323934
{
3935+
auto* self = current(info.GetIsolate());
39333936
auto* node = unwrap_node(info.Holder());
39343937
if (node == nullptr) return;
39353938
const auto index = value->Int32Value(info.GetIsolate()->GetCurrentContext()).FromMaybe(0);
@@ -3955,6 +3958,7 @@
39553958
node->mutable_form_control().selection_end_utf16_suboffset})) {
39563959
node->mutable_form_control().selection_direction = text_selection_direction::none;
39573960
}
3961+
self->mark_primary_selection_publication_dirty(node);
39583962
}
39593963

39603964
static void get_selection_direction(
@@ -3979,6 +3983,7 @@
39793983
v8::Local<v8::Value> value,
39803984
const v8::PropertyCallbackInfo<void>& info)
39813985
{
3986+
auto* self = current(info.GetIsolate());
39823987
auto* node = unwrap_node(info.Holder());
39833988
if (node == nullptr) return;
39843989
const auto direction = to_utf8(info.GetIsolate(), value);
@@ -3995,6 +4000,7 @@
39954000
node->mutable_form_control().selection_end_utf16_suboffset})) {
39964001
node->mutable_form_control().selection_direction = text_selection_direction::none;
39974002
}
4003+
self->mark_primary_selection_publication_dirty(node);
39984004
}
39994005

40004006
static void get_boolean_attribute(

‎experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_navigation.inc‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,11 @@
143143
fullscreen_element = nullptr;
144144
document_fullscreen = false;
145145
{ std::lock_guard lock(host_request_mutex); host_requests.clear(); }
146-
{ std::lock_guard lock(host_request_mutex); typed_host_requests.clear(); }
146+
{
147+
std::lock_guard lock(host_request_mutex);
148+
typed_host_requests.clear();
149+
primary_selection_publication_dirty = false;
150+
}
147151
{ std::lock_guard lock(host_request_mutex); download_requests.clear(); }
148152
{ std::lock_guard lock(host_request_mutex); outbound_drag_requests.clear(); }
149153
object_url_file_data.clear();

‎experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_semantics.inc‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1110,6 +1110,7 @@
11101110
node->mutable_form_control().selection_direction =
11111111
text_selection_direction::none;
11121112
node->mutable_form_control().selection_explicitly_set = true;
1113+
mark_primary_selection_publication_dirty(node);
11131114
document.mark_dirty();
11141115
break;
11151116
}

0 commit comments

Comments
 (0)