@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,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`
|