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