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 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.85.0` SDK contract. Development applications declare
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
- fail('INVALID_ARGUMENT', 'Expected the inspect or act operation.');
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,
@@ -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) |
@@ -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 continue to use `--request` for their complete object.
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 or targeted DOM actions. |
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.83.1",
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 and targeted DOM actions.",
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.act over the existing framed local connection. The window host selects native.launchContext.appControl.endpoint and preserves its existing Core child, origin, profile and lifetime.",
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, and targeted DOM actions through its
6
- own local named pipe. The application keeps its existing Core child, profile,
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. Once a
87
- script or capture has been dispatched, cancellation cannot reverse its effects;
88
- the host still observes its completion. A disconnected caller's completed result
89
- is retained in the host diagnostics instead of being represented as delivered.
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`, `documentGeneration`, and
96
- `url`. Each root retains its complete `html` and `text`; controls retain their
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. Role/name label
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. Frame elements and open-shadow hosts
159
- are described; closed shadow roots cannot be discovered. Capture is the current rendered
160
- viewport, not an offscreen full-document rendering. Covered and minimized-window
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 action parameter object, including the actual
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. The CLI reads the selected file completely.
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. `capture(parameters={},options)` returns a full base64 PNG
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. Options preserve the Core client signal/timeout contract. `close()`
9095
- cancels this connection's requests and returns `closed`, without closing the app.
9096
- Document changes and actual errors remain observable; a dispatched action is
9097
- not reversed by later cancellation. Full parameter/result, CLI and platform
9098
- contracts are in [native application control](native-app-control.md).
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcane-os",
3
- "version": "0.85.0",
3
+ "version": "0.86.1",
4
4
  "description": "Arcane OS JavaScript SDK, project-local CLI, browser runtime, and repository-portable application packager.",
5
5
  "type": "module",
6
6
  "main": "./src/index.mjs",
@@ -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 act.');
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 (action === 'act' && !values.request) usage('App-control act requires --request <json-file>.');
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);
@@ -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", new Dictionary<string, object>
139
- { { "title", Text }, { "state", WindowState.ToString() },
140
- { "width", ClientSize.Width }, { "height", ClientSize.Height } } },
141
- { "operations", new string[] { "status", "inspect", "capture", "act" } }
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
- // The creation marker is checked inside the executing document.
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;
@@ -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'],