mirror of
https://github.com/browseros-ai/BrowserOS.git
synced 2026-05-14 08:03:58 +00:00
Compare commits
9 Commits
fix/fix-al
...
fix/test-g
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b4aaa75b6 | ||
|
|
25320458b9 | ||
|
|
6f9da20fe1 | ||
|
|
f521ebc8dc | ||
|
|
f2a41fdc08 | ||
|
|
b9dc875203 | ||
|
|
20067d90c7 | ||
|
|
67f18bc868 | ||
|
|
2f86020b30 |
121
.github/workflows/test.yml
vendored
121
.github/workflows/test.yml
vendored
@@ -136,3 +136,124 @@ jobs:
|
||||
echo "See the uploaded \`junit-${{ matrix.suite }}\` artifact for details." >> "$GITHUB_STEP_SUMMARY"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
comment:
|
||||
name: PR test summary
|
||||
needs: test
|
||||
if: >-
|
||||
always()
|
||||
&& github.event_name == 'pull_request'
|
||||
&& github.event.pull_request.head.repo.full_name == github.repository
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
actions: read
|
||||
steps:
|
||||
- name: Download JUnit artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
continue-on-error: true
|
||||
with:
|
||||
path: junit
|
||||
pattern: junit-*
|
||||
|
||||
- name: Build comment body
|
||||
run: |
|
||||
python3 <<'PY'
|
||||
import glob, os, xml.etree.ElementTree as ET
|
||||
|
||||
run_url = f"{os.environ['GITHUB_SERVER_URL']}/{os.environ['GITHUB_REPOSITORY']}/actions/runs/{os.environ['GITHUB_RUN_ID']}"
|
||||
marker = "<!-- browseros-agent-tests-summary -->"
|
||||
|
||||
suites = []
|
||||
failed_cases = []
|
||||
total_tests = total_failed = total_skipped = 0
|
||||
|
||||
for xml_path in sorted(glob.glob("junit/junit-*/*.xml")):
|
||||
suite_name = os.path.basename(os.path.dirname(xml_path)).removeprefix("junit-")
|
||||
try:
|
||||
root = ET.parse(xml_path).getroot()
|
||||
except ET.ParseError:
|
||||
suites.append({"name": suite_name, "passed": 0, "failed": 1, "skipped": 0, "total": 1})
|
||||
total_tests += 1
|
||||
total_failed += 1
|
||||
failed_cases.append((suite_name, "(could not parse junit XML)"))
|
||||
continue
|
||||
|
||||
testsuites = root.findall("testsuite") if root.tag == "testsuites" else [root]
|
||||
s_tests = s_fail = s_err = s_skip = 0
|
||||
for ts in testsuites:
|
||||
s_tests += int(ts.get("tests") or 0)
|
||||
s_fail += int(ts.get("failures") or 0)
|
||||
s_err += int(ts.get("errors") or 0)
|
||||
s_skip += int(ts.get("skipped") or 0)
|
||||
for tc in ts.iter("testcase"):
|
||||
if tc.find("failure") is not None or tc.find("error") is not None:
|
||||
cls = tc.get("classname") or ""
|
||||
name = tc.get("name") or "(unnamed)"
|
||||
label = f"{cls} > {name}" if cls else name
|
||||
failed_cases.append((suite_name, label))
|
||||
|
||||
s_failed = s_fail + s_err
|
||||
s_passed = max(s_tests - s_failed - s_skip, 0)
|
||||
suites.append({"name": suite_name, "passed": s_passed, "failed": s_failed, "skipped": s_skip, "total": s_tests})
|
||||
total_tests += s_tests
|
||||
total_failed += s_failed
|
||||
total_skipped += s_skip
|
||||
|
||||
total_passed = max(total_tests - total_failed - total_skipped, 0)
|
||||
|
||||
if total_tests == 0:
|
||||
header = "## :warning: No test results were produced"
|
||||
elif total_failed == 0:
|
||||
header = f"## :white_check_mark: Tests passed — {total_passed}/{total_tests}"
|
||||
else:
|
||||
header = f"## :x: Tests failed — {total_failed}/{total_tests} failed"
|
||||
|
||||
lines = [marker, header, ""]
|
||||
if suites:
|
||||
lines.append("| Suite | Passed | Failed | Skipped |")
|
||||
lines.append("|-------|--------|--------|---------|")
|
||||
for s in suites:
|
||||
icon = ":white_check_mark:" if s["failed"] == 0 and s["total"] > 0 else ":warning:" if s["total"] == 0 else ":x:"
|
||||
lines.append(f"| {icon} `{s['name']}` | {s['passed']}/{s['total']} | {s['failed']} | {s['skipped']} |")
|
||||
|
||||
if failed_cases:
|
||||
lines += ["", "<details open>", "<summary><b>Failed tests</b></summary>", ""]
|
||||
for suite_name, label in failed_cases[:50]:
|
||||
lines.append(f"- **{suite_name}** — `{label}`")
|
||||
if len(failed_cases) > 50:
|
||||
lines.append(f"- …and {len(failed_cases) - 50} more")
|
||||
lines += ["", "</details>"]
|
||||
|
||||
lines += ["", f"[View workflow run]({run_url})"]
|
||||
|
||||
with open("comment.md", "w") as f:
|
||||
f.write("\n".join(lines) + "\n")
|
||||
PY
|
||||
|
||||
- name: Upsert sticky PR comment
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const body = fs.readFileSync('comment.md', 'utf8');
|
||||
const marker = '<!-- browseros-agent-tests-summary -->';
|
||||
const { owner, repo } = context.repo;
|
||||
const issue_number = context.payload.pull_request.number;
|
||||
|
||||
const triggerSha = context.payload.pull_request.head.sha;
|
||||
const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: issue_number });
|
||||
if (pr.head.sha !== triggerSha) {
|
||||
core.info(`PR head has moved (${pr.head.sha} vs ${triggerSha}) — skipping stale comment.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const comments = await github.paginate(github.rest.issues.listComments, {
|
||||
owner, repo, issue_number, per_page: 100,
|
||||
});
|
||||
const existing = comments.find(c => c.body && c.body.includes(marker));
|
||||
if (existing) {
|
||||
await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body });
|
||||
} else {
|
||||
await github.rest.issues.createComment({ owner, repo, issue_number, body });
|
||||
}
|
||||
|
||||
@@ -15,6 +15,9 @@ VITE_PUBLIC_SENTRY_DSN=
|
||||
# BrowserOS API URL
|
||||
VITE_PUBLIC_BROWSEROS_API=https://api.browseros.com
|
||||
|
||||
# Feature flags
|
||||
VITE_ALPHA_FEATURES=false
|
||||
|
||||
# GraphQL Schema Path (optional — falls back to schema/schema.graphql)
|
||||
GRAPHQL_SCHEMA_PATH=
|
||||
|
||||
|
||||
@@ -80,9 +80,19 @@ const primarySettingsSections: NavSection[] = [
|
||||
icon: Palette,
|
||||
feature: Feature.CUSTOMIZATION_SUPPORT,
|
||||
},
|
||||
{ name: 'Tool Approvals', to: '/settings/approvals', icon: ShieldCheck },
|
||||
{
|
||||
name: 'Tool Approvals',
|
||||
to: '/settings/approvals',
|
||||
icon: ShieldCheck,
|
||||
feature: Feature.ALPHA_FEATURES_SUPPORT,
|
||||
},
|
||||
{ name: 'BrowserOS as MCP', to: '/settings/mcp', icon: Server },
|
||||
{ name: 'ACL Rules', to: '/settings/acl', icon: ShieldAlert },
|
||||
{
|
||||
name: 'ACL Rules',
|
||||
to: '/settings/acl',
|
||||
icon: ShieldAlert,
|
||||
feature: Feature.ALPHA_FEATURES_SUPPORT,
|
||||
},
|
||||
{
|
||||
name: 'Usage & Billing',
|
||||
to: '/settings/usage',
|
||||
|
||||
@@ -41,7 +41,12 @@ const primaryNavItems: NavItem[] = [
|
||||
feature: Feature.MANAGED_MCP_SUPPORT,
|
||||
},
|
||||
{ name: 'Scheduled Tasks', to: '/scheduled', icon: CalendarClock },
|
||||
{ name: 'Agents', to: '/agents', icon: Cpu },
|
||||
{
|
||||
name: 'Agents',
|
||||
to: '/agents',
|
||||
icon: Cpu,
|
||||
feature: Feature.ALPHA_FEATURES_SUPPORT,
|
||||
},
|
||||
{
|
||||
name: 'Skills',
|
||||
to: '/home/skills',
|
||||
@@ -60,7 +65,12 @@ const primaryNavItems: NavItem[] = [
|
||||
icon: Sparkles,
|
||||
feature: Feature.SOUL_SUPPORT,
|
||||
},
|
||||
{ name: 'Governance', to: '/admin', icon: Shield },
|
||||
{
|
||||
name: 'Governance',
|
||||
to: '/admin',
|
||||
icon: Shield,
|
||||
feature: Feature.ALPHA_FEATURES_SUPPORT,
|
||||
},
|
||||
{ name: 'Settings', to: '/settings/ai', icon: Settings },
|
||||
]
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import type { FC } from 'react'
|
||||
import { HashRouter, Navigate, Route, Routes, useParams } from 'react-router'
|
||||
import { Feature } from '@/lib/browseros/capabilities'
|
||||
import { useCapabilities } from '@/lib/browseros/useCapabilities'
|
||||
import { NewTab } from '../newtab/index/NewTab'
|
||||
import { NewTabChat } from '../newtab/index/NewTabChat'
|
||||
import { NewTabLayout } from '../newtab/layout/NewTabLayout'
|
||||
import { Personalize } from '../newtab/personalize/Personalize'
|
||||
@@ -65,6 +68,8 @@ const OptionsRedirect: FC = () => {
|
||||
|
||||
export const App: FC = () => {
|
||||
const surveyParams = getSurveyParams()
|
||||
const { supports } = useCapabilities()
|
||||
const alphaEnabled = supports(Feature.ALPHA_FEATURES_SUPPORT)
|
||||
|
||||
return (
|
||||
<HashRouter>
|
||||
@@ -80,16 +85,25 @@ export const App: FC = () => {
|
||||
{/* Main app with sidebar */}
|
||||
<Route element={<SidebarLayout />}>
|
||||
{/* Home routes */}
|
||||
<Route path="home" element={<NewTabLayout />}>
|
||||
<Route element={<AgentCommandLayout />}>
|
||||
<Route index element={<AgentCommandHome />} />
|
||||
<Route
|
||||
path="agents/:agentId"
|
||||
element={<AgentCommandConversation />}
|
||||
/>
|
||||
</Route>
|
||||
<Route path="chat" element={<NewTabChat />} />
|
||||
<Route path="personalize" element={<Personalize />} />
|
||||
<Route
|
||||
path="home"
|
||||
element={<NewTabLayout useChatSessionOnHome={!alphaEnabled} />}
|
||||
>
|
||||
{alphaEnabled ? (
|
||||
<>
|
||||
<Route element={<AgentCommandLayout />}>
|
||||
<Route index element={<AgentCommandHome />} />
|
||||
<Route
|
||||
path="agents/:agentId"
|
||||
element={<AgentCommandConversation />}
|
||||
/>
|
||||
</Route>
|
||||
<Route path="chat" element={<NewTabChat />} />
|
||||
<Route path="personalize" element={<Personalize />} />
|
||||
</>
|
||||
) : (
|
||||
<Route index element={<NewTab />} />
|
||||
)}
|
||||
<Route path="soul" element={<SoulPage />} />
|
||||
<Route path="skills" element={<SkillsPage />} />
|
||||
<Route path="memory" element={<MemoryPage />} />
|
||||
@@ -98,8 +112,12 @@ export const App: FC = () => {
|
||||
{/* Primary nav routes */}
|
||||
<Route path="connect-apps" element={<ConnectMCP />} />
|
||||
<Route path="scheduled" element={<ScheduledTasksPage />} />
|
||||
<Route path="agents" element={<AgentsPage />} />
|
||||
<Route path="admin" element={<AdminDashboardPage />} />
|
||||
{alphaEnabled ? (
|
||||
<Route path="agents" element={<AgentsPage />} />
|
||||
) : null}
|
||||
{alphaEnabled ? (
|
||||
<Route path="admin" element={<AdminDashboardPage />} />
|
||||
) : null}
|
||||
</Route>
|
||||
|
||||
{/* Settings with dedicated sidebar */}
|
||||
@@ -113,8 +131,12 @@ export const App: FC = () => {
|
||||
<Route path="search" element={<SearchProviderPage />} />
|
||||
<Route path="survey" element={<SurveyPage {...surveyParams} />} />
|
||||
<Route path="usage" element={<UsagePage />} />
|
||||
<Route path="acl" element={<AclSettingsPage />} />
|
||||
<Route path="approvals" element={<ToolApprovalsPage />} />
|
||||
{alphaEnabled ? (
|
||||
<>
|
||||
<Route path="acl" element={<AclSettingsPage />} />
|
||||
<Route path="approvals" element={<ToolApprovalsPage />} />
|
||||
</>
|
||||
) : null}
|
||||
</Route>
|
||||
</Route>
|
||||
|
||||
@@ -130,7 +152,12 @@ export const App: FC = () => {
|
||||
<Route path="/" element={<Navigate to="/home" replace />} />
|
||||
<Route
|
||||
path="/personalize"
|
||||
element={<Navigate to="/home/personalize" replace />}
|
||||
element={
|
||||
<Navigate
|
||||
to={alphaEnabled ? '/home/personalize' : '/home'}
|
||||
replace
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/settings/connect-mcp"
|
||||
@@ -144,12 +171,18 @@ export const App: FC = () => {
|
||||
path="/settings/skills"
|
||||
element={<Navigate to="/home/skills" replace />}
|
||||
/>
|
||||
<Route path="/audit" element={<Navigate to="/admin" replace />} />
|
||||
<Route
|
||||
path="/audit"
|
||||
element={<Navigate to={alphaEnabled ? '/admin' : '/home'} replace />}
|
||||
/>
|
||||
<Route
|
||||
path="/observability"
|
||||
element={<Navigate to="/admin" replace />}
|
||||
element={<Navigate to={alphaEnabled ? '/admin' : '/home'} replace />}
|
||||
/>
|
||||
<Route
|
||||
path="/executions"
|
||||
element={<Navigate to={alphaEnabled ? '/admin' : '/home'} replace />}
|
||||
/>
|
||||
<Route path="/executions" element={<Navigate to="/admin" replace />} />
|
||||
<Route path="/options/*" element={<OptionsRedirect />} />
|
||||
|
||||
{/* Fallback to home */}
|
||||
|
||||
@@ -102,6 +102,7 @@ export const NewTab = () => {
|
||||
})
|
||||
const { selectedFolder } = useWorkspace()
|
||||
const { supports } = useCapabilities()
|
||||
const alphaEnabled = supports(Feature.ALPHA_FEATURES_SUPPORT)
|
||||
const { providers, selectedProvider, handleSelectProvider } =
|
||||
useChatSessionContext()
|
||||
const { servers: mcpServers } = useMcpServers()
|
||||
@@ -168,6 +169,8 @@ export const NewTab = () => {
|
||||
selectedTabs,
|
||||
})
|
||||
const searchPlaceholder = `Ask BrowserOS or search ${providerConfig.name}...`
|
||||
const supportsInlineChat =
|
||||
alphaEnabled && supports(Feature.NEWTAB_CHAT_SUPPORT)
|
||||
|
||||
const {
|
||||
isOpen,
|
||||
@@ -357,7 +360,7 @@ export const NewTab = () => {
|
||||
tabs_count: selectedTabs.length,
|
||||
})
|
||||
const searchQuery = `${item.name}${item.description ? ` - ${item.description}` : ''}}`
|
||||
if (supports(Feature.NEWTAB_CHAT_SUPPORT)) {
|
||||
if (supportsInlineChat) {
|
||||
startInlineChat(searchQuery, 'agent', {
|
||||
name: item.name,
|
||||
description: item.description,
|
||||
@@ -383,7 +386,7 @@ export const NewTab = () => {
|
||||
mode: item.mode,
|
||||
tabs_count: selectedTabs.length,
|
||||
})
|
||||
if (supports(Feature.NEWTAB_CHAT_SUPPORT)) {
|
||||
if (supportsInlineChat) {
|
||||
startInlineChat(item.message, item.mode)
|
||||
} else {
|
||||
const action = createBrowserOSAction({
|
||||
|
||||
@@ -4,10 +4,19 @@ import { ChatSessionProvider } from '@/entrypoints/sidepanel/layout/ChatSessionC
|
||||
import { NewTabFocusGrid } from './NewTabFocusGrid'
|
||||
import { shouldHideFocusGrid, shouldUseChatSession } from './route-utils'
|
||||
|
||||
export const NewTabLayout: FC = () => {
|
||||
interface NewTabLayoutProps {
|
||||
useChatSessionOnHome?: boolean
|
||||
}
|
||||
|
||||
export const NewTabLayout: FC<NewTabLayoutProps> = ({
|
||||
useChatSessionOnHome = false,
|
||||
}) => {
|
||||
const location = useLocation()
|
||||
const hideGrid = shouldHideFocusGrid(location.pathname)
|
||||
const useChatSession = shouldUseChatSession(location.pathname)
|
||||
const useChatSession = shouldUseChatSession(
|
||||
location.pathname,
|
||||
useChatSessionOnHome,
|
||||
)
|
||||
const content = (
|
||||
<>
|
||||
{!hideGrid && <NewTabFocusGrid />}
|
||||
|
||||
@@ -13,6 +13,7 @@ describe('route-utils', () => {
|
||||
expect(isAgentConversationPath('/home')).toBe(false)
|
||||
expect(isAgentConversationPath('/home/agents/main')).toBe(true)
|
||||
expect(shouldUseChatSession('/home')).toBe(false)
|
||||
expect(shouldUseChatSession('/home', true)).toBe(true)
|
||||
expect(shouldUseChatSession('/home/agents/main')).toBe(false)
|
||||
expect(shouldUseChatSession('/home/chat')).toBe(true)
|
||||
})
|
||||
|
||||
@@ -19,6 +19,11 @@ export function shouldHideFocusGrid(pathname: string): boolean {
|
||||
)
|
||||
}
|
||||
|
||||
export function shouldUseChatSession(pathname: string): boolean {
|
||||
return pathname === '/home/chat'
|
||||
export function shouldUseChatSession(
|
||||
pathname: string,
|
||||
useChatSessionOnHome = false,
|
||||
): boolean {
|
||||
return (
|
||||
pathname === '/home/chat' || (useChatSessionOnHome && pathname === '/home')
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { describe, expect, it } from 'bun:test'
|
||||
import { resolveStaticFeatureSupport } from './capabilities'
|
||||
|
||||
describe('resolveStaticFeatureSupport', () => {
|
||||
it('enables alpha-gated features automatically in development', () => {
|
||||
expect(
|
||||
resolveStaticFeatureSupport({
|
||||
isDevelopment: true,
|
||||
alphaFeaturesEnabled: false,
|
||||
requiresAlphaFlag: true,
|
||||
}),
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('enables alpha-gated features only when explicitly opted in', () => {
|
||||
expect(
|
||||
resolveStaticFeatureSupport({
|
||||
isDevelopment: false,
|
||||
alphaFeaturesEnabled: true,
|
||||
requiresAlphaFlag: true,
|
||||
}),
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('keeps non-alpha features enabled in development', () => {
|
||||
expect(
|
||||
resolveStaticFeatureSupport({
|
||||
isDevelopment: true,
|
||||
alphaFeaturesEnabled: false,
|
||||
}),
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('leaves non-alpha features unresolved in production', () => {
|
||||
expect(
|
||||
resolveStaticFeatureSupport({
|
||||
isDevelopment: false,
|
||||
alphaFeaturesEnabled: false,
|
||||
}),
|
||||
).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -1,3 +1,4 @@
|
||||
import { env } from '../env'
|
||||
import { BrowserOSAdapter } from './adapter'
|
||||
|
||||
const SERVER_VERSION_PREF = 'browseros.server.version'
|
||||
@@ -7,16 +8,20 @@ type FeatureConfig = {
|
||||
maxBrowserOSVersion?: string
|
||||
minServerVersion?: string
|
||||
maxServerVersion?: string
|
||||
requiresAlphaFlag?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Features gated by BrowserOS version.
|
||||
* Features gated by BrowserOS version or explicit environment flags.
|
||||
* Add new features here with corresponding config in FEATURE_CONFIG.
|
||||
*
|
||||
* Note: In development mode, all features are enabled regardless of version.
|
||||
* Note: In development mode, all features are enabled regardless of version
|
||||
* or alpha flag.
|
||||
* @public
|
||||
*/
|
||||
export enum Feature {
|
||||
// Unfinished UI surfaces behind an explicit alpha opt-in
|
||||
ALPHA_FEATURES_SUPPORT = 'ALPHA_FEATURES_SUPPORT',
|
||||
// support for OpenAI-compatible provider
|
||||
OPENAI_COMPATIBLE_SUPPORT = 'OPENAI_COMPATIBLE_SUPPORT',
|
||||
// Managed MCP servers integration
|
||||
@@ -61,9 +66,11 @@ export enum Feature {
|
||||
* - maxServerVersion: feature enabled when server < this version (for deprecation)
|
||||
*
|
||||
* TypeScript enforces that every Feature has a config entry.
|
||||
* Note: In development mode, all features are enabled regardless of version.
|
||||
* In development mode, all features are enabled regardless of version or
|
||||
* alpha flag.
|
||||
*/
|
||||
const FEATURE_CONFIG: { [K in Feature]: FeatureConfig } = {
|
||||
[Feature.ALPHA_FEATURES_SUPPORT]: { requiresAlphaFlag: true },
|
||||
[Feature.OPENAI_COMPATIBLE_SUPPORT]: { minBrowserOSVersion: '0.33.0.1' },
|
||||
[Feature.MANAGED_MCP_SUPPORT]: { minBrowserOSVersion: '0.34.0.0' },
|
||||
[Feature.PERSONALIZATION_SUPPORT]: { minBrowserOSVersion: '0.36.1.0' },
|
||||
@@ -121,6 +128,24 @@ function checkVersionConstraints(
|
||||
return true
|
||||
}
|
||||
|
||||
export function resolveStaticFeatureSupport({
|
||||
isDevelopment,
|
||||
alphaFeaturesEnabled,
|
||||
requiresAlphaFlag = false,
|
||||
}: {
|
||||
isDevelopment: boolean
|
||||
alphaFeaturesEnabled: boolean
|
||||
requiresAlphaFlag?: boolean
|
||||
}): boolean | null {
|
||||
if (isDevelopment) {
|
||||
return true
|
||||
}
|
||||
if (requiresAlphaFlag) {
|
||||
return alphaFeaturesEnabled
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
type CapabilitiesState = {
|
||||
browserOSVersion: number[] | null
|
||||
serverVersion: number[] | null
|
||||
@@ -128,6 +153,16 @@ type CapabilitiesState = {
|
||||
|
||||
let initPromise: Promise<CapabilitiesState> | null = null
|
||||
|
||||
function getStaticFeatureSupport(feature: Feature): boolean | null {
|
||||
const config = FEATURE_CONFIG[feature]
|
||||
if (!config) return false
|
||||
return resolveStaticFeatureSupport({
|
||||
isDevelopment: import.meta.env.DEV,
|
||||
alphaFeaturesEnabled: env.VITE_ALPHA_FEATURES,
|
||||
requiresAlphaFlag: config.requiresAlphaFlag,
|
||||
})
|
||||
}
|
||||
|
||||
async function doInitialize(): Promise<CapabilitiesState> {
|
||||
const adapter = BrowserOSAdapter.getInstance()
|
||||
const state: CapabilitiesState = {
|
||||
@@ -205,12 +240,17 @@ function checkFeatureSupport(
|
||||
* @public
|
||||
*/
|
||||
export const Capabilities = {
|
||||
getStaticSupport(feature: Feature): boolean | null {
|
||||
return getStaticFeatureSupport(feature)
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a feature is supported.
|
||||
* In development mode, all features are enabled.
|
||||
*/
|
||||
async supports(feature: Feature): Promise<boolean> {
|
||||
if (import.meta.env.DEV) return true
|
||||
const staticSupport = getStaticFeatureSupport(feature)
|
||||
if (staticSupport !== null) return staticSupport
|
||||
const state = await ensureInitialized()
|
||||
return checkFeatureSupport(state, feature)
|
||||
},
|
||||
|
||||
@@ -14,6 +14,19 @@ interface UseCapabilitiesResult {
|
||||
serverVersion: string | null
|
||||
}
|
||||
|
||||
function getInitialSupportedFeatures(): Map<Feature, boolean> {
|
||||
return new Map(
|
||||
Object.values(Feature)
|
||||
.filter((value) => typeof value === 'string')
|
||||
.flatMap((feature) => {
|
||||
const supported = Capabilities.getStaticSupport(feature as Feature)
|
||||
return supported === null
|
||||
? []
|
||||
: ([[feature as Feature, supported]] as const)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* React hook for version-gated feature checks.
|
||||
* Auto-initializes Capabilities and caches feature support results.
|
||||
@@ -28,11 +41,11 @@ interface UseCapabilitiesResult {
|
||||
*/
|
||||
export function useCapabilities(): UseCapabilitiesResult {
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [state, setState] = useState<CapabilitiesState>({
|
||||
const [state, setState] = useState<CapabilitiesState>(() => ({
|
||||
browserOSVersion: null,
|
||||
serverVersion: null,
|
||||
supportedFeatures: new Map(),
|
||||
})
|
||||
supportedFeatures: getInitialSupportedFeatures(),
|
||||
}))
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
@@ -72,10 +85,9 @@ export function useCapabilities(): UseCapabilitiesResult {
|
||||
|
||||
const supports = useCallback(
|
||||
(feature: Feature): boolean => {
|
||||
if (isLoading) return false
|
||||
return state.supportedFeatures.get(feature) ?? false
|
||||
},
|
||||
[isLoading, state.supportedFeatures],
|
||||
[state.supportedFeatures],
|
||||
)
|
||||
|
||||
return {
|
||||
|
||||
@@ -2,11 +2,15 @@ import { ZodError, z } from 'zod'
|
||||
|
||||
const EnvSchema = z.object({
|
||||
VITE_BROWSEROS_SERVER_PORT: z.coerce.number().optional(),
|
||||
VITE_ALPHA_FEATURES: z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((value) => value === 'true'),
|
||||
VITE_PUBLIC_POSTHOG_KEY: z.string().optional(),
|
||||
VITE_PUBLIC_POSTHOG_HOST: z.string().optional(),
|
||||
VITE_PUBLIC_SENTRY_DSN: z.string().optional(),
|
||||
VITE_PUBLIC_BROWSEROS_API: z.string().optional(),
|
||||
PROD: z.boolean(),
|
||||
PROD: z.boolean().optional().default(false),
|
||||
})
|
||||
|
||||
try {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@browseros/server",
|
||||
"version": "0.0.84",
|
||||
"version": "0.0.85",
|
||||
"description": "BrowserOS server",
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
|
||||
@@ -9,6 +9,10 @@ import { LLM_PROVIDERS } from '@browseros/shared/schemas/llm'
|
||||
import { createOpenRouter } from '@openrouter/ai-sdk-provider'
|
||||
import type { LanguageModel } from 'ai'
|
||||
import { createBrowserOSFetch } from '../lib/browseros-fetch'
|
||||
import {
|
||||
createMockBrowserOSLanguageModel,
|
||||
shouldUseMockBrowserOSLLM,
|
||||
} from '../lib/clients/llm/mock-language-model'
|
||||
import { createCodexFetch } from '../lib/clients/oauth/codex-fetch'
|
||||
import { createCopilotFetch } from '../lib/clients/oauth/copilot-fetch'
|
||||
import { logger } from '../lib/logger'
|
||||
@@ -219,6 +223,9 @@ const PROVIDER_FACTORIES: Record<string, ProviderFactory> = {
|
||||
export function createLanguageModel(
|
||||
config: ResolvedAgentConfig,
|
||||
): LanguageModel {
|
||||
if (shouldUseMockBrowserOSLLM(config)) {
|
||||
return createMockBrowserOSLanguageModel()
|
||||
}
|
||||
const provider = config.provider as string
|
||||
const factory = PROVIDER_FACTORIES[provider]
|
||||
if (!factory) throw new Error(`Unknown provider: ${provider}`)
|
||||
|
||||
@@ -8,10 +8,33 @@
|
||||
* On Linux, machine operations are no-ops since Podman runs natively.
|
||||
*/
|
||||
|
||||
import { existsSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
|
||||
const isLinux = process.platform === 'linux'
|
||||
const PODMAN_BUNDLE_PATH = ['bin', 'third_party', 'podman'] as const
|
||||
|
||||
export type LogFn = (msg: string) => void
|
||||
|
||||
function getPodmanBinaryName(platform: NodeJS.Platform): string {
|
||||
return platform === 'win32' ? 'podman.exe' : 'podman'
|
||||
}
|
||||
|
||||
export function resolveBundledPodmanPath(
|
||||
resourcesDir?: string,
|
||||
platform: NodeJS.Platform = process.platform,
|
||||
): string | null {
|
||||
if (!resourcesDir) return null
|
||||
|
||||
const bundledPath = join(
|
||||
resourcesDir,
|
||||
...PODMAN_BUNDLE_PATH,
|
||||
getPodmanBinaryName(platform),
|
||||
)
|
||||
|
||||
return existsSync(bundledPath) ? bundledPath : null
|
||||
}
|
||||
|
||||
export class PodmanRuntime {
|
||||
private podmanPath: string
|
||||
private machineReady = false
|
||||
@@ -243,6 +266,19 @@ export class PodmanRuntime {
|
||||
|
||||
let runtime: PodmanRuntime | null = null
|
||||
|
||||
export function configurePodmanRuntime(config: {
|
||||
resourcesDir?: string
|
||||
podmanPath?: string
|
||||
}): PodmanRuntime {
|
||||
const podmanPath =
|
||||
config.podmanPath ??
|
||||
resolveBundledPodmanPath(config.resourcesDir) ??
|
||||
'podman'
|
||||
|
||||
runtime = new PodmanRuntime({ podmanPath })
|
||||
return runtime
|
||||
}
|
||||
|
||||
export function getPodmanRuntime(): PodmanRuntime {
|
||||
if (!runtime) runtime = new PodmanRuntime()
|
||||
return runtime
|
||||
|
||||
@@ -11,6 +11,10 @@ import { INLINED_ENV } from '../../../env'
|
||||
import { logger } from '../../logger'
|
||||
import { fetchBrowserOSConfig, getLLMConfigFromProvider } from '../gateway'
|
||||
import { getOAuthTokenManager } from '../oauth'
|
||||
import {
|
||||
resolveMockBrowserOSConfig,
|
||||
shouldUseMockBrowserOSLLM,
|
||||
} from './mock-language-model'
|
||||
import type { ResolvedLLMConfig } from './types'
|
||||
|
||||
export async function resolveLLMConfig(
|
||||
@@ -49,6 +53,9 @@ export async function resolveLLMConfig(
|
||||
|
||||
// BrowserOS gateway: fetch config from remote service
|
||||
if (config.provider === LLM_PROVIDERS.BROWSEROS) {
|
||||
if (shouldUseMockBrowserOSLLM(config)) {
|
||||
return resolveMockBrowserOSConfig(config, browserosId)
|
||||
}
|
||||
return resolveBrowserOSConfig(config, browserosId)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import type {
|
||||
LanguageModelV3GenerateResult,
|
||||
LanguageModelV3StreamPart,
|
||||
LanguageModelV3Usage,
|
||||
} from '@ai-sdk/provider'
|
||||
import { LLM_PROVIDERS, type LLMConfig } from '@browseros/shared/schemas/llm'
|
||||
import { type LanguageModel, simulateReadableStream } from 'ai'
|
||||
import { MockLanguageModelV3 } from 'ai/test'
|
||||
import type { ResolvedLLMConfig } from './types'
|
||||
|
||||
export const MOCK_BROWSEROS_MODEL_ID = 'browseros-test-mock'
|
||||
export const MOCK_BROWSEROS_RESPONSE_TEXT = 'Mock BrowserOS test response.'
|
||||
|
||||
const MOCK_USAGE: LanguageModelV3Usage = {
|
||||
inputTokens: {
|
||||
total: 1,
|
||||
noCache: 1,
|
||||
cacheRead: undefined,
|
||||
cacheWrite: undefined,
|
||||
},
|
||||
outputTokens: {
|
||||
total: 4,
|
||||
text: 4,
|
||||
reasoning: undefined,
|
||||
},
|
||||
}
|
||||
|
||||
function createMockResult(): LanguageModelV3GenerateResult {
|
||||
return {
|
||||
content: [{ type: 'text', text: MOCK_BROWSEROS_RESPONSE_TEXT }],
|
||||
finishReason: { unified: 'stop', raw: 'stop' },
|
||||
usage: MOCK_USAGE,
|
||||
warnings: [],
|
||||
}
|
||||
}
|
||||
|
||||
export function isMockBrowserOSLLMEnabled(): boolean {
|
||||
return process.env.BROWSEROS_USE_MOCK_LLM === 'true'
|
||||
}
|
||||
|
||||
export function shouldUseMockBrowserOSLLM(
|
||||
config: Pick<LLMConfig, 'provider'>,
|
||||
): boolean {
|
||||
return (
|
||||
config.provider === LLM_PROVIDERS.BROWSEROS && isMockBrowserOSLLMEnabled()
|
||||
)
|
||||
}
|
||||
|
||||
export function resolveMockBrowserOSConfig(
|
||||
config: LLMConfig,
|
||||
browserosId?: string,
|
||||
): ResolvedLLMConfig {
|
||||
return {
|
||||
...config,
|
||||
model: config.model ?? MOCK_BROWSEROS_MODEL_ID,
|
||||
browserosId,
|
||||
upstreamProvider: LLM_PROVIDERS.OPENAI,
|
||||
}
|
||||
}
|
||||
|
||||
export function createMockBrowserOSLanguageModel(): LanguageModel {
|
||||
const chunks: LanguageModelV3StreamPart[] = [
|
||||
{ type: 'text-start', id: 'text-1' },
|
||||
{
|
||||
type: 'text-delta',
|
||||
id: 'text-1',
|
||||
delta: MOCK_BROWSEROS_RESPONSE_TEXT,
|
||||
},
|
||||
{ type: 'text-end', id: 'text-1' },
|
||||
{
|
||||
type: 'finish',
|
||||
finishReason: { unified: 'stop', raw: 'stop' },
|
||||
usage: MOCK_USAGE,
|
||||
},
|
||||
]
|
||||
|
||||
return new MockLanguageModelV3({
|
||||
doGenerate: async () => createMockResult(),
|
||||
doStream: async () => ({
|
||||
stream: simulateReadableStream({ chunks }),
|
||||
}),
|
||||
}) as LanguageModel
|
||||
}
|
||||
@@ -21,6 +21,10 @@ import { logger } from '../../logger'
|
||||
import { createOpenRouterCompatibleFetch } from '../../openrouter-fetch'
|
||||
import { createCodexFetch } from '../oauth/codex-fetch'
|
||||
import { createCopilotFetch } from '../oauth/copilot-fetch'
|
||||
import {
|
||||
createMockBrowserOSLanguageModel,
|
||||
shouldUseMockBrowserOSLLM,
|
||||
} from './mock-language-model'
|
||||
import type { ResolvedLLMConfig } from './types'
|
||||
|
||||
type ProviderFactory = (config: ResolvedLLMConfig) => LanguageModel
|
||||
@@ -195,6 +199,9 @@ const PROVIDER_FACTORIES: Record<string, ProviderFactory> = {
|
||||
}
|
||||
|
||||
export function createLLMProvider(config: ResolvedLLMConfig): LanguageModel {
|
||||
if (shouldUseMockBrowserOSLLM(config)) {
|
||||
return createMockBrowserOSLanguageModel()
|
||||
}
|
||||
const factory = PROVIDER_FACTORIES[config.provider]
|
||||
if (!factory) throw new Error(`Unknown provider: ${config.provider}`)
|
||||
return factory(config)
|
||||
|
||||
@@ -14,6 +14,7 @@ import path from 'node:path'
|
||||
import { EXIT_CODES } from '@browseros/shared/constants/exit-codes'
|
||||
import { createHttpServer } from './api/server'
|
||||
import { getOpenClawService } from './api/services/openclaw/openclaw-service'
|
||||
import { configurePodmanRuntime } from './api/services/openclaw/podman-runtime'
|
||||
import { CdpBackend } from './browser/backends/cdp'
|
||||
import { Browser } from './browser/browser'
|
||||
import type { ServerConfig } from './config'
|
||||
@@ -55,6 +56,9 @@ export class Application {
|
||||
resourcesDir: path.resolve(this.config.resourcesDir),
|
||||
})
|
||||
|
||||
configurePodmanRuntime({
|
||||
resourcesDir: path.resolve(this.config.resourcesDir),
|
||||
})
|
||||
await this.initCoreServices()
|
||||
|
||||
if (!this.config.cdpPort) {
|
||||
|
||||
@@ -79,7 +79,11 @@ export async function spawnServer(config: ServerConfig): Promise<ServerState> {
|
||||
],
|
||||
{
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
env: { ...globalThis.process.env, NODE_ENV: 'test' },
|
||||
env: {
|
||||
...globalThis.process.env,
|
||||
NODE_ENV: 'test',
|
||||
BROWSEROS_USE_MOCK_LLM: 'true',
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 BrowserOS
|
||||
*/
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'bun:test'
|
||||
import fs from 'node:fs'
|
||||
import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
|
||||
import {
|
||||
configurePodmanRuntime,
|
||||
getPodmanRuntime,
|
||||
resolveBundledPodmanPath,
|
||||
} from '../../../../src/api/services/openclaw/podman-runtime'
|
||||
|
||||
describe('podman runtime', () => {
|
||||
let tempDir: string
|
||||
|
||||
beforeEach(() => {
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'browseros-podman-test-'))
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true })
|
||||
configurePodmanRuntime({ podmanPath: 'podman' })
|
||||
})
|
||||
|
||||
it('returns the bundled podman path when the executable exists', () => {
|
||||
const bundledPath = path.join(
|
||||
tempDir,
|
||||
'bin',
|
||||
'third_party',
|
||||
'podman',
|
||||
'podman',
|
||||
)
|
||||
fs.mkdirSync(path.dirname(bundledPath), { recursive: true })
|
||||
fs.writeFileSync(bundledPath, 'podman')
|
||||
|
||||
expect(resolveBundledPodmanPath(tempDir, 'darwin')).toBe(bundledPath)
|
||||
})
|
||||
|
||||
it('uses the windows executable name for bundled podman', () => {
|
||||
const bundledPath = path.join(
|
||||
tempDir,
|
||||
'bin',
|
||||
'third_party',
|
||||
'podman',
|
||||
'podman.exe',
|
||||
)
|
||||
fs.mkdirSync(path.dirname(bundledPath), { recursive: true })
|
||||
fs.writeFileSync(bundledPath, 'podman')
|
||||
|
||||
expect(resolveBundledPodmanPath(tempDir, 'win32')).toBe(bundledPath)
|
||||
})
|
||||
|
||||
it('returns null when no bundled podman executable exists', () => {
|
||||
expect(resolveBundledPodmanPath(tempDir, 'darwin')).toBeNull()
|
||||
})
|
||||
|
||||
it('configures the runtime to prefer the bundled podman path', () => {
|
||||
const bundledPath = path.join(
|
||||
tempDir,
|
||||
'bin',
|
||||
'third_party',
|
||||
'podman',
|
||||
'podman',
|
||||
)
|
||||
fs.mkdirSync(path.dirname(bundledPath), { recursive: true })
|
||||
fs.writeFileSync(bundledPath, 'podman')
|
||||
|
||||
const runtime = configurePodmanRuntime({ resourcesDir: tempDir })
|
||||
|
||||
expect(runtime.getPodmanPath()).toBe(bundledPath)
|
||||
expect(getPodmanRuntime().getPodmanPath()).toBe(bundledPath)
|
||||
})
|
||||
|
||||
it('falls back to PATH podman when no bundled executable is present', () => {
|
||||
const runtime = configurePodmanRuntime({ resourcesDir: tempDir })
|
||||
|
||||
expect(runtime.getPodmanPath()).toBe('podman')
|
||||
})
|
||||
})
|
||||
@@ -12,6 +12,7 @@ import { URL } from 'node:url'
|
||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
||||
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
|
||||
|
||||
import { MOCK_BROWSEROS_RESPONSE_TEXT } from '../src/lib/clients/llm/mock-language-model'
|
||||
import {
|
||||
cleanupBrowserOS,
|
||||
ensureBrowserOS,
|
||||
@@ -155,7 +156,7 @@ describe('HTTP Server Integration Tests', () => {
|
||||
|
||||
describe('Chat endpoint', () => {
|
||||
it(
|
||||
'streams a chat response with BrowserOS provider',
|
||||
'streams a mocked chat response for BrowserOS provider requests in test mode',
|
||||
async () => {
|
||||
const conversationId = crypto.randomUUID()
|
||||
|
||||
@@ -206,6 +207,10 @@ describe('HTTP Server Integration Tests', () => {
|
||||
fullResponse.includes('data:'),
|
||||
'Should contain SSE data events',
|
||||
)
|
||||
assert.ok(
|
||||
fullResponse.includes(MOCK_BROWSEROS_RESPONSE_TEXT),
|
||||
'Should include the mocked BrowserOS chat response',
|
||||
)
|
||||
|
||||
const deleteResponse = await fetch(
|
||||
`${getBaseUrl()}/chat/${conversationId}`,
|
||||
|
||||
@@ -156,7 +156,7 @@
|
||||
},
|
||||
"apps/server": {
|
||||
"name": "@browseros/server",
|
||||
"version": "0.0.84",
|
||||
"version": "0.0.85",
|
||||
"bin": {
|
||||
"browseros-server": "./src/index.ts",
|
||||
},
|
||||
|
||||
@@ -54,6 +54,136 @@
|
||||
"os": ["windows"],
|
||||
"arch": ["x64"]
|
||||
},
|
||||
{
|
||||
"name": "Podman CLI - macOS ARM64",
|
||||
"source": {
|
||||
"type": "r2",
|
||||
"key": "third_party/podman/podman-darwin-arm64"
|
||||
},
|
||||
"destination": "resources/bin/third_party/podman/podman",
|
||||
"os": ["macos"],
|
||||
"arch": ["arm64"],
|
||||
"executable": true
|
||||
},
|
||||
{
|
||||
"name": "Podman gvproxy - macOS ARM64",
|
||||
"source": {
|
||||
"type": "r2",
|
||||
"key": "third_party/podman/gvproxy-darwin-arm64"
|
||||
},
|
||||
"destination": "resources/bin/third_party/podman/gvproxy",
|
||||
"os": ["macos"],
|
||||
"arch": ["arm64"],
|
||||
"executable": true
|
||||
},
|
||||
{
|
||||
"name": "Podman vfkit - macOS ARM64",
|
||||
"source": {
|
||||
"type": "r2",
|
||||
"key": "third_party/podman/vfkit-darwin-arm64"
|
||||
},
|
||||
"destination": "resources/bin/third_party/podman/vfkit",
|
||||
"os": ["macos"],
|
||||
"arch": ["arm64"],
|
||||
"executable": true
|
||||
},
|
||||
{
|
||||
"name": "Podman krunkit - macOS ARM64",
|
||||
"source": {
|
||||
"type": "r2",
|
||||
"key": "third_party/podman/krunkit-darwin-arm64"
|
||||
},
|
||||
"destination": "resources/bin/third_party/podman/krunkit",
|
||||
"os": ["macos"],
|
||||
"arch": ["arm64"],
|
||||
"executable": true
|
||||
},
|
||||
{
|
||||
"name": "Podman mac helper - macOS ARM64",
|
||||
"source": {
|
||||
"type": "r2",
|
||||
"key": "third_party/podman/podman-mac-helper-darwin-arm64"
|
||||
},
|
||||
"destination": "resources/bin/third_party/podman/podman-mac-helper",
|
||||
"os": ["macos"],
|
||||
"arch": ["arm64"],
|
||||
"executable": true
|
||||
},
|
||||
{
|
||||
"name": "Podman CLI - macOS x64",
|
||||
"notes": "krunkit is intentionally omitted on macOS x64 because the official amd64 Podman installer ships an arm64-only krunkit helper",
|
||||
"source": {
|
||||
"type": "r2",
|
||||
"key": "third_party/podman/podman-darwin-x64"
|
||||
},
|
||||
"destination": "resources/bin/third_party/podman/podman",
|
||||
"os": ["macos"],
|
||||
"arch": ["x64"],
|
||||
"executable": true
|
||||
},
|
||||
{
|
||||
"name": "Podman gvproxy - macOS x64",
|
||||
"source": {
|
||||
"type": "r2",
|
||||
"key": "third_party/podman/gvproxy-darwin-x64"
|
||||
},
|
||||
"destination": "resources/bin/third_party/podman/gvproxy",
|
||||
"os": ["macos"],
|
||||
"arch": ["x64"],
|
||||
"executable": true
|
||||
},
|
||||
{
|
||||
"name": "Podman vfkit - macOS x64",
|
||||
"source": {
|
||||
"type": "r2",
|
||||
"key": "third_party/podman/vfkit-darwin-x64"
|
||||
},
|
||||
"destination": "resources/bin/third_party/podman/vfkit",
|
||||
"os": ["macos"],
|
||||
"arch": ["x64"],
|
||||
"executable": true
|
||||
},
|
||||
{
|
||||
"name": "Podman mac helper - macOS x64",
|
||||
"source": {
|
||||
"type": "r2",
|
||||
"key": "third_party/podman/podman-mac-helper-darwin-x64"
|
||||
},
|
||||
"destination": "resources/bin/third_party/podman/podman-mac-helper",
|
||||
"os": ["macos"],
|
||||
"arch": ["x64"],
|
||||
"executable": true
|
||||
},
|
||||
{
|
||||
"name": "Podman CLI - Windows x64",
|
||||
"source": {
|
||||
"type": "r2",
|
||||
"key": "third_party/podman/podman-windows-x64.exe"
|
||||
},
|
||||
"destination": "resources/bin/third_party/podman/podman.exe",
|
||||
"os": ["windows"],
|
||||
"arch": ["x64"]
|
||||
},
|
||||
{
|
||||
"name": "Podman gvproxy - Windows x64",
|
||||
"source": {
|
||||
"type": "r2",
|
||||
"key": "third_party/podman/gvproxy-windows-x64.exe"
|
||||
},
|
||||
"destination": "resources/bin/third_party/podman/gvproxy.exe",
|
||||
"os": ["windows"],
|
||||
"arch": ["x64"]
|
||||
},
|
||||
{
|
||||
"name": "Podman win-sshproxy - Windows x64",
|
||||
"source": {
|
||||
"type": "r2",
|
||||
"key": "third_party/podman/win-sshproxy-windows-x64.exe"
|
||||
},
|
||||
"destination": "resources/bin/third_party/podman/win-sshproxy.exe",
|
||||
"os": ["windows"],
|
||||
"arch": ["x64"]
|
||||
},
|
||||
{
|
||||
"name": "ripgrep - macOS ARM64",
|
||||
"source": {
|
||||
|
||||
@@ -1 +1 @@
|
||||
142
|
||||
143
|
||||
|
||||
27
packages/browseros/build/modules/sign/macos.py
generated
27
packages/browseros/build/modules/sign/macos.py
generated
@@ -29,16 +29,33 @@ BROWSEROS_SERVER_BINARIES: Dict[str, Dict[str, str]] = {
|
||||
"options": "runtime",
|
||||
"entitlements": "browseros-executable-entitlements.plist",
|
||||
},
|
||||
"codex": {
|
||||
"identifier_suffix": "codex",
|
||||
"options": "runtime",
|
||||
"entitlements": "browseros-executable-entitlements.plist",
|
||||
},
|
||||
"bun": {
|
||||
"identifier_suffix": "bun",
|
||||
"options": "runtime",
|
||||
"entitlements": "browseros-executable-entitlements.plist",
|
||||
},
|
||||
"podman": {
|
||||
"identifier_suffix": "podman",
|
||||
"options": "runtime",
|
||||
},
|
||||
"gvproxy": {
|
||||
"identifier_suffix": "gvproxy",
|
||||
"options": "runtime",
|
||||
},
|
||||
"vfkit": {
|
||||
"identifier_suffix": "vfkit",
|
||||
"options": "runtime",
|
||||
"entitlements": "podman-vfkit-entitlements.plist",
|
||||
},
|
||||
"krunkit": {
|
||||
"identifier_suffix": "krunkit",
|
||||
"options": "runtime",
|
||||
"entitlements": "podman-krunkit-entitlements.plist",
|
||||
},
|
||||
"podman-mac-helper": {
|
||||
"identifier_suffix": "podman_mac_helper",
|
||||
"options": "runtime",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
||||
9
packages/browseros/build/modules/sign/windows.py
generated
9
packages/browseros/build/modules/sign/windows.py
generated
@@ -18,8 +18,11 @@ from ...common.utils import (
|
||||
|
||||
BROWSEROS_SERVER_BINARIES: List[str] = [
|
||||
"browseros_server.exe",
|
||||
"codex.exe",
|
||||
"bun.exe",
|
||||
"third_party/bun.exe",
|
||||
"third_party/rg.exe",
|
||||
"third_party/podman/podman.exe",
|
||||
"third_party/podman/gvproxy.exe",
|
||||
"third_party/podman/win-sshproxy.exe",
|
||||
]
|
||||
|
||||
|
||||
@@ -102,7 +105,7 @@ class WindowsSignModule(CommandModule):
|
||||
def get_browseros_server_binary_paths(build_output_dir: Path) -> List[Path]:
|
||||
"""Return absolute paths to BrowserOS Server binaries for signing."""
|
||||
server_dir = build_output_dir / "BrowserOSServer" / "default" / "resources" / "bin"
|
||||
return [server_dir / binary for binary in BROWSEROS_SERVER_BINARIES]
|
||||
return [server_dir / Path(binary) for binary in BROWSEROS_SERVER_BINARIES]
|
||||
|
||||
|
||||
def build_mini_installer(ctx: Context) -> bool:
|
||||
|
||||
@@ -22,6 +22,8 @@ class ExtractArtifactZipTest(unittest.TestCase):
|
||||
"resources/bin/browseros_server": b"server-binary",
|
||||
"resources/bin/third_party/bun": b"bun-binary",
|
||||
"resources/bin/third_party/rg": b"rg-binary",
|
||||
"resources/bin/third_party/podman/podman": b"podman-binary",
|
||||
"resources/bin/third_party/podman/gvproxy": b"gvproxy-binary",
|
||||
}
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
diff --git a/chrome/browser/extensions/BUILD.gn b/chrome/browser/extensions/BUILD.gn
|
||||
index 5d5af07b158c0..f32c7889de5be 100644
|
||||
index 64e7c1122ba87..47ab667fec018 100644
|
||||
--- a/chrome/browser/extensions/BUILD.gn
|
||||
+++ b/chrome/browser/extensions/BUILD.gn
|
||||
@@ -357,6 +357,12 @@ source_set("extensions") {
|
||||
@@ -354,6 +354,12 @@ source_set("extensions") {
|
||||
"external_install_manager.h",
|
||||
"external_install_manager_factory.cc",
|
||||
"external_install_manager_factory.h",
|
||||
@@ -15,7 +15,7 @@ index 5d5af07b158c0..f32c7889de5be 100644
|
||||
"external_policy_loader.cc",
|
||||
"external_policy_loader.h",
|
||||
"external_pref_loader.cc",
|
||||
@@ -690,6 +696,18 @@ source_set("extensions") {
|
||||
@@ -700,6 +706,14 @@ source_set("extensions") {
|
||||
"api/automation_internal/chrome_automation_internal_api_delegate.h",
|
||||
"api/bookmark_manager_private/bookmark_manager_private_api.cc",
|
||||
"api/bookmark_manager_private/bookmark_manager_private_api.h",
|
||||
@@ -27,14 +27,10 @@ index 5d5af07b158c0..f32c7889de5be 100644
|
||||
+ "api/browser_os/browser_os_api_utils.h",
|
||||
+ "api/browser_os/browser_os_change_detector.cc",
|
||||
+ "api/browser_os/browser_os_change_detector.h",
|
||||
+ "api/browser_os/browser_os_content_processor.cc",
|
||||
+ "api/browser_os/browser_os_content_processor.h",
|
||||
+ "api/browser_os/browser_os_snapshot_processor.cc",
|
||||
+ "api/browser_os/browser_os_snapshot_processor.h",
|
||||
"api/chrome_device_permissions_prompt.h",
|
||||
"api/enterprise_reporting_private/conversion_utils.cc",
|
||||
"api/enterprise_reporting_private/conversion_utils.h",
|
||||
@@ -997,6 +1015,8 @@ source_set("extensions") {
|
||||
@@ -1005,6 +1019,8 @@ source_set("extensions") {
|
||||
"//components/language/core/common",
|
||||
"//components/language/core/language_model",
|
||||
"//components/live_caption:constants",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,9 @@
|
||||
diff --git a/chrome/browser/extensions/api/browser_os/browser_os_api.h b/chrome/browser/extensions/api/browser_os/browser_os_api.h
|
||||
new file mode 100644
|
||||
index 0000000000000..53ece24723da0
|
||||
index 0000000000000..a297a9a2a43fa
|
||||
--- /dev/null
|
||||
+++ b/chrome/browser/extensions/api/browser_os/browser_os_api.h
|
||||
@@ -0,0 +1,371 @@
|
||||
@@ -0,0 +1,169 @@
|
||||
+// Copyright 2024 The Chromium Authors
|
||||
+// Use of this source code is governed by a BSD-style license that can be
|
||||
+// found in the LICENSE file.
|
||||
@@ -11,117 +11,16 @@ index 0000000000000..53ece24723da0
|
||||
+#ifndef CHROME_BROWSER_EXTENSIONS_API_BROWSER_OS_BROWSER_OS_API_H_
|
||||
+#define CHROME_BROWSER_EXTENSIONS_API_BROWSER_OS_BROWSER_OS_API_H_
|
||||
+
|
||||
+#include <cstdint>
|
||||
+
|
||||
+#include "base/memory/weak_ptr.h"
|
||||
+#include "base/values.h"
|
||||
+#include "chrome/browser/extensions/api/browser_os/browser_os_api_utils.h"
|
||||
+#include "chrome/browser/extensions/api/browser_os/browser_os_content_processor.h"
|
||||
+#include "chrome/browser/extensions/api/browser_os/browser_os_snapshot_processor.h"
|
||||
+#include "components/viz/common/frame_sinks/copy_output_result.h"
|
||||
+#include "content/public/browser/render_widget_host_view.h"
|
||||
+#include "extensions/browser/extension_function.h"
|
||||
+#include "third_party/skia/include/core/SkBitmap.h"
|
||||
+#include "ui/shell_dialogs/select_file_dialog.h"
|
||||
+
|
||||
+namespace content {
|
||||
+class WebContents;
|
||||
+}
|
||||
+
|
||||
+namespace ui {
|
||||
+struct AXTreeUpdate;
|
||||
+}
|
||||
+
|
||||
+namespace extensions {
|
||||
+namespace api {
|
||||
+
|
||||
+
|
||||
+class BrowserOSGetAccessibilityTreeFunction : public ExtensionFunction {
|
||||
+ public:
|
||||
+ DECLARE_EXTENSION_FUNCTION("browserOS.getAccessibilityTree",
|
||||
+ BROWSER_OS_GETACCESSIBILITYTREE)
|
||||
+
|
||||
+ BrowserOSGetAccessibilityTreeFunction() = default;
|
||||
+
|
||||
+ protected:
|
||||
+ ~BrowserOSGetAccessibilityTreeFunction() override = default;
|
||||
+
|
||||
+ // ExtensionFunction:
|
||||
+ ResponseAction Run() override;
|
||||
+
|
||||
+ private:
|
||||
+ void OnAccessibilityTreeReceived(ui::AXTreeUpdate& tree_update);
|
||||
+};
|
||||
+
|
||||
+class BrowserOSGetInteractiveSnapshotFunction : public ExtensionFunction {
|
||||
+ public:
|
||||
+ DECLARE_EXTENSION_FUNCTION("browserOS.getInteractiveSnapshot",
|
||||
+ BROWSER_OS_GETINTERACTIVESNAPSHOT)
|
||||
+
|
||||
+ BrowserOSGetInteractiveSnapshotFunction();
|
||||
+
|
||||
+ protected:
|
||||
+ ~BrowserOSGetInteractiveSnapshotFunction() override;
|
||||
+
|
||||
+ // ExtensionFunction:
|
||||
+ ResponseAction Run() override;
|
||||
+
|
||||
+ private:
|
||||
+ void OnAccessibilityTreeReceived(ui::AXTreeUpdate& tree_update);
|
||||
+ void OnSnapshotProcessed(SnapshotProcessingResult result);
|
||||
+
|
||||
+ // Counter for snapshot IDs
|
||||
+ static uint32_t next_snapshot_id_;
|
||||
+
|
||||
+ // Tab ID for storing mappings
|
||||
+ int tab_id_ = -1;
|
||||
+
|
||||
+ // Web contents for processing and drawing
|
||||
+ base::WeakPtr<content::WebContents> web_contents_;
|
||||
+};
|
||||
+
|
||||
+class BrowserOSClickFunction : public ExtensionFunction {
|
||||
+ public:
|
||||
+ DECLARE_EXTENSION_FUNCTION("browserOS.click", BROWSER_OS_CLICK)
|
||||
+
|
||||
+ BrowserOSClickFunction() = default;
|
||||
+
|
||||
+ protected:
|
||||
+ ~BrowserOSClickFunction() override = default;
|
||||
+
|
||||
+ // ExtensionFunction:
|
||||
+ ResponseAction Run() override;
|
||||
+};
|
||||
+
|
||||
+class BrowserOSInputTextFunction : public ExtensionFunction {
|
||||
+ public:
|
||||
+ DECLARE_EXTENSION_FUNCTION("browserOS.inputText", BROWSER_OS_INPUTTEXT)
|
||||
+
|
||||
+ BrowserOSInputTextFunction() = default;
|
||||
+
|
||||
+ protected:
|
||||
+ ~BrowserOSInputTextFunction() override = default;
|
||||
+
|
||||
+ // ExtensionFunction:
|
||||
+ ResponseAction Run() override;
|
||||
+};
|
||||
+
|
||||
+class BrowserOSClearFunction : public ExtensionFunction {
|
||||
+ public:
|
||||
+ DECLARE_EXTENSION_FUNCTION("browserOS.clear", BROWSER_OS_CLEAR)
|
||||
+
|
||||
+ BrowserOSClearFunction() = default;
|
||||
+
|
||||
+ protected:
|
||||
+ ~BrowserOSClearFunction() override = default;
|
||||
+
|
||||
+ // ExtensionFunction:
|
||||
+ ResponseAction Run() override;
|
||||
+};
|
||||
+namespace extensions::api {
|
||||
+
|
||||
+class BrowserOSGetPageLoadStatusFunction : public ExtensionFunction {
|
||||
+ public:
|
||||
+ DECLARE_EXTENSION_FUNCTION("browserOS.getPageLoadStatus",
|
||||
+ DECLARE_EXTENSION_FUNCTION("browserOS.getPageLoadStatus",
|
||||
+ BROWSER_OS_GETPAGELOADSTATUS)
|
||||
+
|
||||
+ BrowserOSGetPageLoadStatusFunction() = default;
|
||||
@@ -129,104 +28,9 @@ index 0000000000000..53ece24723da0
|
||||
+ protected:
|
||||
+ ~BrowserOSGetPageLoadStatusFunction() override = default;
|
||||
+
|
||||
+ // ExtensionFunction:
|
||||
+ ResponseAction Run() override;
|
||||
+};
|
||||
+
|
||||
+class BrowserOSScrollUpFunction : public ExtensionFunction {
|
||||
+ public:
|
||||
+ DECLARE_EXTENSION_FUNCTION("browserOS.scrollUp", BROWSER_OS_SCROLLUP)
|
||||
+
|
||||
+ BrowserOSScrollUpFunction() = default;
|
||||
+
|
||||
+ protected:
|
||||
+ ~BrowserOSScrollUpFunction() override = default;
|
||||
+
|
||||
+ // ExtensionFunction:
|
||||
+ ResponseAction Run() override;
|
||||
+};
|
||||
+
|
||||
+class BrowserOSScrollDownFunction : public ExtensionFunction {
|
||||
+ public:
|
||||
+ DECLARE_EXTENSION_FUNCTION("browserOS.scrollDown", BROWSER_OS_SCROLLDOWN)
|
||||
+
|
||||
+ BrowserOSScrollDownFunction() = default;
|
||||
+
|
||||
+ protected:
|
||||
+ ~BrowserOSScrollDownFunction() override = default;
|
||||
+
|
||||
+ // ExtensionFunction:
|
||||
+ ResponseAction Run() override;
|
||||
+};
|
||||
+
|
||||
+class BrowserOSScrollToNodeFunction : public ExtensionFunction {
|
||||
+ public:
|
||||
+ DECLARE_EXTENSION_FUNCTION("browserOS.scrollToNode", BROWSER_OS_SCROLLTONODE)
|
||||
+
|
||||
+ BrowserOSScrollToNodeFunction() = default;
|
||||
+
|
||||
+ protected:
|
||||
+ ~BrowserOSScrollToNodeFunction() override = default;
|
||||
+
|
||||
+ // ExtensionFunction:
|
||||
+ ResponseAction Run() override;
|
||||
+};
|
||||
+
|
||||
+class BrowserOSSendKeysFunction : public ExtensionFunction {
|
||||
+ public:
|
||||
+ DECLARE_EXTENSION_FUNCTION("browserOS.sendKeys", BROWSER_OS_SENDKEYS)
|
||||
+
|
||||
+ BrowserOSSendKeysFunction() = default;
|
||||
+
|
||||
+ protected:
|
||||
+ ~BrowserOSSendKeysFunction() override = default;
|
||||
+
|
||||
+ // ExtensionFunction:
|
||||
+ ResponseAction Run() override;
|
||||
+};
|
||||
+
|
||||
+class BrowserOSCaptureScreenshotFunction : public ExtensionFunction {
|
||||
+ public:
|
||||
+ DECLARE_EXTENSION_FUNCTION("browserOS.captureScreenshot", BROWSER_OS_CAPTURESCREENSHOT)
|
||||
+
|
||||
+ BrowserOSCaptureScreenshotFunction();
|
||||
+
|
||||
+ protected:
|
||||
+ ~BrowserOSCaptureScreenshotFunction() override;
|
||||
+
|
||||
+ // ExtensionFunction:
|
||||
+ ResponseAction Run() override;
|
||||
+
|
||||
+ private:
|
||||
+ void DrawHighlightsAndCapture();
|
||||
+ void CaptureScreenshotNow();
|
||||
+ void OnScreenshotCaptured(const content::CopyFromSurfaceResult& result);
|
||||
+
|
||||
+ // Store web contents and tab id for highlight operations
|
||||
+ base::WeakPtr<content::WebContents> web_contents_;
|
||||
+ int tab_id_ = -1;
|
||||
+ gfx::Size target_size_;
|
||||
+ bool show_highlights_ = false;
|
||||
+ bool use_exact_dimensions_ = false;
|
||||
+};
|
||||
+
|
||||
+class BrowserOSGetSnapshotFunction : public ExtensionFunction {
|
||||
+ public:
|
||||
+ DECLARE_EXTENSION_FUNCTION("browserOS.getSnapshot", BROWSER_OS_GETSNAPSHOT)
|
||||
+
|
||||
+ BrowserOSGetSnapshotFunction() = default;
|
||||
+
|
||||
+ protected:
|
||||
+ ~BrowserOSGetSnapshotFunction() override = default;
|
||||
+
|
||||
+ // ExtensionFunction:
|
||||
+ ResponseAction Run() override;
|
||||
+
|
||||
+ private:
|
||||
+ void OnAccessibilityTreeReceived(ui::AXTreeUpdate& tree_update);
|
||||
+};
|
||||
+
|
||||
+// Settings API functions
|
||||
+class BrowserOSGetPrefFunction : public ExtensionFunction {
|
||||
+ public:
|
||||
+ DECLARE_EXTENSION_FUNCTION("browserOS.getPref", BROWSER_OS_GETPREF)
|
||||
@@ -236,7 +40,6 @@ index 0000000000000..53ece24723da0
|
||||
+ protected:
|
||||
+ ~BrowserOSGetPrefFunction() override = default;
|
||||
+
|
||||
+ // ExtensionFunction:
|
||||
+ ResponseAction Run() override;
|
||||
+};
|
||||
+
|
||||
@@ -249,7 +52,6 @@ index 0000000000000..53ece24723da0
|
||||
+ protected:
|
||||
+ ~BrowserOSSetPrefFunction() override = default;
|
||||
+
|
||||
+ // ExtensionFunction:
|
||||
+ ResponseAction Run() override;
|
||||
+};
|
||||
+
|
||||
@@ -262,7 +64,6 @@ index 0000000000000..53ece24723da0
|
||||
+ protected:
|
||||
+ ~BrowserOSGetAllPrefsFunction() override = default;
|
||||
+
|
||||
+ // ExtensionFunction:
|
||||
+ ResponseAction Run() override;
|
||||
+};
|
||||
+
|
||||
@@ -275,75 +76,74 @@ index 0000000000000..53ece24723da0
|
||||
+ protected:
|
||||
+ ~BrowserOSLogMetricFunction() override = default;
|
||||
+
|
||||
+ // ExtensionFunction:
|
||||
+ ResponseAction Run() override;
|
||||
+};
|
||||
+
|
||||
+class BrowserOSGetVersionNumberFunction : public ExtensionFunction {
|
||||
+ public:
|
||||
+ DECLARE_EXTENSION_FUNCTION("browserOS.getVersionNumber", BROWSER_OS_GETVERSIONNUMBER)
|
||||
+ DECLARE_EXTENSION_FUNCTION("browserOS.getVersionNumber",
|
||||
+ BROWSER_OS_GETVERSIONNUMBER)
|
||||
+
|
||||
+ BrowserOSGetVersionNumberFunction() = default;
|
||||
+
|
||||
+ protected:
|
||||
+ ~BrowserOSGetVersionNumberFunction() override = default;
|
||||
+
|
||||
+ // ExtensionFunction:
|
||||
+ ResponseAction Run() override;
|
||||
+};
|
||||
+
|
||||
+class BrowserOSGetBrowserosVersionNumberFunction : public ExtensionFunction {
|
||||
+ public:
|
||||
+ DECLARE_EXTENSION_FUNCTION("browserOS.getBrowserosVersionNumber", BROWSER_OS_GETBROWSEROSVERSIONNUMBER)
|
||||
+ DECLARE_EXTENSION_FUNCTION("browserOS.getBrowserosVersionNumber",
|
||||
+ BROWSER_OS_GETBROWSEROSVERSIONNUMBER)
|
||||
+
|
||||
+ BrowserOSGetBrowserosVersionNumberFunction() = default;
|
||||
+
|
||||
+ protected:
|
||||
+ ~BrowserOSGetBrowserosVersionNumberFunction() override = default;
|
||||
+
|
||||
+ // ExtensionFunction:
|
||||
+ ResponseAction Run() override;
|
||||
+};
|
||||
+
|
||||
+class BrowserOSExecuteJavaScriptFunction : public ExtensionFunction {
|
||||
+ public:
|
||||
+ DECLARE_EXTENSION_FUNCTION("browserOS.executeJavaScript", BROWSER_OS_EXECUTEJAVASCRIPT)
|
||||
+ DECLARE_EXTENSION_FUNCTION("browserOS.executeJavaScript",
|
||||
+ BROWSER_OS_EXECUTEJAVASCRIPT)
|
||||
+
|
||||
+ BrowserOSExecuteJavaScriptFunction() = default;
|
||||
+
|
||||
+ protected:
|
||||
+ ~BrowserOSExecuteJavaScriptFunction() override = default;
|
||||
+
|
||||
+ // ExtensionFunction:
|
||||
+ ResponseAction Run() override;
|
||||
+
|
||||
+
|
||||
+ private:
|
||||
+ void OnJavaScriptExecuted(base::Value result);
|
||||
+};
|
||||
+
|
||||
+class BrowserOSClickCoordinatesFunction : public ExtensionFunction {
|
||||
+ public:
|
||||
+ DECLARE_EXTENSION_FUNCTION("browserOS.clickCoordinates", BROWSER_OS_CLICKCOORDINATES)
|
||||
+ DECLARE_EXTENSION_FUNCTION("browserOS.clickCoordinates",
|
||||
+ BROWSER_OS_CLICKCOORDINATES)
|
||||
+
|
||||
+ BrowserOSClickCoordinatesFunction() = default;
|
||||
+
|
||||
+ protected:
|
||||
+ ~BrowserOSClickCoordinatesFunction() override = default;
|
||||
+
|
||||
+ // ExtensionFunction:
|
||||
+ ResponseAction Run() override;
|
||||
+};
|
||||
+
|
||||
+class BrowserOSTypeAtCoordinatesFunction : public ExtensionFunction {
|
||||
+ public:
|
||||
+ DECLARE_EXTENSION_FUNCTION("browserOS.typeAtCoordinates", BROWSER_OS_TYPEATCOORDINATES)
|
||||
+ DECLARE_EXTENSION_FUNCTION("browserOS.typeAtCoordinates",
|
||||
+ BROWSER_OS_TYPEATCOORDINATES)
|
||||
+
|
||||
+ BrowserOSTypeAtCoordinatesFunction() = default;
|
||||
+
|
||||
+ protected:
|
||||
+ ~BrowserOSTypeAtCoordinatesFunction() override = default;
|
||||
+
|
||||
+ // ExtensionFunction:
|
||||
+ ResponseAction Run() override;
|
||||
+};
|
||||
+
|
||||
@@ -364,14 +164,12 @@ index 0000000000000..53ece24723da0
|
||||
+ protected:
|
||||
+ ~BrowserOSChoosePathFunction() override;
|
||||
+
|
||||
+ // ExtensionFunction:
|
||||
+ ResponseAction Run() override;
|
||||
+
|
||||
+ private:
|
||||
+ scoped_refptr<ui::SelectFileDialog> select_file_dialog_;
|
||||
+};
|
||||
+
|
||||
+} // namespace api
|
||||
+} // namespace extensions
|
||||
+} // namespace extensions::api
|
||||
+
|
||||
+#endif // CHROME_BROWSER_EXTENSIONS_API_BROWSER_OS_BROWSER_OS_API_H_
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,9 @@
|
||||
diff --git a/chrome/browser/extensions/api/browser_os/browser_os_api_helpers.h b/chrome/browser/extensions/api/browser_os/browser_os_api_helpers.h
|
||||
new file mode 100644
|
||||
index 0000000000000..2000fa3235290
|
||||
index 0000000000000..24948f72ec077
|
||||
--- /dev/null
|
||||
+++ b/chrome/browser/extensions/api/browser_os/browser_os_api_helpers.h
|
||||
@@ -0,0 +1,137 @@
|
||||
@@ -0,0 +1,49 @@
|
||||
+// Copyright 2024 The Chromium Authors
|
||||
+// Use of this source code is governed by a BSD-style license that can be
|
||||
+// found in the LICENSE file.
|
||||
@@ -12,20 +12,15 @@ index 0000000000000..2000fa3235290
|
||||
+#define CHROME_BROWSER_EXTENSIONS_API_BROWSER_OS_BROWSER_OS_API_HELPERS_H_
|
||||
+
|
||||
+#include <string>
|
||||
+#include <unordered_map>
|
||||
+
|
||||
+#include "base/functional/callback.h"
|
||||
+#include "ui/gfx/geometry/point_f.h"
|
||||
+
|
||||
+namespace content {
|
||||
+class WebContents;
|
||||
+class RenderWidgetHost;
|
||||
+class WebContents;
|
||||
+} // namespace content
|
||||
+
|
||||
+namespace extensions {
|
||||
+namespace api {
|
||||
+
|
||||
+struct NodeInfo;
|
||||
+namespace extensions::api {
|
||||
+
|
||||
+// Returns the multiplicative factor that converts CSS pixels (frame
|
||||
+// coordinates) to widget DIPs for input events. This matches DevTools'
|
||||
@@ -35,109 +30,26 @@ index 0000000000000..2000fa3235290
|
||||
+float CssToWidgetScale(content::WebContents* web_contents,
|
||||
+ content::RenderWidgetHost* rwh);
|
||||
+
|
||||
+// Returns the center point of a node's bounds.
|
||||
+// Bounds are already in CSS pixels from SnapshotProcessor.
|
||||
+gfx::PointF GetNodeCenterPoint(content::WebContents* web_contents,
|
||||
+ const NodeInfo& node_info);
|
||||
+// Dispatches a synthetic left-click (mouse down + up) at |point|, which is
|
||||
+// interpreted as CSS pixels relative to the viewport.
|
||||
+void PointClick(content::WebContents* web_contents, const gfx::PointF& point);
|
||||
+
|
||||
+// Helper to create and dispatch mouse events for clicking
|
||||
+void PointClick(content::WebContents* web_contents,
|
||||
+ const gfx::PointF& point);
|
||||
+// Commits |text| to the currently-focused element via the renderer's IME
|
||||
+// pipeline. Caller must ensure focus is established first.
|
||||
+void NativeType(content::WebContents* web_contents, const std::string& text);
|
||||
+
|
||||
+// Helper to perform HTML-based click using JS (uses ID, class, or tag)
|
||||
+void HtmlClick(content::WebContents* web_contents,
|
||||
+ const NodeInfo& node_info);
|
||||
+
|
||||
+// Helper to perform HTML-based focus using JS (uses ID, class, or tag)
|
||||
+void HtmlFocus(content::WebContents* web_contents,
|
||||
+ const NodeInfo& node_info);
|
||||
+
|
||||
+// Helper to perform accessibility action: DoDefault (click)
|
||||
+// Returns true if action was sent successfully
|
||||
+bool AccessibilityDoDefault(content::WebContents* web_contents,
|
||||
+ const NodeInfo& node_info);
|
||||
+
|
||||
+// Helper to perform accessibility action: Focus
|
||||
+// Returns true if action was sent successfully
|
||||
+bool AccessibilityFocus(content::WebContents* web_contents,
|
||||
+ const NodeInfo& node_info);
|
||||
+
|
||||
+// Helper to perform accessibility action: ScrollToMakeVisible
|
||||
+// center_in_viewport: if true, centers element in viewport; otherwise uses closest edge
|
||||
+// Returns true if action was sent successfully
|
||||
+bool AccessibilityScrollToMakeVisible(content::WebContents* web_contents,
|
||||
+ const NodeInfo& node_info,
|
||||
+ bool center_in_viewport = true);
|
||||
+
|
||||
+// Helper to perform accessibility action: SetValue
|
||||
+// Sets the value of an input field or editable element
|
||||
+// Returns true if action was sent successfully
|
||||
+bool AccessibilitySetValue(content::WebContents* web_contents,
|
||||
+ const NodeInfo& node_info,
|
||||
+ const std::string& text);
|
||||
+
|
||||
+// Helper to perform scroll actions using mouse wheel events
|
||||
+void Scroll(content::WebContents* web_contents,
|
||||
+ int delta_x,
|
||||
+ int delta_y,
|
||||
+ bool precise = false);
|
||||
+
|
||||
+// Helper to send special key events
|
||||
+void KeyPress(content::WebContents* web_contents,
|
||||
+ const std::string& key);
|
||||
+
|
||||
+// Helper to type text into a focused element using native IME
|
||||
+void NativeType(content::WebContents* web_contents,
|
||||
+ const std::string& text);
|
||||
+
|
||||
+// Helper to set text value using JavaScript
|
||||
+void JavaScriptType(content::WebContents* web_contents,
|
||||
+ const NodeInfo& node_info,
|
||||
+ const std::string& text);
|
||||
+
|
||||
+// Helper to perform a click with change detection and retrying
|
||||
+// Returns true if the click caused a change in the page
|
||||
+bool ClickWithDetection(content::WebContents* web_contents,
|
||||
+ const NodeInfo& node_info);
|
||||
+
|
||||
+// Helper to perform typing with change detection
|
||||
+// Returns true if the typing caused a change in the page
|
||||
+bool TypeWithDetection(content::WebContents* web_contents,
|
||||
+ const NodeInfo& node_info,
|
||||
+ const std::string& text);
|
||||
+
|
||||
+// Helper to clear an input field with change detection
|
||||
+// Returns true if the clear caused a change in the page
|
||||
+bool ClearWithDetection(content::WebContents* web_contents,
|
||||
+ const NodeInfo& node_info);
|
||||
+
|
||||
+// Helper to send a key press with change detection
|
||||
+// Returns true if the key press caused a change in the page
|
||||
+bool KeyPressWithDetection(content::WebContents* web_contents,
|
||||
+ const std::string& key);
|
||||
+
|
||||
+// Helper to show highlights for clickable, typeable, and selectable elements that are in viewport
|
||||
+// Only highlights elements that are actually visible and interactable
|
||||
+void ShowHighlights(content::WebContents* web_contents,
|
||||
+ const std::unordered_map<uint32_t, NodeInfo>& node_mappings,
|
||||
+ bool show_labels = true);
|
||||
+
|
||||
+// Helper to remove all bounding box highlights from the page
|
||||
+void RemoveHighlights(content::WebContents* web_contents);
|
||||
+
|
||||
+// Helper to click at specific coordinates with change detection
|
||||
+// Returns true if the click caused a detectable change in the page
|
||||
+// Clicks at |point| and reports whether the page exhibited any observable
|
||||
+// change within a short window (DOM mutation, navigation, focus shift, etc.).
|
||||
+bool ClickCoordinatesWithDetection(content::WebContents* web_contents,
|
||||
+ const gfx::PointF& point);
|
||||
+
|
||||
+// Helper to type text after clicking at coordinates to focus element
|
||||
+// First clicks at the coordinates to focus an element, then types the text
|
||||
+// Returns true if the operation succeeded
|
||||
+// Clicks at |point| to focus, then types |text|. Falls back to JavaScript
|
||||
+// assignment if native IME typing yields no observable change. Returns true
|
||||
+// on detected success or JS fallback completion.
|
||||
+bool TypeAtCoordinatesWithDetection(content::WebContents* web_contents,
|
||||
+ const gfx::PointF& point,
|
||||
+ const std::string& text);
|
||||
+
|
||||
+} // namespace api
|
||||
+} // namespace extensions
|
||||
+} // namespace extensions::api
|
||||
+
|
||||
+#endif // CHROME_BROWSER_EXTENSIONS_API_BROWSER_OS_BROWSER_OS_API_HELPERS_H_
|
||||
|
||||
@@ -1,46 +1,23 @@
|
||||
diff --git a/chrome/browser/extensions/api/browser_os/browser_os_api_utils.cc b/chrome/browser/extensions/api/browser_os/browser_os_api_utils.cc
|
||||
new file mode 100644
|
||||
index 0000000000000..0c3a060fc70de
|
||||
index 0000000000000..715703aca2e4d
|
||||
--- /dev/null
|
||||
+++ b/chrome/browser/extensions/api/browser_os/browser_os_api_utils.cc
|
||||
@@ -0,0 +1,167 @@
|
||||
@@ -0,0 +1,58 @@
|
||||
+// Copyright 2024 The Chromium Authors
|
||||
+// Use of this source code is governed by a BSD-style license that can be
|
||||
+// found in the LICENSE file.
|
||||
+
|
||||
+#include "chrome/browser/extensions/api/browser_os/browser_os_api_utils.h"
|
||||
+
|
||||
+#include "base/hash/hash.h"
|
||||
+#include "base/no_destructor.h"
|
||||
+#include "base/strings/string_number_conversions.h"
|
||||
+#include "base/strings/utf_string_conversions.h"
|
||||
+#include "chrome/browser/extensions/extension_tab_util.h"
|
||||
+#include "chrome/browser/extensions/window_controller.h"
|
||||
+#include "chrome/browser/ui/browser.h"
|
||||
+#include "chrome/browser/ui/browser_finder.h"
|
||||
+#include "chrome/browser/ui/tabs/tab_strip_model.h"
|
||||
+#include "content/public/browser/web_contents.h"
|
||||
+#include "ui/accessibility/ax_role_properties.h"
|
||||
+
|
||||
+namespace extensions {
|
||||
+namespace api {
|
||||
+
|
||||
+// NodeInfo implementation
|
||||
+NodeInfo::NodeInfo() : ax_node_id(0), ax_tree_id(), node_type(browser_os::InteractiveNodeType::kOther), in_viewport(false) {}
|
||||
+NodeInfo::~NodeInfo() = default;
|
||||
+NodeInfo::NodeInfo(const NodeInfo&) = default;
|
||||
+NodeInfo& NodeInfo::operator=(const NodeInfo&) = default;
|
||||
+NodeInfo::NodeInfo(NodeInfo&&) = default;
|
||||
+NodeInfo& NodeInfo::operator=(NodeInfo&&) = default;
|
||||
+
|
||||
+// Global node ID mappings storage
|
||||
+// Use NoDestructor to avoid exit-time destructor
|
||||
+std::unordered_map<int, std::unordered_map<uint32_t, NodeInfo>>&
|
||||
+GetNodeIdMappings() {
|
||||
+ static base::NoDestructor<std::unordered_map<int, std::unordered_map<uint32_t, NodeInfo>>>
|
||||
+ g_node_id_mappings;
|
||||
+ return *g_node_id_mappings;
|
||||
+}
|
||||
+namespace extensions::api {
|
||||
+
|
||||
+std::optional<TabInfo> GetTabFromOptionalId(
|
||||
+ std::optional<int> tab_id_param,
|
||||
@@ -49,9 +26,8 @@ index 0000000000000..0c3a060fc70de
|
||||
+ std::string* error_message) {
|
||||
+ content::WebContents* web_contents = nullptr;
|
||||
+ int tab_id = -1;
|
||||
+
|
||||
+
|
||||
+ if (tab_id_param) {
|
||||
+ // Get specific tab by ID
|
||||
+ WindowController* controller = nullptr;
|
||||
+ int tab_index = -1;
|
||||
+ if (!ExtensionTabUtil::GetTabById(*tab_id_param, browser_context,
|
||||
@@ -65,7 +41,6 @@ index 0000000000000..0c3a060fc70de
|
||||
+ }
|
||||
+ tab_id = *tab_id_param;
|
||||
+ } else {
|
||||
+ // Get active tab
|
||||
+ Browser* browser = chrome::FindLastActive();
|
||||
+ if (!browser) {
|
||||
+ if (error_message) {
|
||||
@@ -73,7 +48,6 @@ index 0000000000000..0c3a060fc70de
|
||||
+ }
|
||||
+ return std::nullopt;
|
||||
+ }
|
||||
+
|
||||
+ web_contents = browser->tab_strip_model()->GetActiveWebContents();
|
||||
+ if (!web_contents) {
|
||||
+ if (error_message) {
|
||||
@@ -87,87 +61,4 @@ index 0000000000000..0c3a060fc70de
|
||||
+ return TabInfo(web_contents, tab_id);
|
||||
+}
|
||||
+
|
||||
+// Helper to determine if a node is interactive (clickable/typeable/selectable)
|
||||
+browser_os::InteractiveNodeType GetInteractiveNodeType(
|
||||
+ const ui::AXNodeData& node_data) {
|
||||
+
|
||||
+ // Skip invisible or ignored nodes early
|
||||
+ if (node_data.IsInvisibleOrIgnored()) {
|
||||
+ return browser_os::InteractiveNodeType::kOther;
|
||||
+ }
|
||||
+
|
||||
+ // Use built-in IsTextField() and related methods for typeable elements
|
||||
+ if (node_data.IsTextField() ||
|
||||
+ node_data.IsPasswordField() ||
|
||||
+ node_data.IsAtomicTextField() ||
|
||||
+ node_data.IsNonAtomicTextField() ||
|
||||
+ node_data.IsSpinnerTextField()) {
|
||||
+ return browser_os::InteractiveNodeType::kTypeable;
|
||||
+ }
|
||||
+
|
||||
+ // Use built-in IsSelectable() for selectable elements
|
||||
+ if (node_data.IsSelectable()) {
|
||||
+ return browser_os::InteractiveNodeType::kSelectable;
|
||||
+ }
|
||||
+
|
||||
+ // Use built-in IsClickable() method
|
||||
+ if (node_data.IsClickable()) {
|
||||
+ return browser_os::InteractiveNodeType::kClickable;
|
||||
+ }
|
||||
+
|
||||
+ // Additional check for combobox and list options which might not be caught by IsSelectable
|
||||
+ using Role = ax::mojom::Role;
|
||||
+ if (node_data.role == Role::kComboBoxSelect ||
|
||||
+ node_data.role == Role::kComboBoxMenuButton ||
|
||||
+ node_data.role == Role::kComboBoxGrouping ||
|
||||
+ node_data.role == Role::kListBox ||
|
||||
+ node_data.role == Role::kListBoxOption ||
|
||||
+ node_data.role == Role::kMenuListOption ||
|
||||
+ node_data.role == Role::kMenuItem ||
|
||||
+ node_data.role == Role::kMenuItemCheckBox ||
|
||||
+ node_data.role == Role::kMenuItemRadio) {
|
||||
+ return browser_os::InteractiveNodeType::kSelectable;
|
||||
+ }
|
||||
+
|
||||
+ return browser_os::InteractiveNodeType::kOther;
|
||||
+}
|
||||
+
|
||||
+// Helper to get the HTML tag name from AX role
|
||||
+std::string GetTagFromRole(ax::mojom::Role role) {
|
||||
+ switch (role) {
|
||||
+ case ax::mojom::Role::kButton:
|
||||
+ return "button";
|
||||
+ case ax::mojom::Role::kLink:
|
||||
+ return "a";
|
||||
+ case ax::mojom::Role::kTextField:
|
||||
+ case ax::mojom::Role::kSearchBox:
|
||||
+ return "input";
|
||||
+ case ax::mojom::Role::kTextFieldWithComboBox:
|
||||
+ return "input";
|
||||
+ case ax::mojom::Role::kComboBoxSelect:
|
||||
+ return "select";
|
||||
+ case ax::mojom::Role::kCheckBox:
|
||||
+ return "input";
|
||||
+ case ax::mojom::Role::kRadioButton:
|
||||
+ return "input";
|
||||
+ case ax::mojom::Role::kImage:
|
||||
+ return "img";
|
||||
+ case ax::mojom::Role::kHeading:
|
||||
+ return "h1"; // Could be h1-h6
|
||||
+ case ax::mojom::Role::kParagraph:
|
||||
+ return "p";
|
||||
+ case ax::mojom::Role::kListItem:
|
||||
+ return "li";
|
||||
+ case ax::mojom::Role::kList:
|
||||
+ return "ul";
|
||||
+ case ax::mojom::Role::kForm:
|
||||
+ return "form";
|
||||
+ case ax::mojom::Role::kTable:
|
||||
+ return "table";
|
||||
+ default:
|
||||
+ return "div";
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+} // namespace api
|
||||
+} // namespace extensions
|
||||
+} // namespace extensions::api
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
diff --git a/chrome/browser/extensions/api/browser_os/browser_os_api_utils.h b/chrome/browser/extensions/api/browser_os/browser_os_api_utils.h
|
||||
new file mode 100644
|
||||
index 0000000000000..f4fdcb73186cd
|
||||
index 0000000000000..f5dd08378e67f
|
||||
--- /dev/null
|
||||
+++ b/chrome/browser/extensions/api/browser_os/browser_os_api_utils.h
|
||||
@@ -0,0 +1,80 @@
|
||||
@@ -0,0 +1,38 @@
|
||||
+// Copyright 2024 The Chromium Authors
|
||||
+// Use of this source code is governed by a BSD-style license that can be
|
||||
+// found in the LICENSE file.
|
||||
@@ -13,75 +13,32 @@ index 0000000000000..f4fdcb73186cd
|
||||
+
|
||||
+#include <optional>
|
||||
+#include <string>
|
||||
+#include <unordered_map>
|
||||
+
|
||||
+#include "base/memory/raw_ptr.h"
|
||||
+#include "base/values.h"
|
||||
+#include "chrome/common/extensions/api/browser_os.h"
|
||||
+#include "ui/accessibility/ax_node_data.h"
|
||||
+#include "ui/accessibility/ax_tree_id.h"
|
||||
+#include "ui/gfx/geometry/rect_f.h"
|
||||
+
|
||||
+namespace content {
|
||||
+class BrowserContext;
|
||||
+class RenderWidgetHost;
|
||||
+class WebContents;
|
||||
+} // namespace content
|
||||
+
|
||||
+namespace extensions {
|
||||
+
|
||||
+class WindowController;
|
||||
+
|
||||
+namespace api {
|
||||
+namespace extensions::api {
|
||||
+
|
||||
+// Result structure for tab retrieval
|
||||
+struct TabInfo {
|
||||
+ raw_ptr<content::WebContents> web_contents;
|
||||
+ int tab_id;
|
||||
+
|
||||
+ TabInfo(content::WebContents* wc, int id)
|
||||
+ : web_contents(wc), tab_id(id) {}
|
||||
+
|
||||
+ TabInfo(content::WebContents* wc, int id) : web_contents(wc), tab_id(id) {}
|
||||
+};
|
||||
+
|
||||
+// Stores mapping information for a node
|
||||
+struct NodeInfo {
|
||||
+ NodeInfo();
|
||||
+ ~NodeInfo();
|
||||
+ NodeInfo(const NodeInfo&);
|
||||
+ NodeInfo& operator=(const NodeInfo&);
|
||||
+ NodeInfo(NodeInfo&&);
|
||||
+ NodeInfo& operator=(NodeInfo&&);
|
||||
+
|
||||
+ int32_t ax_node_id;
|
||||
+ ui::AXTreeID ax_tree_id; // Tree ID for change detection
|
||||
+ gfx::RectF bounds; // Absolute bounds in CSS pixels
|
||||
+ std::unordered_map<std::string, std::string> attributes; // All computed attributes
|
||||
+ browser_os::InteractiveNodeType node_type; // Cached node type to avoid recomputation
|
||||
+ bool in_viewport; // Whether the node is currently visible in viewport
|
||||
+};
|
||||
+
|
||||
+// Global node ID mappings storage
|
||||
+std::unordered_map<int, std::unordered_map<uint32_t, NodeInfo>>&
|
||||
+GetNodeIdMappings();
|
||||
+
|
||||
+// Helper to get WebContents and tab ID from optional tab_id parameter
|
||||
+// Returns nullptr if tab is not found, with error message set
|
||||
+// Helper to get WebContents and tab ID from optional tab_id parameter.
|
||||
+// Returns nullopt if tab is not found, with error message set.
|
||||
+std::optional<TabInfo> GetTabFromOptionalId(
|
||||
+ std::optional<int> tab_id_param,
|
||||
+ content::BrowserContext* browser_context,
|
||||
+ bool include_incognito_information,
|
||||
+ std::string* error_message);
|
||||
+
|
||||
+// Helper to determine if a node is interactive (clickable/typable)
|
||||
+browser_os::InteractiveNodeType GetInteractiveNodeType(
|
||||
+ const ui::AXNodeData& node_data);
|
||||
+
|
||||
+// Helper to get the HTML tag name from AX role
|
||||
+std::string GetTagFromRole(ax::mojom::Role role);
|
||||
+
|
||||
+
|
||||
+} // namespace api
|
||||
+} // namespace extensions
|
||||
+} // namespace extensions::api
|
||||
+
|
||||
+#endif // CHROME_BROWSER_EXTENSIONS_API_BROWSER_OS_BROWSER_OS_API_UTILS_H_
|
||||
\ No newline at end of file
|
||||
|
||||
@@ -1,253 +0,0 @@
|
||||
diff --git a/chrome/browser/extensions/api/browser_os/browser_os_content_processor.cc b/chrome/browser/extensions/api/browser_os/browser_os_content_processor.cc
|
||||
new file mode 100644
|
||||
index 0000000000000..4166f1a38737c
|
||||
--- /dev/null
|
||||
+++ b/chrome/browser/extensions/api/browser_os/browser_os_content_processor.cc
|
||||
@@ -0,0 +1,247 @@
|
||||
+// Copyright 2024 The Chromium Authors
|
||||
+// Use of this source code is governed by a BSD-style license that can be
|
||||
+// found in the LICENSE file.
|
||||
+
|
||||
+#include "chrome/browser/extensions/api/browser_os/browser_os_content_processor.h"
|
||||
+
|
||||
+#include <algorithm>
|
||||
+
|
||||
+#include "base/logging.h"
|
||||
+#include "base/strings/string_util.h"
|
||||
+#include "ui/accessibility/ax_enum_util.h"
|
||||
+#include "ui/accessibility/ax_enums.mojom.h"
|
||||
+#include "ui/accessibility/ax_node_data.h"
|
||||
+#include "ui/accessibility/ax_role_properties.h"
|
||||
+#include "ui/gfx/geometry/rect.h"
|
||||
+#include "ui/gfx/geometry/rect_conversions.h"
|
||||
+
|
||||
+namespace extensions {
|
||||
+namespace api {
|
||||
+
|
||||
+namespace {
|
||||
+
|
||||
+// Clean whitespace from text
|
||||
+std::string CleanText(const std::string& text) {
|
||||
+ std::string cleaned = std::string(base::TrimWhitespaceASCII(text, base::TRIM_ALL));
|
||||
+
|
||||
+ // Replace multiple spaces with single space
|
||||
+ std::string result;
|
||||
+ bool prev_space = false;
|
||||
+ for (char c : cleaned) {
|
||||
+ if (std::isspace(c)) {
|
||||
+ if (!prev_space) {
|
||||
+ result += ' ';
|
||||
+ prev_space = true;
|
||||
+ }
|
||||
+ } else {
|
||||
+ result += c;
|
||||
+ prev_space = false;
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ return result;
|
||||
+}
|
||||
+
|
||||
+} // namespace
|
||||
+
|
||||
+// static
|
||||
+std::vector<browser_os::ContentItem> ContentProcessor::ExtractPageContent(
|
||||
+ const ui::AXTreeUpdate& tree_update) {
|
||||
+
|
||||
+ std::vector<browser_os::ContentItem> items;
|
||||
+
|
||||
+ if (tree_update.nodes.empty()) {
|
||||
+ LOG(INFO) << "browseros: ExtractPageContent - tree is empty";
|
||||
+ return items;
|
||||
+ }
|
||||
+
|
||||
+ LOG(INFO) << "browseros: ExtractPageContent - processing " << tree_update.nodes.size() << " nodes";
|
||||
+
|
||||
+ // Build node map for O(1) lookup
|
||||
+ std::unordered_map<int32_t, ui::AXNodeData> node_map;
|
||||
+ for (const auto& node : tree_update.nodes) {
|
||||
+ node_map[node.id] = node;
|
||||
+ }
|
||||
+
|
||||
+ // Start DFS from root
|
||||
+ TraverseDFS(tree_update.root_id, node_map, items);
|
||||
+
|
||||
+ LOG(INFO) << "browseros: ExtractPageContent - extracted " << items.size() << " items";
|
||||
+
|
||||
+ return items;
|
||||
+}
|
||||
+
|
||||
+// static
|
||||
+void ContentProcessor::TraverseDFS(
|
||||
+ int32_t node_id,
|
||||
+ const std::unordered_map<int32_t, ui::AXNodeData>& node_map,
|
||||
+ std::vector<browser_os::ContentItem>& items) {
|
||||
+
|
||||
+ auto it = node_map.find(node_id);
|
||||
+ if (it == node_map.end()) {
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ const ui::AXNodeData& node = it->second;
|
||||
+
|
||||
+ // Skip extracting from ignored nodes, but still recurse to children
|
||||
+ if (node.IsIgnored()) {
|
||||
+ for (int32_t child_id : node.child_ids) {
|
||||
+ TraverseDFS(child_id, node_map, items);
|
||||
+ }
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ // Extract content at semantic boundaries
|
||||
+ // Don't recurse into these - their children are just formatting
|
||||
+
|
||||
+ if (ui::IsHeading(node.role)) {
|
||||
+ items.push_back(ExtractHeading(node));
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ if (ui::IsLink(node.role)) {
|
||||
+ items.push_back(ExtractLink(node));
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ if (ui::IsImage(node.role)) {
|
||||
+ items.push_back(ExtractImage(node));
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ if (node.role == ax::mojom::Role::kVideo) {
|
||||
+ items.push_back(ExtractVideo(node));
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ if (ui::IsText(node.role)) {
|
||||
+ // Extract text content
|
||||
+ auto item = ExtractText(node);
|
||||
+ if (item.text.has_value() && !item.text->empty()) {
|
||||
+ items.push_back(std::move(item));
|
||||
+ }
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ // For container nodes (divs, sections, etc.), recurse to children
|
||||
+ for (int32_t child_id : node.child_ids) {
|
||||
+ TraverseDFS(child_id, node_map, items);
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+// static
|
||||
+browser_os::ContentItem ContentProcessor::ExtractHeading(
|
||||
+ const ui::AXNodeData& node) {
|
||||
+ browser_os::ContentItem item;
|
||||
+ item.type = browser_os::ContentItemType::kHeading;
|
||||
+
|
||||
+ std::string name = GetAccessibleName(node);
|
||||
+ if (!name.empty()) {
|
||||
+ item.text = CleanText(name);
|
||||
+ }
|
||||
+
|
||||
+ // Get heading level from hierarchical level attribute
|
||||
+ if (node.HasIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel)) {
|
||||
+ int level = node.GetIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel);
|
||||
+ item.level = std::clamp(level, 1, 6);
|
||||
+ } else {
|
||||
+ // Default to level 2 if not specified
|
||||
+ item.level = 2;
|
||||
+ }
|
||||
+
|
||||
+ return item;
|
||||
+}
|
||||
+
|
||||
+// static
|
||||
+browser_os::ContentItem ContentProcessor::ExtractText(
|
||||
+ const ui::AXNodeData& node) {
|
||||
+ browser_os::ContentItem item;
|
||||
+ item.type = browser_os::ContentItemType::kText;
|
||||
+
|
||||
+ std::string name = GetAccessibleName(node);
|
||||
+ if (!name.empty()) {
|
||||
+ item.text = CleanText(name);
|
||||
+ }
|
||||
+
|
||||
+ return item;
|
||||
+}
|
||||
+
|
||||
+// static
|
||||
+browser_os::ContentItem ContentProcessor::ExtractLink(
|
||||
+ const ui::AXNodeData& node) {
|
||||
+ browser_os::ContentItem item;
|
||||
+ item.type = browser_os::ContentItemType::kLink;
|
||||
+
|
||||
+ std::string name = GetAccessibleName(node);
|
||||
+ if (!name.empty()) {
|
||||
+ item.text = CleanText(name);
|
||||
+ }
|
||||
+
|
||||
+ // Get URL from url attribute
|
||||
+ if (node.HasStringAttribute(ax::mojom::StringAttribute::kUrl)) {
|
||||
+ item.url = node.GetStringAttribute(ax::mojom::StringAttribute::kUrl);
|
||||
+ }
|
||||
+
|
||||
+ return item;
|
||||
+}
|
||||
+
|
||||
+// static
|
||||
+browser_os::ContentItem ContentProcessor::ExtractImage(
|
||||
+ const ui::AXNodeData& node) {
|
||||
+ browser_os::ContentItem item;
|
||||
+ item.type = browser_os::ContentItemType::kImage;
|
||||
+
|
||||
+ // Get alt text from name
|
||||
+ std::string name = GetAccessibleName(node);
|
||||
+ if (!name.empty()) {
|
||||
+ item.alt = CleanText(name);
|
||||
+ }
|
||||
+
|
||||
+ // Get image URL
|
||||
+ if (node.HasStringAttribute(ax::mojom::StringAttribute::kUrl)) {
|
||||
+ item.url = node.GetStringAttribute(ax::mojom::StringAttribute::kUrl);
|
||||
+ } else if (node.HasStringAttribute(ax::mojom::StringAttribute::kImageDataUrl)) {
|
||||
+ item.url = node.GetStringAttribute(ax::mojom::StringAttribute::kImageDataUrl);
|
||||
+ }
|
||||
+
|
||||
+ return item;
|
||||
+}
|
||||
+
|
||||
+// static
|
||||
+browser_os::ContentItem ContentProcessor::ExtractVideo(
|
||||
+ const ui::AXNodeData& node) {
|
||||
+ browser_os::ContentItem item;
|
||||
+ item.type = browser_os::ContentItemType::kVideo;
|
||||
+
|
||||
+ // Get video title from name
|
||||
+ std::string name = GetAccessibleName(node);
|
||||
+ if (!name.empty()) {
|
||||
+ item.alt = CleanText(name);
|
||||
+ }
|
||||
+
|
||||
+ // Get video URL
|
||||
+ if (node.HasStringAttribute(ax::mojom::StringAttribute::kUrl)) {
|
||||
+ item.url = node.GetStringAttribute(ax::mojom::StringAttribute::kUrl);
|
||||
+ }
|
||||
+
|
||||
+ return item;
|
||||
+}
|
||||
+
|
||||
+// static
|
||||
+std::string ContentProcessor::GetAccessibleName(const ui::AXNodeData& node) {
|
||||
+ // Try name attribute first
|
||||
+ if (node.HasStringAttribute(ax::mojom::StringAttribute::kName)) {
|
||||
+ return node.GetStringAttribute(ax::mojom::StringAttribute::kName);
|
||||
+ }
|
||||
+
|
||||
+ // Fall back to value attribute
|
||||
+ if (node.HasStringAttribute(ax::mojom::StringAttribute::kValue)) {
|
||||
+ return node.GetStringAttribute(ax::mojom::StringAttribute::kValue);
|
||||
+ }
|
||||
+
|
||||
+ return "";
|
||||
+}
|
||||
+
|
||||
+} // namespace api
|
||||
+} // namespace extensions
|
||||
@@ -1,63 +0,0 @@
|
||||
diff --git a/chrome/browser/extensions/api/browser_os/browser_os_content_processor.h b/chrome/browser/extensions/api/browser_os/browser_os_content_processor.h
|
||||
new file mode 100644
|
||||
index 0000000000000..472c7b114fe97
|
||||
--- /dev/null
|
||||
+++ b/chrome/browser/extensions/api/browser_os/browser_os_content_processor.h
|
||||
@@ -0,0 +1,57 @@
|
||||
+// Copyright 2024 The Chromium Authors
|
||||
+// Use of this source code is governed by a BSD-style license that can be
|
||||
+// found in the LICENSE file.
|
||||
+
|
||||
+#ifndef CHROME_BROWSER_EXTENSIONS_API_BROWSER_OS_BROWSER_OS_CONTENT_PROCESSOR_H_
|
||||
+#define CHROME_BROWSER_EXTENSIONS_API_BROWSER_OS_BROWSER_OS_CONTENT_PROCESSOR_H_
|
||||
+
|
||||
+#include <string>
|
||||
+#include <unordered_map>
|
||||
+#include <vector>
|
||||
+
|
||||
+#include "chrome/common/extensions/api/browser_os.h"
|
||||
+#include "ui/accessibility/ax_tree_update.h"
|
||||
+#include "ui/gfx/geometry/size.h"
|
||||
+
|
||||
+namespace ui {
|
||||
+struct AXNodeData;
|
||||
+} // namespace ui
|
||||
+
|
||||
+namespace extensions {
|
||||
+namespace api {
|
||||
+
|
||||
+// Extracts page content (headings, text, links, images, videos) from
|
||||
+// accessibility tree in document order using depth-first traversal.
|
||||
+class ContentProcessor {
|
||||
+ public:
|
||||
+ ContentProcessor() = delete;
|
||||
+ ContentProcessor(const ContentProcessor&) = delete;
|
||||
+ ContentProcessor& operator=(const ContentProcessor&) = delete;
|
||||
+
|
||||
+ // Extracts page content in document order.
|
||||
+ // Returns content items preserving the order they appear in the document.
|
||||
+ static std::vector<browser_os::ContentItem> ExtractPageContent(
|
||||
+ const ui::AXTreeUpdate& tree_update);
|
||||
+
|
||||
+ private:
|
||||
+ // DFS traversal to extract content in document order
|
||||
+ static void TraverseDFS(
|
||||
+ int32_t node_id,
|
||||
+ const std::unordered_map<int32_t, ui::AXNodeData>& node_map,
|
||||
+ std::vector<browser_os::ContentItem>& items);
|
||||
+
|
||||
+ // Content extraction helpers
|
||||
+ static browser_os::ContentItem ExtractHeading(const ui::AXNodeData& node);
|
||||
+ static browser_os::ContentItem ExtractText(const ui::AXNodeData& node);
|
||||
+ static browser_os::ContentItem ExtractLink(const ui::AXNodeData& node);
|
||||
+ static browser_os::ContentItem ExtractImage(const ui::AXNodeData& node);
|
||||
+ static browser_os::ContentItem ExtractVideo(const ui::AXNodeData& node);
|
||||
+
|
||||
+ // Get accessible name from node
|
||||
+ static std::string GetAccessibleName(const ui::AXNodeData& node);
|
||||
+};
|
||||
+
|
||||
+} // namespace api
|
||||
+} // namespace extensions
|
||||
+
|
||||
+#endif // CHROME_BROWSER_EXTENSIONS_API_BROWSER_OS_BROWSER_OS_CONTENT_PROCESSOR_H_
|
||||
@@ -1,654 +0,0 @@
|
||||
diff --git a/chrome/browser/extensions/api/browser_os/browser_os_snapshot_processor.cc b/chrome/browser/extensions/api/browser_os/browser_os_snapshot_processor.cc
|
||||
new file mode 100644
|
||||
index 0000000000000..885942336dcd6
|
||||
--- /dev/null
|
||||
+++ b/chrome/browser/extensions/api/browser_os/browser_os_snapshot_processor.cc
|
||||
@@ -0,0 +1,648 @@
|
||||
+// Copyright 2024 The Chromium Authors
|
||||
+// Use of this source code is governed by a BSD-style license that can be
|
||||
+// found in the LICENSE file.
|
||||
+
|
||||
+#include "chrome/browser/extensions/api/browser_os/browser_os_snapshot_processor.h"
|
||||
+
|
||||
+#include <algorithm>
|
||||
+#include <atomic>
|
||||
+#include <cctype>
|
||||
+#include <functional>
|
||||
+#include <future>
|
||||
+#include <memory>
|
||||
+#include <queue>
|
||||
+#include <sstream>
|
||||
+#include <unordered_set>
|
||||
+#include <utility>
|
||||
+
|
||||
+#include "base/functional/bind.h"
|
||||
+#include "base/logging.h"
|
||||
+#include "base/memory/raw_ptr.h"
|
||||
+#include "base/memory/ref_counted.h"
|
||||
+#include "base/strings/string_util.h"
|
||||
+#include "base/task/thread_pool.h"
|
||||
+#include "base/time/time.h"
|
||||
+#include "chrome/browser/extensions/api/browser_os/browser_os_api_utils.h"
|
||||
+#include "content/public/browser/browser_thread.h"
|
||||
+#include "content/public/browser/render_widget_host_view.h"
|
||||
+#include "content/browser/renderer_host/render_widget_host_view_base.h"
|
||||
+#include "content/public/browser/web_contents.h"
|
||||
+#include "ui/accessibility/ax_clipping_behavior.h"
|
||||
+#include "ui/accessibility/ax_coordinate_system.h"
|
||||
+#include "ui/accessibility/ax_enum_util.h"
|
||||
+#include "ui/accessibility/ax_node.h"
|
||||
+#include "ui/accessibility/ax_node_data.h"
|
||||
+#include "ui/accessibility/ax_tree.h"
|
||||
+#include "ui/accessibility/ax_tree_id.h"
|
||||
+#include "ui/accessibility/ax_tree_update.h"
|
||||
+#include "ui/gfx/geometry/rect.h"
|
||||
+#include "ui/gfx/geometry/rect_conversions.h"
|
||||
+#include "ui/gfx/geometry/rect_f.h"
|
||||
+#include "ui/gfx/geometry/transform.h"
|
||||
+
|
||||
+namespace extensions {
|
||||
+namespace api {
|
||||
+
|
||||
+// Static method to compute bounds for a node using AXTree and convert to CSS pixels
|
||||
+// This implements the same logic as BrowserAccessibility::GetBoundsRect
|
||||
+gfx::RectF SnapshotProcessor::GetNodeBounds(
|
||||
+ ui::AXTree* tree,
|
||||
+ const ui::AXNode* node,
|
||||
+ const ui::AXCoordinateSystem coordinate_system,
|
||||
+ const ui::AXClippingBehavior clipping_behavior,
|
||||
+ float device_scale_factor,
|
||||
+ bool* out_offscreen) {
|
||||
+ if (!tree || !node) {
|
||||
+ return gfx::RectF();
|
||||
+ }
|
||||
+
|
||||
+ // Start with empty bounds (same as GetBoundsRect does)
|
||||
+ gfx::RectF bounds;
|
||||
+
|
||||
+ // Apply RelativeToTreeBounds to get absolute bounds
|
||||
+ const bool clip_bounds = clipping_behavior == ui::AXClippingBehavior::kClipped;
|
||||
+ bool offscreen = false;
|
||||
+ bounds = tree->RelativeToTreeBounds(node, bounds, &offscreen, clip_bounds);
|
||||
+
|
||||
+ // Return offscreen status to caller
|
||||
+ if (out_offscreen) {
|
||||
+ *out_offscreen = offscreen;
|
||||
+ }
|
||||
+
|
||||
+ // Convert physical pixels to CSS pixels
|
||||
+ if (device_scale_factor > 0.0f && device_scale_factor != 1.0f) {
|
||||
+ bounds.set_x(bounds.x() / device_scale_factor);
|
||||
+ bounds.set_y(bounds.y() / device_scale_factor);
|
||||
+ bounds.set_width(bounds.width() / device_scale_factor);
|
||||
+ bounds.set_height(bounds.height() / device_scale_factor);
|
||||
+ }
|
||||
+
|
||||
+ // Return bounds in CSS pixels
|
||||
+ return bounds;
|
||||
+}
|
||||
+
|
||||
+
|
||||
+// ProcessedNode implementation
|
||||
+SnapshotProcessor::ProcessedNode::ProcessedNode()
|
||||
+ : node_data(nullptr), node_id(0) {}
|
||||
+
|
||||
+SnapshotProcessor::ProcessedNode::ProcessedNode(const ProcessedNode&) = default;
|
||||
+SnapshotProcessor::ProcessedNode::ProcessedNode(ProcessedNode&&) = default;
|
||||
+SnapshotProcessor::ProcessedNode&
|
||||
+SnapshotProcessor::ProcessedNode::operator=(const ProcessedNode&) = default;
|
||||
+SnapshotProcessor::ProcessedNode&
|
||||
+SnapshotProcessor::ProcessedNode::operator=(ProcessedNode&&) = default;
|
||||
+SnapshotProcessor::ProcessedNode::~ProcessedNode() = default;
|
||||
+
|
||||
+
|
||||
+namespace {
|
||||
+
|
||||
+// Helper to sanitize strings to ensure valid UTF-8 by keeping only printable ASCII
|
||||
+std::string SanitizeStringForOutput(const std::string& input) {
|
||||
+ std::string output;
|
||||
+ output.reserve(input.size());
|
||||
+
|
||||
+ for (char c : input) {
|
||||
+ // Only include printable ASCII and whitespace
|
||||
+ if ((c >= 32 && c <= 126) || c == '\t' || c == '\n') {
|
||||
+ output.push_back(c);
|
||||
+ } else {
|
||||
+ output.push_back(' '); // Replace non-printable with space
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ return output;
|
||||
+}
|
||||
+
|
||||
+// Helper to determine if a node should be skipped for the interactive snapshot
|
||||
+bool ShouldSkipNode(const ui::AXNodeData& node_data) {
|
||||
+ // Skip invisible or ignored nodes
|
||||
+ if (node_data.IsInvisibleOrIgnored()) {
|
||||
+ return true;
|
||||
+ }
|
||||
+
|
||||
+ // Get the interactive type and skip if it's not interactive
|
||||
+ browser_os::InteractiveNodeType node_type = GetInteractiveNodeType(node_data);
|
||||
+ if (node_type == browser_os::InteractiveNodeType::kOther) {
|
||||
+ return true;
|
||||
+ }
|
||||
+
|
||||
+ return false;
|
||||
+}
|
||||
+
|
||||
+} // namespace
|
||||
+
|
||||
+// Internal structure for managing async processing
|
||||
+struct SnapshotProcessor::ProcessingContext
|
||||
+ : public base::RefCountedThreadSafe<ProcessingContext> {
|
||||
+ browser_os::InteractiveSnapshot snapshot;
|
||||
+ std::unordered_map<int32_t, ui::AXNodeData> node_map;
|
||||
+ std::unordered_map<int32_t, int32_t> parent_map; // child_id -> parent_id
|
||||
+ std::unordered_map<int32_t, std::vector<int32_t>> children_map; // parent_id -> child_ids
|
||||
+ std::unique_ptr<ui::AXTree> ax_tree; // AXTree for computing accurate bounds
|
||||
+ int tab_id;
|
||||
+ ui::AXTreeID tree_id; // Tree ID for change detection
|
||||
+ float device_scale_factor = 1.0f; // For converting physical to CSS pixels
|
||||
+ gfx::Size viewport_size; // For visibility checks
|
||||
+ base::TimeTicks start_time;
|
||||
+ size_t total_nodes;
|
||||
+ size_t processed_batches;
|
||||
+ size_t total_batches;
|
||||
+ base::OnceCallback<void(SnapshotProcessingResult)> callback;
|
||||
+
|
||||
+ private:
|
||||
+ friend class base::RefCountedThreadSafe<ProcessingContext>;
|
||||
+ ~ProcessingContext() = default;
|
||||
+};
|
||||
+
|
||||
+// Helper to collect text from a node's subtree
|
||||
+std::string CollectTextFromNode(
|
||||
+ int32_t node_id,
|
||||
+ const std::unordered_map<int32_t, ui::AXNodeData>& node_map,
|
||||
+ int max_chars = 200) {
|
||||
+
|
||||
+ auto node_it = node_map.find(node_id);
|
||||
+ if (node_it == node_map.end()) {
|
||||
+ return "";
|
||||
+ }
|
||||
+
|
||||
+ std::vector<std::string> text_parts;
|
||||
+
|
||||
+ // BFS to collect text from this node and its children
|
||||
+ std::queue<int32_t> queue;
|
||||
+ queue.push(node_id);
|
||||
+ int chars_collected = 0;
|
||||
+
|
||||
+ while (!queue.empty() && chars_collected < max_chars) {
|
||||
+ int32_t current_id = queue.front();
|
||||
+ queue.pop();
|
||||
+
|
||||
+ auto current_it = node_map.find(current_id);
|
||||
+ if (current_it == node_map.end()) continue;
|
||||
+
|
||||
+ const ui::AXNodeData& current = current_it->second;
|
||||
+
|
||||
+ // Collect text from this node
|
||||
+ if (current.HasStringAttribute(ax::mojom::StringAttribute::kName)) {
|
||||
+ std::string text = current.GetStringAttribute(ax::mojom::StringAttribute::kName);
|
||||
+ text = std::string(base::TrimWhitespaceASCII(text, base::TRIM_ALL));
|
||||
+ if (!text.empty()) {
|
||||
+ std::string clean_text = SanitizeStringForOutput(text);
|
||||
+ if (!clean_text.empty()) {
|
||||
+ text_parts.push_back(clean_text);
|
||||
+ chars_collected += clean_text.length();
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ // Add children to queue
|
||||
+ for (int32_t child_id : current.child_ids) {
|
||||
+ queue.push(child_id);
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ std::string result = base::JoinString(text_parts, " ");
|
||||
+ if (result.length() > static_cast<size_t>(max_chars)) {
|
||||
+ result = result.substr(0, max_chars - 3) + "...";
|
||||
+ }
|
||||
+ return result;
|
||||
+}
|
||||
+
|
||||
+// Helper to build path using offset_container_id and return depth
|
||||
+std::pair<std::string, int> BuildPathAndDepth(
|
||||
+ int32_t node_id,
|
||||
+ const std::unordered_map<int32_t, ui::AXNodeData>& node_map) {
|
||||
+
|
||||
+ std::vector<std::string> path_parts;
|
||||
+ int32_t current_id = node_id;
|
||||
+ int depth = 0;
|
||||
+ const int max_depth = 10;
|
||||
+
|
||||
+ while (current_id >= 0 && depth < max_depth) {
|
||||
+ auto node_it = node_map.find(current_id);
|
||||
+ if (node_it == node_map.end()) break;
|
||||
+
|
||||
+ const ui::AXNodeData& node = node_it->second;
|
||||
+
|
||||
+ // Just append the role
|
||||
+ path_parts.push_back(ui::ToString(node.role));
|
||||
+
|
||||
+ // Move to offset container
|
||||
+ current_id = node.relative_bounds.offset_container_id;
|
||||
+ depth++;
|
||||
+ }
|
||||
+
|
||||
+ // Reverse to get top-down path
|
||||
+ std::reverse(path_parts.begin(), path_parts.end());
|
||||
+ return std::make_pair(base::JoinString(path_parts, " > "), depth);
|
||||
+}
|
||||
+
|
||||
+// Helper to populate all attributes for a node
|
||||
+void PopulateNodeAttributes(
|
||||
+ const ui::AXNodeData& node_data,
|
||||
+ std::unordered_map<std::string, std::string>& attributes) {
|
||||
+
|
||||
+ // Add role as string
|
||||
+ attributes["role"] = ui::ToString(node_data.role);
|
||||
+
|
||||
+ // Add value attribute for inputs
|
||||
+ if (node_data.HasStringAttribute(ax::mojom::StringAttribute::kValue)) {
|
||||
+ std::string value = node_data.GetStringAttribute(ax::mojom::StringAttribute::kValue);
|
||||
+ attributes["value"] = SanitizeStringForOutput(value);
|
||||
+ }
|
||||
+
|
||||
+ // Add HTML tag if available
|
||||
+ if (node_data.HasStringAttribute(ax::mojom::StringAttribute::kHtmlTag)) {
|
||||
+ attributes["html-tag"] = node_data.GetStringAttribute(ax::mojom::StringAttribute::kHtmlTag);
|
||||
+ }
|
||||
+
|
||||
+ // Add role description
|
||||
+ if (node_data.HasStringAttribute(ax::mojom::StringAttribute::kRoleDescription)) {
|
||||
+ std::string role_desc = node_data.GetStringAttribute(ax::mojom::StringAttribute::kRoleDescription);
|
||||
+ attributes["role-description"] = SanitizeStringForOutput(role_desc);
|
||||
+ }
|
||||
+
|
||||
+ // Add input type
|
||||
+ if (node_data.HasStringAttribute(ax::mojom::StringAttribute::kInputType)) {
|
||||
+ std::string input_type = node_data.GetStringAttribute(ax::mojom::StringAttribute::kInputType);
|
||||
+ attributes["input-type"] = SanitizeStringForOutput(input_type);
|
||||
+ }
|
||||
+
|
||||
+ // Add tooltip
|
||||
+ if (node_data.HasStringAttribute(ax::mojom::StringAttribute::kTooltip)) {
|
||||
+ std::string tooltip = node_data.GetStringAttribute(ax::mojom::StringAttribute::kTooltip);
|
||||
+ attributes["tooltip"] = SanitizeStringForOutput(tooltip);
|
||||
+ }
|
||||
+
|
||||
+ // Add placeholder for input fields
|
||||
+ if (node_data.HasStringAttribute(ax::mojom::StringAttribute::kPlaceholder)) {
|
||||
+ std::string placeholder = node_data.GetStringAttribute(ax::mojom::StringAttribute::kPlaceholder);
|
||||
+ attributes["placeholder"] = SanitizeStringForOutput(placeholder);
|
||||
+ }
|
||||
+
|
||||
+ // Add description for more context
|
||||
+ if (node_data.HasStringAttribute(ax::mojom::StringAttribute::kDescription)) {
|
||||
+ std::string description = node_data.GetStringAttribute(ax::mojom::StringAttribute::kDescription);
|
||||
+ attributes["description"] = SanitizeStringForOutput(description);
|
||||
+ }
|
||||
+
|
||||
+ // Add URL for links
|
||||
+ // if (node_data.HasStringAttribute(ax::mojom::StringAttribute::kUrl)) {
|
||||
+ // std::string url = node_data.GetStringAttribute(ax::mojom::StringAttribute::kUrl);
|
||||
+ // attributes["url"] = SanitizeStringForOutput(url);
|
||||
+ // }
|
||||
+
|
||||
+ // Add checked state description
|
||||
+ if (node_data.HasStringAttribute(ax::mojom::StringAttribute::kCheckedStateDescription)) {
|
||||
+ std::string checked_desc = node_data.GetStringAttribute(ax::mojom::StringAttribute::kCheckedStateDescription);
|
||||
+ attributes["checked-state"] = SanitizeStringForOutput(checked_desc);
|
||||
+ }
|
||||
+
|
||||
+ // Add autocomplete hint
|
||||
+ if (node_data.HasStringAttribute(ax::mojom::StringAttribute::kAutoComplete)) {
|
||||
+ std::string autocomplete = node_data.GetStringAttribute(ax::mojom::StringAttribute::kAutoComplete);
|
||||
+ attributes["autocomplete"] = SanitizeStringForOutput(autocomplete);
|
||||
+ }
|
||||
+
|
||||
+ // Add HTML ID for form associations
|
||||
+ if (node_data.HasStringAttribute(ax::mojom::StringAttribute::kHtmlId)) {
|
||||
+ std::string html_id = node_data.GetStringAttribute(ax::mojom::StringAttribute::kHtmlId);
|
||||
+ attributes["id"] = SanitizeStringForOutput(html_id);
|
||||
+ }
|
||||
+
|
||||
+ // Add HTML class names
|
||||
+ if (node_data.HasStringAttribute(ax::mojom::StringAttribute::kClassName)) {
|
||||
+ std::string class_name = node_data.GetStringAttribute(ax::mojom::StringAttribute::kClassName);
|
||||
+ attributes["class"] = SanitizeStringForOutput(class_name);
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+// Process a batch of nodes
|
||||
+std::vector<SnapshotProcessor::ProcessedNode> SnapshotProcessor::ProcessNodeBatch(
|
||||
+ const std::vector<ui::AXNodeData>& nodes_to_process,
|
||||
+ const std::unordered_map<int32_t, ui::AXNodeData>& node_map,
|
||||
+ ui::AXTree* ax_tree,
|
||||
+ uint32_t start_node_id,
|
||||
+ float device_scale_factor) {
|
||||
+ std::vector<ProcessedNode> results;
|
||||
+ results.reserve(nodes_to_process.size());
|
||||
+
|
||||
+ uint32_t current_node_id = start_node_id;
|
||||
+
|
||||
+ for (const auto& node_data : nodes_to_process) {
|
||||
+ // Skip invisible, ignored, or non-interactive elements
|
||||
+ if (ShouldSkipNode(node_data)) {
|
||||
+ continue;
|
||||
+ }
|
||||
+
|
||||
+ // Double-check invisibility (already done in ShouldSkipNode, but being explicit)
|
||||
+ if (node_data.IsInvisibleOrIgnored()) {
|
||||
+ continue;
|
||||
+ }
|
||||
+
|
||||
+ // Get the interactive node type
|
||||
+ browser_os::InteractiveNodeType node_type = GetInteractiveNodeType(node_data);
|
||||
+
|
||||
+ ProcessedNode data;
|
||||
+ data.node_data = &node_data;
|
||||
+ data.node_id = current_node_id++;
|
||||
+ data.node_type = node_type;
|
||||
+
|
||||
+ // Get accessible name
|
||||
+ if (node_data.HasStringAttribute(ax::mojom::StringAttribute::kName)) {
|
||||
+ std::string name = node_data.GetStringAttribute(ax::mojom::StringAttribute::kName);
|
||||
+ data.name = SanitizeStringForOutput(name);
|
||||
+ }
|
||||
+
|
||||
+ // Compute bounds using AXTree
|
||||
+ bool is_offscreen = false;
|
||||
+ if (ax_tree) {
|
||||
+ ui::AXNode* ax_node = ax_tree->GetFromId(node_data.id);
|
||||
+ if (ax_node) {
|
||||
+ // GetNodeBounds now returns CSS pixels directly
|
||||
+ data.absolute_bounds = GetNodeBounds(
|
||||
+ ax_tree,
|
||||
+ ax_node,
|
||||
+ ui::AXCoordinateSystem::kFrame,
|
||||
+ // Use clipped bounds so the center lies within the visible area of
|
||||
+ // scrolled/clip containers. This matches how clicks should target
|
||||
+ // on-screen rects.
|
||||
+ ui::AXClippingBehavior::kClipped,
|
||||
+ device_scale_factor, // Pass DSF for CSS pixel conversion
|
||||
+ &is_offscreen);
|
||||
+
|
||||
+ VLOG(3) << "[browseros] Node " << node_data.id
|
||||
+ << " CSS bounds: " << data.absolute_bounds.ToString()
|
||||
+ << " offscreen: " << is_offscreen;
|
||||
+ } else {
|
||||
+ // Node not found in AXTree, skip bounds computation
|
||||
+ VLOG(3) << "[browseros] Node " << node_data.id
|
||||
+ << " not found in AXTree, skipping bounds";
|
||||
+ }
|
||||
+ } else {
|
||||
+ // No AXTree available
|
||||
+ LOG(WARNING) << "[browseros] No AXTree available for bounds computation";
|
||||
+ }
|
||||
+
|
||||
+ // Populate all attributes using helper function
|
||||
+ PopulateNodeAttributes(node_data, data.attributes);
|
||||
+
|
||||
+ // Add context from parent node
|
||||
+ int32_t parent_id = node_data.relative_bounds.offset_container_id;
|
||||
+ if (parent_id >= 0) {
|
||||
+ std::string context = CollectTextFromNode(parent_id, node_map, 200);
|
||||
+ if (!context.empty()) {
|
||||
+ data.attributes["context"] = context;
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ // Add path and depth using offset_container_id chain
|
||||
+ auto [path, depth] = BuildPathAndDepth(node_data.id, node_map);
|
||||
+ if (!path.empty()) {
|
||||
+ data.attributes["path"] = path;
|
||||
+ }
|
||||
+ data.attributes["depth"] = std::to_string(depth);
|
||||
+
|
||||
+ // Set viewport status based on offscreen flag
|
||||
+ // Note: offscreen=false means the node IS in viewport (at least partially visible)
|
||||
+ // offscreen=true means the node is NOT in viewport (completely hidden)
|
||||
+ data.attributes["in_viewport"] = is_offscreen ? "false" : "true";
|
||||
+
|
||||
+ results.push_back(std::move(data));
|
||||
+ }
|
||||
+
|
||||
+ return results;
|
||||
+}
|
||||
+
|
||||
+// Helper to handle batch processing results
|
||||
+void SnapshotProcessor::OnBatchProcessed(
|
||||
+ scoped_refptr<ProcessingContext> context,
|
||||
+ std::vector<ProcessedNode> batch_results) {
|
||||
+ // Process batch results
|
||||
+ for (const auto& node_data : batch_results) {
|
||||
+ // Store mapping from our nodeId to AX node ID, bounds, and attributes
|
||||
+ NodeInfo info;
|
||||
+ info.ax_node_id = node_data.node_data->id;
|
||||
+ info.ax_tree_id = context->tree_id; // Store tree ID for change detection
|
||||
+ info.bounds = node_data.absolute_bounds;
|
||||
+ info.attributes = node_data.attributes; // Store all computed attributes
|
||||
+ info.node_type = node_data.node_type; // Store node type for efficient filtering
|
||||
+ // Extract in_viewport from attributes (stored as "true"/"false" string)
|
||||
+ auto viewport_it = node_data.attributes.find("in_viewport");
|
||||
+ info.in_viewport = (viewport_it != node_data.attributes.end() && viewport_it->second == "true");
|
||||
+ GetNodeIdMappings()[context->tab_id][node_data.node_id] = info;
|
||||
+
|
||||
+ // Log the mapping for debugging
|
||||
+ VLOG(2) << "Node ID Mapping: Interactive nodeId=" << node_data.node_id
|
||||
+ << " -> AX node ID=" << info.ax_node_id
|
||||
+ << " (name: " << node_data.name << ")";
|
||||
+
|
||||
+ // Create interactive node
|
||||
+ browser_os::InteractiveNode interactive_node;
|
||||
+ interactive_node.node_id = node_data.node_id;
|
||||
+ interactive_node.type = node_data.node_type;
|
||||
+ interactive_node.name = node_data.name;
|
||||
+
|
||||
+ // Set the bounding rectangle
|
||||
+ browser_os::Rect rect;
|
||||
+ rect.x = node_data.absolute_bounds.x();
|
||||
+ rect.y = node_data.absolute_bounds.y();
|
||||
+ rect.width = node_data.absolute_bounds.width();
|
||||
+ rect.height = node_data.absolute_bounds.height();
|
||||
+ interactive_node.rect = std::move(rect);
|
||||
+
|
||||
+ // Create attributes dictionary by iterating over all key-value pairs
|
||||
+ if (!node_data.attributes.empty()) {
|
||||
+ browser_os::InteractiveNode::Attributes attributes;
|
||||
+
|
||||
+ // Iterate over all attributes and add them to the dictionary
|
||||
+ for (const auto& [key, value] : node_data.attributes) {
|
||||
+ attributes.additional_properties.Set(key, value);
|
||||
+ }
|
||||
+
|
||||
+ interactive_node.attributes = std::move(attributes);
|
||||
+ }
|
||||
+
|
||||
+ context->snapshot.elements.push_back(std::move(interactive_node));
|
||||
+ }
|
||||
+
|
||||
+ context->processed_batches++;
|
||||
+
|
||||
+ // Check if all batches are complete
|
||||
+ if (context->processed_batches == context->total_batches) {
|
||||
+ // Sort elements by node_id to maintain consistent ordering
|
||||
+ std::sort(context->snapshot.elements.begin(),
|
||||
+ context->snapshot.elements.end(),
|
||||
+ [](const browser_os::InteractiveNode& a,
|
||||
+ const browser_os::InteractiveNode& b) {
|
||||
+ return a.node_id < b.node_id;
|
||||
+ });
|
||||
+
|
||||
+ // Leave hierarchical_structure empty for now as requested
|
||||
+ context->snapshot.hierarchical_structure = "";
|
||||
+
|
||||
+ base::TimeDelta processing_time = base::TimeTicks::Now() - context->start_time;
|
||||
+ LOG(INFO) << "[PERF] Interactive snapshot processed in "
|
||||
+ << processing_time.InMilliseconds() << " ms"
|
||||
+ << " (nodes: " << context->snapshot.elements.size() << ")";
|
||||
+
|
||||
+ // Set processing time in the snapshot
|
||||
+ context->snapshot.processing_time_ms = processing_time.InMilliseconds();
|
||||
+
|
||||
+ SnapshotProcessingResult result;
|
||||
+ result.snapshot = std::move(context->snapshot);
|
||||
+ result.nodes_processed = context->total_nodes;
|
||||
+ result.processing_time_ms = processing_time.InMilliseconds();
|
||||
+
|
||||
+ // Run callback (context will be deleted when last ref is released)
|
||||
+ std::move(context->callback).Run(std::move(result));
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+// Main processing function
|
||||
+// Helper function to extract viewport info from WebContents
|
||||
+// Returns viewport size and device scale factor
|
||||
+static std::pair<gfx::Size, float> ExtractViewportInfo(
|
||||
+ content::WebContents* web_contents) {
|
||||
+ gfx::Size viewport_size;
|
||||
+ float device_scale_factor = 1.0f;
|
||||
+
|
||||
+ if (web_contents) {
|
||||
+ if (auto* rwhv = web_contents->GetRenderWidgetHostView()) {
|
||||
+ viewport_size = rwhv->GetVisibleViewportSize();
|
||||
+
|
||||
+ // Get device scale factor for CSS pixel conversion
|
||||
+ if (auto* rwhv_base =
|
||||
+ static_cast<content::RenderWidgetHostViewBase*>(rwhv)) {
|
||||
+ device_scale_factor = rwhv_base->GetDeviceScaleFactor();
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ LOG(INFO) << "[browseros] Viewport: " << viewport_size.ToString()
|
||||
+ << ", DSF: " << device_scale_factor;
|
||||
+
|
||||
+ return {viewport_size, device_scale_factor};
|
||||
+}
|
||||
+
|
||||
+void SnapshotProcessor::ProcessAccessibilityTree(
|
||||
+ const ui::AXTreeUpdate& tree_update,
|
||||
+ int tab_id,
|
||||
+ uint32_t snapshot_id,
|
||||
+ content::WebContents* web_contents,
|
||||
+ base::OnceCallback<void(SnapshotProcessingResult)> callback) {
|
||||
+ base::TimeTicks start_time = base::TimeTicks::Now();
|
||||
+
|
||||
+ // Extract viewport info from WebContents on UI thread
|
||||
+ auto [viewport_size, device_scale_factor] = ExtractViewportInfo(web_contents);
|
||||
+
|
||||
+ // Build node ID map, parent map and children map for efficient lookup
|
||||
+ std::unordered_map<int32_t, ui::AXNodeData> node_map;
|
||||
+ std::unordered_map<int32_t, int32_t> parent_map;
|
||||
+ std::unordered_map<int32_t, std::vector<int32_t>> children_map;
|
||||
+
|
||||
+ for (const auto& node : tree_update.nodes) {
|
||||
+ node_map[node.id] = node;
|
||||
+ // Build parent and children relationships
|
||||
+ for (int32_t child_id : node.child_ids) {
|
||||
+ parent_map[child_id] = node.id;
|
||||
+ children_map[node.id].push_back(child_id);
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ // Clear previous mappings for this tab
|
||||
+ GetNodeIdMappings()[tab_id].clear();
|
||||
+
|
||||
+ // Create an AXTree from the tree update for accurate bounds computation
|
||||
+ std::unique_ptr<ui::AXTree> ax_tree = std::make_unique<ui::AXTree>(tree_update);
|
||||
+
|
||||
+ if (!ax_tree) {
|
||||
+ LOG(ERROR) << "[browseros] Failed to create AXTree from update";
|
||||
+ SnapshotProcessingResult result;
|
||||
+ result.nodes_processed = 0;
|
||||
+ result.processing_time_ms = 0;
|
||||
+ std::move(callback).Run(std::move(result));
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ LOG(INFO) << "[browseros] Created AXTree with " << tree_update.nodes.size()
|
||||
+ << " nodes for bounds computation";
|
||||
+
|
||||
+ // Prepare processing context using RefCounted
|
||||
+ auto context = base::MakeRefCounted<ProcessingContext>();
|
||||
+ context->snapshot.snapshot_id = snapshot_id;
|
||||
+ context->snapshot.timestamp = base::Time::Now().InMillisecondsFSinceUnixEpoch();
|
||||
+ context->tab_id = tab_id;
|
||||
+ context->node_map = std::move(node_map);
|
||||
+ context->parent_map = std::move(parent_map);
|
||||
+ context->children_map = std::move(children_map);
|
||||
+ context->ax_tree = std::move(ax_tree); // Store AXTree for bounds computation
|
||||
+ context->device_scale_factor = device_scale_factor; // For CSS pixel conversion
|
||||
+ context->viewport_size = viewport_size; // For visibility checks
|
||||
+ context->start_time = start_time;
|
||||
+
|
||||
+ // Store the tree ID for change detection
|
||||
+ if (tree_update.has_tree_data) {
|
||||
+ context->tree_id = tree_update.tree_data.tree_id;
|
||||
+ }
|
||||
+
|
||||
+ // Viewport size is passed in but not currently used for viewport bounds calculation
|
||||
+ // TODO: Implement proper viewport detection if needed
|
||||
+ context->callback = std::move(callback);
|
||||
+ context->processed_batches = 0;
|
||||
+
|
||||
+ // Collect all nodes to process and filter
|
||||
+ std::vector<ui::AXNodeData> nodes_to_process;
|
||||
+ for (const auto& node : tree_update.nodes) {
|
||||
+ // Skip invisible, ignored, or non-interactive nodes
|
||||
+ if (ShouldSkipNode(node)) {
|
||||
+ continue;
|
||||
+ }
|
||||
+ nodes_to_process.push_back(node);
|
||||
+ }
|
||||
+
|
||||
+ context->total_nodes = nodes_to_process.size();
|
||||
+
|
||||
+ // Handle empty case
|
||||
+ if (nodes_to_process.empty()) {
|
||||
+ base::TimeDelta processing_time = base::TimeTicks::Now() - start_time;
|
||||
+ context->snapshot.processing_time_ms = processing_time.InMilliseconds();
|
||||
+
|
||||
+ SnapshotProcessingResult result;
|
||||
+ result.snapshot = std::move(context->snapshot);
|
||||
+ result.nodes_processed = 0;
|
||||
+ result.processing_time_ms = processing_time.InMilliseconds();
|
||||
+ std::move(context->callback).Run(std::move(result));
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ // Process nodes in batches using ThreadPool
|
||||
+ const size_t batch_size = 100; // Process 100 nodes per batch
|
||||
+ size_t num_batches = (nodes_to_process.size() + batch_size - 1) / batch_size;
|
||||
+ context->total_batches = num_batches;
|
||||
+
|
||||
+ for (size_t i = 0; i < nodes_to_process.size(); i += batch_size) {
|
||||
+ size_t end = std::min(i + batch_size, nodes_to_process.size());
|
||||
+ std::vector<ui::AXNodeData> batch(
|
||||
+ std::make_move_iterator(nodes_to_process.begin() + i),
|
||||
+ std::make_move_iterator(nodes_to_process.begin() + end));
|
||||
+ uint32_t start_node_id = i + 1; // Node IDs start at 1
|
||||
+
|
||||
+ // Post task to ThreadPool and handle result on UI thread
|
||||
+ base::ThreadPool::PostTaskAndReplyWithResult(
|
||||
+ FROM_HERE,
|
||||
+ {base::TaskPriority::USER_VISIBLE},
|
||||
+ base::BindOnce(&SnapshotProcessor::ProcessNodeBatch,
|
||||
+ std::move(batch),
|
||||
+ context->node_map,
|
||||
+ context->ax_tree.get(), // Pass AXTree pointer for bounds computation
|
||||
+ start_node_id,
|
||||
+ context->device_scale_factor), // Pass DSF for CSS pixel conversion
|
||||
+ base::BindOnce(&SnapshotProcessor::OnBatchProcessed,
|
||||
+ context));
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+
|
||||
+} // namespace api
|
||||
+} // namespace extensions
|
||||
@@ -1,118 +0,0 @@
|
||||
diff --git a/chrome/browser/extensions/api/browser_os/browser_os_snapshot_processor.h b/chrome/browser/extensions/api/browser_os/browser_os_snapshot_processor.h
|
||||
new file mode 100644
|
||||
index 0000000000000..5c85cd73b26f3
|
||||
--- /dev/null
|
||||
+++ b/chrome/browser/extensions/api/browser_os/browser_os_snapshot_processor.h
|
||||
@@ -0,0 +1,111 @@
|
||||
+// Copyright 2024 The Chromium Authors
|
||||
+// Use of this source code is governed by a BSD-style license that can be
|
||||
+// found in the LICENSE file.
|
||||
+
|
||||
+#ifndef CHROME_BROWSER_EXTENSIONS_API_BROWSER_OS_BROWSER_OS_SNAPSHOT_PROCESSOR_H_
|
||||
+#define CHROME_BROWSER_EXTENSIONS_API_BROWSER_OS_BROWSER_OS_SNAPSHOT_PROCESSOR_H_
|
||||
+
|
||||
+#include <cstdint>
|
||||
+#include <string>
|
||||
+#include <unordered_map>
|
||||
+#include <vector>
|
||||
+
|
||||
+#include "base/functional/callback.h"
|
||||
+#include "base/memory/raw_ptr.h"
|
||||
+#include "chrome/common/extensions/api/browser_os.h"
|
||||
+#include "ui/gfx/geometry/rect_f.h"
|
||||
+
|
||||
+namespace content {
|
||||
+class WebContents;
|
||||
+} // namespace content
|
||||
+
|
||||
+namespace ui {
|
||||
+class AXNode;
|
||||
+class AXTree;
|
||||
+struct AXNodeData;
|
||||
+struct AXTreeUpdate;
|
||||
+enum class AXCoordinateSystem;
|
||||
+enum class AXClippingBehavior;
|
||||
+} // namespace ui
|
||||
+
|
||||
+namespace extensions {
|
||||
+namespace api {
|
||||
+
|
||||
+// Result of snapshot processing
|
||||
+struct SnapshotProcessingResult {
|
||||
+ browser_os::InteractiveSnapshot snapshot;
|
||||
+ int nodes_processed = 0;
|
||||
+ int64_t processing_time_ms = 0;
|
||||
+};
|
||||
+
|
||||
+// Processes accessibility trees into interactive snapshots with parallel processing
|
||||
+class SnapshotProcessor {
|
||||
+ public:
|
||||
+ // Structure to hold data for a processed node
|
||||
+ struct ProcessedNode {
|
||||
+ ProcessedNode();
|
||||
+ ProcessedNode(const ProcessedNode&);
|
||||
+ ProcessedNode(ProcessedNode&&);
|
||||
+ ProcessedNode& operator=(const ProcessedNode&);
|
||||
+ ProcessedNode& operator=(ProcessedNode&&);
|
||||
+ ~ProcessedNode();
|
||||
+
|
||||
+ raw_ptr<const ui::AXNodeData> node_data;
|
||||
+ uint32_t node_id;
|
||||
+ browser_os::InteractiveNodeType node_type;
|
||||
+ std::string name;
|
||||
+ gfx::RectF absolute_bounds;
|
||||
+ // All attributes stored as key-value pairs
|
||||
+ std::unordered_map<std::string, std::string> attributes;
|
||||
+ };
|
||||
+
|
||||
+ SnapshotProcessor() = default;
|
||||
+ ~SnapshotProcessor() = default;
|
||||
+
|
||||
+ // Main processing function - handles all threading internally
|
||||
+ // This function processes the accessibility tree into an interactive snapshot
|
||||
+ // using parallel processing on the thread pool. Extracts viewport info from
|
||||
+ // web_contents on UI thread before processing.
|
||||
+ static void ProcessAccessibilityTree(
|
||||
+ const ui::AXTreeUpdate& tree_update,
|
||||
+ int tab_id,
|
||||
+ uint32_t snapshot_id,
|
||||
+ content::WebContents* web_contents,
|
||||
+ base::OnceCallback<void(SnapshotProcessingResult)> callback);
|
||||
+
|
||||
+ // Process a batch of nodes (exposed for testing)
|
||||
+ // The ax_tree is used to compute accurate bounds for each node
|
||||
+ // device_scale_factor is used to convert physical pixels to CSS pixels
|
||||
+ static std::vector<ProcessedNode> ProcessNodeBatch(
|
||||
+ const std::vector<ui::AXNodeData>& nodes_to_process,
|
||||
+ const std::unordered_map<int32_t, ui::AXNodeData>& node_map,
|
||||
+ ui::AXTree* ax_tree,
|
||||
+ uint32_t start_node_id,
|
||||
+ float device_scale_factor = 1.0f);
|
||||
+
|
||||
+ private:
|
||||
+ // Internal processing context
|
||||
+ struct ProcessingContext;
|
||||
+
|
||||
+ // Compute absolute bounds for a node using AXTree and convert to CSS pixels
|
||||
+ // This implements the same logic as BrowserAccessibility::GetBoundsRect
|
||||
+ // Returns bounds in CSS pixels by applying device_scale_factor
|
||||
+ static gfx::RectF GetNodeBounds(ui::AXTree* tree,
|
||||
+ const ui::AXNode* node,
|
||||
+ const ui::AXCoordinateSystem coordinate_system,
|
||||
+ const ui::AXClippingBehavior clipping_behavior,
|
||||
+ float device_scale_factor = 1.0f,
|
||||
+ bool* out_offscreen = nullptr);
|
||||
+
|
||||
+ // Batch processing callback
|
||||
+ static void OnBatchProcessed(scoped_refptr<ProcessingContext> context,
|
||||
+ std::vector<ProcessedNode> batch_results);
|
||||
+
|
||||
+ SnapshotProcessor(const SnapshotProcessor&) = delete;
|
||||
+ SnapshotProcessor& operator=(const SnapshotProcessor&) = delete;
|
||||
+};
|
||||
+
|
||||
+} // namespace api
|
||||
+} // namespace extensions
|
||||
+
|
||||
+#endif // CHROME_BROWSER_EXTENSIONS_API_BROWSER_OS_BROWSER_OS_SNAPSHOT_PROCESSOR_H_
|
||||
\ No newline at end of file
|
||||
@@ -1,5 +1,5 @@
|
||||
diff --git a/chrome/browser/extensions/chrome_extensions_browser_api_provider.cc b/chrome/browser/extensions/chrome_extensions_browser_api_provider.cc
|
||||
index a251aaaa53378..25b5989cedf15 100644
|
||||
index a251aaaa53378..93c5a121684a4 100644
|
||||
--- a/chrome/browser/extensions/chrome_extensions_browser_api_provider.cc
|
||||
+++ b/chrome/browser/extensions/chrome_extensions_browser_api_provider.cc
|
||||
@@ -4,6 +4,7 @@
|
||||
@@ -10,16 +10,11 @@ index a251aaaa53378..25b5989cedf15 100644
|
||||
#include "chrome/browser/extensions/api/generated_api_registration.h"
|
||||
#include "extensions/browser/extension_function_registry.h"
|
||||
#include "extensions/buildflags/buildflags.h"
|
||||
@@ -19,6 +20,14 @@ ChromeExtensionsBrowserAPIProvider::~ChromeExtensionsBrowserAPIProvider() =
|
||||
@@ -19,6 +20,9 @@ ChromeExtensionsBrowserAPIProvider::~ChromeExtensionsBrowserAPIProvider() =
|
||||
|
||||
void ChromeExtensionsBrowserAPIProvider::RegisterExtensionFunctions(
|
||||
ExtensionFunctionRegistry* registry) {
|
||||
+ // BrowserOS API
|
||||
+ registry->RegisterFunction<api::BrowserOSGetAccessibilityTreeFunction>();
|
||||
+ registry->RegisterFunction<api::BrowserOSGetInteractiveSnapshotFunction>();
|
||||
+ registry->RegisterFunction<api::BrowserOSClickFunction>();
|
||||
+ registry->RegisterFunction<api::BrowserOSInputTextFunction>();
|
||||
+ registry->RegisterFunction<api::BrowserOSClearFunction>();
|
||||
+ registry->RegisterFunction<api::BrowserOSExecuteJavaScriptFunction>();
|
||||
+
|
||||
// Generated APIs from Chrome.
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
diff --git a/chrome/common/extensions/api/browser_os.idl b/chrome/common/extensions/api/browser_os.idl
|
||||
new file mode 100644
|
||||
index 0000000000000..da7102fd5b5e3
|
||||
index 0000000000000..c6044b5071033
|
||||
--- /dev/null
|
||||
+++ b/chrome/common/extensions/api/browser_os.idl
|
||||
@@ -0,0 +1,389 @@
|
||||
@@ -0,0 +1,176 @@
|
||||
+// Copyright 2024 The Chromium Authors
|
||||
+// Use of this source code is governed by a BSD-style license that can be
|
||||
+// found in the LICENSE file.
|
||||
@@ -43,97 +43,6 @@ index 0000000000000..da7102fd5b5e3
|
||||
+ DOMString name;
|
||||
+ };
|
||||
+
|
||||
+ dictionary AccessibilityTree {
|
||||
+ // The ID of the root node
|
||||
+ long rootId;
|
||||
+
|
||||
+ // Map of node IDs to complete accessibility node data
|
||||
+ // Each node contains: id, role, states, actions, all attribute maps,
|
||||
+ // childIds, and other ui::AXNodeData fields
|
||||
+ object nodes;
|
||||
+
|
||||
+ // Tree-level metadata (optional)
|
||||
+ // Contains: title, url, doctype, mimetype, loaded, loadingProgress,
|
||||
+ // focusId, and selection information
|
||||
+ object? treeData;
|
||||
+ };
|
||||
+
|
||||
+ // Page content extraction types
|
||||
+ enum ContentItemType {
|
||||
+ heading,
|
||||
+ text,
|
||||
+ link,
|
||||
+ image,
|
||||
+ video
|
||||
+ };
|
||||
+
|
||||
+ // A content item extracted from the page in document order
|
||||
+ dictionary ContentItem {
|
||||
+ ContentItemType type;
|
||||
+ // Text content (for heading, text, link - the accessible name)
|
||||
+ DOMString? text;
|
||||
+ // URL (for link, image, video - the href or src)
|
||||
+ DOMString? url;
|
||||
+ // Heading level (for heading only, 1-6)
|
||||
+ long? level;
|
||||
+ // Alternative text (for image, video - alt attribute or title)
|
||||
+ DOMString? alt;
|
||||
+ };
|
||||
+
|
||||
+ // Page content in document order
|
||||
+ dictionary PageContent {
|
||||
+ // Content items in the order they appear in the document
|
||||
+ ContentItem[] items;
|
||||
+ // Timestamp when extraction was performed
|
||||
+ double timestamp;
|
||||
+ // Time taken to process (milliseconds)
|
||||
+ long processingTimeMs;
|
||||
+ };
|
||||
+
|
||||
+ // Interactive element types
|
||||
+ enum InteractiveNodeType {
|
||||
+ clickable,
|
||||
+ typeable,
|
||||
+ selectable,
|
||||
+ other
|
||||
+ };
|
||||
+
|
||||
+ // Rectangle bounds
|
||||
+ dictionary Rect {
|
||||
+ double x;
|
||||
+ double y;
|
||||
+ double width;
|
||||
+ double height;
|
||||
+ };
|
||||
+
|
||||
+ // Interactive node in the snapshot
|
||||
+ dictionary InteractiveNode {
|
||||
+ long nodeId;
|
||||
+ InteractiveNodeType type;
|
||||
+ DOMString? name;
|
||||
+ // Bounding rectangle of the node
|
||||
+ Rect? rect;
|
||||
+ // Flexible attributes dictionary for extensibility
|
||||
+ // Can include: tag, axValue, htmlTag, role, context, path, and any future attributes
|
||||
+ object? attributes;
|
||||
+ };
|
||||
+
|
||||
+ // Snapshot of interactive elements
|
||||
+ dictionary InteractiveSnapshot {
|
||||
+ long snapshotId;
|
||||
+ double timestamp;
|
||||
+ InteractiveNode[] elements;
|
||||
+ // Hierarchical text representation with context
|
||||
+ DOMString? hierarchicalStructure;
|
||||
+ // Performance metrics
|
||||
+ long processingTimeMs;
|
||||
+ };
|
||||
+
|
||||
+ // Options for getInteractiveSnapshot
|
||||
+ dictionary InteractiveSnapshotOptions {
|
||||
+ boolean? viewportOnly;
|
||||
+ };
|
||||
+
|
||||
+ // Page load status information
|
||||
+ dictionary PageLoadStatus {
|
||||
+ boolean isResourcesLoading;
|
||||
@@ -146,14 +55,8 @@ index 0000000000000..da7102fd5b5e3
|
||||
+ boolean success;
|
||||
+ };
|
||||
+
|
||||
+ callback GetAccessibilityTreeCallback = void(AccessibilityTree tree);
|
||||
+ callback GetInteractiveSnapshotCallback = void(InteractiveSnapshot snapshot);
|
||||
+ callback InteractionCallback = void(InteractionResponse response);
|
||||
+ callback GetPageLoadStatusCallback = void(PageLoadStatus status);
|
||||
+ callback ScrollCallback = void();
|
||||
+ callback ScrollToNodeCallback = void(boolean scrolled);
|
||||
+ callback CaptureScreenshotCallback = void(DOMString dataUrl);
|
||||
+ callback GetSnapshotCallback = void(PageContent content);
|
||||
+
|
||||
+ // Settings-related types
|
||||
+ dictionary PrefObject {
|
||||
@@ -166,13 +69,13 @@ index 0000000000000..da7102fd5b5e3
|
||||
+ callback GetPrefCallback = void(PrefObject pref);
|
||||
+ callback SetPrefCallback = void(boolean success);
|
||||
+ callback GetAllPrefsCallback = void(PrefObject[] prefs);
|
||||
+
|
||||
+
|
||||
+ // Callback for metrics logging
|
||||
+ callback VoidCallback = void();
|
||||
+
|
||||
+
|
||||
+ // Callback for getting version number
|
||||
+ callback GetVersionNumberCallback = void(DOMString version);
|
||||
+
|
||||
+
|
||||
+ // Callback for executeJavaScript
|
||||
+ callback ExecuteJavaScriptCallback = void(any result);
|
||||
+
|
||||
@@ -181,52 +84,6 @@ index 0000000000000..da7102fd5b5e3
|
||||
+ callback ChoosePathCallback = void(optional SelectedPath result);
|
||||
+
|
||||
+ interface Functions {
|
||||
+ // Gets the full accessibility tree for a tab
|
||||
+ // |tabId|: The tab to get the accessibility tree for. Defaults to active tab.
|
||||
+ // |callback|: Called with the accessibility tree data.
|
||||
+ static void getAccessibilityTree(
|
||||
+ optional long tabId,
|
||||
+ GetAccessibilityTreeCallback callback);
|
||||
+
|
||||
+ // Gets a snapshot of interactive elements on the page
|
||||
+ // |tabId|: The tab to get the snapshot for. Defaults to active tab.
|
||||
+ // |options|: Options for the snapshot.
|
||||
+ // |callback|: Called with the interactive snapshot data.
|
||||
+ static void getInteractiveSnapshot(
|
||||
+ optional long tabId,
|
||||
+ optional InteractiveSnapshotOptions options,
|
||||
+ GetInteractiveSnapshotCallback callback);
|
||||
+
|
||||
+
|
||||
+ // Clicks on an element by its nodeId from the interactive snapshot
|
||||
+ // |tabId|: The tab containing the element. Defaults to active tab.
|
||||
+ // |nodeId|: The nodeId from the interactive snapshot.
|
||||
+ // |callback|: Called when the click is complete.
|
||||
+ static void click(
|
||||
+ optional long tabId,
|
||||
+ long nodeId,
|
||||
+ InteractionCallback callback);
|
||||
+
|
||||
+ // Inputs text into an element by its nodeId
|
||||
+ // |tabId|: The tab containing the element. Defaults to active tab.
|
||||
+ // |nodeId|: The nodeId from the interactive snapshot.
|
||||
+ // |text|: The text to input.
|
||||
+ // |callback|: Called when the input is complete.
|
||||
+ static void inputText(
|
||||
+ optional long tabId,
|
||||
+ long nodeId,
|
||||
+ DOMString text,
|
||||
+ InteractionCallback callback);
|
||||
+
|
||||
+ // Clears the content of an input element by its nodeId
|
||||
+ // |tabId|: The tab containing the element. Defaults to active tab.
|
||||
+ // |nodeId|: The nodeId from the interactive snapshot.
|
||||
+ // |callback|: Called when the clear is complete.
|
||||
+ static void clear(
|
||||
+ optional long tabId,
|
||||
+ long nodeId,
|
||||
+ InteractionCallback callback);
|
||||
+
|
||||
+ // Gets the page load status for a tab
|
||||
+ // |tabId|: The tab to check. Defaults to active tab.
|
||||
+ // |callback|: Called with the page load status.
|
||||
@@ -234,51 +91,6 @@ index 0000000000000..da7102fd5b5e3
|
||||
+ optional long tabId,
|
||||
+ GetPageLoadStatusCallback callback);
|
||||
+
|
||||
+ // Scrolls the page up by approximately one viewport height
|
||||
+ // |tabId|: The tab to scroll. Defaults to active tab.
|
||||
+ // |callback|: Called when the scroll is complete.
|
||||
+ static void scrollUp(
|
||||
+ optional long tabId,
|
||||
+ ScrollCallback callback);
|
||||
+
|
||||
+ // Scrolls the page down by approximately one viewport height
|
||||
+ // |tabId|: The tab to scroll. Defaults to active tab.
|
||||
+ // |callback|: Called when the scroll is complete.
|
||||
+ static void scrollDown(
|
||||
+ optional long tabId,
|
||||
+ ScrollCallback callback);
|
||||
+
|
||||
+ // Scrolls the page to bring the specified node into view
|
||||
+ // |tabId|: The tab to scroll. Defaults to active tab.
|
||||
+ // |nodeId|: The node ID from getInteractiveSnapshot to scroll to.
|
||||
+ // |callback|: Called with whether scrolling was needed (false if already in view).
|
||||
+ static void scrollToNode(
|
||||
+ optional long tabId,
|
||||
+ long nodeId,
|
||||
+ ScrollToNodeCallback callback);
|
||||
+
|
||||
+ // Sends special key events to the active element in a tab
|
||||
+ // |tabId|: The tab to send keys to. Defaults to active tab.
|
||||
+ // |key|: The special key to send. Supported keys:
|
||||
+ // - "Enter": Submit forms, activate buttons, insert line break
|
||||
+ // - "Delete": Delete character after cursor
|
||||
+ // - "Backspace": Delete character before cursor
|
||||
+ // - "Tab": Move focus to next element
|
||||
+ // - "Escape": Cancel operations, close dialogs
|
||||
+ // - "ArrowUp": Move cursor/selection up
|
||||
+ // - "ArrowDown": Move cursor/selection down
|
||||
+ // - "ArrowLeft": Move cursor/selection left
|
||||
+ // - "ArrowRight": Move cursor/selection right
|
||||
+ // - "Home": Move to beginning of line/document
|
||||
+ // - "End": Move to end of line/document
|
||||
+ // - "PageUp": Scroll up one page
|
||||
+ // - "PageDown": Scroll down one page
|
||||
+ // |callback|: Called when the key has been sent.
|
||||
+ static void sendKeys(
|
||||
+ optional long tabId,
|
||||
+ DOMString key,
|
||||
+ InteractionCallback callback);
|
||||
+
|
||||
+ // Clicks at specific coordinates on the page
|
||||
+ // |tabId|: The tab to click in. Defaults to active tab.
|
||||
+ // |x|: X coordinate in CSS pixels from viewport origin.
|
||||
@@ -289,7 +101,7 @@ index 0000000000000..da7102fd5b5e3
|
||||
+ double x,
|
||||
+ double y,
|
||||
+ InteractionCallback callback);
|
||||
+
|
||||
+
|
||||
+ // Types text after clicking at coordinates to focus element
|
||||
+ // |tabId|: The tab to type in. Defaults to active tab.
|
||||
+ // |x|: X coordinate to click for focus.
|
||||
@@ -302,30 +114,6 @@ index 0000000000000..da7102fd5b5e3
|
||||
+ double y,
|
||||
+ DOMString text,
|
||||
+ InteractionCallback callback);
|
||||
+
|
||||
+ // Captures a screenshot of the tab as a thumbnail
|
||||
+ // |tabId|: The tab to capture. Defaults to active tab.
|
||||
+ // |thumbnailSize|: Optional max dimension for the thumbnail. If not provided, uses viewport size.
|
||||
+ // The actual size will be the minimum of this value and the viewport dimensions.
|
||||
+ // Ignored if width and height are specified.
|
||||
+ // |showHighlights|: If true, shows bounding boxes around clickable, typeable, and selectable elements that are in viewport.
|
||||
+ // |width|: Optional exact width for screenshot. When used with height, overrides thumbnailSize.
|
||||
+ // |height|: Optional exact height for screenshot. When used with width, overrides thumbnailSize.
|
||||
+ // |callback|: Called with the screenshot as a data URL.
|
||||
+ static void captureScreenshot(
|
||||
+ optional long tabId,
|
||||
+ optional long thumbnailSize,
|
||||
+ optional boolean showHighlights,
|
||||
+ optional long width,
|
||||
+ optional long height,
|
||||
+ CaptureScreenshotCallback callback);
|
||||
+
|
||||
+ // Gets a simple text snapshot of the page
|
||||
+ // |tabId|: The tab to extract content from. Defaults to active tab.
|
||||
+ // |callback|: Called with the page snapshot.
|
||||
+ static void getSnapshot(
|
||||
+ optional long tabId,
|
||||
+ GetSnapshotCallback callback);
|
||||
+
|
||||
+ // Settings API functions - compatible with chrome.settingsPrivate
|
||||
+ // Gets a specific preference value
|
||||
@@ -350,7 +138,7 @@ index 0000000000000..da7102fd5b5e3
|
||||
+ // |callback|: Called with array of preference objects.
|
||||
+ static void getAllPrefs(
|
||||
+ GetAllPrefsCallback callback);
|
||||
+
|
||||
+
|
||||
+ // Logs a metric event with optional properties
|
||||
+ // |eventName|: The name of the event to log (e.g., "extension.action").
|
||||
+ // |properties|: Optional JSON object with additional properties.
|
||||
@@ -359,7 +147,7 @@ index 0000000000000..da7102fd5b5e3
|
||||
+ DOMString eventName,
|
||||
+ optional object properties,
|
||||
+ optional VoidCallback callback);
|
||||
+
|
||||
+
|
||||
+ // Gets the browser version number
|
||||
+ // |callback|: Called with the version string.
|
||||
+ static void getVersionNumber(
|
||||
@@ -369,7 +157,7 @@ index 0000000000000..da7102fd5b5e3
|
||||
+ // |callback|: Called with the version string.
|
||||
+ static void getBrowserosVersionNumber(
|
||||
+ GetVersionNumberCallback callback);
|
||||
+
|
||||
+
|
||||
+ // Executes JavaScript code in the specified tab
|
||||
+ // |tabId|: The tab to execute JavaScript in. Defaults to active tab.
|
||||
+ // |code|: The JavaScript code to execute.
|
||||
@@ -392,4 +180,3 @@ index 0000000000000..da7102fd5b5e3
|
||||
+ ChoosePathCallback callback);
|
||||
+ };
|
||||
+};
|
||||
+
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
diff --git a/extensions/browser/extension_function_histogram_value.h b/extensions/browser/extension_function_histogram_value.h
|
||||
index 2a2f2e26757cc..61daa201a826a 100644
|
||||
index 2a2f2e26757cc..427ff821a61f6 100644
|
||||
--- a/extensions/browser/extension_function_histogram_value.h
|
||||
+++ b/extensions/browser/extension_function_histogram_value.h
|
||||
@@ -2021,6 +2021,32 @@ enum HistogramValue {
|
||||
@@ -7,19 +7,19 @@ index 2a2f2e26757cc..61daa201a826a 100644
|
||||
AUTOFILLPRIVATE_GETREQUIREDATTRIBUTETYPESFORENTITYTYPENAME = 1959,
|
||||
PDFVIEWERPRIVATE_GLICSUMMARIZE = 1960,
|
||||
+ // BrowserOS: extension function histogram values
|
||||
+ BROWSER_OS_GETACCESSIBILITYTREE = 1961,
|
||||
+ BROWSER_OS_GETINTERACTIVESNAPSHOT = 1962,
|
||||
+ BROWSER_OS_CLICK = 1963,
|
||||
+ BROWSER_OS_INPUTTEXT = 1964,
|
||||
+ BROWSER_OS_CLEAR = 1965,
|
||||
+ DELETED_BROWSER_OS_GETACCESSIBILITYTREE = 1961,
|
||||
+ DELETED_BROWSER_OS_GETINTERACTIVESNAPSHOT = 1962,
|
||||
+ DELETED_BROWSER_OS_CLICK = 1963,
|
||||
+ DELETED_BROWSER_OS_INPUTTEXT = 1964,
|
||||
+ DELETED_BROWSER_OS_CLEAR = 1965,
|
||||
+ BROWSER_OS_GETPAGELOADSTATUS = 1966,
|
||||
+ BROWSER_OS_SCROLLUP = 1967,
|
||||
+ BROWSER_OS_SCROLLDOWN = 1968,
|
||||
+ BROWSER_OS_SCROLLTONODE = 1969,
|
||||
+ BROWSER_OS_SENDKEYS = 1970,
|
||||
+ DELETED_BROWSER_OS_SCROLLUP = 1967,
|
||||
+ DELETED_BROWSER_OS_SCROLLDOWN = 1968,
|
||||
+ DELETED_BROWSER_OS_SCROLLTONODE = 1969,
|
||||
+ DELETED_BROWSER_OS_SENDKEYS = 1970,
|
||||
+ BROWSER_OS_GETPAGESTRUCTURE = 1971,
|
||||
+ BROWSER_OS_CAPTURESCREENSHOT = 1972,
|
||||
+ BROWSER_OS_GETSNAPSHOT = 1973,
|
||||
+ DELETED_BROWSER_OS_CAPTURESCREENSHOT = 1972,
|
||||
+ DELETED_BROWSER_OS_GETSNAPSHOT = 1973,
|
||||
+ BROWSER_OS_GETPREF = 1974,
|
||||
+ BROWSER_OS_SETPREF = 1975,
|
||||
+ BROWSER_OS_GETALLPREFS = 1976,
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
diff --git a/tools/metrics/histograms/metadata/extensions/enums.xml b/tools/metrics/histograms/metadata/extensions/enums.xml
|
||||
index aa7937ea189d3..341f957a8b134 100644
|
||||
index aa7937ea189d3..be2f9f0ec1559 100644
|
||||
--- a/tools/metrics/histograms/metadata/extensions/enums.xml
|
||||
+++ b/tools/metrics/histograms/metadata/extensions/enums.xml
|
||||
@@ -2877,6 +2877,31 @@ Called by update_extension_histograms.py.-->
|
||||
<int value="1959"
|
||||
label="AUTOFILLPRIVATE_GETREQUIREDATTRIBUTETYPESFORENTITYTYPENAME"/>
|
||||
<int value="1960" label="PDFVIEWERPRIVATE_GLICSUMMARIZE"/>
|
||||
+ <int value="1961" label="BROWSER_OS_GETACCESSIBILITYTREE"/>
|
||||
+ <int value="1962" label="BROWSER_OS_GETINTERACTIVESNAPSHOT"/>
|
||||
+ <int value="1963" label="BROWSER_OS_CLICK"/>
|
||||
+ <int value="1964" label="BROWSER_OS_INPUTTEXT"/>
|
||||
+ <int value="1965" label="BROWSER_OS_CLEAR"/>
|
||||
+ <int value="1961" label="DELETED_BROWSER_OS_GETACCESSIBILITYTREE"/>
|
||||
+ <int value="1962" label="DELETED_BROWSER_OS_GETINTERACTIVESNAPSHOT"/>
|
||||
+ <int value="1963" label="DELETED_BROWSER_OS_CLICK"/>
|
||||
+ <int value="1964" label="DELETED_BROWSER_OS_INPUTTEXT"/>
|
||||
+ <int value="1965" label="DELETED_BROWSER_OS_CLEAR"/>
|
||||
+ <int value="1966" label="BROWSER_OS_GETPAGELOADSTATUS"/>
|
||||
+ <int value="1967" label="BROWSER_OS_SCROLLUP"/>
|
||||
+ <int value="1968" label="BROWSER_OS_SCROLLDOWN"/>
|
||||
+ <int value="1969" label="BROWSER_OS_SCROLLTONODE"/>
|
||||
+ <int value="1970" label="BROWSER_OS_SENDKEYS"/>
|
||||
+ <int value="1967" label="DELETED_BROWSER_OS_SCROLLUP"/>
|
||||
+ <int value="1968" label="DELETED_BROWSER_OS_SCROLLDOWN"/>
|
||||
+ <int value="1969" label="DELETED_BROWSER_OS_SCROLLTONODE"/>
|
||||
+ <int value="1970" label="DELETED_BROWSER_OS_SENDKEYS"/>
|
||||
+ <int value="1971" label="BROWSER_OS_GETPAGESTRUCTURE"/>
|
||||
+ <int value="1972" label="BROWSER_OS_CAPTURESCREENSHOT"/>
|
||||
+ <int value="1973" label="BROWSER_OS_GETSNAPSHOT"/>
|
||||
+ <int value="1972" label="DELETED_BROWSER_OS_CAPTURESCREENSHOT"/>
|
||||
+ <int value="1973" label="DELETED_BROWSER_OS_GETSNAPSHOT"/>
|
||||
+ <int value="1974" label="BROWSER_OS_GETPREF"/>
|
||||
+ <int value="1975" label="BROWSER_OS_SETPREF"/>
|
||||
+ <int value="1976" label="BROWSER_OS_GETALLPREFS"/>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
BROWSEROS_MAJOR=0
|
||||
BROWSEROS_MINOR=45
|
||||
BROWSEROS_BUILD=0
|
||||
BROWSEROS_PATCH=0
|
||||
BROWSEROS_PATCH=1
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.hypervisor</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.disable-library-validation</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.server</key>
|
||||
<true/>
|
||||
<key>com.apple.security.virtualization</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
Reference in New Issue
Block a user