@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,168 @@
1
+ # Doing things on a page
2
+ <!-- also: l1, verbs, actions, page-verbs, transitions, drain, click, double_click, type_text, clear_and_type, press, focus, hover, scroll_to, select, navigate, here, wait_for, wait_stable, digest, evaluate, dom_click, dom_fill, upload, get_state, snapshot, screenshot, extract_secret, set_hfactor -->
3
+ <!-- group: Working on a website -->
4
+
5
+ `use_widget` and `drive` get you onto a page. This is everything you then *do*
6
+ on it.
7
+
8
+ **The verbs are right here, on `api`.** There is nothing to wrap them in:
9
+
10
+ ```python
11
+ r = await api.use_widget("sentry_tokens")
12
+ print(r["state"], r["transitions"]) # where you landed, and the way out
13
+
14
+ await api.transition("create_token")
15
+ await api.transition("configure", token_name="ci-deploy")
16
+ ```
17
+
18
+ `api.here()` is the current `PageState` if you want to look before you leap —
19
+ `describe()` lists what each transition does. `api.transition(name, **params)`
20
+ is `(await api.here()).execute(name, **params)`, which is the same thing the
21
+ bare `registry` in scope will give you:
22
+
23
+ ```python
24
+ state = await registry.here(api)
25
+ print(state.describe())
26
+ ```
27
+
28
+ That is the whole shape of a browser task. `registry`, `PageState`,
29
+ `Observation`, `Transition` and `Step` are bound in this shell alongside `api`,
30
+ so a widget's state machine is something you call directly.
31
+
32
+ This used to be untrue, and a lot of writing still assumes it. The browser ran
33
+ in a second Python process, and reaching it meant shipping source text across
34
+ with `api.browser_exec(session_id, "...")`. That process is gone. If you find
35
+ yourself building a string of Python to run somewhere else, you are working
36
+ from the old model — `browser_exec` now means *another session's* browser and
37
+ nothing else.
38
+
39
+ ## Load a chain; do not stand in it
40
+
41
+ Actions queue as you write them, in order. Awaiting each one is allowed and
42
+ hands back that action's own record — but it is not required, and one
43
+ `api.drain()` at the end collects the lot:
44
+
45
+ ```python
46
+ api.click(api.find(role="button", like="create token"))
47
+ api.type_text(api.find(role="textbox", right_of="Name"), "ci-deploy")
48
+ api.click(await api.one(role="button", text_exact="Create", under='dialog"Create Token"'))
49
+ report = await api.drain()
50
+ ```
51
+
52
+ The rule, and it is the one thing worth remembering here: **anything that
53
+ answers a question is awaited; anything that acts on the page is not.**
54
+ `api.find` builds a description and runs nothing, so it needs no `await`.
55
+ `api.one` and `api.search` go and look, so they do — passing an un-awaited
56
+ `api.one(...)` hands the action a coroutine instead of an element.
57
+
58
+ One report comes back for the whole chain: every action with its `outcome`, plus
59
+ `ok` and `failed` counts. An action that cannot resolve its target comes back
60
+ `ambiguous` or `not_found` **with the candidates that tied**, which is what you
61
+ narrow on.
62
+
63
+ Do not assume a failure halts what follows. A chain of three where the middle
64
+ action is `ambiguous` still runs the third — measured, not inferred. If a later
65
+ step only makes sense once an earlier one worked, await that one:
66
+
67
+ ```python
68
+ first = await api.click(api.find(role="button", like="create token"))
69
+ if not first["ok"]:
70
+ print("could not open it:", first.get("candidates"))
71
+ else:
72
+ api.clear_and_type('dialog"Create Token"/textbox"Name"', "ci-deploy")
73
+ print(await api.drain())
74
+ ```
75
+
76
+ ## The verbs
77
+
78
+ **Locating**: `find` (a deferred search), `search` (run it now, ranked
79
+ candidates), `one` (exactly one or the candidates that tied), `outline` (the
80
+ shape of the page), `snapshot` (the interactive tree, filtered), `digest`.
81
+
82
+ **Acting**: `click`, `double_click`, `type_text`, `clear_and_type`, `select`,
83
+ `press`, `focus`, `scroll_to`, `hover`, `navigate`, `drain`.
84
+
85
+ `double_click` is `click` with a count — the two presses land back-to-back at
86
+ one point, so the page sees `dblclick` rather than two clicks. You need it only
87
+ where single and double click on the same element mean different things. If
88
+ there is a button for what the double click does, prefer the button: it is what
89
+ a keyboard user has to press anyway, and it is addressable by name.
90
+
91
+ **Waiting**: `wait_for`, `wait_stable`. Rarely needed — an action already waits
92
+ for its own target.
93
+
94
+ **Files**: `upload(target, paths)` attaches files from disk to an
95
+ `<input type=file>` (the input, or the button/drop zone around it) — script
96
+ cannot, so this is the only way. Absolute path or a list of them.
97
+
98
+ **Other**: `dom_click`, `dom_fill` (for a page that never paints), `evaluate`,
99
+ `screenshot`, `extract_secret`, `get_state`, `set_hfactor` (humanisation
100
+ speed — **higher is faster**; it divides the delays, so 6 is six times quicker
101
+ than 1).
102
+
103
+ ## When you do not know what is there
104
+
105
+ ```python
106
+ print(await api.outline()) # the shape, cheapest
107
+ hits = await api.search(role="button", like="create")
108
+ for h in hits:
109
+ print(h.score, h.path, h.text)
110
+ ```
111
+
112
+ Anchor on what is visible when structure is not enough. This is the alternative
113
+ to a screenshot, and it costs nothing:
114
+
115
+ ```python
116
+ api.find(role="textbox", right_of="Name")
117
+ api.find(role="button", in_row='row"prod-key"')
118
+ ```
119
+
120
+ ## find or search?
121
+
122
+ They look alike and are not interchangeable. Neither is legacy.
123
+
124
+ | | |
125
+ |---|---|
126
+ | `api.find(**kw) -> FindSpec` | **Describes** a search. Runs nothing, costs no round trip, and is resolved by Layer 0 *at the moment the action fires*. |
127
+ | `await api.search(**kw) -> list[Ref]` | **Runs** it now and hands back the ranked candidates, each with its score. |
128
+ | `await api.one(**kw) -> Ref` | Runs it now and insists on exactly one, raising with whatever tied. |
129
+
130
+ The difference that matters is *when they resolve*. `find` is what lets a chain
131
+ click a button inside a dialog that does not exist yet:
132
+
133
+ ```python
134
+ api.click(api.find(role="button", like="create token")) # opens the dialog
135
+ api.click(api.find(role="button", text_exact="Create", # resolved after it opens
136
+ under='dialog"Create Token"'))
137
+ ```
138
+
139
+ Had the second line been a `search`, it would have run against the page as it
140
+ was *before* the first click, found nothing, and there would be nothing to act
141
+ on. So: **`find` to act, `search` to look.** Use `search` while you are working
142
+ out what is on a page, and paste the paths it prints into your widget.
143
+
144
+ ## Getting it wrong
145
+
146
+ **Wrapping it in `browser_exec`.** The commonest mistake, because most examples
147
+ written before the shells merged do exactly that. `api.click(...)` works here.
148
+ Building a string of Python to send somewhere costs a round trip, loses the
149
+ traceback, and is only correct when the browser belongs to another session.
150
+
151
+ **Bare verbs.** `click(...)` is a `NameError` — every verb is `api.click(...)`.
152
+
153
+ **Awaiting the wrong half.** Locating is awaited, acting is not. An un-awaited
154
+ `api.one(...)` passes a coroutine where an element belongs.
155
+
156
+ **Calling `registry.here()` without `api`.** It needs the api object to take its
157
+ snapshot: `await registry.here(api)`.
158
+
159
+ **Assuming `here()` found something.** It falls back to `UNKNOWN_STATE` when no
160
+ url_pattern matched. Check `state.name` before you execute a transition on it.
161
+
162
+ **Writing selectors when a transition exists.** `state.describe()` lists the
163
+ transitions the widget author tested. Yours is not tested.
164
+
165
+ ## See also
166
+
167
+ `use_widget`, `drive`, `widget-model`, `finding`, `writing-a-widget`,
168
+ `browser_exec`
@@ -0,0 +1,112 @@
1
+ # api.edit_file(path, old_string, new_string, replace_all=False, new_tab=False) -> str
2
+ <!-- group: Coding -->
3
+
4
+ Replace exact text in a file. Gated. **Reports failure by returning a string.**
5
+
6
+ The single most important thing on this page: `edit_file` does **not raise** when
7
+ the edit does not apply. It returns a string beginning `Error:` and leaves the
8
+ file untouched. Code that ignores the return value cannot tell a successful edit
9
+ from one that never happened.
10
+
11
+ ## Options
12
+
13
+ - **`path`** — absolute, or relative to the shell's cwd (`api.cd` moves it).
14
+ - **`old_string`** — the exact text to find. Byte-for-byte: whitespace,
15
+ indentation and line endings all count. It is a literal, not a regex.
16
+ - **`new_string`** — what replaces it. May be `""` to delete.
17
+ - **`replace_all`** — `False` (default) replaces the first occurrence and
18
+ *refuses* if there is more than one. `True` replaces every occurrence.
19
+ - **`new_tab`** — accepted and ignored. See `write_file`.
20
+
21
+ The file is read as UTF-8 and written back with `\n` line endings.
22
+
23
+ ## Returns
24
+
25
+ A string, always. There are four of them:
26
+
27
+ | Return | Meaning |
28
+ |---|---|
29
+ | `Replaced 1 occurrence(s) in <path>` | The edit landed. |
30
+ | `Replaced N occurrence(s) in <path>` | `replace_all=True`, N sites changed. |
31
+ | `Error: old_string not found in <path>` | Nothing changed. |
32
+ | `Error: old_string found N times in <path>. Use replace_all=True or provide more context to make it unique.` | Nothing changed. |
33
+
34
+ Only a genuinely broken file operation raises — a missing file is
35
+ `FileNotFoundError`, an undecodable one `UnicodeDecodeError`. Permission being
36
+ declined raises `PermissionError`.
37
+
38
+ ## Examples
39
+
40
+ Check the result. Every time:
41
+
42
+ ```python
43
+ r = await api.edit_file("src/app.py", "DEBUG = True", "DEBUG = False")
44
+ if r.startswith("Error:"):
45
+ return f"edit did not apply: {r}"
46
+ return r
47
+ ```
48
+
49
+ Several edits in one exec, stopping at the first that fails:
50
+
51
+ ```python
52
+ edits = [
53
+ ("timeout = 30", "timeout = 120"),
54
+ ("retries = 1", "retries = 3"),
55
+ ]
56
+ done = []
57
+ for old, new in edits:
58
+ r = await api.edit_file("src/config.py", old, new)
59
+ if r.startswith("Error:"):
60
+ return {"applied": done, "failed_on": old, "why": r}
61
+ done.append(old)
62
+ return {"applied": done}
63
+ ```
64
+
65
+ Renaming a symbol everywhere in one file:
66
+
67
+ ```python
68
+ return await api.edit_file(
69
+ "src/util.py", "getCwd", "getCurrentWorkingDirectory", replace_all=True,
70
+ )
71
+ ```
72
+
73
+ ## Getting it wrong
74
+
75
+ **Ignoring the return value.**
76
+
77
+ ```python
78
+ # Wrong — this reports success whatever happened.
79
+ await api.edit_file("a.py", "foo", "bar")
80
+ return "done"
81
+
82
+ # Right.
83
+ r = await api.edit_file("a.py", "foo", "bar")
84
+ return r # or branch on r.startswith("Error:")
85
+ ```
86
+
87
+ **Pasting `old_string` out of `api.read_file` output.** `read_file` returns
88
+ `f"{n:>6}\t{line}"` — six columns of line number and a tab in front of every
89
+ line, and trailing whitespace stripped off the end. That text can never match
90
+ the file. Read the file to *find* the edit, then write `old_string` as the file
91
+ actually contains it. See `man('read_file')`.
92
+
93
+ **Reaching for `replace_all=True` to get past the "found N times" error.** That
94
+ error is telling you the anchor is ambiguous, and `replace_all` changes all N
95
+ sites rather than the one you meant. If you want one, widen `old_string` with a
96
+ line above or below until it is unique. Use `replace_all` only when changing
97
+ every occurrence is the actual intent.
98
+
99
+ **Chaining edits whose anchors overlap.** Each call re-reads the file, so a
100
+ second edit whose `old_string` covers text the first one rewrote silently
101
+ returns `Error: old_string not found`. Order the edits so they do not overlap,
102
+ or write the whole file with `api.write_file`.
103
+
104
+ **Using it to create a file.** `edit_file` requires the file to exist and
105
+ raises `FileNotFoundError` otherwise. `api.write_file` creates, including any
106
+ missing parent directories.
107
+
108
+ ## See also
109
+
110
+ `write_file`, `read_file`, `permissions`, `xylon` (edits are journalled, so
111
+ they appear in `xylon_get_diff` and can be reverted — `api.bash` changes are
112
+ not)
@@ -0,0 +1,194 @@
1
+ # Finding things on a page
2
+ <!-- also: find, one, search, outline, refs, uid, path -->
3
+ <!-- group: Working on a website -->
4
+
5
+ You know what you want. The page is graphical. This is the bridge.
6
+
7
+ The hard part of driving a website is not clicking — it is *locating*. So
8
+ locating and acting are separate verbs, and the engine will rank candidates for
9
+ you but will never pick between close ones on your behalf. That decision needs
10
+ what you know and it does not have.
11
+
12
+ ## Naming an element
13
+
14
+ A **uid** — short, content-derived, `e7f3a1` — or a **path**:
15
+
16
+ ```
17
+ main/table/row"prod-key"/button"Delete"
18
+ dialog"Create Token"/button"Create"
19
+ form#login/textbox"Username"
20
+ ```
21
+
22
+ Both resolve — **as the thing you hand to an action**, not as an argument to
23
+ the search verbs:
24
+
25
+ ```python
26
+ await api.click("e7f3a1") # a uid, straight to the action
27
+ await api.instruct("Click Save.", target="e7f3a1")
28
+ await api.one(role="tab", like="Providers") # keyword spec, never a string
29
+ ```
30
+
31
+ `find` and `one` take a **keyword spec** and nothing positional, so
32
+ `api.one('tab"Providers"')` raises *"takes 1 positional argument but 2 were
33
+ given"* — an error that says what you did and not what to do instead. If you
34
+ already have a uid or a path you do not need `one` at all: pass it to the action.
35
+
36
+ A path runs through the page's *structure*, not its DOM: wrapper
37
+ `div`s are invisible, so a framework inserting one changes nothing. Landmarks,
38
+ tables and their rows, lists and their items, dialogs, and interactive controls
39
+ are what make up a segment.
40
+
41
+ Nothing is written into the page. A reference is a **description**, re-derived
42
+ from the live DOM every time it is used — so a stale one matches nothing, or
43
+ matches several, and both are errors you see. It can never quietly resolve to
44
+ the wrong element.
45
+
46
+ > **A uid is a memory. A path is an assertion.**
47
+ > A uid identifies something you observed. A path is a claim about the page as it
48
+ > is right now. `main/dialog"Confirm"/button"Yes"` *should* resolve against
49
+ > whatever dialog is open when you use it.
50
+
51
+ ## The four verbs
52
+
53
+ ### `api.find(**kw) -> FindSpec`
54
+
55
+ Describes a search **without running it**. Put it inside an action:
56
+
57
+ ```python
58
+ api.click(api.find(role="button", like="create token"))
59
+ ```
60
+
61
+ Every verb is a method on `api`; there are no bare functions in this scope.
62
+ `find` needs no `await` because it runs nothing — it is a description, and
63
+ Layer 0 resolves it when the action fires.
64
+
65
+ This is the form to reach for by default. It costs nothing to build, and it
66
+ resolves against the page as it will be *when the click happens* — which is how
67
+ you write a chain that opens a dialog and then clicks a button inside it, in one
68
+ pass, before the dialog exists.
69
+
70
+ ### `await api.search(**kw) -> list[Ref]`
71
+
72
+ Runs it now and hands back the ranked candidates. **Zero results is an ordinary
73
+ answer**, not an error — "I cannot find it" is the normal case here, and it is
74
+ information.
75
+
76
+ ```python
77
+ for r in await api.search(role="button", like="create"):
78
+ print(r.score, r.path, r.text)
79
+ # 0.90 main/button"Create Token" 'Create Token'
80
+ # 0.90 main/button"Create Team" 'Create Team'
81
+ ```
82
+
83
+ Two things scoring the same is the page telling you the description was not
84
+ specific enough.
85
+
86
+ ### `await api.one(**kw) -> Ref`
87
+
88
+ Insists on exactly one, and raises with the candidates when it cannot get it.
89
+ The exception body **is** the refinement hint.
90
+
91
+ ### `await api.outline(under=None, depth=2) -> dict`
92
+
93
+ Roles and names, no geometry. The cheapest way to orient on a page you have
94
+ never seen. Read this before reaching for a screenshot.
95
+
96
+ ## Narrowing
97
+
98
+ | | |
99
+ |---|---|
100
+ | `role=` | An ARIA role, or a list. `button`, `link`, `textbox`, `row`, `dialog`. Same words the paths use. |
101
+ | `like=` | Approximate text. **Ranked**, not filtered. |
102
+ | `text_exact=` | Exact, after normalisation. |
103
+ | `under=` | Restrict to a subtree. The cheapest and most effective narrowing there is. |
104
+ | `attr=` | `{"data-testid": "row-delete"}` |
105
+ | `state=` | `{"enabled": True}` |
106
+ | `in_view=` | Default `True`. Off-screen matches are excluded **and counted**. |
107
+ | `margin=` | How decisive the ranking must be. Below. |
108
+
109
+ ## Anchoring on what you can see
110
+
111
+ The reason locating is hard is that the UI is graphical. These let a visual
112
+ intuition become a structural query, with no picture involved:
113
+
114
+ ```python
115
+ api.find(role="textbox", right_of="Name") # the field beside a label
116
+ api.find(role="button", in_row='row"prod-key"') # the one in that table row
117
+ api.find(role="button", below='heading"Danger"')
118
+ api.find(role="link", near='text"Billing"')
119
+ ```
120
+
121
+ `near`, `above`, `below`, `left_of`, `right_of`, `in_row`, `in_section`.
122
+
123
+ The anchor may be a ref, a path, **or just the visible text of a label**. That
124
+ last form matters: a bare `<span>Name</span>` is not interactive, so it is not
125
+ itself addressable — but it is exactly what a person would point at.
126
+
127
+ ## The margin
128
+
129
+ Layer 0 ranks. Whether the winner is clear enough to act on is yours to set:
130
+
131
+ ```python
132
+ api.click(api.find(role="button", like="create token")) # default 0.15
133
+ api.click(api.find(role="button", like="create", margin=0)) # take the top
134
+ api.click(api.find(role="button", like="create", margin=1.0)) # sole match only
135
+ ```
136
+
137
+ - **`0`** — always take the top rank. You are certain; you do not want a gate.
138
+ - **`0.15`** (default) — act when the winner is clear, hand back the tie when it
139
+ is not.
140
+ - **`1.0`** — act only when nothing else matched at all.
141
+
142
+ When the gate does not open, the action reports `ambiguous` and the report
143
+ carries the candidates. That is not a failure to route around; it is the page
144
+ telling you which two things your description could not separate.
145
+
146
+ Scores are a fixed ladder, so you can predict them: exact `1.00`, prefix `0.90`,
147
+ word-boundary substring `0.80`, substring `0.70`, token overlap `0.30–0.60`,
148
+ fuzzy below that. Disabled costs ×0.85, off-screen ×0.95.
149
+
150
+ ## Modals are handled for you
151
+
152
+ While an `aria-modal` dialog is open, everything behind it is **inert and
153
+ excluded**. That is not a heuristic — the browser genuinely will not deliver a
154
+ click out there. So `find(role="button", like="Create")` with a dialog open
155
+ returns one result, not two. Pass `include_inert=True` to see past it.
156
+
157
+ ## What you are not being shown
158
+
159
+ Every result carries `omitted`:
160
+
161
+ ```python
162
+ {"offscreen": 12, "inert": 3, "nonInteractive": 40, "overLimit": 0}
163
+ ```
164
+
165
+ Silent filtering is how an agent gets stuck looking for something that was there
166
+ all along. If the thing you want is missing, check this before concluding the
167
+ page does not have it — then widen with `in_view=False` or `include_inert=True`.
168
+
169
+ ## Getting it wrong
170
+
171
+ **Taking `search()[0]` without looking at the scores.** If the top two are level,
172
+ you have just guessed. That is what `margin` exists to stop.
173
+
174
+ **Awaiting `find()`.** It is a description, not a call. `search()` is the one
175
+ that runs.
176
+
177
+ **Hand-writing a path from a screenshot.** Paths match the *computed accessible
178
+ name*, which is not always the visible text. Get paths from `search()`.
179
+ `outline()`'s `seg` is only the last segment (`button"Settings"`), not a path:
180
+ pass its `uid` instead.
181
+
182
+ **Using `xh_3`.** That scheme is gone. It was a page-resident counter that
183
+ dangled silently whenever a framework re-rendered.
184
+
185
+ **Screenshotting to find something.** An image cannot be searched, and looking at
186
+ one costs a round trip through you. Spatial predicates cost one round trip
187
+ through the page.
188
+
189
+ **Holding a uid across a navigation.** It will be refused. Paths survive; uids
190
+ are scoped to the document you observed them in.
191
+
192
+ ## See also
193
+
194
+ `driving`, `widget-model`, `browser-driver`, `drive`, `browser_screenshot`
@@ -0,0 +1,45 @@
1
+ # api.fork_agent(board, session=None) / api.move_agent(board, session=None) -> dict
2
+ <!-- also: move_agent, slots, slot -->
3
+
4
+ <!-- group: Sessions -->
5
+
6
+ Put an agent of your task to work on another board, **in the same task**.
7
+
8
+ A channel is one repository or one drive folder. Work that reaches a second
9
+ one happens in the task's **slot** on that board: a working copy of that
10
+ board's own source, on the task's branch name. A slot is not a new task. It
11
+ shares, with the task's other slots:
12
+
13
+ - its status and lane, its phases and steps, its title and asks;
14
+ - who may act on it, and the folders approved for it;
15
+ - folder access: an agent in any slot reads and writes every slot's working
16
+ copy on this machine without being asked.
17
+
18
+ The task is listed on every board it has a slot on. The slot is made the first
19
+ time it is needed.
20
+
21
+ ```python
22
+ await api.fork_agent("billing") # a copy of you, working in #billing
23
+ await api.move_agent("#billing", "session3") # session3 carries on from #billing
24
+ ```
25
+
26
+ - **fork** starts a new agent there with the conversation so far.
27
+ - **move** does the same and then closes the agent where it was, with a
28
+ `moved-to` line in its transcript. An agent moving *itself* is closed once
29
+ its current turn ends; another agent in the middle of a turn is refused.
30
+
31
+ Both ask the person first. Both also need authority over the task (its
32
+ assignee, its owner, or a manager) and a live board of this project bound to a
33
+ repository or drive folder; a board with no source of its own (`#just me`)
34
+ cannot hold a slot. The refusal says which is missing.
35
+
36
+ In `wgx` both work the same, with two exceptions that the IDE still handles:
37
+ a project manager moving somebody else's task, and a first slot on a board
38
+ bound to a drive folder, which needs the IDE's folder picker.
39
+
40
+ `session` is a session id, or a transcript name in your task (`session3`).
41
+ Left out, it is you.
42
+
43
+ ## Returns
44
+
45
+ `{ "session": <new agent's session id>, "task": "project/repo/issue" of the slot, "mode": "fork" | "move" }`
@@ -0,0 +1,83 @@
1
+ # api.glob(pattern, path=None, include_ignored=False) -> list[str]
2
+ <!-- group: Coding -->
3
+
4
+ Find files by name pattern. Sorted, capped at 500. Not gated.
5
+
6
+ ## Options
7
+
8
+ - **`pattern`** — a glob. `*` matches within one path segment, `**`
9
+ matches across segments, `?` one character, `[abc]` a set.
10
+ - **`path`** — directory to search from. Absolute, or relative to the shell's
11
+ cwd. Defaults to the cwd.
12
+ - **`include_ignored`** — walk the pruned directories too.
13
+
14
+ ## What it does not walk
15
+
16
+ The same list `grep` skips — `node_modules`, `.git`, `out`, `dist`, `build`
17
+ and their neighbours, by directory name at any depth. See `PRUNED_DIRS` in
18
+ `stdlib_api.py`, and `grep`'s manual for why it is not only about speed.
19
+
20
+ The pattern is relative to `path`, never absolute. `api.glob("/etc/*")` does
21
+ not do what it looks like — pass the directory as `path` instead.
22
+
23
+ ## Returns
24
+
25
+ A sorted `list[str]` of **absolute** paths, **truncated to the first 500**.
26
+ Directories are included, not just files. No matches gives `[]`.
27
+
28
+ The 500 cap is silent: a list of exactly 500 means you probably have more.
29
+ Narrow the pattern rather than assuming that is all of them.
30
+
31
+ ## Examples
32
+
33
+ ```python
34
+ return await api.glob("**/*.test.ts")
35
+ ```
36
+
37
+ Scoped to a subtree:
38
+
39
+ ```python
40
+ return await api.glob("*.py", path="widgex-ide/extension/python/xactor")
41
+ ```
42
+
43
+ Existence check that does not raise:
44
+
45
+ ```python
46
+ if not await api.glob("package.json"):
47
+ return "not a node project"
48
+ ```
49
+
50
+ Recent files first, when you care about what changed lately — `glob` sorts by
51
+ name, so ask the shell instead:
52
+
53
+ ```python
54
+ r = await api.bash("git ls-files -m")
55
+ return r["stdout"].splitlines()
56
+ ```
57
+
58
+ ## Getting it wrong
59
+
60
+ **Expecting `*` to cross directories.** `src/*.ts` finds only the files
61
+ directly in `src`. `src/**/*.ts` walks the tree.
62
+
63
+ **Expecting the results filtered to files.** Directories match too. Filter with
64
+ `api.ls` on the parent, or check by pattern.
65
+
66
+ **Hitting the cap without noticing.** `len(hits) == 500` is a warning sign, not
67
+ a count.
68
+
69
+ **Using it to search file *contents*.** That is `api.grep`. `glob` only ever
70
+ looks at names.
71
+
72
+ **Assuming it respects `.gitignore`.** It does not. It skips a fixed list of
73
+ generated directory names, which covers the usual suspects and is not the same
74
+ thing: anything else your `.gitignore` names still matches. Use
75
+ `api.bash("git ls-files ...")` when you want the tracked set exactly.
76
+
77
+ **Assuming the prune list is never wrong for your project.** It is by name, so
78
+ a project whose real source lives in `build/` loses it. Pass
79
+ `include_ignored=True` there.
80
+
81
+ ## See also
82
+
83
+ `grep`, `ls`, `read_file`, `bash`