arcane-os 0.85.0 → 0.86.1
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/CHANGELOG.md +30 -0
- package/README.md +1 -1
- package/browser-runtime/core/app-control.mjs +27 -1
- package/docs/reference/README.md +1 -0
- package/docs/reference/cli.md +5 -2
- package/docs/reference/inventory/package-api.json +4 -4
- package/docs/reference/native-app-control.md +132 -16
- package/docs/reference/sdk-api.md +42 -7
- package/package.json +1 -1
- package/src/cli/app-control.mjs +14 -3
- package/src/core/app-control.mjs +2 -0
- package/src/core/hosts/windows/ArcaneAppControl.cs +2 -1
- package/src/core/hosts/windows/ArcaneWindowControl.cs +216 -31
- package/src/import-map.mjs +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.86.1
|
|
4
|
+
|
|
5
|
+
- Include the existing public `arcane-os/ai/model-observation` export in
|
|
6
|
+
generated browser import maps. Both managed application maps and standalone
|
|
7
|
+
document maps resolve the same published browser module, including direct
|
|
8
|
+
installed-package URLs and nested documents.
|
|
9
|
+
- Consumers adopt the published package and run their normal import-map
|
|
10
|
+
generation before rebuilding or serving their application. No manual map
|
|
11
|
+
edits, alternate imports or new native host build are required. The matching
|
|
12
|
+
Windows and Whisper runtime assets are unchanged from 0.86.0.
|
|
13
|
+
|
|
14
|
+
## 0.86.0
|
|
15
|
+
|
|
16
|
+
- Add fixed `Tab`, `Shift+Tab`, `Enter` and `Space` key operations to
|
|
17
|
+
`connectAppControl` and the public `arcane app-control key` command. Each
|
|
18
|
+
request sends an ordered press/release pair to the selected WebView's current
|
|
19
|
+
focus without desktop activation or global input. Preserve complete phase
|
|
20
|
+
outcomes, original errors and observation of a dispatched release after
|
|
21
|
+
cancellation or navigation; never replay an ambiguous action.
|
|
22
|
+
- Add `resize` to the same client and CLI for a Normal window's native client
|
|
23
|
+
dimensions. Return actual previous and resulting dimensions plus the observed
|
|
24
|
+
CSS viewport; restoration is an explicit second request. Preserve window
|
|
25
|
+
state, position, profile, origin and zoom, and report minimized or maximized
|
|
26
|
+
state without changing it.
|
|
27
|
+
- Include viewport, document focus and accessible open-shadow focus-path
|
|
28
|
+
observations in inspection. These are DOM/native observations, not proof of
|
|
29
|
+
an application action or completed asynchronous handler. The matching Windows
|
|
30
|
+
host asset is required for the new operations; existing consumer artifacts
|
|
31
|
+
must adopt the public package and rebuild through their owning workflow.
|
|
32
|
+
|
|
3
33
|
## 0.85.0
|
|
4
34
|
|
|
5
35
|
- Add `createModelObservationService({runtime, localAI, image, decisions})`
|
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ selected SDK projection or an explicit live SDK source mount. Both profiles
|
|
|
25
25
|
share the theme, packaging, event, cancellation,
|
|
26
26
|
and browser run contracts while retaining their selected application layout.
|
|
27
27
|
|
|
28
|
-
This checkout defines the `0.
|
|
28
|
+
This checkout defines the `0.86.1` SDK contract. Development applications declare
|
|
29
29
|
`arcane-os` as `latest` and retain their lockfile. Exact SDK pins begin when Roshi
|
|
30
30
|
designates a production build. Follow [Consumer updates](docs/consumer-updates.md)
|
|
31
31
|
to refresh and compare the installed SDK and lockfile with the published channel;
|
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
* returned completely. Frame documents and unselected shadow-root contents stay
|
|
10
10
|
* outside this scope. Closed shadow roots cannot be discovered or entered.
|
|
11
11
|
* domRole/domName are DOM-derived hints, not an accessibility-tree computation.
|
|
12
|
+
* Document-level viewport/focus observations also describe the active open-shadow
|
|
13
|
+
* branch, independently of the selected inspection root. The native-only view
|
|
14
|
+
* operation reads that observation without collecting HTML/control roots.
|
|
12
15
|
*
|
|
13
16
|
* act({action,selector,shadowPath?,...}) requires one current CSS match in the
|
|
14
17
|
* selected root. fill uses an exact
|
|
@@ -36,7 +39,10 @@ export function runAppControl(operation, params = {}) {
|
|
|
36
39
|
if (operation === 'act') {
|
|
37
40
|
return {ok: true, result: actOnElement()};
|
|
38
41
|
}
|
|
39
|
-
|
|
42
|
+
if (operation === 'view') {
|
|
43
|
+
return {ok: true, result: readView()};
|
|
44
|
+
}
|
|
45
|
+
fail('INVALID_ARGUMENT', 'Expected the inspect, act, or view operation.');
|
|
40
46
|
} catch (error) {
|
|
41
47
|
return {
|
|
42
48
|
ok: false,
|
|
@@ -66,6 +72,25 @@ export function runAppControl(operation, params = {}) {
|
|
|
66
72
|
return Array.from(root.querySelectorAll(selector));
|
|
67
73
|
}
|
|
68
74
|
|
|
75
|
+
// Native keys and resize need only this fixed observation, not a DOM scan.
|
|
76
|
+
function readView() {
|
|
77
|
+
const focusPath = [];
|
|
78
|
+
let element = document.activeElement;
|
|
79
|
+
while (element) {
|
|
80
|
+
focusPath.push(describeElement(element, null));
|
|
81
|
+
element = element.shadowRoot?.activeElement ?? null;
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
viewport: {
|
|
85
|
+
width: globalThis.innerWidth,
|
|
86
|
+
height: globalThis.innerHeight,
|
|
87
|
+
devicePixelRatio: globalThis.devicePixelRatio
|
|
88
|
+
},
|
|
89
|
+
documentHasFocus: document.hasFocus(),
|
|
90
|
+
focusPath
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
69
94
|
function selectedRoot() {
|
|
70
95
|
const shadowPath = params.shadowPath === undefined ? [] : params.shadowPath;
|
|
71
96
|
if (!Array.isArray(shadowPath)) {
|
|
@@ -156,6 +181,7 @@ export function runAppControl(operation, params = {}) {
|
|
|
156
181
|
...(isDocument ? {} : {shadowPath}),
|
|
157
182
|
title: document.title,
|
|
158
183
|
readyState: document.readyState,
|
|
184
|
+
...readView(),
|
|
159
185
|
doctype: document.doctype
|
|
160
186
|
? new XMLSerializer().serializeToString(document.doctype)
|
|
161
187
|
: null,
|
package/docs/reference/README.md
CHANGED
|
@@ -70,6 +70,7 @@ alone does not make bare module names resolve in a browser.
|
|
|
70
70
|
| Own an explicitly selected native Codex App Server session | [Codex App Server](codex-app-server.md) |
|
|
71
71
|
| Expose application-owned tools and static resources through Node STDIO | [MCP STDIO server](mcp-stdio.md) |
|
|
72
72
|
| Run application-selected llama.cpp, Ollama or ONNX through Core | [Local AI through Core](local-ai.md) |
|
|
73
|
+
| Observe existing renderer and Core model state, progress and failures | [Existing model observation](model-observation.md) |
|
|
73
74
|
| Generate complete local PNG images with a retained native model | [Local image generation](local-image-generation.md) |
|
|
74
75
|
| Select FLUX.2 Klein 4B and its three complementary model resources | [FLUX.2 Klein 4B through Core](local-image-flux.md) |
|
|
75
76
|
| Select the complete SDXL Base 1.0 checkpoint for generation and whole-image editing | [SDXL Base 1.0 through Core](local-image-sdxl.md) |
|
package/docs/reference/cli.md
CHANGED
|
@@ -17,7 +17,10 @@ JSON to stderr and a nonzero exit. Capture writes its image to the explicit
|
|
|
17
17
|
the general output-format parser below applies to the other commands.
|
|
18
18
|
Inspection accepts either `--selector` or `--request <json-file>`; the latter
|
|
19
19
|
reads the complete inspection object, including an explicit `shadowPath` through
|
|
20
|
-
open shadow roots. Actions
|
|
20
|
+
open shadow roots. Actions, fixed keys and Normal-window resizing use `--request`
|
|
21
|
+
for their complete parameter object. Keys require the observed document
|
|
22
|
+
generation; resize returns actual previous native client dimensions for an
|
|
23
|
+
explicit restoration request.
|
|
21
24
|
|
|
22
25
|
## Command inventory
|
|
23
26
|
|
|
@@ -41,7 +44,7 @@ open shadow roots. Actions continue to use `--request` for their complete object
|
|
|
41
44
|
| `arcane run` | Serves an existing browser release, or packages, plans, builds, and launches one paired native artifact. |
|
|
42
45
|
| `arcane update-check` | Performs one explicit, read-only npm dist-tag query for the installed SDK version. |
|
|
43
46
|
| `arcane targets` | Lists target ids, declared status, formats, architectures, signing profiles, methods, and pairing reason. |
|
|
44
|
-
| `arcane app-control status\|inspect\|capture\|act` | Connects to an explicit running native window endpoint for document inspection, viewport capture
|
|
47
|
+
| `arcane app-control status\|inspect\|capture\|act\|key\|resize` | Connects to an explicit running native window endpoint for document inspection, viewport capture, targeted DOM actions, fixed keys or Normal-window client resizing. |
|
|
45
48
|
| `arcane repo status\|pull\|push` | Runs one selected repository operation for the current app workspace. |
|
|
46
49
|
| `arcane mail key set\|status\|delete` | Manages one server-only Resend API-key profile in `.arcane.env.json`. |
|
|
47
50
|
| `arcane mail send` | Performs one explicit, idempotency-keyed Resend attempt from a complete JSON report on redirected stdin. |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"sdkVersion": "0.
|
|
3
|
+
"sdkVersion": "0.86.0",
|
|
4
4
|
"environment": {
|
|
5
5
|
"runtime": "Node.js for Node entrypoints; browser for browser-only entrypoints",
|
|
6
6
|
"minimumVersion": "22.23.2 for Node entrypoints",
|
|
@@ -63,10 +63,10 @@
|
|
|
63
63
|
"entrypoints": ["arcane-os/core/app-control"],
|
|
64
64
|
"primaryImport": "arcane-os/core/app-control",
|
|
65
65
|
"group": "Native application control",
|
|
66
|
-
"summary": "Connects to one explicit running native window for background document inspection, viewport capture
|
|
66
|
+
"summary": "Connects to one explicit running native window for background document inspection, viewport capture, targeted DOM actions, fixed keys and Normal-window client resizing.",
|
|
67
67
|
"availability": "Node local IPC on Windows, Linux and macOS; the current window adapter is Windows WebView2, with other native adapters unavailable",
|
|
68
|
-
"protocol": "arcane/1 app.control.status, app.control.inspect, app.control.capture and app.control.
|
|
69
|
-
"normalization": "Returns endpoint, closed, status(options), inspect(parameters={},options), capture(parameters={},options), act(parameters,options), and close(). Parameters pass unchanged; per-operation options preserve Core signal/timeout behavior. Status reports actual app/window/document readiness. Inspect returns complete selected DOM HTML/text and live control state with DOM-derived role/name hints. Optional shadowPath on inspect and act is an ordered array of unique CSS host selectors through open shadow roots; selector resolves in the final root, with omitted/empty paths retaining top-level light DOM. Whole-shadow-root inspection returns complete innerHTML/textContent with null selector/state; nested shadow-host descriptors provide their full path. Frames remain descriptors and closed roots are unavailable. Capture returns mimeType image/png, complete base64 data, documentGeneration and URL for the rendered viewport. Act requires documentGeneration and one CSS selector with click, exact-string fill, exact-value select or absolute-offset scroll. The native document marker prevents queued actions crossing replacement; cancellation before dispatch prevents execution while dispatched work is observed through completion without retry or reversal. close() cancels only this connection, never the application or Core. No launch, automatic reconnect, desktop activation, global input or public arbitrary evaluator. CLI inspection accepts either --selector or a complete --request JSON object. See native-app-control.md for complete result fields, CLI and platform scope."
|
|
68
|
+
"protocol": "arcane/1 app.control.status, app.control.inspect, app.control.capture, app.control.act, app.control.key and app.control.resize over the existing framed local connection. The window host selects native.launchContext.appControl.endpoint and preserves its existing Core child, origin, profile and lifetime; assembly requires a matching published Windows host asset.",
|
|
69
|
+
"normalization": "Returns endpoint, closed, status(options), inspect(parameters={},options), capture(parameters={},options), act(parameters,options), key(parameters,options), resize(parameters,options), and close(). Parameters pass unchanged; per-operation options preserve Core signal/timeout behavior. Status reports actual app/window/document readiness. Inspect returns complete selected DOM HTML/text and live control state with DOM-derived role/name hints, plus top-level viewport CSS dimensions/devicePixelRatio, documentHasFocus and focusPath through the active open-shadow branch; focus records are observations with null selectors, separate from the selected root's activeElement. Optional shadowPath on inspect and act is an ordered array of unique CSS host selectors through open shadow roots; selector resolves in the final root, with omitted/empty paths retaining top-level light DOM. Whole-shadow-root inspection returns complete innerHTML/textContent with null selector/state; nested shadow-host descriptors provide their full path. Frames remain descriptors and closed roots are unavailable. Capture returns mimeType image/png, complete base64 data, documentGeneration and URL for the rendered viewport. Act requires documentGeneration and one CSS selector with click, exact-string fill, exact-value select or absolute-offset scroll. Key requires documentGeneration and Tab, Enter or Space; shiftKey:true is supported only for Tab. One keyboard owner keeps each press/release pair ordered at the selected WebView's current focus. An attempted press owns a matching release even after cancellation, navigation or failure; no failed or ambiguous press is replayed. Generation checks bracket native input without atomic document binding. Resize accepts positive integral native client width/height supported by WinForms and optional documentGeneration, changes only a Normal window's ClientSize, and returns immediate previous/actual native dimensions plus the later observed CSS viewport. Native and CSS snapshots are not atomic; platform constraints may change actual dimensions. Restoration is an explicit resize using the operation's previous native dimensions; unsupported window states fail before mutation. The native document marker prevents queued DOM actions crossing replacement; cancellation before dispatch prevents execution while dispatched work is observed through completion without retry or reversal, including owned key release. Partial results remain in actual errors and complete later responses remain in host diagnostics after cancellation or disconnection. Dispatch completion does not establish application handler completion or a successful business action. close() cancels only this connection, never the application or Core. No launch, automatic reconnect, desktop activation, global input or public arbitrary evaluator. CLI inspection accepts either --selector or a complete --request JSON object; act, key and resize forward their complete --request parameter objects. See native-app-control.md for complete result fields, CLI and platform scope."
|
|
70
70
|
},
|
|
71
71
|
{
|
|
72
72
|
"id": "local-ai-execution-devices:createExecutionDeviceCatalog",
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
`connectAppControl` from `arcane-os/core/app-control` connects to one explicitly
|
|
4
4
|
selected running application window. The Windows WebView2 host provides document
|
|
5
|
-
inspection, a PNG of its rendered viewport,
|
|
6
|
-
own local named pipe. The application
|
|
7
|
-
origin and window lifecycle. These operations do not activate the desktop window
|
|
5
|
+
inspection, a PNG of its rendered viewport, targeted DOM actions, fixed keys and
|
|
6
|
+
Normal-window client resizing through its own local named pipe. The application
|
|
7
|
+
keeps its existing Core child, profile, origin and window lifecycle. These operations do not activate the desktop window
|
|
8
8
|
or send global keyboard or pointer input.
|
|
9
9
|
|
|
10
10
|
The Node client uses the SDK's existing `arcane/1` framing and local pipe/socket
|
|
@@ -77,29 +77,45 @@ connection or other business result.
|
|
|
77
77
|
| `inspect(parameters={}, options?)` | Optional `shadowPath` selects an open shadow root. Optional CSS `selector` selects zero or more elements within the selected root; omission selects the whole root. Optional `documentGeneration` selects the expected document. Returns complete HTML/text, live control state and DOM-derived role/name hints. |
|
|
78
78
|
| `capture(parameters={}, options?)` | Returns `{mimeType:'image/png', data, documentGeneration, url}`. `data` contains the complete base64 PNG. Optional `documentGeneration` selects the expected document. |
|
|
79
79
|
| `act(parameters, options?)` | Requires `documentGeneration`, one unambiguous CSS `selector` within the root selected by optional `shadowPath`, and the action fields below. |
|
|
80
|
+
| `key(parameters, options?)` | Requires `documentGeneration` and `key:'Tab'`, `'Enter'`, or `'Space'`. Optional `shiftKey:true` is supported only for Tab. Sends one ordered press/release pair to the selected WebView's current focus. |
|
|
81
|
+
| `resize(parameters, options?)` | Requires positive integral native client `width` and `height` supported by WinForms. Optional `documentGeneration` selects the expected document. Resizes only a Normal window and returns immediate previous/actual client dimensions plus the subsequently observed CSS viewport. |
|
|
80
82
|
| `close()` | Closes this caller's connection and cancels its pending requests. It never closes the app or Core. Returns the connection's completion promise. |
|
|
81
83
|
| `closed` | Connection lifetime promise; actual transport failures remain observable. |
|
|
82
84
|
| `endpoint` | The caller-selected local endpoint. |
|
|
83
85
|
|
|
84
86
|
Each operation's separate `options` uses the existing Core client request
|
|
85
87
|
contract, including `signal` and `timeoutMs`. Connection cancellation closes only
|
|
86
|
-
that connection. Cancellation before UI dispatch prevents execution
|
|
87
|
-
|
|
88
|
-
the
|
|
89
|
-
|
|
88
|
+
that connection. Cancellation before UI dispatch prevents execution; keys also
|
|
89
|
+
check cancellation after waiting for the keyboard owner and immediately before
|
|
90
|
+
the press. Once a script, capture, key press or resize has been dispatched,
|
|
91
|
+
cancellation cannot reverse its effects. The host observes its actual completion,
|
|
92
|
+
including the owned key release. The caller receives its ordinary abort result;
|
|
93
|
+
the complete later response is retained in host diagnostics for a cancelled
|
|
94
|
+
request or closed connection instead of being represented as delivered.
|
|
90
95
|
|
|
91
96
|
## Document operations
|
|
92
97
|
|
|
93
98
|
Inspection defaults to the top-level light DOM. Its result contains `scope`, `title`,
|
|
94
99
|
`readyState`, `doctype`, `roots`, `controls`, `frames`, `shadowHosts`,
|
|
95
|
-
`activeElement`, `scrollingElement`, `selection`, `
|
|
96
|
-
`url`. Each root retains its complete
|
|
100
|
+
`activeElement`, `scrollingElement`, `selection`, `viewport`, `documentHasFocus`,
|
|
101
|
+
`focusPath`, `documentGeneration`, and `url`. Each root retains its complete
|
|
102
|
+
`html` and `text`; controls retain their
|
|
97
103
|
attributes and current form, selection, disabled, scrolling and related state.
|
|
98
104
|
Generated CSS selectors are useful for the inspected document; a later DOM edit
|
|
99
105
|
can change their meaning. Application-authored stable selectors are preferable
|
|
100
106
|
when available. Component replacement or mutation can make a selector stale
|
|
101
107
|
without changing the native document generation.
|
|
102
108
|
|
|
109
|
+
`viewport` contains the top-level document's `innerWidth` and `innerHeight` as
|
|
110
|
+
CSS-pixel `width`/`height`, plus its current `devicePixelRatio`.
|
|
111
|
+
`documentHasFocus` records `document.hasFocus()`. `focusPath` describes the
|
|
112
|
+
top-level active element followed by each active element in accessible open
|
|
113
|
+
shadow roots along that focus branch. These records retain complete attributes,
|
|
114
|
+
DOM-derived names and control state, with `selector:null`; they are observations,
|
|
115
|
+
not selector targets. Reading a focused body or host's name can traverse its
|
|
116
|
+
complete text subtree. The path stops at a frame element or closed shadow host;
|
|
117
|
+
it does not enter frame documents or closed roots.
|
|
118
|
+
|
|
103
119
|
### Open shadow roots
|
|
104
120
|
|
|
105
121
|
`inspect` and `act` accept `shadowPath`, an ordered array of CSS host selectors.
|
|
@@ -134,7 +150,10 @@ inspection; pair selectors from its result with that same path for actions.
|
|
|
134
150
|
`activeElement` comes from the selected root. `scrollingElement` is `null` for a
|
|
135
151
|
shadow-root scope because the root has no document scrolling element; scroll
|
|
136
152
|
actions still target elements. `selection.text`, `title`, `readyState`, `doctype`
|
|
137
|
-
and native URL/generation remain document-level information.
|
|
153
|
+
and native URL/generation remain document-level information. The added
|
|
154
|
+
`viewport`, `documentHasFocus` and `focusPath` also always describe the top-level
|
|
155
|
+
document: the focus path can enter open roots outside the selected inspection
|
|
156
|
+
root. The selected root's `activeElement` remains separate. Role/name label
|
|
138
157
|
references resolve within the element's own tree. Inspection follows the DOM
|
|
139
158
|
tree, not the composed rendering tree: slot-assigned light-DOM nodes remain in
|
|
140
159
|
their own containing DOM tree and are not duplicated inside the selected shadow
|
|
@@ -155,9 +174,10 @@ actual target state after dispatch, including disconnection by the action.
|
|
|
155
174
|
|
|
156
175
|
Role/name hints are derived from DOM attributes and labels, not a native
|
|
157
176
|
accessibility-tree computation. Frame documents and unselected shadow-root
|
|
158
|
-
contents are outside the selected scope
|
|
159
|
-
|
|
160
|
-
|
|
177
|
+
contents are outside the selected HTML/control scope; the document-level focus
|
|
178
|
+
path separately describes its active open-shadow branch. Frame elements and
|
|
179
|
+
open-shadow hosts are described; closed shadow roots cannot be discovered.
|
|
180
|
+
Capture is the current rendered viewport, not an offscreen full-document rendering. Covered and minimized-window
|
|
161
181
|
capture behavior requires execution evidence in the selected environment.
|
|
162
182
|
|
|
163
183
|
| Action | Parameters and immediate behavior |
|
|
@@ -174,11 +194,101 @@ rewritten by the SDK.
|
|
|
174
194
|
|
|
175
195
|
The host publishes a new generation when an actual replacement document becomes
|
|
176
196
|
ready. A creation-time document marker is also checked inside the executing
|
|
177
|
-
script so a queued action cannot run against a successor page. During navigation,
|
|
197
|
+
script so a queued DOM action cannot run against a successor page. During navigation,
|
|
178
198
|
operations report that the document is not ready. A change during completion
|
|
179
199
|
reports the actual result with that failure; actions are never retried. These
|
|
180
200
|
markers are transient document lifecycle state and are not written to saved data.
|
|
181
201
|
|
|
202
|
+
## Fixed keys
|
|
203
|
+
|
|
204
|
+
```js
|
|
205
|
+
const page = await app.inspect();
|
|
206
|
+
const moved = await app.key({documentGeneration: page.documentGeneration, key: 'Tab'});
|
|
207
|
+
console.log(moved.previous.focusPath, moved.actual.focusPath);
|
|
208
|
+
await app.key({documentGeneration: moved.documentGeneration, key: 'Tab', shiftKey: true});
|
|
209
|
+
await app.key({documentGeneration: moved.documentGeneration, key: 'Enter'});
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Keys use the selected WebView's current focus. No selector, shadow path, frame
|
|
213
|
+
selection, focus assignment or desktop activation is performed. Tab and
|
|
214
|
+
Shift+Tab use a fixed Tab `rawKeyDown`/`keyUp` pair, with the Shift modifier on
|
|
215
|
+
both events for Shift+Tab. Enter and Space use `keyDown` with their corresponding
|
|
216
|
+
text, followed by `keyUp`, preserving browser default actions and application
|
|
217
|
+
`preventDefault` handling. Shift+Tab does not send a separate Shift key pair.
|
|
218
|
+
The API exposes these fixed keys, not an arbitrary protocol method or script.
|
|
219
|
+
|
|
220
|
+
The result contains `key`, `shiftKey`, `previous` and `actual` document views,
|
|
221
|
+
`press`, `release`, `documentGeneration` and `url`. Each view contains the same
|
|
222
|
+
`viewport`, `documentHasFocus` and `focusPath` described above. Each key phase
|
|
223
|
+
records its fixed `parameters`, `attempted`, `completed`, and the complete
|
|
224
|
+
protocol `response` when returned. Dispatch completion describes the native
|
|
225
|
+
command, not application handler completion or a successful business action.
|
|
226
|
+
Inspect the resulting page to establish the application outcome.
|
|
227
|
+
|
|
228
|
+
Accepted key pairs share one keyboard owner so another key request cannot
|
|
229
|
+
interleave between press and release. Other independent control operations keep
|
|
230
|
+
their existing scheduling. The host awaits each native phase in order. Once
|
|
231
|
+
the press is attempted, the host observes that task and attempts the matching
|
|
232
|
+
release on the same WebView even after cancellation, navigation or press
|
|
233
|
+
failure. Release can itself activate Space. A failed or ambiguous press is
|
|
234
|
+
never replayed.
|
|
235
|
+
|
|
236
|
+
Generation checks occur before dispatch and after completion; the native input
|
|
237
|
+
command has no atomic document binding. Navigation or focus changes can occur
|
|
238
|
+
between the checks and phases, and Tab intentionally changes focus before its
|
|
239
|
+
release. A detected document change is reported with the actual attempted and
|
|
240
|
+
completed phases. Dispatch, release or later observation failures retain the
|
|
241
|
+
complete partial result at `error.data.find(entry => entry.key === 'result').value`
|
|
242
|
+
under `ARCANE_APP_CONTROL_FAILED`. Original exceptions remain in `cause`; if
|
|
243
|
+
both native phases fail, both errors are also retained in the aggregate and
|
|
244
|
+
their respective phase records. An unavailable `actual` view is omitted rather
|
|
245
|
+
than inferred.
|
|
246
|
+
|
|
247
|
+
## Resize and restore a Normal window
|
|
248
|
+
|
|
249
|
+
```js
|
|
250
|
+
const resized = await app.resize({width: 1200, height: 800});
|
|
251
|
+
try {
|
|
252
|
+
console.log(resized.previous, resized.actual, resized.viewport);
|
|
253
|
+
console.log(await app.inspect({selector: '#pm-content'}));
|
|
254
|
+
} finally {
|
|
255
|
+
await app.resize({width: resized.previous.width, height: resized.previous.height});
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
`width` and `height` select the native WinForms client dimensions, in the same
|
|
260
|
+
units returned by `status().window`. They are not CSS viewport dimensions.
|
|
261
|
+
The host changes only `ClientSize`; it does not assign window state, position,
|
|
262
|
+
restore bounds, activation or WebView zoom, and it preserves the profile and
|
|
263
|
+
origin. Read `viewport` for the resulting CSS dimensions instead of assuming a
|
|
264
|
+
particular DPI or zoom conversion.
|
|
265
|
+
|
|
266
|
+
Immediately before the UI-thread mutation, the host checks the actual
|
|
267
|
+
`WindowState` and records `previous:{title,state,width,height}`. It records
|
|
268
|
+
`actual` in the same shape immediately after the native setter, then observes
|
|
269
|
+
the CSS `viewport` asynchronously. The result also contains `requested`,
|
|
270
|
+
`resizeAttempted`, `resizeCompleted`, `documentGeneration` and `url`.
|
|
271
|
+
Platform constraints may make actual dimensions differ from requested ones.
|
|
272
|
+
Other input or concurrent resize requests can change the window between the
|
|
273
|
+
native snapshot and the document observation; these records are not an atomic
|
|
274
|
+
native/CSS snapshot. Completion does not establish that asynchronous application
|
|
275
|
+
resize handlers have settled.
|
|
276
|
+
|
|
277
|
+
Maximized or minimized windows return `ARCANE_APP_CONTROL_FAILED` before any
|
|
278
|
+
size mutation, with `reason:'unsupported-window-state'`,
|
|
279
|
+
`supportedWindowState:'Normal'` and the complete current `window` in the error's
|
|
280
|
+
`data` entries. The host never automatically restores, maximizes or minimizes a
|
|
281
|
+
window to satisfy this request. Invalid dimensions return `INVALID_ARGUMENT`.
|
|
282
|
+
The same state rule applies to explicit restoration.
|
|
283
|
+
|
|
284
|
+
Restore using the actual `previous.width` and `previous.height` from the
|
|
285
|
+
operation being reversed, never a historical status snapshot or CSS viewport
|
|
286
|
+
dimensions. Restoration is an explicit second request; cancellation does not
|
|
287
|
+
resize the window back. If a dispatched resize or its later observation fails,
|
|
288
|
+
the complete partial `result` remains in the error's `data`, including the
|
|
289
|
+
recorded previous dimensions, actual dimensions and attempted/completed flags.
|
|
290
|
+
Use that actual record when deciding whether and how to restore.
|
|
291
|
+
|
|
182
292
|
## CLI
|
|
183
293
|
|
|
184
294
|
```powershell
|
|
@@ -187,14 +297,20 @@ arcane app-control inspect --endpoint '\\.\pipe\arcane-pm-control' --selector '#
|
|
|
187
297
|
arcane app-control inspect --endpoint '\\.\pipe\arcane-pm-control' --request '.arcane\inspection.json'
|
|
188
298
|
arcane app-control capture --endpoint '\\.\pipe\arcane-pm-control' --output '.arcane\view.png'
|
|
189
299
|
arcane app-control act --endpoint '\\.\pipe\arcane-pm-control' --request '.arcane\action.json'
|
|
300
|
+
arcane app-control key --endpoint '\\.\pipe\arcane-pm-control' --request '.arcane\key.json'
|
|
301
|
+
arcane app-control resize --endpoint '\\.\pipe\arcane-pm-control' --request '.arcane\resize.json'
|
|
190
302
|
```
|
|
191
303
|
|
|
192
|
-
The action file is the complete
|
|
304
|
+
The action or key file is the complete parameter object, including the actual
|
|
193
305
|
`documentGeneration` returned by the preceding status or inspection. Inspection
|
|
194
306
|
also accepts `--request` for a complete parameter object, such as
|
|
195
307
|
`{"shadowPath":["#appearance-panel","theme-switcher"]}` or the same object with
|
|
196
308
|
`selector` and `documentGeneration`. Inspection's `--request` and `--selector`
|
|
197
|
-
options are mutually exclusive.
|
|
309
|
+
options are mutually exclusive. A key file can contain
|
|
310
|
+
`{"documentGeneration":3,"key":"Tab","shiftKey":true}`, using the observed
|
|
311
|
+
generation. A resize file contains native dimensions such as
|
|
312
|
+
`{"width":1200,"height":800}`. Each requires `--request`; the CLI reads the
|
|
313
|
+
selected file completely and forwards the parsed parameter object unchanged.
|
|
198
314
|
Paths are relative to the current directory. Capture
|
|
199
315
|
writes the PNG to the explicitly selected file and prints its metadata. The other
|
|
200
316
|
operations print complete JSON. `app-control --help` describes this command's
|
|
@@ -9076,7 +9076,7 @@ connectAppControl({endpoint,signal,onError}={})
|
|
|
9076
9076
|
```
|
|
9077
9077
|
|
|
9078
9078
|
Connects once to the explicit running native window and resolves
|
|
9079
|
-
`{endpoint,closed,status,inspect,capture,act,close}`. The Windows host selects its
|
|
9079
|
+
`{endpoint,closed,status,inspect,capture,act,key,resize,close}`. The Windows host selects its
|
|
9080
9080
|
own listener through `native.launchContext.appControl.endpoint` and preserves the
|
|
9081
9081
|
existing Core child, origin, profile and application lifetime. The client uses
|
|
9082
9082
|
portable Node local IPC; other native host adapters do not yet implement these
|
|
@@ -9088,14 +9088,49 @@ and no automatic reconnect or action retry occurs.
|
|
|
9088
9088
|
and live control state. Inspection and actions accept optional `shadowPath`, an
|
|
9089
9089
|
ordered array of unique CSS host selectors through open shadow roots; their
|
|
9090
9090
|
`selector` resolves inside the final root. Omission retains top-level light DOM.
|
|
9091
|
-
Closed roots remain unavailable.
|
|
9091
|
+
Closed roots remain unavailable. Inspection also returns top-level `viewport`
|
|
9092
|
+
CSS-pixel width/height and `devicePixelRatio`, `documentHasFocus`, and `focusPath`
|
|
9093
|
+
through the active open-shadow branch. These focus records have null selectors
|
|
9094
|
+
and remain separate from the selected root's `activeElement`; frames and closed
|
|
9095
|
+
roots end the path. `capture(parameters={},options)` returns a full base64 PNG
|
|
9092
9096
|
of the rendered viewport. `act(parameters,options)` requires a returned
|
|
9093
9097
|
`documentGeneration`, one CSS selector, and `click`, `fill`, `select` or `scroll`
|
|
9094
|
-
fields.
|
|
9095
|
-
|
|
9096
|
-
|
|
9097
|
-
|
|
9098
|
-
|
|
9098
|
+
fields.
|
|
9099
|
+
|
|
9100
|
+
`key(parameters,options)` requires the observed `documentGeneration` and
|
|
9101
|
+
`key:'Tab'`, `'Enter'` or `'Space'`; `shiftKey:true` is supported only for Tab.
|
|
9102
|
+
It sends a fixed ordered press/release pair to the selected WebView's current
|
|
9103
|
+
focus, without selecting an element or activating the desktop window. Accepted
|
|
9104
|
+
pairs share one keyboard owner. Once a press is attempted, its matching release
|
|
9105
|
+
is attempted on the same WebView even after cancellation, navigation or failure;
|
|
9106
|
+
a failed or ambiguous press is never replayed. Results retain previous/actual
|
|
9107
|
+
document views and complete attempted/completed phase records. Generation checks
|
|
9108
|
+
bracket native input rather than atomically binding it to a document, so focus
|
|
9109
|
+
or navigation can change between phases.
|
|
9110
|
+
|
|
9111
|
+
`resize(parameters,options)` accepts positive integral native client `width`
|
|
9112
|
+
and `height` supported by WinForms, plus optional `documentGeneration`. It changes
|
|
9113
|
+
only `ClientSize` while the actual window is `Normal`; maximized or minimized
|
|
9114
|
+
windows fail before size mutation. Results retain immediate `previous`/`actual`
|
|
9115
|
+
native dimensions, requested dimensions, attempted/completed flags and a later
|
|
9116
|
+
observed CSS `viewport`. Platform constraints can change actual dimensions;
|
|
9117
|
+
native and CSS observations are not an atomic snapshot. Restore explicitly with
|
|
9118
|
+
the same operation's `previous.width` and `previous.height`, not CSS dimensions
|
|
9119
|
+
or a historical status snapshot. Cancellation does not restore the window.
|
|
9120
|
+
|
|
9121
|
+
Options preserve the Core client signal/timeout contract. Cancellation before
|
|
9122
|
+
dispatch prevents execution; dispatched work and owned key release remain
|
|
9123
|
+
observed through completion. Actual errors retain complete partial results;
|
|
9124
|
+
responses completed after cancellation or disconnection remain in host
|
|
9125
|
+
diagnostics. `close()` cancels this connection's requests and returns `closed`,
|
|
9126
|
+
without closing the app. Document changes and actual errors remain observable.
|
|
9127
|
+
Dispatch completion does not establish application handler completion or a
|
|
9128
|
+
successful business action; inspect the resulting built application to establish
|
|
9129
|
+
that outcome. Assembly requires a matching published Windows host asset.
|
|
9130
|
+
|
|
9131
|
+
The CLI provides `app-control key` and `app-control resize`, each forwarding the
|
|
9132
|
+
complete parsed `--request` object. Full parameter/result, error, cancellation,
|
|
9133
|
+
CLI and platform contracts are in [native application control](native-app-control.md).
|
|
9099
9134
|
|
|
9100
9135
|
## runSharedCoreHost()
|
|
9101
9136
|
|
package/package.json
CHANGED
package/src/cli/app-control.mjs
CHANGED
|
@@ -8,6 +8,8 @@ const HELP_TEXT=`Usage:
|
|
|
8
8
|
arcane app-control inspect --endpoint <pipe-or-socket> [--selector <css-selector> | --request <json-file>]
|
|
9
9
|
arcane app-control capture --endpoint <pipe-or-socket> --output <png-file>
|
|
10
10
|
arcane app-control act --endpoint <pipe-or-socket> --request <json-file>
|
|
11
|
+
arcane app-control key --endpoint <pipe-or-socket> --request <json-file>
|
|
12
|
+
arcane app-control resize --endpoint <pipe-or-socket> --request <json-file>
|
|
11
13
|
|
|
12
14
|
Connects to the selected running native window. Results are complete JSON.
|
|
13
15
|
Capture writes the PNG to --output and prints its metadata. Request and output
|
|
@@ -16,6 +18,9 @@ an application or change its profile. --output selects the capture filename;
|
|
|
16
18
|
the other Arcane commands retain their existing output-format option.
|
|
17
19
|
Inspect --request reads the complete parameter object, including shadowPath and
|
|
18
20
|
optional documentGeneration. It cannot be combined with --selector.
|
|
21
|
+
Key --request selects Tab (optionally shiftKey:true), Enter, or Space and the
|
|
22
|
+
observed documentGeneration. Resize --request supplies native client width and
|
|
23
|
+
height for a Normal window; its result includes the previous dimensions.
|
|
19
24
|
`;
|
|
20
25
|
|
|
21
26
|
function usage(message) {
|
|
@@ -29,9 +34,11 @@ function parseArguments(argv) {
|
|
|
29
34
|
status: ['endpoint'],
|
|
30
35
|
inspect: ['endpoint', 'selector', 'request'],
|
|
31
36
|
capture: ['endpoint', 'output'],
|
|
32
|
-
act: ['endpoint', 'request']
|
|
37
|
+
act: ['endpoint', 'request'],
|
|
38
|
+
key: ['endpoint', 'request'],
|
|
39
|
+
resize: ['endpoint', 'request']
|
|
33
40
|
}[action];
|
|
34
|
-
if (!Array.isArray(allowed)) usage('Expected app-control status, inspect, capture, or
|
|
41
|
+
if (!Array.isArray(allowed)) usage('Expected app-control status, inspect, capture, act, key, or resize.');
|
|
35
42
|
const values = {};
|
|
36
43
|
for (let index = 1; index < argv.length; index += 1) {
|
|
37
44
|
const argument = argv[index];
|
|
@@ -48,7 +55,9 @@ function parseArguments(argv) {
|
|
|
48
55
|
}
|
|
49
56
|
if (!values.endpoint) usage('App control requires --endpoint <pipe-or-socket>.');
|
|
50
57
|
if (action === 'capture' && !values.output) usage('App-control capture requires --output <png-file>.');
|
|
51
|
-
if (
|
|
58
|
+
if (['act', 'key', 'resize'].includes(action) && !values.request) {
|
|
59
|
+
usage(`App-control ${action} requires --request <json-file>.`);
|
|
60
|
+
}
|
|
52
61
|
if (action === 'inspect' && values.request !== undefined && values.selector !== undefined) {
|
|
53
62
|
usage('App-control inspect accepts --selector or --request, not both.');
|
|
54
63
|
}
|
|
@@ -83,6 +92,8 @@ export async function runAppControlCli(argv, {cwd = process.cwd(), stdout = proc
|
|
|
83
92
|
if (selection.action === 'status') result = await connection.status({signal});
|
|
84
93
|
else if (selection.action === 'inspect') result = await connection.inspect(parameters, {signal});
|
|
85
94
|
else if (selection.action === 'act') result = await connection.act(parameters, {signal});
|
|
95
|
+
else if (selection.action === 'key') result = await connection.key(parameters, {signal});
|
|
96
|
+
else if (selection.action === 'resize') result = await connection.resize(parameters, {signal});
|
|
86
97
|
else {
|
|
87
98
|
result = await connection.capture(parameters, {signal});
|
|
88
99
|
const output = path.resolve(cwd, selection.output);
|
package/src/core/app-control.mjs
CHANGED
|
@@ -18,6 +18,8 @@ export async function connectAppControl({endpoint, signal, onError = reportAppCo
|
|
|
18
18
|
inspect(parameters = {}, options) { return client.invoke('app.control.inspect', parameters, options); },
|
|
19
19
|
capture(parameters = {}, options) { return client.invoke('app.control.capture', parameters, options); },
|
|
20
20
|
act(parameters, options) { return client.invoke('app.control.act', parameters, options); },
|
|
21
|
+
key(parameters, options) { return client.invoke('app.control.key', parameters, options); },
|
|
22
|
+
resize(parameters, options) { return client.invoke('app.control.resize', parameters, options); },
|
|
21
23
|
close() { return connection.close(); }
|
|
22
24
|
};
|
|
23
25
|
}
|
|
@@ -453,7 +453,8 @@ namespace Arcane.Core.Hosts.Windows
|
|
|
453
453
|
{
|
|
454
454
|
cancellation.Token.ThrowIfCancellationRequested();
|
|
455
455
|
if (method != "app.control.status" && method != "app.control.inspect"
|
|
456
|
-
&& method != "app.control.capture" && method != "app.control.act"
|
|
456
|
+
&& method != "app.control.capture" && method != "app.control.act"
|
|
457
|
+
&& method != "app.control.key" && method != "app.control.resize")
|
|
457
458
|
throw new NotSupportedException("The app does not expose " + method + ".");
|
|
458
459
|
Dictionary<string, object> arguments = parameters as Dictionary<string, object>;
|
|
459
460
|
if (arguments == null)
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
using System;
|
|
2
2
|
using System.Collections.Generic;
|
|
3
|
+
using System.Drawing;
|
|
3
4
|
using System.IO;
|
|
4
5
|
using System.Threading;
|
|
5
6
|
using System.Threading.Tasks;
|
|
7
|
+
using System.Windows.Forms;
|
|
6
8
|
using Microsoft.Web.WebView2.Core;
|
|
7
9
|
|
|
8
10
|
namespace Arcane.Core.Hosts.Windows
|
|
@@ -13,6 +15,7 @@ namespace Arcane.Core.Hosts.Windows
|
|
|
13
15
|
{
|
|
14
16
|
private const string AppControlDocumentKey = "Symbol.for('arcane-os.app-control.document')";
|
|
15
17
|
private readonly HashSet<Task> appControlDocumentTasks = new HashSet<Task>();
|
|
18
|
+
private readonly SemaphoreSlim appControlKeyOwner = new SemaphoreSlim(1, 1);
|
|
16
19
|
private ArcaneAppControl appControl;
|
|
17
20
|
private Task appControlClosing;
|
|
18
21
|
private string appControlScript;
|
|
@@ -59,6 +62,7 @@ namespace Arcane.Core.Hosts.Windows
|
|
|
59
62
|
Task documents = Task.WhenAll(appControlDocumentTasks);
|
|
60
63
|
try { await documents; }
|
|
61
64
|
catch (Exception error) { ReportTaskFailure(documents, error); }
|
|
65
|
+
appControlKeyOwner.Dispose();
|
|
62
66
|
}
|
|
63
67
|
|
|
64
68
|
private void AppControlNavigationStarting(object sender, CoreWebView2NavigationStartingEventArgs args)
|
|
@@ -135,10 +139,17 @@ namespace Arcane.Core.Hosts.Windows
|
|
|
135
139
|
{ "platform", "windows" }, { "host", "webview2" },
|
|
136
140
|
{ "url", webView.Source == null ? null : webView.Source.AbsoluteUri },
|
|
137
141
|
{ "documentGeneration", appControlGeneration }, { "ready", AppControlDocumentReady() },
|
|
138
|
-
{ "window",
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
+
{ "window", AppControlWindow() },
|
|
143
|
+
{ "operations", new string[] { "status", "inspect", "capture", "act", "key", "resize" } }
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private Dictionary<string, object> AppControlWindow()
|
|
148
|
+
{
|
|
149
|
+
return new Dictionary<string, object>
|
|
150
|
+
{
|
|
151
|
+
{ "title", Text }, { "state", WindowState.ToString() },
|
|
152
|
+
{ "width", ClientSize.Width }, { "height", ClientSize.Height }
|
|
142
153
|
};
|
|
143
154
|
}
|
|
144
155
|
|
|
@@ -147,14 +158,16 @@ namespace Arcane.Core.Hosts.Windows
|
|
|
147
158
|
{
|
|
148
159
|
cancellation.ThrowIfCancellationRequested();
|
|
149
160
|
if (method == "app.control.status") return AppControlStatus();
|
|
150
|
-
if (method != "app.control.inspect" && method != "app.control.capture" && method != "app.control.act"
|
|
161
|
+
if (method != "app.control.inspect" && method != "app.control.capture" && method != "app.control.act"
|
|
162
|
+
&& method != "app.control.key" && method != "app.control.resize")
|
|
151
163
|
throw new ArgumentException("Unknown app-control method: " + method);
|
|
164
|
+
if (method == "app.control.resize") RequireAppControlNormalWindow();
|
|
152
165
|
if (!AppControlDocumentReady()) throw new InvalidOperationException("The app document is not ready for control.");
|
|
153
166
|
long selectedGeneration = appControlGeneration;
|
|
154
167
|
string selectedDocument = appControlDocument;
|
|
155
168
|
string selectedUrl = webView.Source == null ? null : webView.Source.AbsoluteUri;
|
|
156
169
|
object expected;
|
|
157
|
-
if (method == "app.control.act" && !parameters.ContainsKey("documentGeneration"))
|
|
170
|
+
if ((method == "app.control.act" || method == "app.control.key") && !parameters.ContainsKey("documentGeneration"))
|
|
158
171
|
throw new ArgumentException("An action requires the documentGeneration returned by status or inspect.");
|
|
159
172
|
if (parameters.TryGetValue("documentGeneration", out expected)
|
|
160
173
|
&& (!(expected is int || expected is long || expected is decimal || expected is double)
|
|
@@ -173,34 +186,14 @@ namespace Arcane.Core.Hosts.Windows
|
|
|
173
186
|
};
|
|
174
187
|
}
|
|
175
188
|
}
|
|
189
|
+
else if (method == "app.control.key")
|
|
190
|
+
result = await AppControlKeyAsync(parameters, cancellation, selectedGeneration, selectedDocument);
|
|
191
|
+
else if (method == "app.control.resize")
|
|
192
|
+
result = await AppControlResizeAsync(parameters, selectedGeneration, selectedDocument);
|
|
176
193
|
else
|
|
177
194
|
{
|
|
178
195
|
string operation = method == "app.control.inspect" ? "inspect" : "act";
|
|
179
|
-
|
|
180
|
-
// Native generation checks alone cannot stop a queued script
|
|
181
|
-
// from reaching a successor document during navigation.
|
|
182
|
-
string script = "(function(expected,operation,parameters){"
|
|
183
|
-
+ "if(globalThis[" + AppControlDocumentKey + "]!==expected)"
|
|
184
|
-
+ "return {ok:false,error:{code:'APP_CONTROL_DOCUMENT_REPLACED',message:'The selected app document has been replaced.'}};"
|
|
185
|
-
+ "return (" + options.AppControlSource + ")(operation,parameters);})("
|
|
186
|
-
+ ArcaneHost.Serializer().Serialize(selectedDocument) + ","
|
|
187
|
-
+ ArcaneHost.Serializer().Serialize(operation) + ","
|
|
188
|
-
+ ArcaneHost.Serializer().Serialize(parameters) + ")";
|
|
189
|
-
string json = await webView.CoreWebView2.ExecuteScriptAsync(script);
|
|
190
|
-
Dictionary<string, object> envelope = ArcaneHost.Serializer().DeserializeObject(json) as Dictionary<string, object>;
|
|
191
|
-
object ok;
|
|
192
|
-
if (envelope == null || !envelope.TryGetValue("ok", out ok) || !(ok is bool))
|
|
193
|
-
throw new InvalidDataException("The document returned no app-control result.");
|
|
194
|
-
if (!(bool)ok)
|
|
195
|
-
{
|
|
196
|
-
object detail;
|
|
197
|
-
envelope.TryGetValue("error", out detail);
|
|
198
|
-
IOException error = new IOException("The document app-control operation failed.");
|
|
199
|
-
error.Data["documentError"] = detail;
|
|
200
|
-
throw error;
|
|
201
|
-
}
|
|
202
|
-
if (!envelope.TryGetValue("result", out result))
|
|
203
|
-
throw new InvalidDataException("The document omitted the app-control result.");
|
|
196
|
+
result = await ReadAppControlDocumentAsync(operation, parameters, selectedDocument);
|
|
204
197
|
}
|
|
205
198
|
// Completion is observed after dispatch even if a caller cancelled.
|
|
206
199
|
// A document change is reported with the actual returned result; it
|
|
@@ -220,6 +213,198 @@ namespace Arcane.Core.Hosts.Windows
|
|
|
220
213
|
return record;
|
|
221
214
|
}
|
|
222
215
|
|
|
216
|
+
private async Task<Dictionary<string, object>> ReadAppControlDocumentAsync(string operation,
|
|
217
|
+
Dictionary<string, object> parameters, string selectedDocument)
|
|
218
|
+
{
|
|
219
|
+
// The creation marker is checked inside the executing document.
|
|
220
|
+
// Native generation checks alone cannot stop a queued script
|
|
221
|
+
// from reaching a successor document during navigation.
|
|
222
|
+
string script = "(function(expected,operation,parameters){"
|
|
223
|
+
+ "if(globalThis[" + AppControlDocumentKey + "]!==expected)"
|
|
224
|
+
+ "return {ok:false,error:{code:'APP_CONTROL_DOCUMENT_REPLACED',message:'The selected app document has been replaced.'}};"
|
|
225
|
+
+ "return (" + options.AppControlSource + ")(operation,parameters);})("
|
|
226
|
+
+ ArcaneHost.Serializer().Serialize(selectedDocument) + ","
|
|
227
|
+
+ ArcaneHost.Serializer().Serialize(operation) + ","
|
|
228
|
+
+ ArcaneHost.Serializer().Serialize(parameters) + ")";
|
|
229
|
+
string json = await webView.CoreWebView2.ExecuteScriptAsync(script);
|
|
230
|
+
Dictionary<string, object> envelope = ArcaneHost.Serializer().DeserializeObject(json) as Dictionary<string, object>;
|
|
231
|
+
object ok;
|
|
232
|
+
if (envelope == null || !envelope.TryGetValue("ok", out ok) || !(ok is bool))
|
|
233
|
+
throw new InvalidDataException("The document returned no app-control result.");
|
|
234
|
+
if (!(bool)ok)
|
|
235
|
+
{
|
|
236
|
+
object detail;
|
|
237
|
+
envelope.TryGetValue("error", out detail);
|
|
238
|
+
IOException error = new IOException("The document app-control operation failed.");
|
|
239
|
+
error.Data["documentError"] = detail;
|
|
240
|
+
throw error;
|
|
241
|
+
}
|
|
242
|
+
object result;
|
|
243
|
+
if (!envelope.TryGetValue("result", out result))
|
|
244
|
+
throw new InvalidDataException("The document omitted the app-control result.");
|
|
245
|
+
Dictionary<string, object> record = result as Dictionary<string, object>;
|
|
246
|
+
if (record == null) throw new InvalidDataException("The app-control result must be an object.");
|
|
247
|
+
return record;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
private async Task<object> AppControlKeyAsync(Dictionary<string, object> parameters,
|
|
251
|
+
CancellationToken cancellation, long selectedGeneration, string selectedDocument)
|
|
252
|
+
{
|
|
253
|
+
object value;
|
|
254
|
+
string key = parameters.TryGetValue("key", out value) ? value as string : null;
|
|
255
|
+
if (key != "Tab" && key != "Enter" && key != "Space")
|
|
256
|
+
throw new ArgumentException("key must be Tab, Enter, or Space.");
|
|
257
|
+
bool shiftKey = false;
|
|
258
|
+
if (parameters.TryGetValue("shiftKey", out value))
|
|
259
|
+
{
|
|
260
|
+
if (!(value is bool)) throw new ArgumentException("shiftKey must be a boolean.");
|
|
261
|
+
shiftKey = (bool)value;
|
|
262
|
+
}
|
|
263
|
+
if (shiftKey && key != "Tab") throw new ArgumentException("shiftKey is supported only with Tab.");
|
|
264
|
+
Dictionary<string, object> down = new Dictionary<string, object>
|
|
265
|
+
{
|
|
266
|
+
{ "type", key == "Tab" ? "rawKeyDown" : "keyDown" },
|
|
267
|
+
{ "key", key == "Space" ? " " : key }, { "code", key },
|
|
268
|
+
{ "windowsVirtualKeyCode", key == "Tab" ? 9 : key == "Enter" ? 13 : 32 },
|
|
269
|
+
{ "modifiers", shiftKey ? 8 : 0 }
|
|
270
|
+
};
|
|
271
|
+
Dictionary<string, object> up = new Dictionary<string, object>(down);
|
|
272
|
+
up["type"] = "keyUp";
|
|
273
|
+
if (key != "Tab")
|
|
274
|
+
{
|
|
275
|
+
down["text"] = key == "Enter" ? "\r" : " ";
|
|
276
|
+
down["unmodifiedText"] = down["text"];
|
|
277
|
+
}
|
|
278
|
+
Dictionary<string, object> press = new Dictionary<string, object>
|
|
279
|
+
{ { "parameters", down }, { "attempted", false }, { "completed", false } };
|
|
280
|
+
Dictionary<string, object> release = new Dictionary<string, object>
|
|
281
|
+
{ { "parameters", up }, { "attempted", false }, { "completed", false } };
|
|
282
|
+
Dictionary<string, object> result = new Dictionary<string, object>
|
|
283
|
+
{
|
|
284
|
+
{ "key", key }, { "shiftKey", shiftKey }, { "press", press }, { "release", release },
|
|
285
|
+
{ "documentGeneration", selectedGeneration },
|
|
286
|
+
{ "url", webView.Source == null ? null : webView.Source.AbsoluteUri }
|
|
287
|
+
};
|
|
288
|
+
// Only keyboard pairs share this owner: an awaited down must not
|
|
289
|
+
// allow another accepted key to interleave before its owned up.
|
|
290
|
+
await appControlKeyOwner.WaitAsync(cancellation);
|
|
291
|
+
try
|
|
292
|
+
{
|
|
293
|
+
cancellation.ThrowIfCancellationRequested();
|
|
294
|
+
RequireAppControlDocument(selectedGeneration, selectedDocument);
|
|
295
|
+
result["previous"] = await ReadAppControlDocumentAsync("view", new Dictionary<string, object>(), selectedDocument);
|
|
296
|
+
cancellation.ThrowIfCancellationRequested();
|
|
297
|
+
RequireAppControlDocument(selectedGeneration, selectedDocument);
|
|
298
|
+
CoreWebView2 browser = webView.CoreWebView2;
|
|
299
|
+
string downJson = ArcaneHost.Serializer().Serialize(down);
|
|
300
|
+
string upJson = ArcaneHost.Serializer().Serialize(up);
|
|
301
|
+
Exception pressError = null;
|
|
302
|
+
Exception releaseError = null;
|
|
303
|
+
press["attempted"] = true;
|
|
304
|
+
try
|
|
305
|
+
{
|
|
306
|
+
press["response"] = await browser.CallDevToolsProtocolMethodAsync("Input.dispatchKeyEvent", downJson);
|
|
307
|
+
press["completed"] = true;
|
|
308
|
+
}
|
|
309
|
+
catch (Exception error) { pressError = error; }
|
|
310
|
+
// C# 5 cannot await in finally. This unconditional phase follows
|
|
311
|
+
// the observed down task, including failure/cancellation/navigation.
|
|
312
|
+
// Release can itself activate Space; it cannot undo the press.
|
|
313
|
+
release["attempted"] = true;
|
|
314
|
+
try
|
|
315
|
+
{
|
|
316
|
+
release["response"] = await browser.CallDevToolsProtocolMethodAsync("Input.dispatchKeyEvent", upJson);
|
|
317
|
+
release["completed"] = true;
|
|
318
|
+
}
|
|
319
|
+
catch (Exception error) { releaseError = error; }
|
|
320
|
+
try
|
|
321
|
+
{
|
|
322
|
+
if (pressError != null) press["error"] = ArcaneHost.ErrorRecord(pressError, "ARCANE_APP_CONTROL_FAILED");
|
|
323
|
+
if (releaseError != null) release["error"] = ArcaneHost.ErrorRecord(releaseError, "ARCANE_APP_CONTROL_FAILED");
|
|
324
|
+
if (pressError != null && releaseError != null)
|
|
325
|
+
throw new AggregateException("The key press and release failed.", pressError, releaseError);
|
|
326
|
+
if (pressError != null) throw new IOException("The key press failed.", pressError);
|
|
327
|
+
if (releaseError != null) throw new IOException("The key release failed.", releaseError);
|
|
328
|
+
RequireAppControlDocument(selectedGeneration, selectedDocument);
|
|
329
|
+
result["actual"] = await ReadAppControlDocumentAsync("view", new Dictionary<string, object>(), selectedDocument);
|
|
330
|
+
return result;
|
|
331
|
+
}
|
|
332
|
+
catch (Exception error)
|
|
333
|
+
{
|
|
334
|
+
throw AppControlOutcomeError("The app key operation did not complete normally.", error, result);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
finally { appControlKeyOwner.Release(); }
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
private void RequireAppControlDocument(long selectedGeneration, string selectedDocument)
|
|
341
|
+
{
|
|
342
|
+
if (!AppControlDocumentReady() || appControlGeneration != selectedGeneration || appControlDocument != selectedDocument)
|
|
343
|
+
throw new InvalidOperationException("The selected app document has been replaced or is no longer ready.");
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
private void RequireAppControlNormalWindow()
|
|
347
|
+
{
|
|
348
|
+
if (WindowState == FormWindowState.Normal) return;
|
|
349
|
+
InvalidOperationException error = new InvalidOperationException("App-control resize requires a Normal window.");
|
|
350
|
+
error.Data["reason"] = "unsupported-window-state";
|
|
351
|
+
error.Data["supportedWindowState"] = "Normal";
|
|
352
|
+
error.Data["window"] = AppControlWindow();
|
|
353
|
+
throw error;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
private static int AppControlDimension(Dictionary<string, object> parameters, string name)
|
|
357
|
+
{
|
|
358
|
+
object value;
|
|
359
|
+
if (!parameters.TryGetValue(name, out value)
|
|
360
|
+
|| !(value is int || value is long || value is decimal || value is double))
|
|
361
|
+
throw new ArgumentException(name + " must be a positive integral native client dimension.");
|
|
362
|
+
double dimension = Convert.ToDouble(value);
|
|
363
|
+
if (Double.IsNaN(dimension) || dimension < 1 || dimension > Int32.MaxValue || dimension != Math.Truncate(dimension)
|
|
364
|
+
|| (value is decimal && (decimal)value != Decimal.Truncate((decimal)value)))
|
|
365
|
+
throw new ArgumentException(name + " must be a positive integral native client dimension.");
|
|
366
|
+
return (int)dimension;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
private async Task<object> AppControlResizeAsync(Dictionary<string, object> parameters,
|
|
370
|
+
long selectedGeneration, string selectedDocument)
|
|
371
|
+
{
|
|
372
|
+
int width = AppControlDimension(parameters, "width");
|
|
373
|
+
int height = AppControlDimension(parameters, "height");
|
|
374
|
+
// This check and immediate previous snapshot run in the same UI turn
|
|
375
|
+
// as the setter. Never normalize a maximized/minimized window.
|
|
376
|
+
RequireAppControlNormalWindow();
|
|
377
|
+
Dictionary<string, object> result = new Dictionary<string, object>
|
|
378
|
+
{
|
|
379
|
+
{ "requested", new Dictionary<string, object> { { "width", width }, { "height", height } } },
|
|
380
|
+
{ "previous", AppControlWindow() }, { "resizeAttempted", false }, { "resizeCompleted", false },
|
|
381
|
+
{ "documentGeneration", selectedGeneration },
|
|
382
|
+
{ "url", webView.Source == null ? null : webView.Source.AbsoluteUri }
|
|
383
|
+
};
|
|
384
|
+
try
|
|
385
|
+
{
|
|
386
|
+
result["resizeAttempted"] = true;
|
|
387
|
+
ClientSize = new Size(width, height);
|
|
388
|
+
result["resizeCompleted"] = true;
|
|
389
|
+
result["actual"] = AppControlWindow();
|
|
390
|
+
Dictionary<string, object> view = await ReadAppControlDocumentAsync("view", new Dictionary<string, object>(), selectedDocument);
|
|
391
|
+
result["viewport"] = view["viewport"];
|
|
392
|
+
return result;
|
|
393
|
+
}
|
|
394
|
+
catch (Exception error)
|
|
395
|
+
{
|
|
396
|
+
if (!result.ContainsKey("actual")) result["actual"] = AppControlWindow();
|
|
397
|
+
throw AppControlOutcomeError("The app resize operation did not complete normally.", error, result);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
private static IOException AppControlOutcomeError(string message, Exception cause, Dictionary<string, object> result)
|
|
402
|
+
{
|
|
403
|
+
IOException error = new IOException(message, cause);
|
|
404
|
+
error.Data["result"] = result;
|
|
405
|
+
return error;
|
|
406
|
+
}
|
|
407
|
+
|
|
223
408
|
private sealed class AppControlRequest
|
|
224
409
|
{
|
|
225
410
|
private readonly ArcaneHostForm window;
|
package/src/import-map.mjs
CHANGED
|
@@ -53,6 +53,7 @@ const SDK_BROWSER_SELF_IMPORTS=new Map([
|
|
|
53
53
|
['arcane-os/ai/core-onnx','sdk/ai/core-onnx.mjs'],
|
|
54
54
|
['arcane-os/ai/core-image','sdk/ai/core-image.mjs'],
|
|
55
55
|
['arcane-os/ai/core-model-assets','sdk/ai/core-model-assets.mjs'],
|
|
56
|
+
['arcane-os/ai/model-observation','sdk/ai/model-observation.mjs'],
|
|
56
57
|
['arcane-os/ai/browser-decisions','sdk/ai/browser-decisions.mjs'],
|
|
57
58
|
['arcane-os/ai/tool-text-stream','sdk/ai/tool-text-stream.mjs'],
|
|
58
59
|
['arcane-os/ai/twin-cloud','sdk/ai/twin-cloud.mjs'],
|