@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.
Files changed (131) hide show
  1. package/LICENSE.txt +49 -0
  2. package/README.md +33 -2
  3. package/ThirdPartyNotices.txt +4685 -0
  4. package/package.json +30 -4
  5. package/provider-clis.json +1211 -0
  6. package/python/XACTOR.md +146 -0
  7. package/python/browser_actor_host.py +456 -0
  8. package/python/cdp_page.py +363 -0
  9. package/python/harness_check.py +77 -0
  10. package/python/requirements.txt +1 -0
  11. package/python/test_line_endings.py +185 -0
  12. package/python/xactor/__init__.py +18 -0
  13. package/python/xactor/__main__.py +226 -0
  14. package/python/xactor/api.py +882 -0
  15. package/python/xactor/bridge.py +784 -0
  16. package/python/xactor/dom.py +117 -0
  17. package/python/xactor/man/add_card.md +89 -0
  18. package/python/xactor/man/agent-vs-fork.md +108 -0
  19. package/python/xactor/man/agent.md +333 -0
  20. package/python/xactor/man/ask_model.md +110 -0
  21. package/python/xactor/man/ask_reference.md +56 -0
  22. package/python/xactor/man/ask_user.md +103 -0
  23. package/python/xactor/man/attach_app.md +595 -0
  24. package/python/xactor/man/attach_ide.md +65 -0
  25. package/python/xactor/man/attach_ide_surface.md +31 -0
  26. package/python/xactor/man/await_user.md +214 -0
  27. package/python/xactor/man/bash.md +142 -0
  28. package/python/xactor/man/browser-driver.md +137 -0
  29. package/python/xactor/man/browser_activity.md +53 -0
  30. package/python/xactor/man/browser_attach.md +84 -0
  31. package/python/xactor/man/browser_close_tab.md +50 -0
  32. package/python/xactor/man/browser_detach.md +50 -0
  33. package/python/xactor/man/browser_exec.md +66 -0
  34. package/python/xactor/man/browser_focus_tab.md +47 -0
  35. package/python/xactor/man/browser_hybrid.md +47 -0
  36. package/python/xactor/man/browser_open.md +50 -0
  37. package/python/xactor/man/browser_open_tab.md +58 -0
  38. package/python/xactor/man/browser_plumbing.md +107 -0
  39. package/python/xactor/man/browser_screenshot.md +56 -0
  40. package/python/xactor/man/browser_snapshot.md +52 -0
  41. package/python/xactor/man/browser_tabs.md +67 -0
  42. package/python/xactor/man/bulletin.md +88 -0
  43. package/python/xactor/man/capture_page.md +54 -0
  44. package/python/xactor/man/cd.md +64 -0
  45. package/python/xactor/man/chess_move.md +53 -0
  46. package/python/xactor/man/children.md +89 -0
  47. package/python/xactor/man/close_task.md +14 -0
  48. package/python/xactor/man/compact.md +75 -0
  49. package/python/xactor/man/drive.md +140 -0
  50. package/python/xactor/man/drive_space.md +72 -0
  51. package/python/xactor/man/driving.md +168 -0
  52. package/python/xactor/man/edit_file.md +112 -0
  53. package/python/xactor/man/finding.md +194 -0
  54. package/python/xactor/man/fork_agent.md +45 -0
  55. package/python/xactor/man/glob.md +83 -0
  56. package/python/xactor/man/grep.md +115 -0
  57. package/python/xactor/man/harness.md +165 -0
  58. package/python/xactor/man/harness_check.md +70 -0
  59. package/python/xactor/man/harness_format.md +257 -0
  60. package/python/xactor/man/highlight.md +66 -0
  61. package/python/xactor/man/instruct.md +111 -0
  62. package/python/xactor/man/ls.md +63 -0
  63. package/python/xactor/man/man.md +86 -0
  64. package/python/xactor/man/merge_wiz.md +40 -0
  65. package/python/xactor/man/notebook.md +98 -0
  66. package/python/xactor/man/overview.md +89 -0
  67. package/python/xactor/man/permissions.md +137 -0
  68. package/python/xactor/man/phase.md +150 -0
  69. package/python/xactor/man/powershell.md +74 -0
  70. package/python/xactor/man/profiles.md +60 -0
  71. package/python/xactor/man/project_drive.md +49 -0
  72. package/python/xactor/man/read_bulletin.md +89 -0
  73. package/python/xactor/man/read_file.md +90 -0
  74. package/python/xactor/man/relay.md +216 -0
  75. package/python/xactor/man/reply.md +72 -0
  76. package/python/xactor/man/reply_message.md +78 -0
  77. package/python/xactor/man/report_bug.md +118 -0
  78. package/python/xactor/man/request_answer.md +142 -0
  79. package/python/xactor/man/request_merge_decisions.md +121 -0
  80. package/python/xactor/man/request_operator_action.md +152 -0
  81. package/python/xactor/man/request_review.md +122 -0
  82. package/python/xactor/man/request_user_action.md +126 -0
  83. package/python/xactor/man/return_card.md +71 -0
  84. package/python/xactor/man/run.md +99 -0
  85. package/python/xactor/man/sandbox.md +144 -0
  86. package/python/xactor/man/session_fork.md +74 -0
  87. package/python/xactor/man/session_harness.md +109 -0
  88. package/python/xactor/man/session_join.md +102 -0
  89. package/python/xactor/man/session_land.md +73 -0
  90. package/python/xactor/man/session_nudge.md +73 -0
  91. package/python/xactor/man/session_rest.md +73 -0
  92. package/python/xactor/man/session_scope.md +84 -0
  93. package/python/xactor/man/session_terminate.md +58 -0
  94. package/python/xactor/man/session_token.md +78 -0
  95. package/python/xactor/man/sessions.md +93 -0
  96. package/python/xactor/man/set_harness.md +15 -0
  97. package/python/xactor/man/spawn_swarm.md +80 -0
  98. package/python/xactor/man/submit_evidence.md +54 -0
  99. package/python/xactor/man/swarm_brief.md +30 -0
  100. package/python/xactor/man/task_status.md +82 -0
  101. package/python/xactor/man/tasks.md +47 -0
  102. package/python/xactor/man/todo_write.md +104 -0
  103. package/python/xactor/man/update_card.md +70 -0
  104. package/python/xactor/man/use_profile.md +90 -0
  105. package/python/xactor/man/use_widget.md +82 -0
  106. package/python/xactor/man/wait_for_task.md +105 -0
  107. package/python/xactor/man/widget-model.md +284 -0
  108. package/python/xactor/man/widgets.md +59 -0
  109. package/python/xactor/man/write_file.md +76 -0
  110. package/python/xactor/man/writing-a-widget.md +142 -0
  111. package/python/xactor/man/xylon.md +120 -0
  112. package/python/xactor/man/xylon_blame.md +80 -0
  113. package/python/xactor/man/xylon_read.md +131 -0
  114. package/python/xactor/man/xylon_search.md +125 -0
  115. package/python/xactor/manual.py +195 -0
  116. package/python/xactor/parent_watch.py +115 -0
  117. package/python/xactor/plan_mode.py +350 -0
  118. package/python/xactor/sandbox.py +856 -0
  119. package/python/xactor/seals.py +438 -0
  120. package/python/xactor/shell_hints.py +210 -0
  121. package/python/xactor/states/__init__.py +13 -0
  122. package/python/xactor/states/base.py +523 -0
  123. package/python/xactor/states/registry.py +274 -0
  124. package/python/xactor/stdlib_api.py +6345 -0
  125. package/python/xactor/widgets/__init__.py +1 -0
  126. package/python/xactor/widgets/github_tokens.py +314 -0
  127. package/python/xactor/widgets/google_aistudio.py +365 -0
  128. package/settings-schema.json +403 -0
  129. package/viewer/index.css +160 -0
  130. package/viewer/index.js +15 -0
  131. 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`