@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,595 @@
|
|
|
1
|
+
# api.attach_app(surface=None, env=None, present=None, preparing=None, config=None, show=None) -> dict
|
|
2
|
+
|
|
3
|
+
<!-- group: Driving a surface -->
|
|
4
|
+
|
|
5
|
+
Launch this workspace's own application and drive it. **Requires permission.**
|
|
6
|
+
|
|
7
|
+
For a product that is not a web page: an Electron app, an iOS app on the
|
|
8
|
+
Simulator (`kind: ios`), or an Android app on the Emulator (`kind: android`).
|
|
9
|
+
`widgex.yml` names which with `kind`.
|
|
10
|
+
|
|
11
|
+
## Why this exists rather than `drive`
|
|
12
|
+
|
|
13
|
+
A web app can be started any way at all — `npm run dev` in a shell, a deployed
|
|
14
|
+
URL, a file on disk — and reached *afterwards* with `api.drive(url, why)`.
|
|
15
|
+
Nothing about how it started has to be known.
|
|
16
|
+
|
|
17
|
+
An Electron app can only be attached to if `--remote-debugging-port` was on its
|
|
18
|
+
command line **at spawn**. There is no attaching to one that is already running.
|
|
19
|
+
So "how does this app start" stops being a convenience and becomes the
|
|
20
|
+
precondition of the app being observable at all — and that is why it is declared
|
|
21
|
+
in a file that everyone shares rather than improvised by whichever session got
|
|
22
|
+
there first.
|
|
23
|
+
|
|
24
|
+
## Never the copy they are running
|
|
25
|
+
|
|
26
|
+
The user is usually working *in* the application you are changing, or in one
|
|
27
|
+
beside it. This call starts a **separate** copy, with its own profile and
|
|
28
|
+
debugging port, so they can judge your change without leaving what they were
|
|
29
|
+
doing.
|
|
30
|
+
|
|
31
|
+
**Never deploy into, overwrite, or ask for a reload of the application the user
|
|
32
|
+
is running** — not to check your work, not to "just quickly look", and not as a
|
|
33
|
+
fallback when attaching fails. A reload costs them their place and their train
|
|
34
|
+
of thought, and installing over a running app means every session on that
|
|
35
|
+
machine is now running your half-finished change. When attaching fails, report
|
|
36
|
+
it and say what you could not confirm: a wrong spec is worth fixing, and
|
|
37
|
+
restarting their editor to save yourself a diagnosis is not.
|
|
38
|
+
|
|
39
|
+
## It builds first
|
|
40
|
+
|
|
41
|
+
`app.build` runs before every spawn, so what you attach to is the code you just
|
|
42
|
+
changed. If the build fails the call refuses rather than handing you the copy
|
|
43
|
+
that was already running — a preview of the previous build, presented as your
|
|
44
|
+
change, is worse than no preview.
|
|
45
|
+
|
|
46
|
+
## Writing the file for a project that has none
|
|
47
|
+
|
|
48
|
+
Work out how the app already starts — its package scripts, its launch
|
|
49
|
+
configuration, how a developer runs it by hand — and propose a `widgex.yml`
|
|
50
|
+
from that.
|
|
51
|
+
|
|
52
|
+
**Never ask the project to change itself to be testable.** No markers to add, no
|
|
53
|
+
preload script, no test-only build, no flag in their source. Selecting on markup
|
|
54
|
+
the app already renders is fine; requiring it to render something new for your
|
|
55
|
+
benefit is not. If a project cannot be previewed without changing it, say so —
|
|
56
|
+
that is a real answer, and a spec that quietly cannot work is not.
|
|
57
|
+
|
|
58
|
+
The requirement is one thing: a Chromium you can attach to. `app.kind` says
|
|
59
|
+
which of two ways it gets one:
|
|
60
|
+
|
|
61
|
+
- `kind: electron` — the app *is* Chromium and takes a debugging port on its
|
|
62
|
+
command line at spawn. There is no attaching to one already running, which is
|
|
63
|
+
why how it starts has to be declared.
|
|
64
|
+
- `kind: web` — `app.serve` boots the dev server and a browser is pointed at
|
|
65
|
+
`app.url`. Both carry `{port}`, which is allocated per preview: never write a
|
|
66
|
+
fixed one, because it works from a single worktree and then silently drives
|
|
67
|
+
that server from a second one.
|
|
68
|
+
|
|
69
|
+
Either way the app has to let its profile and single-instance behaviour be set
|
|
70
|
+
from outside, and show a surface you can pick out of markup it already renders.
|
|
71
|
+
A native binary or a canvas-only interface meets none of that and cannot be
|
|
72
|
+
driven by configuration alone. Say that plainly rather than writing a file that
|
|
73
|
+
will fail at the first attach.
|
|
74
|
+
|
|
75
|
+
## The file
|
|
76
|
+
|
|
77
|
+
`widgex.yml`, at the workspace root. The minimum:
|
|
78
|
+
|
|
79
|
+
```yaml
|
|
80
|
+
app:
|
|
81
|
+
kind: electron
|
|
82
|
+
exec: node_modules/.bin/electron
|
|
83
|
+
args: ['.']
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Everything else is optional:
|
|
87
|
+
|
|
88
|
+
```yaml
|
|
89
|
+
app:
|
|
90
|
+
build: [pnpm, build] # run before a spawn, never before a reuse
|
|
91
|
+
debugPort: '--remote-debugging-port={port}'
|
|
92
|
+
envPolicy: inherit # or `allowlist` to keep only the bare minimum
|
|
93
|
+
env:
|
|
94
|
+
NODE_ENV: development
|
|
95
|
+
profile:
|
|
96
|
+
arg: --user-data-dir # or: env: SOME_APP_HOME
|
|
97
|
+
files: # seeded into the profile before the app starts
|
|
98
|
+
settings.json: '{"skipOnboarding": true}'
|
|
99
|
+
target:
|
|
100
|
+
urlIncludes: index.html
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Which machine it runs on
|
|
104
|
+
|
|
105
|
+
`surface` picks a window to look at. `env` picks the *machine* to run on, and
|
|
106
|
+
they are different questions.
|
|
107
|
+
|
|
108
|
+
```yaml
|
|
109
|
+
app:
|
|
110
|
+
environments:
|
|
111
|
+
fresh:
|
|
112
|
+
description: A machine that has never run an agent CLI.
|
|
113
|
+
home: isolated
|
|
114
|
+
profile: fresh
|
|
115
|
+
returning:
|
|
116
|
+
home: inherit
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
`home: isolated` gives the app its own home directory **and a different value
|
|
120
|
+
for `$USER`**, so it finds nothing of yours — no CLI config in your home, no
|
|
121
|
+
credential in your keychain. Both halves are needed and the second is the one
|
|
122
|
+
that is easy to miss: on macOS the keychain is per-user, not per-home, so a home
|
|
123
|
+
swap alone leaves a "fresh" machine still finding the credentials you signed in
|
|
124
|
+
with. Measured, not assumed.
|
|
125
|
+
|
|
126
|
+
Be precise about what the second half is, because this page used to say "its own
|
|
127
|
+
user" and that reads as more than it is. It is `HOME` and `USER`, two
|
|
128
|
+
environment variables. There is no account, no uid, nothing the kernel enforces.
|
|
129
|
+
It defeats a probe that reads `$USER` — which is exactly what Claude Code's
|
|
130
|
+
`security find-generic-password -a $USER` does, and why it works — and it
|
|
131
|
+
defeats nothing that asks the operating system who it really is. `PATH` is
|
|
132
|
+
inherited either way, so an isolated machine still *finds* your installed CLIs;
|
|
133
|
+
what it does not find is their credentials.
|
|
134
|
+
|
|
135
|
+
**`home: inherit` is not the default. Naming no environment at all is.** They
|
|
136
|
+
behave the same and they are not the same code path: with no `env=` the spec
|
|
137
|
+
carries no environment, so the block above is never consulted and `HOME`/`USER`
|
|
138
|
+
are never touched. The app runs here, on your machine, and sees everything you
|
|
139
|
+
see — your home, your CLIs, your keychain — while opening your own checkout.
|
|
140
|
+
A separate process with a separate profile and a separate port is not a separate
|
|
141
|
+
machine, and the attach result now says which you got.
|
|
142
|
+
|
|
143
|
+
`profile: fresh` throws the app's own state away before each launch. It is a
|
|
144
|
+
different question from `home`: `home` hides *your* machine from the app, and
|
|
145
|
+
the profile is what the environment's own last run left behind. Without it a
|
|
146
|
+
first-run test is a first run exactly once — the next launch restores a profile
|
|
147
|
+
that has already been through onboarding, and says nothing about it. `persist`
|
|
148
|
+
is the default, and is what `returning` wants.
|
|
149
|
+
|
|
150
|
+
Two more optional keys:
|
|
151
|
+
|
|
152
|
+
- **`files`** — seeded into the environment's home before the app starts.
|
|
153
|
+
Relative paths only, nothing escaping the root. `{home}` is substituted.
|
|
154
|
+
- **`setup`** — commands run once when the machine is built, each a list of
|
|
155
|
+
arguments rather than a shell string. Re-run only when `files`, `env` or
|
|
156
|
+
`setup` change; a failure refuses the attach rather than handing back a
|
|
157
|
+
half-built machine.
|
|
158
|
+
|
|
159
|
+
Each environment is its own instance and its own profile, so two of them run
|
|
160
|
+
side by side without seeing each other.
|
|
161
|
+
|
|
162
|
+
### Why you want one
|
|
163
|
+
|
|
164
|
+
Testing a first run on your own machine does not test a first run. It tests
|
|
165
|
+
your machine. That mistake produced a report saying the setup wizard's
|
|
166
|
+
first-run flow was fine — it had never been exercised, because the wizard found
|
|
167
|
+
the developer's own CLI and opened on "already connected", and nothing in the
|
|
168
|
+
run said so.
|
|
169
|
+
|
|
170
|
+
### Paths
|
|
171
|
+
|
|
172
|
+
`exec` is resolved against the workspace when it is relative. `{root}`
|
|
173
|
+
substitutes the workspace folder anywhere in `exec` or `args`, and `${VAR}`
|
|
174
|
+
expands from the environment the app will run with — an unset one is an error
|
|
175
|
+
rather than an empty string, because expanding it away produces a failure much
|
|
176
|
+
later that names nothing.
|
|
177
|
+
|
|
178
|
+
If `exec` names a macOS `.app` bundle, the executable inside it is found for
|
|
179
|
+
you; its name is the branding's, not something you should have to write down.
|
|
180
|
+
|
|
181
|
+
### `profile.files`
|
|
182
|
+
|
|
183
|
+
Files written into the profile directory before the app starts, with `{port}`
|
|
184
|
+
substituted. Some apps cannot be tested without one — a marker that skips
|
|
185
|
+
onboarding, a preset config, a feature flag. Contents are written verbatim and
|
|
186
|
+
nothing is merged. Paths may not escape the profile directory.
|
|
187
|
+
|
|
188
|
+
WidgeX's own `widgex.yml` uses this for `argv.json`, which is how a spawned
|
|
189
|
+
instance learns the debugging port it was started with.
|
|
190
|
+
|
|
191
|
+
### `debugPort` is a template
|
|
192
|
+
|
|
193
|
+
`{port}` is filled in. Put the literal token `{debugPort}` in `args` to control
|
|
194
|
+
*where* it lands, because position matters: `electron . --remote-debugging-port=N`
|
|
195
|
+
can reach the app as its own argv rather than Chromium's.
|
|
196
|
+
|
|
197
|
+
For a command whose arguments we cannot compose — `make dev`, a shell script, a
|
|
198
|
+
task runner — use the escape hatch:
|
|
199
|
+
|
|
200
|
+
```yaml
|
|
201
|
+
debugPort:
|
|
202
|
+
env: WIDGEX_DEBUG_PORT
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Nothing is added to the command line; the variable is exported and **your script
|
|
206
|
+
must forward it**. That is one line in your own file, and it is the only way a
|
|
207
|
+
wrapped launch is testable at all. Forget it and the failure is "started but
|
|
208
|
+
never opened a window", which looks like four other things.
|
|
209
|
+
|
|
210
|
+
### `profile`
|
|
211
|
+
|
|
212
|
+
Where the app keeps its own state, so two branches testing at once do not share
|
|
213
|
+
one. Omit it and they do.
|
|
214
|
+
|
|
215
|
+
### `target` and `surfaces`
|
|
216
|
+
|
|
217
|
+
`target` is the default, used when you call `attach_app()` with no argument.
|
|
218
|
+
`surfaces` names things you can ask for individually:
|
|
219
|
+
|
|
220
|
+
```yaml
|
|
221
|
+
surfaces:
|
|
222
|
+
prefs:
|
|
223
|
+
titleIncludes: Preferences
|
|
224
|
+
dashboard:
|
|
225
|
+
includeFrames: true
|
|
226
|
+
documentMatches: '#dashboard-root'
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Then `api.attach_app(surface='prefs')`. A name the file does not declare is
|
|
230
|
+
refused with the list of ones it does.
|
|
231
|
+
|
|
232
|
+
### More than one app in one repository
|
|
233
|
+
|
|
234
|
+
`config` picks *which app*, where `surface` picks a window inside it. A
|
|
235
|
+
repository often holds two things worth previewing — a desktop app and the
|
|
236
|
+
website that documents it — and they share nothing: a different `kind`, a
|
|
237
|
+
different way of starting, different surfaces. So each goes under `apps:`, by
|
|
238
|
+
name, carrying a whole definition in the same shape as `app:`:
|
|
239
|
+
|
|
240
|
+
```yaml
|
|
241
|
+
app: # the default, used when no config is named
|
|
242
|
+
kind: electron
|
|
243
|
+
exec: node_modules/.bin/electron
|
|
244
|
+
|
|
245
|
+
apps:
|
|
246
|
+
website:
|
|
247
|
+
kind: web
|
|
248
|
+
serve: [npm, run, docs, --, --port={port}]
|
|
249
|
+
url: http://localhost:{port}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Then `api.attach_app(config='website')`. A name is a whole definition and not
|
|
253
|
+
a patch over `app:` — merging would mean every field needing a rule about
|
|
254
|
+
which side wins, and a reader never being sure what a name actually launches.
|
|
255
|
+
|
|
256
|
+
Two configurations are two instances, so a website preview and a desktop
|
|
257
|
+
preview can be up at once without either standing in for the other. A name the
|
|
258
|
+
file does not declare is refused with the ones it does; asking for one when
|
|
259
|
+
there is no `apps:` block prints the block to write.
|
|
260
|
+
|
|
261
|
+
Either can select three ways:
|
|
262
|
+
|
|
263
|
+
- `urlIncludes` / `titleIncludes` — free, straight out of the target listing.
|
|
264
|
+
- `documentMatches` — a CSS selector asked of the document itself, for targets
|
|
265
|
+
that cannot be told apart from outside. Costs a connection per candidate, so
|
|
266
|
+
it runs after the other two have narrowed the field. It reaches one frame deep,
|
|
267
|
+
because an app's real markup is often inside one.
|
|
268
|
+
- `includeFrames` — also consider frame targets. Off by default: an embedded
|
|
269
|
+
player or OAuth widget is a target too, and driving one by accident looks like
|
|
270
|
+
driving the app while touching almost none of it.
|
|
271
|
+
|
|
272
|
+
If several match and nothing distinguishes them, this refuses and lists them
|
|
273
|
+
rather than picking. An app with a splash screen would otherwise hand you the
|
|
274
|
+
splash screen, and every check you ran against it would pass.
|
|
275
|
+
|
|
276
|
+
WidgeX's own `widgex.yml` uses `documentMatches` for its `panel` and `chat`
|
|
277
|
+
surfaces: VS Code reports both as `vscode-webview://<uuid>` with an empty title,
|
|
278
|
+
so only the document can say which is which.
|
|
279
|
+
|
|
280
|
+
It also declares `workbench`, which is the *window* rather than a webview in it
|
|
281
|
+
— the command palette, the notifications, the tabs, the menus. Reach for it
|
|
282
|
+
whenever what you are verifying is a contributed command rather than something
|
|
283
|
+
drawn in a panel: a keystroke sent to a webview frame is delivered to the frame,
|
|
284
|
+
which has no keybinding service, so `api.press('Control+Shift+P')` on `panel`
|
|
285
|
+
does exactly nothing and says so in no way at all.
|
|
286
|
+
|
|
287
|
+
```python
|
|
288
|
+
await api.attach_app(surface='workbench')
|
|
289
|
+
await api.press('Control+Shift+P')
|
|
290
|
+
await api.type_text('>My Command')
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
The same shape applies to any Electron-hosted editor, and it is the reason the
|
|
294
|
+
app-target listing excludes only DevTools and extension pages rather than every
|
|
295
|
+
scheme an IDE uses internally: on the app's own debugging port, the window is
|
|
296
|
+
not "internal", it is the app.
|
|
297
|
+
|
|
298
|
+
A window surface is also the one you can screenshot directly. A frame is an
|
|
299
|
+
out-of-process iframe, and `Page.captureScreenshot` refuses one outright —
|
|
300
|
+
`api.screenshot()` on `panel` works only because it quietly shoots the window
|
|
301
|
+
containing it, and hands you the whole window rather than the panel.
|
|
302
|
+
|
|
303
|
+
## An iOS app: `kind: ios`
|
|
304
|
+
|
|
305
|
+
For an app that runs on the iOS Simulator (Swift, React Native, Expo,
|
|
306
|
+
Flutter). On macOS with Xcode, `attach_app()` boots the simulator **without
|
|
307
|
+
opening Simulator.app**, builds, installs and launches the app. The device's
|
|
308
|
+
screen then shows up in a tab in this task's workbench, where the person can
|
|
309
|
+
tap, swipe, scroll and type into it. No window opens anywhere else.
|
|
310
|
+
|
|
311
|
+
```yaml
|
|
312
|
+
app:
|
|
313
|
+
kind: ios
|
|
314
|
+
device: iPhone 17 # name or UDID; default: a booted iPhone, else the newest
|
|
315
|
+
build: [xcodebuild, -scheme, MyApp, -sdk, iphonesimulator,
|
|
316
|
+
-destination, 'generic/platform=iOS Simulator', -derivedDataPath, build]
|
|
317
|
+
app: build/Build/Products/Debug-iphonesimulator/MyApp.app # `*` allowed in a segment
|
|
318
|
+
bundleId: com.example.MyApp # optional: read from the .app when absent
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
Every field is optional:
|
|
322
|
+
- With no `build`, the app was built some other way.
|
|
323
|
+
- With no `app`, the app is already installed and only `bundleId` is launched.
|
|
324
|
+
- With neither, you just get the device.
|
|
325
|
+
|
|
326
|
+
An Expo or React Native project whose own command builds and installs can leave
|
|
327
|
+
`app` out and launch by `bundleId`:
|
|
328
|
+
|
|
329
|
+
```yaml
|
|
330
|
+
app:
|
|
331
|
+
kind: ios
|
|
332
|
+
build: [npx, expo, run:ios, --no-bundler]
|
|
333
|
+
bundleId: com.example.myapp
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
`exec`, `args`, `serve` and `url` are refused: a simulator has no program to
|
|
337
|
+
run and no page to point at.
|
|
338
|
+
|
|
339
|
+
**Any booted simulator gets a tab, however it was booted.** If you boot one
|
|
340
|
+
with `xcrun simctl boot` or `npx expo run:ios` from `api.bash`, it appears in
|
|
341
|
+
the workbench of the task on screen within a few seconds. `attach_app` exists
|
|
342
|
+
so that the tab lands in *your* task and the app is built and launched for you.
|
|
343
|
+
|
|
344
|
+
**The page verbs do not reach a simulator.** `api.click`, `api.outline` and
|
|
345
|
+
friends are CDP, and a simulator has no markup. Use `xcrun simctl` through
|
|
346
|
+
`api.bash` with the returned `device.udid`:
|
|
347
|
+
- `xcrun simctl io <udid> screenshot shot.png` takes a picture you can look at.
|
|
348
|
+
- `xcrun simctl openurl <udid> <url>` follows a deep link.
|
|
349
|
+
- `xcrun simctl launch --terminate-running-process <udid> <bundleId>` relaunches the app.
|
|
350
|
+
|
|
351
|
+
To check that something works by touch, ask the person. They can use the
|
|
352
|
+
device in the tab.
|
|
353
|
+
|
|
354
|
+
Returns `{status: 'attached', kind: 'ios', device: {name, os, udid},
|
|
355
|
+
booted_by_widgex, stream_id, shown, note}`. `shown` says whether this task's
|
|
356
|
+
workbench is the one on screen.
|
|
357
|
+
|
|
358
|
+
## An Android app: `kind: android`
|
|
359
|
+
|
|
360
|
+
The same idea for the Android Emulator, on any OS with the Android SDK
|
|
361
|
+
installed. `attach_app()` does five things:
|
|
362
|
+
1. Starts the AVD **with no emulator window**, or uses it if it is already
|
|
363
|
+
running.
|
|
364
|
+
2. Waits for Android to finish booting.
|
|
365
|
+
3. Builds the app.
|
|
366
|
+
4. Installs the APK.
|
|
367
|
+
5. Opens the app.
|
|
368
|
+
|
|
369
|
+
The emulator's screen is a tab in this task's workbench. Tapping, swiping,
|
|
370
|
+
scrolling and typing all work there, and the tab has Back, Home, Recents and
|
|
371
|
+
Power.
|
|
372
|
+
|
|
373
|
+
```yaml
|
|
374
|
+
app:
|
|
375
|
+
kind: android
|
|
376
|
+
avd: Pixel_8 # default: a running emulator, else the first AVD
|
|
377
|
+
build: [./gradlew, assembleDebug]
|
|
378
|
+
apk: app/build/outputs/apk/debug/app-debug.apk # `*` allowed in a segment
|
|
379
|
+
package: com.example.myapp # optional: read from the APK when absent
|
|
380
|
+
activity: .MainActivity # optional: default is the launcher activity
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
For Expo or React Native, whose own command builds and installs:
|
|
384
|
+
|
|
385
|
+
```yaml
|
|
386
|
+
app:
|
|
387
|
+
kind: android
|
|
388
|
+
build: [npx, expo, run:android, --no-bundler]
|
|
389
|
+
package: com.example.myapp
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
Every field is optional, with the same meanings as for iOS. `exec`, `args`,
|
|
393
|
+
`serve` and `url` are refused.
|
|
394
|
+
|
|
395
|
+
**Any running emulator gets a tab, however it was started:** from `api.bash`
|
|
396
|
+
(`emulator -avd Pixel_8`), by `npx expo run:android`, or from Android Studio's
|
|
397
|
+
Device Manager.
|
|
398
|
+
|
|
399
|
+
Drive it with `adb -s <serial>` through `api.bash`. The page verbs do not
|
|
400
|
+
reach it.
|
|
401
|
+
- `adb -s emulator-5554 exec-out screencap -p > shot.png` takes a picture.
|
|
402
|
+
- `adb -s emulator-5554 shell am start -a android.intent.action.VIEW -d <url>`
|
|
403
|
+
follows a deep link.
|
|
404
|
+
|
|
405
|
+
Returns `{status: 'attached', kind: 'android', device: {name, os, avd,
|
|
406
|
+
serial}, booted_by_widgex, stream_id, shown, note}`.
|
|
407
|
+
|
|
408
|
+
## Saying a demo is coming: `preparing`
|
|
409
|
+
|
|
410
|
+
The window opens before your first `instruct` is written, and writing one takes
|
|
411
|
+
however long you take. Until this, the app came up in front of somebody with
|
|
412
|
+
nothing on it to say why — and the first thing they saw was a card appearing out
|
|
413
|
+
of nowhere some seconds later.
|
|
414
|
+
|
|
415
|
+
So attaching draws the assistant straight away, saying what is being prepared:
|
|
416
|
+
|
|
417
|
+
```python
|
|
418
|
+
await api.attach_app(surface='panel', preparing='the drafts autosave')
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
Omit it and he says *"Preparing your demo…"*, which is true. He is not given a
|
|
422
|
+
feature name guessed from the surface: the surface is a panel, not the thing you
|
|
423
|
+
changed. Your first `instruct` replaces him wherever he has got to.
|
|
424
|
+
|
|
425
|
+
He is drawn in the **window**, not in the surface being driven, which is what
|
|
426
|
+
lets somebody drag him anywhere in the application rather than around one panel.
|
|
427
|
+
Where they put him outranks where the next step would have placed him.
|
|
428
|
+
|
|
429
|
+
## How they see it: `present`
|
|
430
|
+
|
|
431
|
+
`present='window'` (the default) gives the app a real window on their screen —
|
|
432
|
+
once it is shown; see `show` below. Their pointer, their keyboard, their
|
|
433
|
+
cursor, at whatever rate it composites.
|
|
434
|
+
Nothing is translating anything, so nothing can translate it wrongly. A guided
|
|
435
|
+
tour requires one: it draws its instruction and its ring inside the app the
|
|
436
|
+
person is using.
|
|
437
|
+
|
|
438
|
+
`present='machine'` runs the app on a guest machine of its own, with this
|
|
439
|
+
Issue's worktree mounted in.
|
|
440
|
+
|
|
441
|
+
There used to be a third, `mirror`, which hid the window and streamed a
|
|
442
|
+
screencast into a card. It is gone: `present='mirror'` and an
|
|
443
|
+
`app.presentations.mirror` entry are both refused. Omit `present` for a window.
|
|
444
|
+
|
|
445
|
+
```python
|
|
446
|
+
await api.attach_app(surface='panel', present='window')
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
The presentations are separate instances. Asking for one never hands you the
|
|
450
|
+
process another session is using — the presentation is part of the launch's
|
|
451
|
+
identity, the same way the environment is.
|
|
452
|
+
|
|
453
|
+
`app.present` in `widgex.yml` moves the default, and `app.presentations` says
|
|
454
|
+
what each one needs in the environment:
|
|
455
|
+
|
|
456
|
+
```yaml
|
|
457
|
+
app:
|
|
458
|
+
present: window
|
|
459
|
+
presentations:
|
|
460
|
+
window: {}
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
## Whether they can see it: `show`
|
|
464
|
+
|
|
465
|
+
**The window starts hidden.** Attaching never puts it over their editor or
|
|
466
|
+
takes their keyboard, because most attaches are you checking something for
|
|
467
|
+
yourself, and a window that jumps in front of somebody mid-sentence is worse
|
|
468
|
+
than one they have to ask for. You can drive it, read it and screenshot it
|
|
469
|
+
while it is hidden.
|
|
470
|
+
|
|
471
|
+
It comes up when a person is meant to look at it. That is either of two things:
|
|
472
|
+
|
|
473
|
+
- **You say so when you attach.** `show=True` brings it up straight away. Use
|
|
474
|
+
it when you attached in order to show them something.
|
|
475
|
+
`preparing='…'` announces a demo, so it implies `show=True` unless you also
|
|
476
|
+
pass `show=False`.
|
|
477
|
+
- **You ask them something about it.** These bring the window up by
|
|
478
|
+
themselves, once, when they ask:
|
|
479
|
+
- the first `instruct` of a tour;
|
|
480
|
+
- `request_operator_action`;
|
|
481
|
+
- `phase_complete`, for an app the lead attached.
|
|
482
|
+
|
|
483
|
+
So a check that ends in one of these needs no `show` at all.
|
|
484
|
+
|
|
485
|
+
A preview started from the panel's own button is shown, since a person pressed
|
|
486
|
+
it. "Show me the app" on a chat row brings it up again at any time.
|
|
487
|
+
|
|
488
|
+
```python
|
|
489
|
+
await api.attach_app(surface='panel') # for yourself: stays hidden
|
|
490
|
+
await api.attach_app(surface='panel', show=True) # to show them: comes up now
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
Leave `show` off for anything nobody is watching: a check you are running on
|
|
494
|
+
your own, a build you are verifying, a loop of attaches. `cold-attach-runs.ps1`
|
|
495
|
+
and `drive-attach-check.mjs` never show anything; they launch through the
|
|
496
|
+
instance registry, not through this call.
|
|
497
|
+
|
|
498
|
+
### What "shown" means on each platform
|
|
499
|
+
|
|
500
|
+
- **macOS** — the window covers the display WidgeX is on, in WidgeX's Space,
|
|
501
|
+
above the menu bar, and takes the keyboard. That is the only way to be in
|
|
502
|
+
front of a full-screen WidgeX.
|
|
503
|
+
- **Windows and Linux** — an ordinary window over WidgeX's own rectangle, on
|
|
504
|
+
WidgeX's display. It is on top for under a second, so it actually comes
|
|
505
|
+
in front, and then it is not. Clicking WidgeX, Alt-Tab and the taskbar work
|
|
506
|
+
the way they do for any window. It never covers the whole screen.
|
|
507
|
+
|
|
508
|
+
Either way it is put back on a real display, so a window that last closed on a
|
|
509
|
+
monitor that is no longer there does not come up off-screen (which looks like a
|
|
510
|
+
white or missing window).
|
|
511
|
+
|
|
512
|
+
### The ways out
|
|
513
|
+
|
|
514
|
+
Each still works on a shown preview:
|
|
515
|
+
|
|
516
|
+
- **"Back to WidgeX · or press Esc twice"**, the tab at the top of the app.
|
|
517
|
+
Both are handled inside the app, so they work even after the editor that
|
|
518
|
+
attached it has reloaded.
|
|
519
|
+
- **WidgeX's own button over the preview**, on macOS. The app cannot cover it.
|
|
520
|
+
|
|
521
|
+
Showing the window does not change what you are driving, and putting it away
|
|
522
|
+
does not detach you. The preview is hidden again and you carry on.
|
|
523
|
+
|
|
524
|
+
## Permission
|
|
525
|
+
|
|
526
|
+
Gated, because `build` and `exec` are commands and `widgex.yml` is a file like
|
|
527
|
+
any other — it can arrive on a branch or in a pull request. This is `api.bash`
|
|
528
|
+
with extra steps and it asks the same way.
|
|
529
|
+
|
|
530
|
+
`api.attach_ide_surface` is gated too, and was not always. That was defensible
|
|
531
|
+
while it launched from a spec hardcoded in WidgeX's own source: the command came
|
|
532
|
+
from us. It reads `widgex.yml` now, so it runs what a branch says to run, and
|
|
533
|
+
the distinction stopped being real.
|
|
534
|
+
|
|
535
|
+
## One at a time
|
|
536
|
+
|
|
537
|
+
One instance per workspace and launch. A second session asking for the same app
|
|
538
|
+
waits for the lease rather than starting a second copy — the same rule as
|
|
539
|
+
browser tabs, for the same reason: two drivers on one surface produce confident
|
|
540
|
+
conclusions about a thing neither was looking at.
|
|
541
|
+
|
|
542
|
+
## It can act
|
|
543
|
+
|
|
544
|
+
`can_act` comes back True. Nothing here is drawing the conversation you are
|
|
545
|
+
having — it is a throwaway instance, which is exactly what an agent should be
|
|
546
|
+
allowed to click.
|
|
547
|
+
|
|
548
|
+
(Attaching to WidgeX's own dashboard was once observable-only, for the good
|
|
549
|
+
reason that a misresolved click could close the view you were being watched
|
|
550
|
+
through. That stopped applying when the surface became a separate instance
|
|
551
|
+
rather than the window the user is sitting in front of.)
|
|
552
|
+
|
|
553
|
+
## Returns
|
|
554
|
+
|
|
555
|
+
`{status, title, url, port, kind, can_act, environment, isolated, presentation, shown}`
|
|
556
|
+
|
|
557
|
+
Read `title` and `url`. They name the window it chose, which is the one thing
|
|
558
|
+
worth checking before you believe anything else you observe.
|
|
559
|
+
|
|
560
|
+
Read `shown` before you say anything to the user about what they can see. It
|
|
561
|
+
is the window's own answer: visible, not minimised, and on a display.
|
|
562
|
+
|
|
563
|
+
- `True` — it is in front of them. Do not tell them to go and open it.
|
|
564
|
+
- `False` — nobody can see it. That is the normal result without `show=True`,
|
|
565
|
+
and a `note` says so. With `show=True` you also get `shown_reason`, saying
|
|
566
|
+
why it did not come up. Pass that on rather than saying "it's open".
|
|
567
|
+
- `None` — nothing could be asked (a guest machine, or an app whose main
|
|
568
|
+
process cannot be reached). Do not claim either way.
|
|
569
|
+
|
|
570
|
+
`presentation` says what kind of window it is. It does not say whether the
|
|
571
|
+
window is on screen. Under `machine` there is nothing on their screen until
|
|
572
|
+
you ask for a card.
|
|
573
|
+
|
|
574
|
+
Then read `environment` and `isolated`, which name the *machine*. `environment`
|
|
575
|
+
is the name you passed, or `None` when you passed nothing; `isolated` says
|
|
576
|
+
whether the instance actually got a home of its own. They are not the same
|
|
577
|
+
question — an environment may declare `home: inherit` — which is why both are
|
|
578
|
+
returned rather than one.
|
|
579
|
+
|
|
580
|
+
`{'environment': None, 'isolated': False}` means the app is running as you, with
|
|
581
|
+
your home and your credentials, on your checkout. That is the default and it is
|
|
582
|
+
often what you want; it is never what you want when you are testing a first run.
|
|
583
|
+
|
|
584
|
+
With `reach=`, a `prelude` key rides along: `{replayed, of, ms, reached}`, plus
|
|
585
|
+
`stopped_at`, `wanted`, `outcome` and `saw` when it did not arrive. You do not
|
|
586
|
+
have to check `reached` — a route that stopped has already raised by the time
|
|
587
|
+
you hold this dict.
|
|
588
|
+
|
|
589
|
+
## See also
|
|
590
|
+
|
|
591
|
+
- `api.attach_ide_surface(surface)` — the older spelling of
|
|
592
|
+
`attach_app(surface=…)`. Same code path; prefer this one.
|
|
593
|
+
- `api.drive(url, why)` — a web page, including a dev server
|
|
594
|
+
- `api.phase_complete(...)` — ask for the phase's review once the app shows it
|
|
595
|
+
- `api.request_operator_action(...)` — hand the running app to the user for a step that is theirs
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
<!-- also: self, support, sprite -->
|
|
2
|
+
<!-- group: Working on a website -->
|
|
3
|
+
|
|
4
|
+
# api.attach_ide(surface='workbench') -> dict
|
|
5
|
+
|
|
6
|
+
The Support Sprite starts attached to `column`; pass a surface to move.
|
|
7
|
+
|
|
8
|
+
Drive the WidgeX window this session lives in — the one the user is sitting
|
|
9
|
+
at. **Support Sprite only**: any other session is refused, and should use
|
|
10
|
+
`attach_app`, which starts a separate copy nobody is using.
|
|
11
|
+
|
|
12
|
+
Nothing is launched. The running IDE already listens on a debugging port, and
|
|
13
|
+
this attaches to *its own* window on that port, so a ring drawn here is drawn
|
|
14
|
+
in front of the person you are helping, and a click here is a click they see.
|
|
15
|
+
|
|
16
|
+
## Options
|
|
17
|
+
|
|
18
|
+
- **`surface`** — which part of the window the page verbs see:
|
|
19
|
+
|
|
20
|
+
| surface | what it is |
|
|
21
|
+
|---|---|
|
|
22
|
+
| `column` | **the WidgeX column — start here.** The side bar on the left, whatever it is showing: the task sidebar down its left edge (folder picker, Settings cog, Board, Main, Your tasks) and the page beside it — a conversation with its Agent / Files / Source Control / Overview tabs, or a Settings page. |
|
|
23
|
+
| `workbench` | VS Code itself: menus, editor tabs, the status bar, notifications. The column shows here only as an empty frame titled "WidgeX". |
|
|
24
|
+
| `panel` | a WidgeX page in the editor area — the board, or a Settings page someone moved there with Settings → Layout. Absent unless one is open. |
|
|
25
|
+
| `chat` | an agent's chat page, wherever it is drawn — usually the column, or an editor tab opened with "Open in editor". |
|
|
26
|
+
| `lead` | the project conversation (Main), in the column. |
|
|
27
|
+
| `xylon` | the Xylon manager. |
|
|
28
|
+
|
|
29
|
+
That is the default layout; Settings → Layout can move panels. A panel titled
|
|
30
|
+
"CHAT" on the right, if one is ever showing, is VS Code's own chat — it is not
|
|
31
|
+
part of WidgeX and none of these surfaces is in it.
|
|
32
|
+
|
|
33
|
+
Each webview is a **separate document**. Attached to `workbench`, the verbs see
|
|
34
|
+
VS Code's chrome and *not* the inside of the column; attached to `column`, the
|
|
35
|
+
reverse. Re-attach to move between them — it is cheap.
|
|
36
|
+
|
|
37
|
+
## Returns
|
|
38
|
+
|
|
39
|
+
`{"status": "attached", "surface": ..., "url": ...}`, or `{"error": "…"}`:
|
|
40
|
+
|
|
41
|
+
| what `error` says | what to do |
|
|
42
|
+
|---|---|
|
|
43
|
+
| `no debugging port` | WidgeX was started without one. Tell the user; it needs a restart after WidgeX adds it. |
|
|
44
|
+
| `… is not open in this window` | open it first from the column — the Settings cog, Board, or a task in Your tasks — then attach again. |
|
|
45
|
+
|
|
46
|
+
## Examples
|
|
47
|
+
|
|
48
|
+
Walking someone to their models:
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
await api.attach_ide("column")
|
|
52
|
+
await api.instruct("Click the Settings cog, at the top of the task sidebar.",
|
|
53
|
+
api.find(role="button", text_exact="Settings"))
|
|
54
|
+
r = await api.await_user(api.find(role="button", text_exact="Settings"))
|
|
55
|
+
|
|
56
|
+
await api.instruct("Now Model configuration, in the Settings list.",
|
|
57
|
+
api.find(role="tab", text_exact="Model configuration"))
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Prefer this to a palette command: most "WidgeX: …" commands predate the
|
|
61
|
+
column and do not land where the feature lives now.
|
|
62
|
+
|
|
63
|
+
## See also
|
|
64
|
+
|
|
65
|
+
`highlight`, `instruct`, `await_user`, `attach_app`, `driving`
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# api.attach_ide_surface(surface='panel') -> dict
|
|
2
|
+
<!-- also: ide-surface, dashboard, panel-surface, drive-the-ide -->
|
|
3
|
+
<!-- group: Working on a website -->
|
|
4
|
+
|
|
5
|
+
**The older spelling of `api.attach_app(surface=…)`.** Use that one — see
|
|
6
|
+
`api.man('attach_app')`. This name is kept because sessions and notes from
|
|
7
|
+
before the rename still use it; there is no second code path behind it.
|
|
8
|
+
|
|
9
|
+
It does what `attach_app` does: starts a **separate copy** of WidgeX from the
|
|
10
|
+
workspace's `widgex.yml` and attaches to the named surface in it. It does not
|
|
11
|
+
attach to the window the user is working in — only the Support Sprite does
|
|
12
|
+
that, with `attach_ide`.
|
|
13
|
+
|
|
14
|
+
What it used to say no longer holds: the surface is **not** read-only. The copy
|
|
15
|
+
is a throwaway nobody else is using, so the page verbs act on it like any other
|
|
16
|
+
page.
|
|
17
|
+
|
|
18
|
+
## Options
|
|
19
|
+
|
|
20
|
+
- **`surface`** — a name from `app.surfaces` in `widgex.yml`; defaults to
|
|
21
|
+
`"panel"`. In WidgeX's own: `panel` is the dashboard (an editor tab in the
|
|
22
|
+
middle of the window), `chat` is an agent's chat page (usually drawn in the
|
|
23
|
+
WidgeX column on the left), `column` is that column whatever it shows.
|
|
24
|
+
|
|
25
|
+
## Returns
|
|
26
|
+
|
|
27
|
+
The same as `attach_app`.
|
|
28
|
+
|
|
29
|
+
## See also
|
|
30
|
+
|
|
31
|
+
`attach_app`, `request_operator_action`
|