arcane-os 0.84.0 → 0.86.0

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.
@@ -55,6 +55,8 @@ runtime layouts.
55
55
  | `arcane-os/core/contracts` | Shared Core protocol, frame, method and diagnostic error contracts. |
56
56
  | `arcane-os/core/classic-source` | Node generator for the canonical classic-script Core client projection. |
57
57
  | `arcane-os/core/runtime` | App-neutral native Core dispatcher and service lifecycle. |
58
+ | `arcane-os/core/model-observation` | Passive observation of an existing Core and its actual native model service owners. |
59
+ | `arcane-os/ai/model-observation` | Attach existing renderer model owners and subscribe through an existing Core client. |
58
60
  | `arcane-os/core/development` | App-service composition and document-owned Core transport during source development, independently of local AI. |
59
61
  | `arcane-os/core/stdio` | Framed Node stdio transport with graceful runtime drain. |
60
62
  | `arcane-os/core/host` | Node-only reusable Core host lifecycle, app-selected native launch defaults and shared native location resolution. |
@@ -420,6 +422,9 @@ The remaining data-only subpaths are eight JSON Schemas and package metadata.
420
422
  | `McpProtocolError` | class | `arcane-os/mcp/stdio` | MCP STDIO | Node |
421
423
  | `createCoreClassicSource()` | function | `arcane-os/core/classic-source` | Core classic client projection | Node; generated source runs in browser and native WebView renderers |
422
424
  | `createCoreRuntime()` | function | `arcane-os/core/runtime` | Native Core runtime | Node on Windows, Linux and macOS native hosts; Android requires host adaptation |
425
+ | `createModelObservationService()` | function | `arcane-os/core/model-observation` | Existing model observation | Node with an existing Core runtime and optional existing native model services |
426
+ | `attachModelObservation()` | function | `arcane-os/ai/model-observation` | Existing model observation | JavaScript with an existing Core client and optional existing renderer owners |
427
+ | `subscribeModelObservation()` | function | `arcane-os/ai/model-observation` | Existing model observation | JavaScript with an existing Core client and composed observation service |
423
428
  | `encodeCoreFrame()` | function | `arcane-os/core/stdio` | Native Core stdio transport | Node |
424
429
  | `createCoreFrameDecoder()` | function | `arcane-os/core/stdio` | Native Core stdio transport | Node |
425
430
  | `startCoreStdio()` | function | `arcane-os/core/stdio` | Native Core stdio transport | Node |
@@ -7395,13 +7400,17 @@ Import it from `arcane-os/core/runtime`. The runtime exposes `current()`,
7395
7400
  `subscribe()`, `onFrame()`, `emit()`, `registerService()`, `getService()`, `start()`, `handle()`
7396
7401
  and idempotent `close()`.
7397
7402
 
