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
7 changes: 7 additions & 0 deletions scripts/check-core-boundaries.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3081,6 +3081,10 @@ test('third-party capability profiles reject ambient feature unions and unreview
uses_default_features: false,
features: ['vendored-libgit2'],
}]),
packageAt('openbitfun-webdriver', 'src/crates/adapters/webdriver/Cargo.toml', [{
name: 'image', kind: null, optional: false, uses_default_features: false,
features: ['jpeg', 'png'],
}]),
];

assert.deepEqual(findThirdPartyCapabilityFeatureViolations(validPackages), []);
Expand All @@ -3091,6 +3095,7 @@ test('third-party capability profiles reject ambient feature unions and unreview
mutatedPackages[2].dependencies[0].features.push('rustls-tls-native-roots');
mutatedPackages[3].dependencies[0].features.push('https');
mutatedPackages[3].dependencies[0].rename = 'private-git2';
mutatedPackages[4].dependencies[0].features = ['png', 'webp'];
mutatedPackages.push(packageAt('future-image-owner', 'src/apps/future/Cargo.toml', [{
name: 'image',
kind: null,
Expand All @@ -3107,6 +3112,8 @@ test('third-party capability profiles reject ambient feature unions and unreview
assert.match(messages, /openbitfun-core Tokio Tungstenite dependency has unexpected features: rustls-tls-native-roots/);
assert.match(messages, /openbitfun-services-core Git2 dependency has unexpected features: https/);
assert.match(messages, /openbitfun-services-core Git2 dependency does not match its reviewed owner shape/);
assert.match(messages, /openbitfun-webdriver Image dependency missing features: jpeg/);
assert.match(messages, /openbitfun-webdriver Image dependency has unexpected features: webp/);
assert.match(messages, /future-image-owner Image dependency is missing a reviewed owner profile/);
});

Expand Down
3 changes: 2 additions & 1 deletion scripts/core-boundaries/cargo-dependency-boundaries.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -574,7 +574,8 @@ const THIRD_PARTY_CAPABILITY_PROFILES = new Map([
['openbitfun-skin-market-service', dependencyProfile(['gif', 'jpeg', 'png', 'webp'], {
useDefaultFeatures: false,
})],
['openbitfun-webdriver', dependencyProfile(['png'], {
// The embedded bridge captures PNG and encodes JPEG previews itself.
['openbitfun-webdriver', dependencyProfile(['jpeg', 'png'], {
useDefaultFeatures: false,
})],
]),
Expand Down
4 changes: 2 additions & 2 deletions src/crates/adapters/webdriver/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ tokio = { workspace = true, features = ["net", "rt", "sync", "time"] }
serde = { workspace = true }
serde_json = { workspace = true }
log = { workspace = true }
tauri = { workspace = true }
tauri = { workspace = true, features = ["unstable"] }
uuid = { workspace = true }
base64 = { workspace = true }
image = { workspace = true, features = ["png"] }
image = { workspace = true, features = ["jpeg", "png"] }

[target.'cfg(target_os = "macos")'.dependencies]
block2 = { workspace = true }
Expand Down
4 changes: 2 additions & 2 deletions src/crates/adapters/webdriver/src/runtime/api/element.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,11 @@ pub(crate) fn clear() -> &'static str {
}

pub(crate) fn send_keys() -> &'static str {
"(id, text) => { const el = window.__openbitfunWd.getElement(id); if (!el) { throw new Error('Element not found'); } window.__openbitfunWd.insertText(el, text); return null; }"
"(id, text) => { const el = window.__openbitfunWd.getElement(id); if (!el) { throw new Error('Element not found'); } if ('value' in el || el.isContentEditable) { window.__openbitfunWd.scrollElementIntoView(el); window.__openbitfunWd.focusWithoutScroll(el); } window.__openbitfunWd.insertText(el, text); return null; }"
}

pub(crate) fn screenshot_metadata() -> &'static str {
"(id) => { const el = window.__openbitfunWd.getElement(id); if (!el || !el.isConnected) { throw new Error('stale element reference'); } el.scrollIntoView({ block: 'center', inline: 'center' }); const rect = el.getBoundingClientRect(); return { x: rect.x, y: rect.y, width: rect.width, height: rect.height, devicePixelRatio: window.devicePixelRatio || 1 }; }"
"(id) => { const el = window.__openbitfunWd.getElement(id); if (!el || !el.isConnected) { throw new Error('stale element reference'); } window.__openbitfunWd.scrollElementIntoView(el); const rect = el.getBoundingClientRect(); return { x: rect.x, y: rect.y, width: rect.width, height: rect.height, devicePixelRatio: window.devicePixelRatio || 1 }; }"
}

pub(crate) fn get_shadow_root() -> &'static str {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ pub(super) fn script() -> &'static str {
validateFrameElement,
getShadowRoot,
isDisplayed,
scrollElementIntoView,
focusWithoutScroll,
clearElement,
insertText,
setElementText,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
pub(super) fn script() -> &'static str {
r####"
// Reveal an offscreen target with minimal movement. Centering also scrolls
// overflow:hidden ancestors, which can displace the surrounding app chrome.
const scrollElementIntoView = (element) => {
element.scrollIntoView({ behavior: "instant", block: "nearest", inline: "nearest" });
};

// Positioning belongs to the caller; focus must not move it again, especially
// after pointer coordinates have already been resolved.
const focusWithoutScroll = (element) => {
if (typeof element.focus === "function") {
element.focus({ preventScroll: true });
}
};

// A large target can remain only partly visible after nearest scrolling.
// Use its visible center rather than a point outside the viewport.
const getInViewCenter = (element) => {
const rect = element.getClientRects()[0] || element.getBoundingClientRect();
const ownerWindow = element.ownerDocument.defaultView || window;
return {
x: (Math.max(0, rect.left) + Math.min(ownerWindow.innerWidth, rect.right)) / 2,
y: (Math.max(0, rect.top) + Math.min(ownerWindow.innerHeight, rect.bottom)) / 2
};
};
"####
}
6 changes: 4 additions & 2 deletions src/crates/adapters/webdriver/src/runtime/script/core/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ mod alert;
mod context;
mod cookie;
mod execution;
mod interaction;
mod locator;
mod runtime;
mod shadow;
Expand All @@ -10,13 +11,14 @@ mod visibility;

pub(super) fn head() -> String {
format!(
"{runtime}{store}{context}{locator}{shadow}{visibility}",
"{runtime}{store}{context}{locator}{shadow}{visibility}{interaction}",
runtime = runtime::script(),
store = store::script(),
context = context::script(),
locator = locator::script(),
shadow = shadow::script(),
visibility = visibility::script()
visibility = visibility::script(),
interaction = interaction::script()
)
}

Expand Down
8 changes: 5 additions & 3 deletions src/crates/adapters/webdriver/src/runtime/script/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,11 @@ pub(super) fn script() -> &'static str {
if (!element) {
return;
}
if ("value" in element || element.isContentEditable) {
scrollElementIntoView(element);
focusWithoutScroll(element);
}
if ("value" in element) {
element.focus();
if (!dispatchBeforeInputEvent(element, "deleteContentBackward", null)) {
return;
}
Expand All @@ -82,7 +85,6 @@ pub(super) fn script() -> &'static str {
return;
}
if (element.isContentEditable) {
element.focus();
element.textContent = "";
emitInputEvents(element, "deleteContentBackward", null);
}
Expand Down Expand Up @@ -112,7 +114,7 @@ pub(super) fn script() -> &'static str {
return;
}
const selection = ownerWindow.getSelection();
element.focus();
focusWithoutScroll(element);
if (selection && selection.rangeCount > 0) {
selection.deleteFromDocument();
selection.getRangeAt(0).insertNode(element.ownerDocument.createTextNode(text));
Expand Down
21 changes: 9 additions & 12 deletions src/crates/adapters/webdriver/src/runtime/script/pointer/mouse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,16 @@ pub(super) fn script() -> &'static str {
};
}

if (origin && typeof origin === "object" && typeof origin[ELEMENT_KEY] === "string") {
const element = getElement(origin[ELEMENT_KEY]);
// Script arguments have already deserialized wire element references.
if (isElementLike(origin) || (origin && typeof origin === "object" && typeof origin[ELEMENT_KEY] === "string")) {
const element = isElementLike(origin) ? origin : getElement(origin[ELEMENT_KEY]);
if (!element) {
throw new Error("Element not found");
}
const rect = element.getBoundingClientRect();
const { x, y } = getInViewCenter(element);
return {
x: rect.left + rect.width / 2 + (Number(action?.x) || 0),
y: rect.top + rect.height / 2 + (Number(action?.y) || 0),
x: x + (Number(action?.x) || 0),
y: y + (Number(action?.y) || 0),
target: element
};
}
Expand Down Expand Up @@ -108,13 +109,9 @@ pub(super) fn script() -> &'static str {
if (!element) {
throw new Error("Element not found");
}
element.scrollIntoView({ block: "center", inline: "center" });
if (typeof element.focus === "function") {
element.focus();
}
const rect = element.getBoundingClientRect();
const x = rect.left + rect.width / 2;
const y = rect.top + rect.height / 2;
scrollElementIntoView(element);
focusWithoutScroll(element);
const { x, y } = getInViewCenter(element);
updatePointerTarget(getFrameContext(), x, y, element);
const runtime = ensureRuntimeState();
const buttonMask = pointerButtonMask(button);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,7 @@ pub(super) fn script() -> &'static str {
if (action.duration) {
await sleep(action.duration);
}
if (typeof target.focus === "function") {
target.focus();
}
focusWithoutScroll(target);
runtime.pointer.buttons |= buttonMask;
dispatchMouseEvent(target, "mousedown", runtime.pointer.x, runtime.pointer.y, button, runtime.pointer.buttons, frameContext);
continue;
Expand Down
35 changes: 27 additions & 8 deletions src/crates/adapters/webdriver/src/runtime/script/pointer/wheel.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,41 @@
pub(super) fn script() -> &'static str {
r####"
const findScrollableTarget = (target, doc) => {
const findScrollableTarget = (target, doc, deltaX, deltaY) => {
let current = target;
while (current && current !== doc.body && current !== doc.documentElement) {
if (
(current.scrollHeight > current.clientHeight || current.scrollWidth > current.clientWidth) &&
current instanceof Element
) {
return current;
if (isElementLike(current)) {
const style = getOwnerWindow(current).getComputedStyle(current);
const wheelScrollable = (overflow) => ["auto", "scroll", "overlay"].includes(overflow);
// overflow:hidden can be scrolled by script, but never by a user wheel.
if (
(deltaX && current.scrollWidth > current.clientWidth && wheelScrollable(style.overflowX)) ||
(deltaY && current.scrollHeight > current.clientHeight && wheelScrollable(style.overflowY))
) {
return current;
}
}
current = current.parentElement;
}
return doc.scrollingElement || doc.documentElement || doc.body;
const viewport = doc.scrollingElement || doc.documentElement || doc.body;
if (!viewport) {
return null;
}
const ownerWindow = doc.defaultView || window;
const style = ownerWindow.getComputedStyle(viewport);
const bodyStyle = doc.body ? ownerWindow.getComputedStyle(doc.body) : style;
// Root overflow:visible propagates the body's overflow to the viewport.
const overflowX = style.overflowX === "visible" ? bodyStyle.overflowX : style.overflowX;
const overflowY = style.overflowY === "visible" ? bodyStyle.overflowY : style.overflowY;
const wheelScrollable = (overflow) => overflow !== "hidden" && overflow !== "clip";
return (
(deltaX && viewport.scrollWidth > viewport.clientWidth && wheelScrollable(overflowX)) ||
(deltaY && viewport.scrollHeight > viewport.clientHeight && wheelScrollable(overflowY))
) ? viewport : null;
};

const applyWheelScroll = (target, deltaX, deltaY, frameContext = currentFrameContext) => {
const doc = getCurrentDocument(frameContext);
const scrollTarget = findScrollableTarget(target, doc);
const scrollTarget = findScrollableTarget(target, doc, deltaX, deltaY);
if (!scrollTarget) {
return;
}
Expand Down
12 changes: 7 additions & 5 deletions src/crates/assembly/core/builtin_skills/commit-push-pr/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ Invoking this skill is authorization for the requested commit/push/PR flow. It i

## OpenBitFun attribution contract

Unless the user explicitly opts out, always use the PR footer below exactly once. When this flow creates one or more real commits, also use the commit trailer exactly once in each new commit:
Unless the user explicitly opts out, use the PR footer below exactly once. Commit co-author attribution follows the execution host's setting, resolved when this built-in skill is loaded:

- Commit trailer for each new commit, which GitHub uses for contributor/co-author attribution:
{{OPENBITFUN_GIT_COMMIT_COAUTHOR_POLICY}}

- Commit trailer when co-author attribution is enabled, which GitHub uses for contributor/co-author attribution:

```text
Co-authored-by: OpenBitFun <318544290+bitfun-ai@users.noreply.github.com>
Expand All @@ -37,7 +39,7 @@ Do not duplicate either attribution. Preserve an equivalent existing trailer/foo
2. Confirm scope; stage only intended paths.
3. Create `openbitfun/<description>` only when currently on the base branch.
4. Run the repository's focused verification.
5. Commit real changes with the exact OpenBitFun trailer once.
5. Commit real changes, following the resolved commit co-author setting above.
6. Push normally; never force-push automatically.
7. Update the exact matching PR or create a draft PR.
8. End the PR body with the exact OpenBitFun link footer once.
Expand Down Expand Up @@ -104,11 +106,11 @@ If verification remains failed, blocked, or was explicitly declined by the user:

- Derive the subject from the actual diff and repository conventions; do not create a generic `prepare PR` commit.
- Keep the real human author/committer identity. The OpenBitFun line is a co-author trailer, not a replacement author.
- Add a blank line before the exact OpenBitFun trailer.
- Only when commit co-author attribution is enabled and the user has not opted out, add a blank line before the exact OpenBitFun trailer. When disabled, do not add this trailer to new commits.
- If the intended changes are already committed, do not create an empty attribution commit.
- Do not squash multiple existing commits or amend published commits without explicit approval.

Example:
Example when commit co-author attribution is enabled:

```text
fix: preserve session state during reconnect
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -648,10 +648,12 @@ impl<'a> BrowserActions<'a> {
format!(
r#"(function(){{
{js}
el.scrollIntoView({{ block: 'center', inline: 'center', behavior: 'instant' }});
const rect = el.getBoundingClientRect();
const localX = rect.x + rect.width / 2;
const localY = rect.y + rect.height / 2;
// Reveal the target without repositioning already visible content.
el.scrollIntoView({{ block: 'nearest', inline: 'nearest', behavior: 'instant' }});
const rect = el.getClientRects()[0] || el.getBoundingClientRect();
const ownerWindow = el.ownerDocument.defaultView || window;
const localX = (Math.max(0, rect.left) + Math.min(ownerWindow.innerWidth, rect.right)) / 2;
const localY = (Math.max(0, rect.top) + Math.min(ownerWindow.innerHeight, rect.bottom)) / 2;
let x = localX;
let y = localY;
try {{
Expand Down Expand Up @@ -721,7 +723,7 @@ impl<'a> BrowserActions<'a> {
pub async fn fill(&self, selector: &str, value: &str) -> OpenBitFunResult<Value> {
let js = Self::resolve_element_js(selector);
let focus_js = format!(
r#"(function(){{ {} el.focus(); el.value = ''; el.dispatchEvent(new Event('input', {{ bubbles: true }})); return true; }})()"#,
r#"(function(){{ {} el.scrollIntoView({{ block: 'nearest', inline: 'nearest', behavior: 'instant' }}); el.focus({{ preventScroll: true }}); el.value = ''; el.dispatchEvent(new Event('input', {{ bubbles: true }})); return true; }})()"#,
js
);
self.evaluate(&focus_js).await?;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub mod mode_overrides;
pub mod policy;
pub mod registry;
pub mod resolver;
mod runtime_settings;
#[cfg(feature = "file-watch")]
mod source_cache;
pub mod types;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use super::mode_overrides::{
load_globally_disabled_project_skills, load_globally_disabled_user_skills,
load_user_mode_skill_overrides, SkillPolicyWorkspace, UserModeSkillOverrides,
};
use super::runtime_settings::apply_builtin_runtime_settings;
#[cfg(feature = "file-watch")]
use super::source_cache::{LocalSkillWatchMonitor, LocalSkillWatchRoot, VersionedSnapshotCache};
use super::types::{
Expand Down Expand Up @@ -1996,6 +1997,7 @@ impl SkillRegistry {
data.source_id = info.source_id;
data.source_label = info.source_label;
data.dir_name = info.dir_name;
apply_builtin_runtime_settings(&mut data).await?;
Ok(data)
}

Expand Down Expand Up @@ -2040,6 +2042,7 @@ impl SkillRegistry {
data.source_id = info.source_id;
data.source_label = info.source_label;
data.dir_name = info.dir_name;
apply_builtin_runtime_settings(&mut data).await?;
Ok(data)
}

Expand Down Expand Up @@ -2075,6 +2078,7 @@ impl SkillRegistry {
data.source_id = info.source_id;
data.source_label = info.source_label;
data.dir_name = info.dir_name;
apply_builtin_runtime_settings(&mut data).await?;
Ok(data)
}

Expand Down Expand Up @@ -2118,6 +2122,7 @@ impl SkillRegistry {
data.source_id = info.source_id;
data.source_label = info.source_label;
data.dir_name = info.dir_name;
apply_builtin_runtime_settings(&mut data).await?;
Ok(data)
}

Expand Down
Loading
Loading