@widgex/wgx 0.0.0-stage → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.txt +49 -0
- package/README.md +33 -2
- package/ThirdPartyNotices.txt +4685 -0
- package/package.json +30 -4
- package/provider-clis.json +1211 -0
- package/python/XACTOR.md +146 -0
- package/python/browser_actor_host.py +456 -0
- package/python/cdp_page.py +363 -0
- package/python/harness_check.py +77 -0
- package/python/requirements.txt +1 -0
- package/python/test_line_endings.py +185 -0
- package/python/xactor/__init__.py +18 -0
- package/python/xactor/__main__.py +226 -0
- package/python/xactor/api.py +882 -0
- package/python/xactor/bridge.py +784 -0
- package/python/xactor/dom.py +117 -0
- package/python/xactor/man/add_card.md +89 -0
- package/python/xactor/man/agent-vs-fork.md +108 -0
- package/python/xactor/man/agent.md +333 -0
- package/python/xactor/man/ask_model.md +110 -0
- package/python/xactor/man/ask_reference.md +56 -0
- package/python/xactor/man/ask_user.md +103 -0
- package/python/xactor/man/attach_app.md +595 -0
- package/python/xactor/man/attach_ide.md +65 -0
- package/python/xactor/man/attach_ide_surface.md +31 -0
- package/python/xactor/man/await_user.md +214 -0
- package/python/xactor/man/bash.md +142 -0
- package/python/xactor/man/browser-driver.md +137 -0
- package/python/xactor/man/browser_activity.md +53 -0
- package/python/xactor/man/browser_attach.md +84 -0
- package/python/xactor/man/browser_close_tab.md +50 -0
- package/python/xactor/man/browser_detach.md +50 -0
- package/python/xactor/man/browser_exec.md +66 -0
- package/python/xactor/man/browser_focus_tab.md +47 -0
- package/python/xactor/man/browser_hybrid.md +47 -0
- package/python/xactor/man/browser_open.md +50 -0
- package/python/xactor/man/browser_open_tab.md +58 -0
- package/python/xactor/man/browser_plumbing.md +107 -0
- package/python/xactor/man/browser_screenshot.md +56 -0
- package/python/xactor/man/browser_snapshot.md +52 -0
- package/python/xactor/man/browser_tabs.md +67 -0
- package/python/xactor/man/bulletin.md +88 -0
- package/python/xactor/man/capture_page.md +54 -0
- package/python/xactor/man/cd.md +64 -0
- package/python/xactor/man/chess_move.md +53 -0
- package/python/xactor/man/children.md +89 -0
- package/python/xactor/man/close_task.md +14 -0
- package/python/xactor/man/compact.md +75 -0
- package/python/xactor/man/drive.md +140 -0
- package/python/xactor/man/drive_space.md +72 -0
- package/python/xactor/man/driving.md +168 -0
- package/python/xactor/man/edit_file.md +112 -0
- package/python/xactor/man/finding.md +194 -0
- package/python/xactor/man/fork_agent.md +45 -0
- package/python/xactor/man/glob.md +83 -0
- package/python/xactor/man/grep.md +115 -0
- package/python/xactor/man/harness.md +165 -0
- package/python/xactor/man/harness_check.md +70 -0
- package/python/xactor/man/harness_format.md +257 -0
- package/python/xactor/man/highlight.md +66 -0
- package/python/xactor/man/instruct.md +111 -0
- package/python/xactor/man/ls.md +63 -0
- package/python/xactor/man/man.md +86 -0
- package/python/xactor/man/merge_wiz.md +40 -0
- package/python/xactor/man/notebook.md +98 -0
- package/python/xactor/man/overview.md +89 -0
- package/python/xactor/man/permissions.md +137 -0
- package/python/xactor/man/phase.md +150 -0
- package/python/xactor/man/powershell.md +74 -0
- package/python/xactor/man/profiles.md +60 -0
- package/python/xactor/man/project_drive.md +49 -0
- package/python/xactor/man/read_bulletin.md +89 -0
- package/python/xactor/man/read_file.md +90 -0
- package/python/xactor/man/relay.md +216 -0
- package/python/xactor/man/reply.md +72 -0
- package/python/xactor/man/reply_message.md +78 -0
- package/python/xactor/man/report_bug.md +118 -0
- package/python/xactor/man/request_answer.md +142 -0
- package/python/xactor/man/request_merge_decisions.md +121 -0
- package/python/xactor/man/request_operator_action.md +152 -0
- package/python/xactor/man/request_review.md +122 -0
- package/python/xactor/man/request_user_action.md +126 -0
- package/python/xactor/man/return_card.md +71 -0
- package/python/xactor/man/run.md +99 -0
- package/python/xactor/man/sandbox.md +144 -0
- package/python/xactor/man/session_fork.md +74 -0
- package/python/xactor/man/session_harness.md +109 -0
- package/python/xactor/man/session_join.md +102 -0
- package/python/xactor/man/session_land.md +73 -0
- package/python/xactor/man/session_nudge.md +73 -0
- package/python/xactor/man/session_rest.md +73 -0
- package/python/xactor/man/session_scope.md +84 -0
- package/python/xactor/man/session_terminate.md +58 -0
- package/python/xactor/man/session_token.md +78 -0
- package/python/xactor/man/sessions.md +93 -0
- package/python/xactor/man/set_harness.md +15 -0
- package/python/xactor/man/spawn_swarm.md +80 -0
- package/python/xactor/man/submit_evidence.md +54 -0
- package/python/xactor/man/swarm_brief.md +30 -0
- package/python/xactor/man/task_status.md +82 -0
- package/python/xactor/man/tasks.md +47 -0
- package/python/xactor/man/todo_write.md +104 -0
- package/python/xactor/man/update_card.md +70 -0
- package/python/xactor/man/use_profile.md +90 -0
- package/python/xactor/man/use_widget.md +82 -0
- package/python/xactor/man/wait_for_task.md +105 -0
- package/python/xactor/man/widget-model.md +284 -0
- package/python/xactor/man/widgets.md +59 -0
- package/python/xactor/man/write_file.md +76 -0
- package/python/xactor/man/writing-a-widget.md +142 -0
- package/python/xactor/man/xylon.md +120 -0
- package/python/xactor/man/xylon_blame.md +80 -0
- package/python/xactor/man/xylon_read.md +131 -0
- package/python/xactor/man/xylon_search.md +125 -0
- package/python/xactor/manual.py +195 -0
- package/python/xactor/parent_watch.py +115 -0
- package/python/xactor/plan_mode.py +350 -0
- package/python/xactor/sandbox.py +856 -0
- package/python/xactor/seals.py +438 -0
- package/python/xactor/shell_hints.py +210 -0
- package/python/xactor/states/__init__.py +13 -0
- package/python/xactor/states/base.py +523 -0
- package/python/xactor/states/registry.py +274 -0
- package/python/xactor/stdlib_api.py +6345 -0
- package/python/xactor/widgets/__init__.py +1 -0
- package/python/xactor/widgets/github_tokens.py +314 -0
- package/python/xactor/widgets/google_aistudio.py +365 -0
- package/settings-schema.json +403 -0
- package/viewer/index.css +160 -0
- package/viewer/index.js +15 -0
- package/wgx.mjs +290659 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# api.browser_detach(target_session_id) -> dict
|
|
2
|
+
<!-- group: Working on a website -->
|
|
3
|
+
|
|
4
|
+
Give a borrowed browser back, waking whoever is waiting next.
|
|
5
|
+
|
|
6
|
+
The other half of `browser_attach`. Until you call it, the owner and every other
|
|
7
|
+
session waiting for that browser stays blocked.
|
|
8
|
+
|
|
9
|
+
## Options
|
|
10
|
+
|
|
11
|
+
- **`target_session_id`** — the session you borrowed from.
|
|
12
|
+
|
|
13
|
+
## Returns
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
{"status": "detached", "dispatcher_id": str, "held_ms": int}
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Detaching something you were not holding is not an error — you get
|
|
20
|
+
`{"status": "detached", "note": ...}`. Only the session that took the lease can
|
|
21
|
+
release it; anyone else gets an error naming the real holder, so one session
|
|
22
|
+
cannot cut another's work short.
|
|
23
|
+
|
|
24
|
+
Your shell is pointed back at your own browser, if you have one.
|
|
25
|
+
|
|
26
|
+
## Examples
|
|
27
|
+
|
|
28
|
+
The shape to use every time:
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
r = await api.browser_attach("session-42")
|
|
32
|
+
if r.get("occupied"):
|
|
33
|
+
return r["error"]
|
|
34
|
+
try:
|
|
35
|
+
await api.browser_exec(None, "print(await api.outline())")
|
|
36
|
+
finally:
|
|
37
|
+
await api.browser_detach("session-42")
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Getting it wrong
|
|
41
|
+
|
|
42
|
+
**Detaching on the happy path only.** An exception between attach and detach
|
|
43
|
+
leaves the browser held with nobody able to take it. `finally` is the point.
|
|
44
|
+
|
|
45
|
+
**Assuming a turn ending releases it.** It does not. The lease outlives your
|
|
46
|
+
turn; only this call clears it.
|
|
47
|
+
|
|
48
|
+
## See also
|
|
49
|
+
|
|
50
|
+
`browser_attach`, `browser_exec`, `sessions`, `browser_plumbing`
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# api.browser_exec(session_id, code, timeout=None) -> dict
|
|
2
|
+
<!-- group: Working on a website -->
|
|
3
|
+
|
|
4
|
+
Run Python in **another session's** browser shell. Gated.
|
|
5
|
+
|
|
6
|
+
Almost certainly not what you want. For your own browser there is nothing to
|
|
7
|
+
wrap: the page verbs and the widget symbols are bound in this shell, so
|
|
8
|
+
`api.click(...)` and `registry` work directly. See `driving`.
|
|
9
|
+
|
|
10
|
+
```python
|
|
11
|
+
# Your own browser — no browser_exec anywhere.
|
|
12
|
+
await api.use_widget("sentry_tokens")
|
|
13
|
+
state = await registry.here(api)
|
|
14
|
+
state = await state.execute("create_token", token_name="ci-deploy")
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
This call was once the only way in, because the browser ran in a second Python
|
|
18
|
+
process and its namespace was unreachable from here — `code` was shipped across
|
|
19
|
+
as source text. That process is gone. What is left is the case the name now
|
|
20
|
+
describes: reaching a browser that belongs to a session that is not yours.
|
|
21
|
+
|
|
22
|
+
## Options
|
|
23
|
+
|
|
24
|
+
- **`session_id`** — the session whose browser to use. Pass `None` for your
|
|
25
|
+
own, though if you are doing that you want `driving` instead.
|
|
26
|
+
- **`code`** — Python. `await` is supported at the top level.
|
|
27
|
+
- **`timeout`** — milliseconds. Default 60,000.
|
|
28
|
+
|
|
29
|
+
## Borrowing someone else's browser
|
|
30
|
+
|
|
31
|
+
`browser_attach` first, or the tabs are not yours to drive and the call is
|
|
32
|
+
refused. Give it back when you are done, or whoever is queued behind you waits
|
|
33
|
+
for the timeout:
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
await api.browser_attach(target_session_id="session-12")
|
|
37
|
+
try:
|
|
38
|
+
out = await api.browser_exec("session-12", """
|
|
39
|
+
state = await registry.here(api)
|
|
40
|
+
print(state.name)
|
|
41
|
+
""")
|
|
42
|
+
finally:
|
|
43
|
+
await api.browser_detach(target_session_id="session-12")
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## The scope over there
|
|
47
|
+
|
|
48
|
+
The same names this shell has: `api` with the page verbs on it, plus
|
|
49
|
+
`registry`, `PageState`, `Observation`, `Transition`, `Step`, `asyncio` and
|
|
50
|
+
`json`. `__import__`, `open`, `eval`, `exec`, `compile`, `input` and
|
|
51
|
+
`breakpoint` are blocked, as they are here.
|
|
52
|
+
|
|
53
|
+
## Getting it wrong
|
|
54
|
+
|
|
55
|
+
**Using it for your own browser.** A round trip, a lost traceback and a string
|
|
56
|
+
of Python you have to escape, to reach verbs that were already in scope.
|
|
57
|
+
|
|
58
|
+
**Expecting values back other than through `print`.** What you get is the
|
|
59
|
+
shell's output. Print what you need.
|
|
60
|
+
|
|
61
|
+
**Skipping `browser_attach`.** Another session's tabs are owned. Reaching for
|
|
62
|
+
them without attaching is refused rather than shared.
|
|
63
|
+
|
|
64
|
+
## See also
|
|
65
|
+
|
|
66
|
+
`driving`, `browser_attach`, `browser_detach`, `use_widget`, `widget-model`
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# api.browser_focus_tab(tab_id) -> dict
|
|
2
|
+
<!-- also: focus-tab, switch-tab -->
|
|
3
|
+
<!-- group: Working on a website -->
|
|
4
|
+
|
|
5
|
+
Drive a different tab of your session's window.
|
|
6
|
+
|
|
7
|
+
Brings the tab to the front and repoints your browser shell at it. Whatever you
|
|
8
|
+
were part-way through on the tab you left is still there when you come back — a
|
|
9
|
+
half-filled form stays half-filled.
|
|
10
|
+
|
|
11
|
+
## Options
|
|
12
|
+
|
|
13
|
+
- **`tab_id`** — from `api.browser_tabs()`.
|
|
14
|
+
|
|
15
|
+
## Returns
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
{"status": "focused", "tab_id": str, "url": str}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`{"error": ...}` if the tab is not in your window, or if its page has gone.
|
|
22
|
+
|
|
23
|
+
## Examples
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
tabs = (await api.browser_tabs())["tabs"]
|
|
27
|
+
target = next(t for t in tabs if "console" in t["url"])
|
|
28
|
+
await api.browser_focus_tab(target["tab_id"])
|
|
29
|
+
print((await api.browser_exec(None, "print(await api.outline())"))["output"])
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Getting it wrong
|
|
33
|
+
|
|
34
|
+
**Focusing another session's tab.** You cannot; only your window's tabs are
|
|
35
|
+
addressable. Use `api.browser_attach` to borrow another session's window.
|
|
36
|
+
|
|
37
|
+
**Forgetting the state registry follows the page.** After focusing, run
|
|
38
|
+
`registry.here(api)` again — the widget state that applied on the old tab says
|
|
39
|
+
nothing about the new one.
|
|
40
|
+
|
|
41
|
+
**Assuming focus is instant for the user.** It repoints your driver. The user's
|
|
42
|
+
own view is theirs.
|
|
43
|
+
|
|
44
|
+
## See also
|
|
45
|
+
|
|
46
|
+
`browser_tabs`, `browser_open_tab`, `browser_close_tab`, `browser_exec`,
|
|
47
|
+
`widget-model`
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# api.browser_hybrid / api.query — withdrawn
|
|
2
|
+
<!-- also: hybrid_snapshot, query, query_one, hybrid -->
|
|
3
|
+
<!-- group: Working on a website -->
|
|
4
|
+
|
|
5
|
+
Both returned a flat list. Use `find`, `one`, `outline` or `snapshot`.
|
|
6
|
+
|
|
7
|
+
## Why they went
|
|
8
|
+
|
|
9
|
+
The list had no containment. Eight "Delete" buttons on eight table rows came
|
|
10
|
+
back as eight identical entries, and nothing in the payload said which row each
|
|
11
|
+
belonged to — so the only way to disambiguate was a screenshot, which is a round
|
|
12
|
+
trip through the model for something the page already knew.
|
|
13
|
+
|
|
14
|
+
Each node also carried five raw CSS strings (`color`, `bgColor`, `border`,
|
|
15
|
+
`fontWeight`, `opacity`) that nothing read, at roughly forty tokens apiece. A
|
|
16
|
+
console page cost 9 000–25 000 tokens to look at, and one rode along on *every*
|
|
17
|
+
`browser_exec` result whether anyone wanted it or not.
|
|
18
|
+
|
|
19
|
+
The ids were worse. `xh_3` was a counter stamped into the page as an attribute,
|
|
20
|
+
so a framework re-render destroyed the node and the id silently dangled — or
|
|
21
|
+
matched whatever was minted into that slot next.
|
|
22
|
+
|
|
23
|
+
## What to use
|
|
24
|
+
|
|
25
|
+
| Was | Now |
|
|
26
|
+
|---|---|
|
|
27
|
+
| `browser_hybrid(disp)` | `await api.outline()` for the shape, `await api.snapshot()` for the interactive tree |
|
|
28
|
+
| `query(role=..., name=...)` | `await api.search(role=..., like=...)` — ranked, with scores |
|
|
29
|
+
| `query_one(...)` | `await api.one(...)` — exactly one, or the candidates that tied |
|
|
30
|
+
| holding an `xh_3` | a uid (`e7f3a1`) or a path (`main/table/row"prod-key"/button"Delete"`) |
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
# was: hits = [n for n in snap["nodes"] if "Create token" in str(n)]
|
|
34
|
+
hits = await api.search(role="button", like="create token")
|
|
35
|
+
|
|
36
|
+
# was: impossible without a screenshot
|
|
37
|
+
btn = await api.one(role="button", text_exact="Delete",
|
|
38
|
+
under='main/table/row"prod-key"')
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Calling either raises with this list rather than returning an `AttributeError`,
|
|
42
|
+
because a missing method invites a guess and the guess is usually another name
|
|
43
|
+
that is also missing.
|
|
44
|
+
|
|
45
|
+
## See also
|
|
46
|
+
|
|
47
|
+
`finding`, `browser_exec`, `widget-model`, `browser_snapshot`
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# api.browser_open(...) — withdrawn
|
|
2
|
+
<!-- group: Working on a website -->
|
|
3
|
+
|
|
4
|
+
This call no longer opens anything. It raises, and names its replacements.
|
|
5
|
+
|
|
6
|
+
```
|
|
7
|
+
api.browser_open() is withdrawn — opening a browser at a bare URL says
|
|
8
|
+
nothing about why.
|
|
9
|
+
api.widgets() what is already modelled
|
|
10
|
+
api.use_widget(name) work through one of them
|
|
11
|
+
api.drive(url, why) no widget exists for this site yet
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
It was kept as a refusal rather than deleted on purpose: an agent that calls a
|
|
15
|
+
method which no longer exists gets an `AttributeError` and guesses, while this
|
|
16
|
+
tells it what to call instead. It is deliberately **not** gated — it never opens
|
|
17
|
+
anything, so there is nothing to approve.
|
|
18
|
+
|
|
19
|
+
## Why it went
|
|
20
|
+
|
|
21
|
+
A bare URL records the destination and nothing else. The user approving it
|
|
22
|
+
learns where you are going, not what you are going to do there. `use_widget`
|
|
23
|
+
names a written automation; `drive` demands a `why`. Both are things a person
|
|
24
|
+
can consent to.
|
|
25
|
+
|
|
26
|
+
## What to call
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
found = await api.widgets(url)
|
|
30
|
+
match = next((w for w in found if w["matches"]), None)
|
|
31
|
+
if match:
|
|
32
|
+
return await api.use_widget(match["name"])
|
|
33
|
+
return await api.drive(url, why="what you are there to do, in one line")
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Getting it wrong
|
|
37
|
+
|
|
38
|
+
**Believing a stale tool description.** An older bundle may still advertise
|
|
39
|
+
`api.browser_open`. The advertisement is wrong; the call is right. This page is
|
|
40
|
+
the only thing that can tell you which is which.
|
|
41
|
+
|
|
42
|
+
**Retrying it with different arguments.** It ignores all of them and raises
|
|
43
|
+
regardless.
|
|
44
|
+
|
|
45
|
+
**Reading the raise as a bug.** It is the intended behaviour. There is nothing
|
|
46
|
+
to report.
|
|
47
|
+
|
|
48
|
+
## See also
|
|
49
|
+
|
|
50
|
+
`widgets`, `use_widget`, `drive`, `widget-model`
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# api.browser_open_tab(url, background=False) -> dict
|
|
2
|
+
<!-- also: open-tab, new-tab -->
|
|
3
|
+
<!-- group: Working on a website -->
|
|
4
|
+
|
|
5
|
+
Open a new tab in your session's window and drive it.
|
|
6
|
+
|
|
7
|
+
The new tab becomes the focused one, so `browser_exec` runs against it from
|
|
8
|
+
then on. The tab you were on stays open and keeps its state — go back with
|
|
9
|
+
`browser_focus_tab`.
|
|
10
|
+
|
|
11
|
+
## Options
|
|
12
|
+
|
|
13
|
+
- **`url`** — where to open. `http`, `https` or `file`.
|
|
14
|
+
- **`background`** — open without taking the foreground. The tab is still
|
|
15
|
+
driven; it just does not steal the user's view. Useful when you are opening
|
|
16
|
+
several at once.
|
|
17
|
+
|
|
18
|
+
## Returns
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
{"status": "opened", "tab_id": str, "url": str, "shell_status": str}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Or `{"error": ...}` if the page opened but could not be attached to.
|
|
25
|
+
|
|
26
|
+
## Examples
|
|
27
|
+
|
|
28
|
+
Keep a reference page open beside the thing you are working on:
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
work = (await api.browser_tabs())["focused_tab_id"]
|
|
32
|
+
docs = await api.browser_open_tab("https://docs.example.com/api", background=True)
|
|
33
|
+
# ... read the docs tab ...
|
|
34
|
+
await api.browser_focus_tab(work)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Compare two accounts without losing either:
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
a = await api.browser_open_tab("https://console.example.com/org/a")
|
|
41
|
+
b = await api.browser_open_tab("https://console.example.com/org/b")
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Getting it wrong
|
|
45
|
+
|
|
46
|
+
**Using it as navigation.** To move the *current* tab, navigate inside
|
|
47
|
+
`browser_exec`. Opening a tab per page leaves a pile of them behind.
|
|
48
|
+
|
|
49
|
+
**Not closing what you opened.** Tabs persist for the life of the session.
|
|
50
|
+
Close the ones you are finished with.
|
|
51
|
+
|
|
52
|
+
**Expecting the first browser to come from here.** `api.drive` and
|
|
53
|
+
`api.use_widget` are the gated entry points that reach a site and are what the
|
|
54
|
+
user approves. This adds a tab to a window you already have.
|
|
55
|
+
|
|
56
|
+
## See also
|
|
57
|
+
|
|
58
|
+
`browser_tabs`, `browser_focus_tab`, `browser_close_tab`, `drive`, `use_widget`
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# Browser plumbing — connect / disconnect / hfactor / load / flush / context
|
|
2
|
+
<!-- also: browser_connect, browser_disconnect, browser_hfactor, browser_load, browser_flush, browser_context, browser -->
|
|
3
|
+
<!-- group: Working on a website -->
|
|
4
|
+
|
|
5
|
+
The six registry and lifecycle calls. None of them is gated; none is meaningful
|
|
6
|
+
on its own, which is why they share a page.
|
|
7
|
+
|
|
8
|
+
`use_widget` and `drive` do the connecting and loading for you. These are the
|
|
9
|
+
manual controls for when that is not enough.
|
|
10
|
+
|
|
11
|
+
## `browser_connect(session_id=None) -> dict`
|
|
12
|
+
|
|
13
|
+
Attach to a browser CDP target on port 9222 and return a dispatcher. `session_id`
|
|
14
|
+
is inferred from the shell when omitted.
|
|
15
|
+
|
|
16
|
+
## `browser_disconnect(dispatcher_id) -> dict`
|
|
17
|
+
|
|
18
|
+
Detach a dispatcher. Requires the dispatcher id — this one is not inferred.
|
|
19
|
+
|
|
20
|
+
## `browser_hfactor(value) -> dict`
|
|
21
|
+
|
|
22
|
+
The humanization speed factor. Every dispatcher delay becomes `seconds / value`,
|
|
23
|
+
so **higher is faster**. Default `1.0`, clamped at a minimum of `0.01` seconds
|
|
24
|
+
per delay. `2.0` halves the pauses; `0.5` doubles them.
|
|
25
|
+
|
|
26
|
+
## `browser_load(name=None, code=None, session_id=None) -> dict`
|
|
27
|
+
|
|
28
|
+
Load page states into the registry.
|
|
29
|
+
|
|
30
|
+
- **`name`** — a widget, spelled as `api.widgets()` lists it. The ordinary case.
|
|
31
|
+
- **`code`** — raw Python defining `PageState`s, for a site with no saved
|
|
32
|
+
widget.
|
|
33
|
+
- Give **one** of them. Neither raises `give either name= ... or code=`; both
|
|
34
|
+
raises `give name= or code=, not both`.
|
|
35
|
+
- `session_id` is inferred; there is no reason to name your own session.
|
|
36
|
+
|
|
37
|
+
`use_widget` already loads. Call this directly only when you are assembling
|
|
38
|
+
states by hand.
|
|
39
|
+
|
|
40
|
+
## `browser_flush(session_id=None) -> dict`
|
|
41
|
+
|
|
42
|
+
Clear every widget state from the registry. Use it when a stale machine is
|
|
43
|
+
matching pages it should not.
|
|
44
|
+
|
|
45
|
+
## `browser_context(session_id=None, widget_name=None) -> dict`
|
|
46
|
+
|
|
47
|
+
What is currently loaded, and the current dispatcher. This is how you find a
|
|
48
|
+
`dispatcher_id` for `browser_hybrid`, `browser_snapshot` or
|
|
49
|
+
`browser_screenshot`.
|
|
50
|
+
|
|
51
|
+
## Examples
|
|
52
|
+
|
|
53
|
+
Finding the dispatcher for a capture:
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
ctx = await api.browser_context()
|
|
57
|
+
return await api.browser_hybrid(ctx["dispatcherId"])
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Loading hand-written states for an unmodelled site:
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
await api.drive("https://status.example.com", why="read the incident timeline")
|
|
64
|
+
return await api.browser_load(code=open_states_source)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Speeding up a long scripted sequence:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
await api.browser_hfactor(3.0) # three times faster
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Clearing a registry that is matching the wrong pages:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
await api.browser_flush()
|
|
77
|
+
return await api.use_widget("github")
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Getting it wrong
|
|
81
|
+
|
|
82
|
+
**Reading `hfactor` backwards.** The commonest mistake with it, and it is worth
|
|
83
|
+
saying twice: it divides the delay, it is not the delay. Higher is *faster* —
|
|
84
|
+
`hfactor=6` finishes in a sixth of the time `hfactor=1` takes, and `0.5` takes
|
|
85
|
+
twice as long. Reaching for a small number to speed a run up does the opposite,
|
|
86
|
+
and reaching for a big one to be gentle on a flaky page makes it much flakier.
|
|
87
|
+
|
|
88
|
+
**Calling `browser_load` when `use_widget` would do.** `use_widget` loads,
|
|
89
|
+
navigates, and shows the user a named action. Loading by hand skips the
|
|
90
|
+
approval story.
|
|
91
|
+
|
|
92
|
+
**Passing `browser_load` both `name` and `code`, or neither.** Both raise, with
|
|
93
|
+
the fix in the message.
|
|
94
|
+
|
|
95
|
+
**Naming your own `session_id`.** It is inferred. Passing it is noise, and
|
|
96
|
+
passing the wrong one is a bug.
|
|
97
|
+
|
|
98
|
+
**Passing a session id where a dispatcher id is wanted.** `disconnect`,
|
|
99
|
+
`hybrid`, `snapshot` want the dispatcher. `browser_context` has it.
|
|
100
|
+
|
|
101
|
+
**Flushing mid-task.** Every loaded state goes, including the one you are
|
|
102
|
+
standing in. `registry.here()` will then return `UNKNOWN_STATE`.
|
|
103
|
+
|
|
104
|
+
## See also
|
|
105
|
+
|
|
106
|
+
`widget-model`, `driving`, `use_widget`, `drive`, `browser_hybrid`,
|
|
107
|
+
`widgets`
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# api.browser_screenshot(dispatcher_id=None, session_id=None, max_width=None) -> dict
|
|
2
|
+
<!-- group: Working on a website -->
|
|
3
|
+
|
|
4
|
+
Capture the page as an image you can look at. Gated.
|
|
5
|
+
|
|
6
|
+
The image is **seen, not queryable**. That single fact decides when to use it:
|
|
7
|
+
for layout, rendering, and confirming that something a human was asked to do
|
|
8
|
+
actually happened — never for finding an element.
|
|
9
|
+
|
|
10
|
+
## Options
|
|
11
|
+
|
|
12
|
+
- **`dispatcher_id`** — the dispatcher to capture.
|
|
13
|
+
- **`session_id`** — the session whose dispatcher to use, when you do not have
|
|
14
|
+
the dispatcher id. Give one or the other.
|
|
15
|
+
- **`max_width`** — capture width in pixels. Default 900. Smaller is cheaper.
|
|
16
|
+
|
|
17
|
+
## Returns
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
{"$image": {...}}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Which arrives as an image, not text. At the default width it costs roughly 675
|
|
24
|
+
tokens.
|
|
25
|
+
|
|
26
|
+
## Examples
|
|
27
|
+
|
|
28
|
+
Confirming a human-performed step:
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
await api.request_user_action("Please complete the 2FA challenge in the browser.")
|
|
32
|
+
return await api.browser_screenshot(session_id=session_id)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Checking a rendering problem, cheaply:
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
return await api.browser_screenshot(dispatcher_id=did, max_width=600)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Getting it wrong
|
|
42
|
+
|
|
43
|
+
**Using it to locate something.** You cannot grep an image. `browser_hybrid`
|
|
44
|
+
gives you structure, costs nothing to search, and does not prompt.
|
|
45
|
+
|
|
46
|
+
**Taking one after every action.** ~675 tokens each, and none of them can be
|
|
47
|
+
referred back to by anything but you looking again.
|
|
48
|
+
|
|
49
|
+
**Taking one at full width by habit.** `max_width=600` is often enough and
|
|
50
|
+
meaningfully cheaper.
|
|
51
|
+
|
|
52
|
+
**Passing neither id.** It needs one of `dispatcher_id` or `session_id`.
|
|
53
|
+
|
|
54
|
+
## See also
|
|
55
|
+
|
|
56
|
+
`browser_hybrid`, `browser_snapshot`, `request_user_action`, `browser_plumbing`
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# api.browser_snapshot(dispatcher_id) -> dict
|
|
2
|
+
<!-- group: Working on a website -->
|
|
3
|
+
|
|
4
|
+
Full DOM snapshot: URL, **cookies**, accessibility tree. Gated.
|
|
5
|
+
|
|
6
|
+
It requires permission for a specific reason — the snapshot includes the page's
|
|
7
|
+
cookies, so it reads the session credentials for whatever site is open. That is
|
|
8
|
+
why it prompts and `browser_hybrid` does not.
|
|
9
|
+
|
|
10
|
+
Reach for it when you genuinely need the whole document. Most of the time you do
|
|
11
|
+
not.
|
|
12
|
+
|
|
13
|
+
## Options
|
|
14
|
+
|
|
15
|
+
- **`dispatcher_id`** — the browser dispatcher to capture.
|
|
16
|
+
|
|
17
|
+
## Returns
|
|
18
|
+
|
|
19
|
+
A dict with the page URL, its cookies, and the accessibility tree. Large.
|
|
20
|
+
|
|
21
|
+
## Examples
|
|
22
|
+
|
|
23
|
+
When the accessibility tree is the actual question:
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
snap = await api.browser_snapshot(dispatcher_id)
|
|
27
|
+
return snap.get("url")
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Preferring the cheaper call, which is almost always right:
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
return await api.browser_hybrid(dispatcher_id) # no cookies, no prompt
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Getting it wrong
|
|
37
|
+
|
|
38
|
+
**Using it to find an element.** `browser_hybrid` returns searchable structure
|
|
39
|
+
without reading credentials and without a prompt. This is the wrong reflex and
|
|
40
|
+
the most common misuse.
|
|
41
|
+
|
|
42
|
+
**Treating the prompt as bureaucracy.** The user is being asked whether you may
|
|
43
|
+
read their logged-in session's cookies. Do not ask unless you need them.
|
|
44
|
+
|
|
45
|
+
**Putting the result somewhere it persists.** It contains credentials. It
|
|
46
|
+
belongs in the turn, not in a file.
|
|
47
|
+
|
|
48
|
+
**Passing a session id.** It wants a *dispatcher* id.
|
|
49
|
+
|
|
50
|
+
## See also
|
|
51
|
+
|
|
52
|
+
`browser_hybrid`, `browser_screenshot`, `browser_plumbing`, `permissions`
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# api.browser_tabs() -> dict
|
|
2
|
+
<!-- also: tabs, window, browser-window, session-window, tab-ownership -->
|
|
3
|
+
<!-- group: Working on a website -->
|
|
4
|
+
|
|
5
|
+
The tabs in your session's browser window.
|
|
6
|
+
|
|
7
|
+
Your session owns a **window** — a set of browser tabs. You can open new ones,
|
|
8
|
+
close them, and move between them freely. No other session can reach any tab in
|
|
9
|
+
your window, and you cannot reach theirs.
|
|
10
|
+
|
|
11
|
+
## Why a window, and not just a tab
|
|
12
|
+
|
|
13
|
+
Browsers used to be chosen rather than owned: every session attached to whatever
|
|
14
|
+
tab the browser listed first. Two sessions running at once drove the same tab
|
|
15
|
+
and navigated it out from under each other. Nothing errored — each one saw a
|
|
16
|
+
plausible page and drew confident conclusions about a site it was not looking
|
|
17
|
+
at. A session mapping Supabase decided the site was behind a login wall while
|
|
18
|
+
actually reading another session's Netlify tab.
|
|
19
|
+
|
|
20
|
+
Owning one tab each was not enough either: a session that opened a second tab
|
|
21
|
+
left it looking unowned, and the next session took it. So the boundary is the
|
|
22
|
+
whole window.
|
|
23
|
+
|
|
24
|
+
## Returns
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
{"tabs": [{"tab_id": str, "url": str, "focused": bool}],
|
|
28
|
+
"focused_tab_id": str | None}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`focused` marks the tab your `browser_exec` currently runs against. Tabs whose
|
|
32
|
+
page has been closed by hand are dropped before you are answered, so the list
|
|
33
|
+
is what actually exists.
|
|
34
|
+
|
|
35
|
+
An empty list means you have no browser open yet — `api.drive` or
|
|
36
|
+
`api.use_widget` is how you get the first one.
|
|
37
|
+
|
|
38
|
+
## Examples
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
for t in (await api.browser_tabs())["tabs"]:
|
|
42
|
+
print("*" if t["focused"] else " ", t["tab_id"][:8], t["url"])
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Work two pages at once — the docs and the console:
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
console = await api.browser_open_tab("https://dash.example.com/keys")
|
|
49
|
+
docs = await api.browser_open_tab("https://docs.example.com/api")
|
|
50
|
+
await api.browser_focus_tab(console["tab_id"])
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Getting it wrong
|
|
54
|
+
|
|
55
|
+
**Expecting to see another session's tabs.** You will not. That is the point.
|
|
56
|
+
`api.browser_attach` borrows another session's window deliberately.
|
|
57
|
+
|
|
58
|
+
**Holding a `tab_id` across a close.** Ids are not reused, and a closed tab is
|
|
59
|
+
gone. Re-list rather than remembering.
|
|
60
|
+
|
|
61
|
+
**Assuming the focused tab is what the user is looking at.** Focus here means
|
|
62
|
+
which tab *you* drive. The user may be looking at anything.
|
|
63
|
+
|
|
64
|
+
## See also
|
|
65
|
+
|
|
66
|
+
`browser_open_tab`, `browser_focus_tab`, `browser_close_tab`, `browser_attach`,
|
|
67
|
+
`drive`, `use_widget`, `browser_exec`
|