@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,284 @@
|
|
|
1
|
+
# The widget model
|
|
2
|
+
<!-- group: Concepts -->
|
|
3
|
+
|
|
4
|
+
How WidgeX drives a website: named page states and named transitions.
|
|
5
|
+
|
|
6
|
+
There is no "open a browser" call. You reach a site either through a **widget**
|
|
7
|
+
— a written model of that site — or through `api.drive`, which is the admission
|
|
8
|
+
that no model exists yet. Which one you call is what the user approves and what
|
|
9
|
+
the panel shows them.
|
|
10
|
+
|
|
11
|
+
That is the point of the whole design. "ran create_token" is a thing a user can
|
|
12
|
+
consent to. "clicked the third button" is not.
|
|
13
|
+
|
|
14
|
+
## The parts
|
|
15
|
+
|
|
16
|
+
A widget is a Python file defining **page states** and the **transitions**
|
|
17
|
+
between them.
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
login = PageState(
|
|
21
|
+
"Login",
|
|
22
|
+
url_pattern=r"https://example\.com/login",
|
|
23
|
+
entry="https://example.com/login",
|
|
24
|
+
keywords=["Sign in", "Password"],
|
|
25
|
+
observations=[
|
|
26
|
+
Observation("error", description="the login error banner",
|
|
27
|
+
selector=".error", extract="text"),
|
|
28
|
+
],
|
|
29
|
+
transitions=[
|
|
30
|
+
Transition("sign_in",
|
|
31
|
+
[Step("type_text", 'form#login/textbox"Email"', text="$email"),
|
|
32
|
+
Step("type_text", 'form#login/textbox"Password"', text="$password"),
|
|
33
|
+
Step("click", 'form#login/button"Sign in"')],
|
|
34
|
+
params={"email": "str", "password": "str"},
|
|
35
|
+
description="sign in and land on the dashboard"),
|
|
36
|
+
],
|
|
37
|
+
)
|
|
38
|
+
registry.add(login, transitions_to=["Dashboard"])
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
- **`PageState(name, *, url_pattern='.*', entry=None, keywords=None,
|
|
42
|
+
observations=None, transitions=None, expected_timeout=None)`** — one page.
|
|
43
|
+
`url_pattern` is how it is recognised; `keywords` disambiguate two states with
|
|
44
|
+
the same URL by scoring against the page text; `entry` is where
|
|
45
|
+
`use_widget(name, state=...)` navigates to.
|
|
46
|
+
- **`Observation(name, *, description='', js=None, selector=None,
|
|
47
|
+
extract=None)`** — something you can read off the page by name.
|
|
48
|
+
- **`Transition(name, steps, *, params=None, description='')`** — a named,
|
|
49
|
+
parameterised action. `params` is a **dict** of name to type.
|
|
50
|
+
- **`Step(action, target, **kwargs)`** — one L1 action. `target` is a path, a
|
|
51
|
+
uid, or a `find(...)` spec.
|
|
52
|
+
|
|
53
|
+
Two things that used to be documented wrongly here, both of which failed
|
|
54
|
+
silently:
|
|
55
|
+
|
|
56
|
+
- Parameters substitute as **`$name`**, never `{name}`. A `{name}` passes
|
|
57
|
+
through untouched, so the field is filled with the literal text and nothing
|
|
58
|
+
raises.
|
|
59
|
+
- **`expects_transition` does nothing.** Readiness is intrinsic — every step waits
|
|
60
|
+
for its own target — and a step that fails now raises rather than letting the
|
|
61
|
+
transition report the state it was aiming for. `Step` still accepts the flag
|
|
62
|
+
(as a third positional) so old widgets load, but it is ignored: never pass it.
|
|
63
|
+
- **`Observation(selector=...)` is CSS**, not a path. `button"Create Token"` is
|
|
64
|
+
refused at construction; use `js=` for anything else. A parameter may be called
|
|
65
|
+
`name`; the transition name is positional-only in `execute`.
|
|
66
|
+
- The verb in a `Step` is the one on `api`: `click`, `type_text`,
|
|
67
|
+
`clear_and_type`, `select`, … (`clear_type` is accepted as an alias).
|
|
68
|
+
|
|
69
|
+
## Writing targets that survive
|
|
70
|
+
|
|
71
|
+
A target is a **path** through the page's structure, and paths are the whole
|
|
72
|
+
reason a widget is worth writing:
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
main/table/row"prod-key"/button"Delete"
|
|
76
|
+
dialog"Create Token"/button"Create"
|
|
77
|
+
form#login/textbox"Username"
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Get these from `api.search()` or `api.one()` while exploring, then paste the
|
|
81
|
+
path into the widget. Do not hand-write them from a screenshot — the accessible
|
|
82
|
+
name the engine computes is what a path matches on, and it is not always the
|
|
83
|
+
text you can see.
|
|
84
|
+
|
|
85
|
+
**Prefer, in order:**
|
|
86
|
+
|
|
87
|
+
1. A path with a **name discriminator** — `button"Create Token"`. Readable, and
|
|
88
|
+
it survives re-renders and re-orderings.
|
|
89
|
+
2. A path scoped by its **container** — `dialog"Create Token"/button"Create"`.
|
|
90
|
+
This is how you tell apart controls that share a name.
|
|
91
|
+
3. A **testid** — `@row-delete`. Authored for automation, so it is the most
|
|
92
|
+
stable thing on the page when it exists.
|
|
93
|
+
4. A `find(...)` **spec**, when the exact text varies. Set `margin` to say how
|
|
94
|
+
sure you need to be.
|
|
95
|
+
|
|
96
|
+
**Avoid** a bare positional index (`button[2]`). It is emitted only when nothing
|
|
97
|
+
else discriminates, and it is the one form a sibling insertion can silently
|
|
98
|
+
shift.
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
# Efficient navigation
|
|
103
|
+
|
|
104
|
+
The engine is fast. Most slow widgets are slow because of how they are written,
|
|
105
|
+
not because of what they are driving. Five patterns account for nearly all of it.
|
|
106
|
+
|
|
107
|
+
## 1. Load the whole chain; do not step through it
|
|
108
|
+
|
|
109
|
+
Actions queue. A transition's steps already run as one chain, so write them as
|
|
110
|
+
one — do not split a flow into several transitions that each settle and
|
|
111
|
+
re-resolve.
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
# Good: one chain, one settle at the end.
|
|
115
|
+
Transition("create_token", [
|
|
116
|
+
Step("click", 'main/button"Create Token"'),
|
|
117
|
+
Step("clear_and_type", 'dialog"Create Token"/textbox"Name"', text="$name"),
|
|
118
|
+
Step("select", 'dialog"Create Token"/combobox"Expiration"', label="$expiry"),
|
|
119
|
+
Step("click", 'dialog"Create Token"/button"Create"'),
|
|
120
|
+
], params={"name": "str", "expiry": "str"})
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
# Bad: four transitions. Each one settles the page and re-resolves the state
|
|
125
|
+
# machine, so the agent pays the round trip four times and has to decide what
|
|
126
|
+
# to do next three times it did not need to.
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
The measure: **a transition should get from one state the agent cares about to
|
|
130
|
+
the next in a single pass.** If a state exists only because the flow passes
|
|
131
|
+
through it, it should not be a state.
|
|
132
|
+
|
|
133
|
+
## 2. Do not wait for things on purpose
|
|
134
|
+
|
|
135
|
+
Every action already waits for its own target. An explicit `wait_for_element`
|
|
136
|
+
before a click is almost always a step that costs a round trip and buys nothing.
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
# Redundant — the click already waits for the dialog's button.
|
|
140
|
+
Step("wait_for", 'dialog"Create Token"')
|
|
141
|
+
Step("click", 'dialog"Create Token"/button"Create"')
|
|
142
|
+
|
|
143
|
+
# Just the click.
|
|
144
|
+
Step("click", 'dialog"Create Token"/button"Create"')
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Wait explicitly only for something you are **not** about to touch: a spinner to
|
|
148
|
+
go (`condition="gone"`), or a result to appear that you only want to read.
|
|
149
|
+
|
|
150
|
+
## 3. Scope every lookup
|
|
151
|
+
|
|
152
|
+
An unscoped search walks the page. A scoped one walks a subtree. On a console
|
|
153
|
+
with four hundred controls that is the difference between a big payload and a
|
|
154
|
+
small one, and it removes the ambiguity that would have sent the agent back for
|
|
155
|
+
another look.
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
# The page behind a modal is inert and excluded automatically, but scoping is
|
|
159
|
+
# still the cheapest way to be unambiguous on an ordinary page.
|
|
160
|
+
api.find(role="button", under='main/section"API keys"', like="revoke")
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## 4. Anchor on what is visible, not on a picture
|
|
164
|
+
|
|
165
|
+
When structure alone will not identify something, use a spatial relation. It
|
|
166
|
+
costs one round trip. A screenshot costs a round trip **through the agent**,
|
|
167
|
+
which is the expensive kind.
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
api.find(role="textbox", right_of="Name")
|
|
171
|
+
api.find(role="button", in_row='row"prod-key"')
|
|
172
|
+
api.find(role="button", below='heading"Danger zone"')
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## 5. Read once, in bulk
|
|
176
|
+
|
|
177
|
+
An `Observation` is one JavaScript evaluation. Prefer one observation that
|
|
178
|
+
returns a whole table to five that each return a column.
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
Observation("tokens", js="""
|
|
182
|
+
Array.from(document.querySelectorAll('tbody tr')).map(r => ({
|
|
183
|
+
name: r.cells[0]?.textContent?.trim(),
|
|
184
|
+
scope: r.cells[1]?.textContent?.trim(),
|
|
185
|
+
expires: r.cells[3]?.textContent?.trim(),
|
|
186
|
+
}))
|
|
187
|
+
""")
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Speed knobs
|
|
191
|
+
|
|
192
|
+
- **`hfactor`** — humanisation speed, per action or per chain. **Higher is
|
|
193
|
+
faster.** It divides every delay rather than being one, so `hfactor=6` is six
|
|
194
|
+
times quicker than `hfactor=1`, and `hfactor=0.5` is half the speed. If you
|
|
195
|
+
want a run to go faster, put the number *up*.
|
|
196
|
+
A login nobody is watching can run at `hfactor=6`; leave it alone for anything
|
|
197
|
+
a user is looking at, and for anything that might trip bot detection.
|
|
198
|
+
- **`timeout_ms`** — a single action's ceiling, default 10s. A `type` derives its
|
|
199
|
+
own from the length of the string and the `hfactor`, and adds it to the
|
|
200
|
+
chain's budget, so never set this on one: a type that times out is a field
|
|
201
|
+
refusing keystrokes, and a bigger number will not change that.
|
|
202
|
+
- **`batch_ms`** — the whole chain's, default 30s. Raise it for a deliberately
|
|
203
|
+
long flow; if you are raising it because the flow is *slow*, fix the flow.
|
|
204
|
+
|
|
205
|
+
## Widgets written against the old engine
|
|
206
|
+
|
|
207
|
+
A widget that names elements the way the previous engine resolved them — by
|
|
208
|
+
visible text, by a bare tag, by a CSS attribute selector — still runs. Layer 2
|
|
209
|
+
translates those targets into an explicit ranked search before handing them
|
|
210
|
+
down:
|
|
211
|
+
|
|
212
|
+
| Old | Becomes |
|
|
213
|
+
|---|---|
|
|
214
|
+
| `Step("click", "Create Token")` | `find(like="Create Token")` |
|
|
215
|
+
| `Step("clear_and_type", "TOKEN NAME", ...)` | `find(like="TOKEN NAME", role="textbox")` |
|
|
216
|
+
| `Step("select", "select", ...)` | `find(role="combobox")` |
|
|
217
|
+
| `Step("click", '[data-testid="x"]')` | `find(attr={"data-testid": "x"})` |
|
|
218
|
+
| `container="…"` | `under="…"` |
|
|
219
|
+
| `expects_transition=True` | dropped — readiness is intrinsic |
|
|
220
|
+
| `index=2` | a `search()`, then the third result — **one extra round trip** |
|
|
221
|
+
|
|
222
|
+
That is a bridge, not a destination, and it is deliberately at Layer 2: Layer 0
|
|
223
|
+
still refuses to guess, and this is Layer 2 opting in where you can see it.
|
|
224
|
+
|
|
225
|
+
Two reasons to move off it. A `like=` search ranks and can come back
|
|
226
|
+
`ambiguous`, where a path is exact. And `index=` costs a lookup on every run —
|
|
227
|
+
the row it is counting into has a name, and naming it is both faster and
|
|
228
|
+
readable.
|
|
229
|
+
|
|
230
|
+
The DOM-level verbs — `dom_click`, `dom_fill`, `wait_for_element`,
|
|
231
|
+
`extract_secret` — kept their own CSS resolution and are **not** translated. A
|
|
232
|
+
widget built on those needs no changes at all.
|
|
233
|
+
|
|
234
|
+
## Working through it
|
|
235
|
+
|
|
236
|
+
```python
|
|
237
|
+
await api.widgets() # what exists — opens nothing
|
|
238
|
+
await api.use_widget("vercel_tokens") # approve, load, navigate
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Then, right here in this shell — see `driving`:
|
|
242
|
+
|
|
243
|
+
```python
|
|
244
|
+
state = await registry.here(api) # where am I?
|
|
245
|
+
print(state.describe()) # what can I do here?
|
|
246
|
+
state = await state.execute("create_token", name="ci")
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
`describe()` before `execute()` is the loop. You do not need to know the DOM;
|
|
250
|
+
you need to know the state's name and its transitions.
|
|
251
|
+
|
|
252
|
+
## When there is no widget
|
|
253
|
+
|
|
254
|
+
`api.drive(url, why)`. Every action is then a raw L1 call against whatever the
|
|
255
|
+
page happens to look like, and `why` is the only record of intent that will
|
|
256
|
+
exist. `registry.here()` returns `UnknownPage`.
|
|
257
|
+
|
|
258
|
+
If you find yourself driving the same site repeatedly, that site wants a widget.
|
|
259
|
+
See `writing-a-widget` for how to build one from a live session.
|
|
260
|
+
|
|
261
|
+
## Getting it wrong
|
|
262
|
+
|
|
263
|
+
**Splitting a flow into a state per click.** A state is somewhere the agent might
|
|
264
|
+
reasonably stop and decide. A half-filled dialog is not.
|
|
265
|
+
|
|
266
|
+
**Writing `{param}`.** Substitution is `$param`. `{param}` types the braces.
|
|
267
|
+
|
|
268
|
+
**Hand-writing a path from a screenshot.** Paths match the computed accessible
|
|
269
|
+
name, which is not always the visible text. Get them from `api.search()`.
|
|
270
|
+
|
|
271
|
+
**A bare index in a list that scrolls.** The engine refuses to emit one inside a
|
|
272
|
+
virtualised container for exactly this reason; do not add one by hand.
|
|
273
|
+
|
|
274
|
+
**Reaching for a screenshot to find an element.** An image is seen, not
|
|
275
|
+
queryable, and looking at one costs a round trip through the agent.
|
|
276
|
+
`api.search()` and the spatial predicates return structure.
|
|
277
|
+
|
|
278
|
+
**Assuming `here()` is right.** It can return `UNKNOWN_STATE` — after a redirect,
|
|
279
|
+
an interstitial, or a login wall. Check `state.name` before executing.
|
|
280
|
+
|
|
281
|
+
## See also
|
|
282
|
+
|
|
283
|
+
`writing-a-widget`, `driving`, `finding`, `widgets`, `use_widget`, `drive`,
|
|
284
|
+
`browser-driver`
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# api.widgets(url=None) -> list[dict]
|
|
2
|
+
<!-- group: Working on a website -->
|
|
3
|
+
|
|
4
|
+
What sites are already modelled. Opens nothing, asks nothing. Not gated.
|
|
5
|
+
|
|
6
|
+
The first call of any browser task. It reads the widget directories and tells
|
|
7
|
+
you what is there, so you can work through a named automation instead of
|
|
8
|
+
clicking at a page nobody has modelled.
|
|
9
|
+
|
|
10
|
+
## Options
|
|
11
|
+
|
|
12
|
+
- **`url`** — optional. Flags which widgets match that page, via the `matches`
|
|
13
|
+
key. Without it you get everything.
|
|
14
|
+
|
|
15
|
+
## Returns
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
[{"name": str, "domain": str, "description": str, "matches": bool}]
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
An empty list means nothing is modelled — that is the case for `api.drive`, not
|
|
22
|
+
a failure.
|
|
23
|
+
|
|
24
|
+
## Examples
|
|
25
|
+
|
|
26
|
+
Before deciding how to approach a site:
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
found = await api.widgets("https://vercel.com/account/tokens")
|
|
30
|
+
hits = [w["name"] for w in found if w["matches"]]
|
|
31
|
+
if hits:
|
|
32
|
+
return await api.use_widget(hits[0])
|
|
33
|
+
return await api.drive("https://vercel.com/account/tokens",
|
|
34
|
+
why="create a deploy token for CI")
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Just seeing what exists:
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
return [(w["name"], w["description"]) for w in await api.widgets()]
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Getting it wrong
|
|
44
|
+
|
|
45
|
+
**Not calling it.** It is free — no browser, no permission, no prompt — and
|
|
46
|
+
skipping it is how you end up driving a site that already has a tested model.
|
|
47
|
+
|
|
48
|
+
**Treating an empty list as an error.** It means no widget exists yet. `drive`
|
|
49
|
+
is the answer.
|
|
50
|
+
|
|
51
|
+
**Ignoring `matches` when you passed a `url`.** Every widget comes back; only
|
|
52
|
+
some of them are for that page.
|
|
53
|
+
|
|
54
|
+
**Assuming a match means it is loaded.** It is not. `use_widget` loads and
|
|
55
|
+
navigates; this call only reports.
|
|
56
|
+
|
|
57
|
+
## See also
|
|
58
|
+
|
|
59
|
+
`use_widget`, `drive`, `widget-model`, `browser_plumbing`
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# api.write_file(path, content, new_tab=False) -> str
|
|
2
|
+
<!-- group: Coding -->
|
|
3
|
+
|
|
4
|
+
Write a whole file, creating it and its parent directories. Gated.
|
|
5
|
+
|
|
6
|
+
## Options
|
|
7
|
+
|
|
8
|
+
- **`path`** — absolute, or relative to the shell's cwd.
|
|
9
|
+
- **`content`** — the *entire* new contents. This is a replace, not an append:
|
|
10
|
+
whatever was there is gone. A `str` is written as text; `bytes` are written
|
|
11
|
+
as they are, for a binary file:
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
import base64
|
|
15
|
+
shot = await api.screenshot()
|
|
16
|
+
await api.write_file("shot.png", base64.b64decode(shot["$image"]["data"]))
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Anything else — `None` from a lookup that missed, say — is refused before
|
|
20
|
+
you are asked to approve the write.
|
|
21
|
+
|
|
22
|
+
- **`new_tab`** — accepted and ignored. Writing a file opens no tab: the
|
|
23
|
+
user sees what you are editing as a queue on the new tab's Files door.
|
|
24
|
+
|
|
25
|
+
Missing parent directories are created. Text is written as UTF-8, exactly as
|
|
26
|
+
given: `\n` stays `\n`, whatever the platform.
|
|
27
|
+
|
|
28
|
+
## Returns
|
|
29
|
+
|
|
30
|
+
`"Wrote {n} bytes to {path}"`, where `n` is `len(content)` — characters for
|
|
31
|
+
text, bytes for bytes.
|
|
32
|
+
|
|
33
|
+
Unlike `edit_file`, there is no failure return — a write that could not happen
|
|
34
|
+
raises (`PermissionError` if declined, `OSError` for a genuinely bad path).
|
|
35
|
+
|
|
36
|
+
## Examples
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
return await api.write_file("src/config.json", '{\n "debug": false\n}\n')
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Creating a file several directories deep, with no `mkdir` first:
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
return await api.write_file("src/features/auth/index.ts", "export * from './login';\n")
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Rewriting a file wholesale after transforming it:
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
raw = (await api.bash("cat src/list.txt"))["stdout"]
|
|
52
|
+
kept = [ln for ln in raw.splitlines() if not ln.startswith("#")]
|
|
53
|
+
return await api.write_file("src/list.txt", "\n".join(kept) + "\n")
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Getting it wrong
|
|
57
|
+
|
|
58
|
+
**Using it to change one line of an existing file.** You have to reproduce the
|
|
59
|
+
whole file to do that, and any part you paraphrase is silently destroyed.
|
|
60
|
+
`api.edit_file` changes exactly what you name and tells you if the anchor did
|
|
61
|
+
not match.
|
|
62
|
+
|
|
63
|
+
**Rebuilding a file from `api.read_file` output.** The line numbers and tabs
|
|
64
|
+
would be written into the file, and trailing whitespace would be lost. If you
|
|
65
|
+
must round-trip a file's text, get it raw with `api.bash("cat ...")`.
|
|
66
|
+
|
|
67
|
+
**Expecting an append.** There is none. Read, concatenate, write — or use
|
|
68
|
+
`api.bash("echo ... >> file")`, accepting that bash changes are not journalled
|
|
69
|
+
and so cannot be reverted with the turn.
|
|
70
|
+
|
|
71
|
+
**Forgetting the trailing newline.** Most tooling expects files to end with
|
|
72
|
+
one, and its absence shows up later as a spurious diff line.
|
|
73
|
+
|
|
74
|
+
## See also
|
|
75
|
+
|
|
76
|
+
`edit_file`, `read_file`, `permissions`, `bash`
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# Writing a widget from a live session
|
|
2
|
+
<!-- also: new-widget, authoring, record -->
|
|
3
|
+
<!-- group: Working on a website -->
|
|
4
|
+
|
|
5
|
+
Drive the site once by hand, keep what worked, save it as a model.
|
|
6
|
+
|
|
7
|
+
A widget is worth writing when you have driven the same site twice. The second
|
|
8
|
+
time, write it down.
|
|
9
|
+
|
|
10
|
+
## The loop
|
|
11
|
+
|
|
12
|
+
**1. Get there and look.**
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
await api.drive("https://vercel.com/account/settings/tokens",
|
|
16
|
+
why="work out how token creation is laid out, to model it")
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
print(await api.outline()) # the shape, ~250 tokens
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
**2. Find the controls, and keep the paths.**
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
for r in await api.search(role="button"):
|
|
27
|
+
print(r.score, r.path, repr(r.text))
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Copy the paths out of that output. They are what goes in the widget — do not
|
|
31
|
+
retype them from what you can see on screen, because a path matches the computed
|
|
32
|
+
accessible name and that is not always the visible text.
|
|
33
|
+
|
|
34
|
+
**3. Try the chain for real, in one pass.**
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
api.click(api.find(role="button", like="create token"))
|
|
38
|
+
api.clear_and_type(api.find(role="textbox", right_of="Name"), "scratch-test")
|
|
39
|
+
report = await api.drain()
|
|
40
|
+
print(report)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Every verb is a method on `api` — there are no bare functions in that scope.
|
|
44
|
+
Locating is awaited (`api.search`, `api.one`); acting is not, because actions
|
|
45
|
+
queue and `api.drain()` collects them.
|
|
46
|
+
|
|
47
|
+
The report tells you which paths resolved and which did not, and what each one
|
|
48
|
+
actually hit. **That report is your draft.** A step that came back `ambiguous`
|
|
49
|
+
needs a scope; one that came back `not_found` needs a different discriminator.
|
|
50
|
+
|
|
51
|
+
**4. Write it down.**
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
tokens = PageState(
|
|
55
|
+
"TokensPage",
|
|
56
|
+
url_pattern=r"vercel\.com/account/settings/tokens",
|
|
57
|
+
keywords=["Tokens", "Create Token"],
|
|
58
|
+
observations=[
|
|
59
|
+
Observation("tokens", js="""
|
|
60
|
+
Array.from(document.querySelectorAll('tbody tr')).map(r => ({
|
|
61
|
+
name: r.cells[0]?.textContent?.trim(),
|
|
62
|
+
expires: r.cells[3]?.textContent?.trim(),
|
|
63
|
+
}))
|
|
64
|
+
"""),
|
|
65
|
+
],
|
|
66
|
+
transitions=[
|
|
67
|
+
Transition("create_token",
|
|
68
|
+
[Step("click", 'main/button"Create Token"'),
|
|
69
|
+
Step("clear_and_type", 'dialog"Create Token"/textbox"Name"', text="$name"),
|
|
70
|
+
Step("select", 'dialog"Create Token"/combobox"Expiration"', label="$expiry"),
|
|
71
|
+
Step("click", 'dialog"Create Token"/button"Create"')],
|
|
72
|
+
params={"name": "str", "expiry": "str"},
|
|
73
|
+
description="create a token and land on the success dialog"),
|
|
74
|
+
],
|
|
75
|
+
)
|
|
76
|
+
registry.add(tokens, transitions_to=["TokenCreated"])
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**5. Save and check it.**
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
await api.write_file(f"{home}/.widgex/widgets/vercel_tokens.py", source)
|
|
83
|
+
await api.browser_flush()
|
|
84
|
+
await api.use_widget("vercel_tokens")
|
|
85
|
+
state = await registry.here(api)
|
|
86
|
+
print(state.name, state.describe())
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## What makes a good state
|
|
90
|
+
|
|
91
|
+
**A state is somewhere the agent might reasonably stop and decide.** The token
|
|
92
|
+
list is a state. A half-filled dialog is not — it is the middle of a transition.
|
|
93
|
+
|
|
94
|
+
If a state has exactly one transition out of it and nothing worth reading, it
|
|
95
|
+
should not exist; fold it into the transition that passes through.
|
|
96
|
+
|
|
97
|
+
Give a state `keywords` whenever another state shares its `url_pattern`. Dialogs
|
|
98
|
+
usually do — that is what keywords are for.
|
|
99
|
+
|
|
100
|
+
## What makes a good transition
|
|
101
|
+
|
|
102
|
+
- **One objective, one transition.** Four clicks that get from "looking at the
|
|
103
|
+
list" to "the token exists" are one transition, not four.
|
|
104
|
+
- **Name the paths, scope the ambiguous ones.** `dialog"X"/button"Create"` rather
|
|
105
|
+
than `button"Create"` when the page behind also has one.
|
|
106
|
+
- **No explicit waits.** Every step already waits for its own target.
|
|
107
|
+
- **`params` is a dict**, and substitution is `$name`. `{name}` types the braces.
|
|
108
|
+
|
|
109
|
+
## Getting it started faster
|
|
110
|
+
|
|
111
|
+
Have a driver session do the exploring and hand you the draft — it is a good use
|
|
112
|
+
of a small model, and it keeps the page structure out of your context:
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
child = await api.agent(
|
|
116
|
+
prompt=("Explore https://example.com/settings and draft a widget for it. "
|
|
117
|
+
"Use api.outline and api.search to find the controls, try the chain "
|
|
118
|
+
"with api.drain, and relay me the PageState source. Paths from "
|
|
119
|
+
"search() only -- do not invent them, do not screenshot."),
|
|
120
|
+
model="claude-haiku-4-5",
|
|
121
|
+
)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
See `browser-driver`.
|
|
125
|
+
|
|
126
|
+
## Getting it wrong
|
|
127
|
+
|
|
128
|
+
**Modelling every page you passed through.** A state you never stop at is
|
|
129
|
+
overhead in the graph and a round trip at runtime.
|
|
130
|
+
|
|
131
|
+
**Recording a transition you have not run.** The batch report is the evidence a
|
|
132
|
+
chain works. Write down what the report says resolved, not what you expect to.
|
|
133
|
+
|
|
134
|
+
**Using a bare index.** `button[2]` is the one path form a sibling insertion can
|
|
135
|
+
shift silently. Prefer a name, a testid, or a scope.
|
|
136
|
+
|
|
137
|
+
**Leaving `scratch-test` rows behind.** Exploring a console creates things. Clean
|
|
138
|
+
up, or say what you left.
|
|
139
|
+
|
|
140
|
+
## See also
|
|
141
|
+
|
|
142
|
+
`widget-model`, `driving`, `finding`, `drive`, `browser-driver`, `widgets`
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# Xylon
|
|
2
|
+
<!-- group: Concepts -->
|
|
3
|
+
|
|
4
|
+
Every session in this project, and what each one changed.
|
|
5
|
+
|
|
6
|
+
A xylon tree records sessions as **files**, four levels down:
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
{project}/{repo}/{issue}/session3
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
An **Issue** is a working copy on a branch of its own — the unit of isolation
|
|
13
|
+
between agents — and every session that has ever worked in one is a sibling file
|
|
14
|
+
inside it. Yours is one of them. Each holds its transcript, a summary, the files
|
|
15
|
+
it touched and what it cost.
|
|
16
|
+
|
|
17
|
+
Sessions are siblings, never nested. A child created by `agent` or
|
|
18
|
+
`session_fork` is the next `sessionN` beside you, with `parent` on its header
|
|
19
|
+
naming the one it came from; so is an extension, and so is a compaction — which
|
|
20
|
+
does not create anything at all, but carries on writing into the same file.
|
|
21
|
+
|
|
22
|
+
You read the tree with the `xylon_*` calls. You do not write to it — it is
|
|
23
|
+
written for you.
|
|
24
|
+
|
|
25
|
+
**To find something, search.** `xylon_search` looks through what was said in
|
|
26
|
+
every conversation in the project and returns the tasks it came up in;
|
|
27
|
+
`xylon_read_turns` then reads the few turns around a hit. `xylon_read_messages`
|
|
28
|
+
is the whole transcript, tool output included, and is for when you need exactly
|
|
29
|
+
that.
|
|
30
|
+
|
|
31
|
+
## Session paths
|
|
32
|
+
|
|
33
|
+
The grammar matters more than any individual call:
|
|
34
|
+
|
|
35
|
+
- **`.`** — this session.
|
|
36
|
+
- **`..`** — the session yours came from, which is `parent` on your header
|
|
37
|
+
rather than whichever session happened to spawn you. For most children that is
|
|
38
|
+
the spawning session; for an unbriefed `agent(extend=True)` child it is the
|
|
39
|
+
checkpoint it inherited, and the live parent is a sibling.
|
|
40
|
+
`xylon_read_messages('..')` reads whichever it is.
|
|
41
|
+
- **A bare `sessionN`** is a sibling in your own issue.
|
|
42
|
+
- **Anything with slashes is absolute**: `widgex-ide/fix-relay/session3`.
|
|
43
|
+
|
|
44
|
+
Relative paths need a session that already has an address, and `..` on the first
|
|
45
|
+
session in an issue is refused — it came from nothing. A relative path combined
|
|
46
|
+
with a different `tree_name` is refused too: there is nothing for it to be
|
|
47
|
+
relative to over there.
|
|
48
|
+
|
|
49
|
+
## Working and saved trees
|
|
50
|
+
|
|
51
|
+
`xylon_list_trees` returns two lists:
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
{"working": [{"name", "sourceName", "path", "createdAt"}],
|
|
55
|
+
"saved": [{"name", "sizeBytes", "modifiedAt"}]}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**Working** trees are unpacked on disk and live. **Saved** trees are `.wxylon`
|
|
59
|
+
archives — history, not something in progress. A name can appear in both.
|
|
60
|
+
|
|
61
|
+
A checkpoint is written whenever the conversation crosses a boundary it cannot
|
|
62
|
+
carry: a compaction, a change of provider, or an `agent(extend=True)` that
|
|
63
|
+
supplies a summary — a summary written by hand is as good a checkpoint as one
|
|
64
|
+
derived at a compaction, so it is banked rather than thrown away. All three write
|
|
65
|
+
onto the session they describe, and in all three the chat itself is unbroken,
|
|
66
|
+
because the boundary is a fact about context, not about the conversation.
|
|
67
|
+
|
|
68
|
+
Each session's transcript is a `.cosm` file: a JSON header, a `---` line, then
|
|
69
|
+
the conversation as JSONL. The header carries `name`, `repo`, `createdAt`,
|
|
70
|
+
`branch` (the one the issue started on, which cannot change), `parent`,
|
|
71
|
+
`summary`, `summaryKind` (`'handoff'` or `'final'`), `files`, and `usage` —
|
|
72
|
+
tokens and cost per `<provider>/<model>`.
|
|
73
|
+
|
|
74
|
+
**Once a session is sealed its transcript is gzipped in place**, and inflated
|
|
75
|
+
again whenever anything reads it. The header stays plain text on the first line
|
|
76
|
+
either way, which is what keeps listing the tree cheap.
|
|
77
|
+
|
|
78
|
+
## What is recorded, and what is not
|
|
79
|
+
|
|
80
|
+
The header's `files` is the paths `api.write_file` and `api.edit_file` touched,
|
|
81
|
+
in the order they were first touched. **That is all of it** — no contents, no
|
|
82
|
+
before-and-after, no per-turn breakdown. What the file *said* at any point is
|
|
83
|
+
git's to answer, on the branch the Issue owns.
|
|
84
|
+
|
|
85
|
+
**Changes made by `api.bash` are not recorded at all.** A `sed -i`, a
|
|
86
|
+
`git checkout`, a generated file — none of it appears in `files`. If you want a
|
|
87
|
+
change on the record, make it with the file api.
|
|
88
|
+
|
|
89
|
+
## Reading it
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
me = await api.xylon_get_node(".") # header: summary, files, repo
|
|
93
|
+
parent = await api.xylon_read_messages("..") # what spawned me
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Getting it wrong
|
|
97
|
+
|
|
98
|
+
**Expecting bash edits in `files`.** They are not there. This is the single
|
|
99
|
+
most common surprise about the tree.
|
|
100
|
+
|
|
101
|
+
**Reading `..` for context by default.** It works, and a transcript is large. A
|
|
102
|
+
child briefed with `agent(wait=False, summary=...)` costs a fraction of it; the
|
|
103
|
+
fallback exists for when the brief turns out to be insufficient.
|
|
104
|
+
|
|
105
|
+
**Assuming `..` is the session that spawned you.** It is the node whose summary
|
|
106
|
+
you hold. Briefed, that is your spawner's sealed node; unbriefed, it is an older
|
|
107
|
+
checkpoint and your spawner is running alongside you, not above you.
|
|
108
|
+
|
|
109
|
+
**Combining a relative path with `tree_name`.** Refused, with a message saying
|
|
110
|
+
so.
|
|
111
|
+
|
|
112
|
+
**Assuming `saved` trees are live.** They are archives. Working trees are the
|
|
113
|
+
ones with a `path`.
|
|
114
|
+
|
|
115
|
+
**Expecting to write to the tree.** There is no such call. Summaries and the
|
|
116
|
+
file list are produced by the IDE.
|
|
117
|
+
|
|
118
|
+
## See also
|
|
119
|
+
|
|
120
|
+
`xylon_read`, `agent`, `session_fork`, `sessions`, `bash`
|