7398
- `runtime.handle(frame, {contextRequestId})` optionally preserves a transport's
7399
- original client ID in handler `context.requestId`, while `context.coreRequestId`
7400
- always exposes the actual internal `frame.id`. Active requests, cancellation,
7401
- response IDs and request-event envelopes retain that internal ID. Ordinary
7402
- `handle(frame)` exposes both context IDs equal to `frame.id`. This metadata
7403
- does not change parameters or event data; transports own client-facing protocol
7404
- correlation. See [the runtime contract](core-runtime.md#state-and-frames).
7403
+ `runtime.handle(frame, {contextRequestId, contextClientRequestId})` accepts
7404
+ optional nonempty transport metadata strings. Handler `requestId` remains
7405
+ `contextRequestId ?? frame.id`; additive `clientRequestId` is
7406
+ `contextClientRequestId ?? contextRequestId ?? frame.id`; `coreRequestId` always
7407
+ exposes the actual internal `frame.id`. Direct `handle(frame)` exposes all three
7408
+ equal. Stdio and both IPC paths supply the actual client identity separately,
7409
+ preserving the owning shared-host adapter's established internal `requestId`.
7410
+ Active requests, cancellation, response IDs and request-event envelopes retain
7411
+ the internal ID. Parameters, results and event data stay unchanged; transports
7412
+ own client-facing protocol correlation. See
7413
+ [the runtime contract](core-runtime.md#state-and-frames).
7405
7414
 
7406
7415
  `await runtime.getService(name)` and service `context.getService(name)` return
7407
7416
  the actual registered service after its shared startup. They wait for that
@@ -7588,6 +7597,80 @@ const transport = startCoreStdio({runtime});
7588
7597
  await transport.closed;
7589
7598
  ```
7590
7599
 
7600
+ ## createModelObservationService()
7601
+
7602
+ ```text
7603
+ createModelObservationService({runtime,localAI=null,image=null,decisions=null}={})
7604
+ ```
7605
+
7606
+ Import from `arcane-os/core/model-observation`. Supply the existing Core runtime
7607
+ and actual native service definitions, including their `current()` and `methods`.
7608
+ Returns `{name:'model-observation',current,dispose,methods}`. Register it on that
7609
+ runtime after ordinary host creation and before opening an additional listener.
7610
+ Construction starts no service, model, worker or readiness wait.
7611
+
7612
+ The four methods are `model.observation.current`, `model.observation.watch`,
7613
+ `model.observation.renderer.attach` and `model.observation.renderer.publish`.
7614
+ `current()` returns `{core,native:{localAI,image,decisions},renderers}` from the
7615
+ supplied owners. Absent model owners are `null`; absent attachments produce `[]`.
7616
+ The watch replays current state and remains pending until cancellation. Its
7617
+ `diagnostics:false` default excludes live failure frames; explicit `true`
7618
+ observes complete failed Core responses for the supplied owners' method keys.
7619
+ It retains no failure history and invents no model-operation identities.
7620
+
7621
+ Renderer attachment and watch acknowledgements use actual `clientRequestId`;
7622
+ attachments are keyed by the actual `coreRequestId` to distinguish clients with
7623
+ equal local IDs. `dispose()` retires observation requests/subscriptions only.
7624
+ Core still owns model services and shutdown. See
7625
+ [complete methods, events and evidence limits](model-observation.md).
7626
+
7627
+ ## attachModelObservation()
7628
+
7629
+ ```text
7630
+ attachModelObservation({client,aiRuntimeState=null,imageRuntime=null,decisionModel=null,modelController=null,signal,onError}={})
7631
+ ```
7632
+
7633
+ Import from `arcane-os/ai/model-observation`. Pass the existing Core client and
7634
+ the application-owned instances already doing the work. Supported owner methods
7635
+ are AIRuntimeState's `getAIRuntimeState/subscribeAIRuntimeState`, image runtime's
7636
+ `current/subscribe`, decision model's `status/subscribe`, and ModelController's
7637
+ `status/on`. It creates no replacement owner or transport.
7638
+
7639
+ Returns `{ready,closed,current,close}`. `ready` resolves with the Core-supplied
7640
+ `{attachmentRequestId,coreRequestId}` acknowledgement. Subscriptions then publish
7641
+ complete current owner snapshots, including a fresh read after acknowledgement.
7642
+ `current()` returns the latest renderer snapshot, initially `null`. The existing
7643
+ Core error serializer represents owner `Error` fields at the JSON boundary;
7644
+ other fields remain unchanged.
7645
+
7646
+ Caller abort, page retirement, retirement of the exact installed client,
7647
+ transport failure or `close()` retires only observation RPCs/subscriptions.
7648
+ `close()` returns `closed`; it never closes the supplied client or unloads models.
7649
+ Normal cancellation closes cleanly; cancellation before acknowledgement rejects
7650
+ `ready`. Other errors reject public promises and reach `onError`, defaulting to
7651
+ complete console diagnostics. A replacement client requires a new explicit
7652
+ attachment. Keep attachment readiness independent of page rendering. See
7653
+ [renderer ownership and lifecycle](model-observation.md#attach-existing-renderer-owners).
7654
+
7655
+ ## subscribeModelObservation()
7656
+
7657
+ ```text
7658
+ subscribeModelObservation({client,signal,onState,onDiagnostic,onError}={})
7659
+ ```
7660
+
7661
+ Import from `arcane-os/ai/model-observation`. Requires an existing client and
7662
+ `onState`; returns `{ready,closed,current,close}`. `ready` resolves with the first
7663
+ current-state snapshot and `current()` retains the latest snapshot, initially
7664
+ `null`. Providing `onDiagnostic` explicitly selects live complete correlated Core
7665
+ model failure frames; omission selects state only. These are developer
7666
+ diagnostics, not ordinary user status or saved conversation content.
7667
+
7668
+ The observer uses the same observation-only cancellation/error lifecycle as
7669
+ the attachment. Callback failures retire it and reach `onError`; callback
7670
+ promises may await `close()` without becoming its completion barrier. It starts
7671
+ no model, refresh, polling, endpoint discovery, host or reconnect. See
7672
+ [observation records and limits](model-observation.md#read-or-subscribe-from-an-existing-client).
7673
+
7591
7674
  ## createPortableNativeProvider()
7592
7675
 
7593
7676
  ### Overview
@@ -7621,6 +7704,13 @@ published SDK/runtime dependencies and the canonical Core entry/client into a
7621
7704
  fresh output directory. Service factories are imported only when a real native
7622
7705
  host later launches the generated Core entry, never during packaging.
7623
7706
 
7707
+ Explicit `native.launchContext.coreListener:{endpoint}` also composes one passive
7708
+ model observation service after `startCoreHost()` and before listener binding,
7709
+ using that runtime and existing services exposing `localai.status`, `image.status`
7710
+ and `decisions.status`. Missing owners remain `null`; renderer attachment stays
7711
+ application-owned. Ordinary stdio and shared/headless branches gain no model
7712
+ startup or readiness barrier. See [native observation composition](core-native-packaging.md#launch-time-locations).
7713
+
7624
7714
  An optional `appDescriptor.native.window` record carries `width`, `height` and
7625
7715
  `resizable` into `manifest.window`. Dimensions are positive integral logical
7626
7716
  client units; `resizable` is an optional boolean. Omitted fields remain omitted.
@@ -8848,7 +8938,7 @@ borrowed listener implements no `core.host.shutdown` operation; use client
8848
8938
  `close()` for disconnection and the existing runtime's owner for host shutdown.
8849
8939
 
8850
8940
  Each connection owns response/event correlation and request cancellation.
8851
- Handler `requestId` preserves the client ID; `coreRequestId` is the distinct
8941
+ Handler `requestId` and `clientRequestId` preserve the client ID; `coreRequestId` is the distinct
8852
8942
  internal dispatch ID. Parameters, results and event data, including
8853
8943
  `data.requestId`, remain unchanged. Runtime-state snapshots retain internal
8854
8944
  active IDs; no service event is invented or retained for later replay.
@@ -8867,6 +8957,9 @@ try {
8867
8957
  `host` is the application's existing stdio host. Generated native entries
8868
8958
  select this attachment with `native.launchContext.coreListener:{endpoint}`,
8869
8959
  preserving their actual runtime, service set, profile, origin and state root.
8960
+ That generated opt-in registers the passive observation service from existing
8961
+ native model owners before binding; direct `startCoreListener()` itself never
8962
+ registers services. Renderer owners are attached separately by the application.
8870
8963
  Omit `sharedHost`; the two options describe alternative lifetimes. Full
8871
8964
  contract: [existing-runtime listener](core-shared-host.md#attach-to-an-existing-window-owned-core).
8872
8965
 
@@ -8894,6 +8987,11 @@ service-owned. The supplied signal begins the same shutdown. `closed` rejects
8894
8987
  on shutdown failure; `onError` observes transport/background failures, with
8895
8988
  complete console diagnostics by default.
8896
8989
 
8990
+ The owning adapter preserves its existing internal handler `requestId`, equal
8991
+ to `coreRequestId`, and its matching `data.requestId` compatibility mapping.
8992
+ Additive `clientRequestId` exposes the original client ID independently; other
8993
+ parameters, results and event data remain unchanged.
8994
+
8897
8995
  ### Availability and example
8898
8996
 
8899
8997
  Node supports app-selected named pipes on Windows and Unix-domain sockets on
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcane-os",
3
- "version": "0.84.0",
3
+ "version": "0.86.0",
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",
@@ -43,6 +43,7 @@
43
43
  "./ai/core-onnx": "./browser-runtime/ai/core-onnx.mjs",
44
44
  "./ai/core-image": "./browser-runtime/ai/core-image.mjs",
45
45
  "./ai/core-model-assets": "./browser-runtime/ai/core-model-assets.mjs",
46
+ "./ai/model-observation": "./browser-runtime/ai/model-observation.mjs",
46
47
  "./ai/browser-decisions": "./browser-runtime/ai/browser-decisions.mjs",
47
48
  "./ai/tool-text-stream": "./browser-runtime/ai/tool-text-stream.mjs",
48
49
  "./ai/twin-cloud": "./browser-runtime/ai/twin-cloud.mjs",
@@ -75,6 +76,7 @@
75
76
  "./core/image": "./src/core/services/image.mjs",
76
77
  "./core/speech": "./src/core/services/speech.mjs",
77
78
  "./core/model-assets": "./src/core/services/model-assets.mjs",
79
+ "./core/model-observation": "./src/core/services/model-observation.mjs",
78
80
  "./diarization": "./src/diarization/index.mjs",
79
81
  "./diarization/build": "./src/diarization/build.mjs",
80
82
  "./local-ai": "./src/local-ai/install.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;
@@ -137,7 +137,10 @@ export function createCoreRuntimeConnection({
137
137
  try {
138
138
  return await runtime.handle(
139
139
  {...frame, id: record.internalId},
140
- preserveContextRequestId ? {contextRequestId: record.id} : undefined
140
+ {
141
+ contextRequestId: preserveContextRequestId ? record.id : undefined,
142
+ contextClientRequestId: record.id
143
+ }
141
144
  );
142
145
  } finally {
143
146
  releaseRequest();