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.
- package/CHANGELOG.md +41 -0
- package/README.md +1 -1
- package/browser-runtime/ai/model-observation.mjs +315 -0
- package/browser-runtime/core/app-control.mjs +27 -1
- package/docs/reference/README.md +1 -0
- package/docs/reference/cli.md +5 -2
- package/docs/reference/core-native-packaging.md +14 -0
- package/docs/reference/core-runtime.md +20 -11
- package/docs/reference/core-shared-host.md +9 -3
- package/docs/reference/execution-devices.md +6 -0
- package/docs/reference/inventory/package-api.json +49 -7
- package/docs/reference/model-observation.md +220 -0
- package/docs/reference/native-app-control.md +132 -16
- package/docs/reference/sdk-api.md +106 -8
- package/package.json +3 -1
- package/src/cli/app-control.mjs +14 -3
- package/src/core/app-control.mjs +2 -0
- package/src/core/hosts/windows/ArcaneAppControl.cs +2 -1
- package/src/core/hosts/windows/ArcaneWindowControl.cs +216 -31
- package/src/core/runtime-connection.mjs +4 -1
- package/src/core/runtime.mjs +15 -7
- package/src/core/services/model-observation.mjs +231 -0
- package/src/local-ai/execution-devices-windows.ps1 +3 -1
- package/src/native/portable-layout.mjs +16 -0
|
@@ -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})`
|
|
7399
|
-
|
|
7400
|
-
|
|
7401
|
-
|
|
7402
|
-
|
|
7403
|
-
|
|
7404
|
-
|
|
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`
|
|
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.
|
|
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",
|
package/src/cli/app-control.mjs
CHANGED
|
@@ -8,6 +8,8 @@ const HELP_TEXT=`Usage:
|
|
|
8
8
|
arcane app-control inspect --endpoint <pipe-or-socket> [--selector <css-selector> | --request <json-file>]
|
|
9
9
|
arcane app-control capture --endpoint <pipe-or-socket> --output <png-file>
|
|
10
10
|
arcane app-control act --endpoint <pipe-or-socket> --request <json-file>
|
|
11
|
+
arcane app-control key --endpoint <pipe-or-socket> --request <json-file>
|
|
12
|
+
arcane app-control resize --endpoint <pipe-or-socket> --request <json-file>
|
|
11
13
|
|
|
12
14
|
Connects to the selected running native window. Results are complete JSON.
|
|
13
15
|
Capture writes the PNG to --output and prints its metadata. Request and output
|
|
@@ -16,6 +18,9 @@ an application or change its profile. --output selects the capture filename;
|
|
|
16
18
|
the other Arcane commands retain their existing output-format option.
|
|
17
19
|
Inspect --request reads the complete parameter object, including shadowPath and
|
|
18
20
|
optional documentGeneration. It cannot be combined with --selector.
|
|
21
|
+
Key --request selects Tab (optionally shiftKey:true), Enter, or Space and the
|
|
22
|
+
observed documentGeneration. Resize --request supplies native client width and
|
|
23
|
+
height for a Normal window; its result includes the previous dimensions.
|
|
19
24
|
`;
|
|
20
25
|
|
|
21
26
|
function usage(message) {
|
|
@@ -29,9 +34,11 @@ function parseArguments(argv) {
|
|
|
29
34
|
status: ['endpoint'],
|
|
30
35
|
inspect: ['endpoint', 'selector', 'request'],
|
|
31
36
|
capture: ['endpoint', 'output'],
|
|
32
|
-
act: ['endpoint', 'request']
|
|
37
|
+
act: ['endpoint', 'request'],
|
|
38
|
+
key: ['endpoint', 'request'],
|
|
39
|
+
resize: ['endpoint', 'request']
|
|
33
40
|
}[action];
|
|
34
|
-
if (!Array.isArray(allowed)) usage('Expected app-control status, inspect, capture, or
|
|
41
|
+
if (!Array.isArray(allowed)) usage('Expected app-control status, inspect, capture, act, key, or resize.');
|
|
35
42
|
const values = {};
|
|
36
43
|
for (let index = 1; index < argv.length; index += 1) {
|
|
37
44
|
const argument = argv[index];
|
|
@@ -48,7 +55,9 @@ function parseArguments(argv) {
|
|
|
48
55
|
}
|
|
49
56
|
if (!values.endpoint) usage('App control requires --endpoint <pipe-or-socket>.');
|
|
50
57
|
if (action === 'capture' && !values.output) usage('App-control capture requires --output <png-file>.');
|
|
51
|
-
if (
|
|
58
|
+
if (['act', 'key', 'resize'].includes(action) && !values.request) {
|
|
59
|
+
usage(`App-control ${action} requires --request <json-file>.`);
|
|
60
|
+
}
|
|
52
61
|
if (action === 'inspect' && values.request !== undefined && values.selector !== undefined) {
|
|
53
62
|
usage('App-control inspect accepts --selector or --request, not both.');
|
|
54
63
|
}
|
|
@@ -83,6 +92,8 @@ export async function runAppControlCli(argv, {cwd = process.cwd(), stdout = proc
|
|
|
83
92
|
if (selection.action === 'status') result = await connection.status({signal});
|
|
84
93
|
else if (selection.action === 'inspect') result = await connection.inspect(parameters, {signal});
|
|
85
94
|
else if (selection.action === 'act') result = await connection.act(parameters, {signal});
|
|
95
|
+
else if (selection.action === 'key') result = await connection.key(parameters, {signal});
|
|
96
|
+
else if (selection.action === 'resize') result = await connection.resize(parameters, {signal});
|
|
86
97
|
else {
|
|
87
98
|
result = await connection.capture(parameters, {signal});
|
|
88
99
|
const output = path.resolve(cwd, selection.output);
|
package/src/core/app-control.mjs
CHANGED
|
@@ -18,6 +18,8 @@ export async function connectAppControl({endpoint, signal, onError = reportAppCo
|
|
|
18
18
|
inspect(parameters = {}, options) { return client.invoke('app.control.inspect', parameters, options); },
|
|
19
19
|
capture(parameters = {}, options) { return client.invoke('app.control.capture', parameters, options); },
|
|
20
20
|
act(parameters, options) { return client.invoke('app.control.act', parameters, options); },
|
|
21
|
+
key(parameters, options) { return client.invoke('app.control.key', parameters, options); },
|
|
22
|
+
resize(parameters, options) { return client.invoke('app.control.resize', parameters, options); },
|
|
21
23
|
close() { return connection.close(); }
|
|
22
24
|
};
|
|
23
25
|
}
|
|
@@ -453,7 +453,8 @@ namespace Arcane.Core.Hosts.Windows
|
|
|
453
453
|
{
|
|
454
454
|
cancellation.Token.ThrowIfCancellationRequested();
|
|
455
455
|
if (method != "app.control.status" && method != "app.control.inspect"
|
|
456
|
-
&& method != "app.control.capture" && method != "app.control.act"
|
|
456
|
+
&& method != "app.control.capture" && method != "app.control.act"
|
|
457
|
+
&& method != "app.control.key" && method != "app.control.resize")
|
|
457
458
|
throw new NotSupportedException("The app does not expose " + method + ".");
|
|
458
459
|
Dictionary<string, object> arguments = parameters as Dictionary<string, object>;
|
|
459
460
|
if (arguments == null)
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
using System;
|
|
2
2
|
using System.Collections.Generic;
|
|
3
|
+
using System.Drawing;
|
|
3
4
|
using System.IO;
|
|
4
5
|
using System.Threading;
|
|
5
6
|
using System.Threading.Tasks;
|
|
7
|
+
using System.Windows.Forms;
|
|
6
8
|
using Microsoft.Web.WebView2.Core;
|
|
7
9
|
|
|
8
10
|
namespace Arcane.Core.Hosts.Windows
|
|
@@ -13,6 +15,7 @@ namespace Arcane.Core.Hosts.Windows
|
|
|
13
15
|
{
|
|
14
16
|
private const string AppControlDocumentKey = "Symbol.for('arcane-os.app-control.document')";
|
|
15
17
|
private readonly HashSet<Task> appControlDocumentTasks = new HashSet<Task>();
|
|
18
|
+
private readonly SemaphoreSlim appControlKeyOwner = new SemaphoreSlim(1, 1);
|
|
16
19
|
private ArcaneAppControl appControl;
|
|
17
20
|
private Task appControlClosing;
|
|
18
21
|
private string appControlScript;
|
|
@@ -59,6 +62,7 @@ namespace Arcane.Core.Hosts.Windows
|
|
|
59
62
|
Task documents = Task.WhenAll(appControlDocumentTasks);
|
|
60
63
|
try { await documents; }
|
|
61
64
|
catch (Exception error) { ReportTaskFailure(documents, error); }
|
|
65
|
+
appControlKeyOwner.Dispose();
|
|
62
66
|
}
|
|
63
67
|
|
|
64
68
|
private void AppControlNavigationStarting(object sender, CoreWebView2NavigationStartingEventArgs args)
|
|
@@ -135,10 +139,17 @@ namespace Arcane.Core.Hosts.Windows
|
|
|
135
139
|
{ "platform", "windows" }, { "host", "webview2" },
|
|
136
140
|
{ "url", webView.Source == null ? null : webView.Source.AbsoluteUri },
|
|
137
141
|
{ "documentGeneration", appControlGeneration }, { "ready", AppControlDocumentReady() },
|
|
138
|
-
{ "window",
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
+
{ "window", AppControlWindow() },
|
|
143
|
+
{ "operations", new string[] { "status", "inspect", "capture", "act", "key", "resize" } }
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private Dictionary<string, object> AppControlWindow()
|
|
148
|
+
{
|
|
149
|
+
return new Dictionary<string, object>
|
|
150
|
+
{
|
|
151
|
+
{ "title", Text }, { "state", WindowState.ToString() },
|
|
152
|
+
{ "width", ClientSize.Width }, { "height", ClientSize.Height }
|
|
142
153
|
};
|
|
143
154
|
}
|
|
144
155
|
|
|
@@ -147,14 +158,16 @@ namespace Arcane.Core.Hosts.Windows
|
|
|
147
158
|
{
|
|
148
159
|
cancellation.ThrowIfCancellationRequested();
|
|
149
160
|
if (method == "app.control.status") return AppControlStatus();
|
|
150
|
-
if (method != "app.control.inspect" && method != "app.control.capture" && method != "app.control.act"
|
|
161
|
+
if (method != "app.control.inspect" && method != "app.control.capture" && method != "app.control.act"
|
|
162
|
+
&& method != "app.control.key" && method != "app.control.resize")
|
|
151
163
|
throw new ArgumentException("Unknown app-control method: " + method);
|
|
164
|
+
if (method == "app.control.resize") RequireAppControlNormalWindow();
|
|
152
165
|
if (!AppControlDocumentReady()) throw new InvalidOperationException("The app document is not ready for control.");
|
|
153
166
|
long selectedGeneration = appControlGeneration;
|
|
154
167
|
string selectedDocument = appControlDocument;
|
|
155
168
|
string selectedUrl = webView.Source == null ? null : webView.Source.AbsoluteUri;
|
|
156
169
|
object expected;
|
|
157
|
-
if (method == "app.control.act" && !parameters.ContainsKey("documentGeneration"))
|
|
170
|
+
if ((method == "app.control.act" || method == "app.control.key") && !parameters.ContainsKey("documentGeneration"))
|
|
158
171
|
throw new ArgumentException("An action requires the documentGeneration returned by status or inspect.");
|
|
159
172
|
if (parameters.TryGetValue("documentGeneration", out expected)
|
|
160
173
|
&& (!(expected is int || expected is long || expected is decimal || expected is double)
|
|
@@ -173,34 +186,14 @@ namespace Arcane.Core.Hosts.Windows
|
|
|
173
186
|
};
|
|
174
187
|
}
|
|
175
188
|
}
|
|
189
|
+
else if (method == "app.control.key")
|
|
190
|
+
result = await AppControlKeyAsync(parameters, cancellation, selectedGeneration, selectedDocument);
|
|
191
|
+
else if (method == "app.control.resize")
|
|
192
|
+
result = await AppControlResizeAsync(parameters, selectedGeneration, selectedDocument);
|
|
176
193
|
else
|
|
177
194
|
{
|
|
178
195
|
string operation = method == "app.control.inspect" ? "inspect" : "act";
|
|
179
|
-
|
|
180
|
-
// Native generation checks alone cannot stop a queued script
|
|
181
|
-
// from reaching a successor document during navigation.
|
|
182
|
-
string script = "(function(expected,operation,parameters){"
|
|
183
|
-
+ "if(globalThis[" + AppControlDocumentKey + "]!==expected)"
|
|
184
|
-
+ "return {ok:false,error:{code:'APP_CONTROL_DOCUMENT_REPLACED',message:'The selected app document has been replaced.'}};"
|
|
185
|
-
+ "return (" + options.AppControlSource + ")(operation,parameters);})("
|
|
186
|
-
+ ArcaneHost.Serializer().Serialize(selectedDocument) + ","
|
|
187
|
-
+ ArcaneHost.Serializer().Serialize(operation) + ","
|
|
188
|
-
+ ArcaneHost.Serializer().Serialize(parameters) + ")";
|
|
189
|
-
string json = await webView.CoreWebView2.ExecuteScriptAsync(script);
|
|
190
|
-
Dictionary<string, object> envelope = ArcaneHost.Serializer().DeserializeObject(json) as Dictionary<string, object>;
|
|
191
|
-
object ok;
|
|
192
|
-
if (envelope == null || !envelope.TryGetValue("ok", out ok) || !(ok is bool))
|
|
193
|
-
throw new InvalidDataException("The document returned no app-control result.");
|
|
194
|
-
if (!(bool)ok)
|
|
195
|
-
{
|
|
196
|
-
object detail;
|
|
197
|
-
envelope.TryGetValue("error", out detail);
|
|
198
|
-
IOException error = new IOException("The document app-control operation failed.");
|
|
199
|
-
error.Data["documentError"] = detail;
|
|
200
|
-
throw error;
|
|
201
|
-
}
|
|
202
|
-
if (!envelope.TryGetValue("result", out result))
|
|
203
|
-
throw new InvalidDataException("The document omitted the app-control result.");
|
|
196
|
+
result = await ReadAppControlDocumentAsync(operation, parameters, selectedDocument);
|
|
204
197
|
}
|
|
205
198
|
// Completion is observed after dispatch even if a caller cancelled.
|
|
206
199
|
// A document change is reported with the actual returned result; it
|
|
@@ -220,6 +213,198 @@ namespace Arcane.Core.Hosts.Windows
|
|
|
220
213
|
return record;
|
|
221
214
|
}
|
|
222
215
|
|
|
216
|
+
private async Task<Dictionary<string, object>> ReadAppControlDocumentAsync(string operation,
|
|
217
|
+
Dictionary<string, object> parameters, string selectedDocument)
|
|
218
|
+
{
|
|
219
|
+
// The creation marker is checked inside the executing document.
|
|
220
|
+
// Native generation checks alone cannot stop a queued script
|
|
221
|
+
// from reaching a successor document during navigation.
|
|
222
|
+
string script = "(function(expected,operation,parameters){"
|
|
223
|
+
+ "if(globalThis[" + AppControlDocumentKey + "]!==expected)"
|
|
224
|
+
+ "return {ok:false,error:{code:'APP_CONTROL_DOCUMENT_REPLACED',message:'The selected app document has been replaced.'}};"
|
|
225
|
+
+ "return (" + options.AppControlSource + ")(operation,parameters);})("
|
|
226
|
+
+ ArcaneHost.Serializer().Serialize(selectedDocument) + ","
|
|
227
|
+
+ ArcaneHost.Serializer().Serialize(operation) + ","
|
|
228
|
+
+ ArcaneHost.Serializer().Serialize(parameters) + ")";
|
|
229
|
+
string json = await webView.CoreWebView2.ExecuteScriptAsync(script);
|
|
230
|
+
Dictionary<string, object> envelope = ArcaneHost.Serializer().DeserializeObject(json) as Dictionary<string, object>;
|
|
231
|
+
object ok;
|
|
232
|
+
if (envelope == null || !envelope.TryGetValue("ok", out ok) || !(ok is bool))
|
|
233
|
+
throw new InvalidDataException("The document returned no app-control result.");
|
|
234
|
+
if (!(bool)ok)
|
|
235
|
+
{
|
|
236
|
+
object detail;
|
|
237
|
+
envelope.TryGetValue("error", out detail);
|
|
238
|
+
IOException error = new IOException("The document app-control operation failed.");
|
|
239
|
+
error.Data["documentError"] = detail;
|
|
240
|
+
throw error;
|
|
241
|
+
}
|
|
242
|
+
object result;
|
|
243
|
+
if (!envelope.TryGetValue("result", out result))
|
|
244
|
+
throw new InvalidDataException("The document omitted the app-control result.");
|
|
245
|
+
Dictionary<string, object> record = result as Dictionary<string, object>;
|
|
246
|
+
if (record == null) throw new InvalidDataException("The app-control result must be an object.");
|
|
247
|
+
return record;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
private async Task<object> AppControlKeyAsync(Dictionary<string, object> parameters,
|
|
251
|
+
CancellationToken cancellation, long selectedGeneration, string selectedDocument)
|
|
252
|
+
{
|
|
253
|
+
object value;
|
|
254
|
+
string key = parameters.TryGetValue("key", out value) ? value as string : null;
|
|
255
|
+
if (key != "Tab" && key != "Enter" && key != "Space")
|
|
256
|
+
throw new ArgumentException("key must be Tab, Enter, or Space.");
|
|
257
|
+
bool shiftKey = false;
|
|
258
|
+
if (parameters.TryGetValue("shiftKey", out value))
|
|
259
|
+
{
|
|
260
|
+
if (!(value is bool)) throw new ArgumentException("shiftKey must be a boolean.");
|
|
261
|
+
shiftKey = (bool)value;
|
|
262
|
+
}
|
|
263
|
+
if (shiftKey && key != "Tab") throw new ArgumentException("shiftKey is supported only with Tab.");
|
|
264
|
+
Dictionary<string, object> down = new Dictionary<string, object>
|
|
265
|
+
{
|
|
266
|
+
{ "type", key == "Tab" ? "rawKeyDown" : "keyDown" },
|
|
267
|
+
{ "key", key == "Space" ? " " : key }, { "code", key },
|
|
268
|
+
{ "windowsVirtualKeyCode", key == "Tab" ? 9 : key == "Enter" ? 13 : 32 },
|
|
269
|
+
{ "modifiers", shiftKey ? 8 : 0 }
|
|
270
|
+
};
|
|
271
|
+
Dictionary<string, object> up = new Dictionary<string, object>(down);
|
|
272
|
+
up["type"] = "keyUp";
|
|
273
|
+
if (key != "Tab")
|
|
274
|
+
{
|
|
275
|
+
down["text"] = key == "Enter" ? "\r" : " ";
|
|
276
|
+
down["unmodifiedText"] = down["text"];
|
|
277
|
+
}
|
|
278
|
+
Dictionary<string, object> press = new Dictionary<string, object>
|
|
279
|
+
{ { "parameters", down }, { "attempted", false }, { "completed", false } };
|
|
280
|
+
Dictionary<string, object> release = new Dictionary<string, object>
|
|
281
|
+
{ { "parameters", up }, { "attempted", false }, { "completed", false } };
|
|
282
|
+
Dictionary<string, object> result = new Dictionary<string, object>
|
|
283
|
+
{
|
|
284
|
+
{ "key", key }, { "shiftKey", shiftKey }, { "press", press }, { "release", release },
|
|
285
|
+
{ "documentGeneration", selectedGeneration },
|
|
286
|
+
{ "url", webView.Source == null ? null : webView.Source.AbsoluteUri }
|
|
287
|
+
};
|
|
288
|
+
// Only keyboard pairs share this owner: an awaited down must not
|
|
289
|
+
// allow another accepted key to interleave before its owned up.
|
|
290
|
+
await appControlKeyOwner.WaitAsync(cancellation);
|
|
291
|
+
try
|
|
292
|
+
{
|
|
293
|
+
cancellation.ThrowIfCancellationRequested();
|
|
294
|
+
RequireAppControlDocument(selectedGeneration, selectedDocument);
|
|
295
|
+
result["previous"] = await ReadAppControlDocumentAsync("view", new Dictionary<string, object>(), selectedDocument);
|
|
296
|
+
cancellation.ThrowIfCancellationRequested();
|
|
297
|
+
RequireAppControlDocument(selectedGeneration, selectedDocument);
|
|
298
|
+
CoreWebView2 browser = webView.CoreWebView2;
|
|
299
|
+
string downJson = ArcaneHost.Serializer().Serialize(down);
|
|
300
|
+
string upJson = ArcaneHost.Serializer().Serialize(up);
|
|
301
|
+
Exception pressError = null;
|
|
302
|
+
Exception releaseError = null;
|
|
303
|
+
press["attempted"] = true;
|
|
304
|
+
try
|
|
305
|
+
{
|
|
306
|
+
press["response"] = await browser.CallDevToolsProtocolMethodAsync("Input.dispatchKeyEvent", downJson);
|
|
307
|
+
press["completed"] = true;
|
|
308
|
+
}
|
|
309
|
+
catch (Exception error) { pressError = error; }
|
|
310
|
+
// C# 5 cannot await in finally. This unconditional phase follows
|
|
311
|
+
// the observed down task, including failure/cancellation/navigation.
|
|
312
|
+
// Release can itself activate Space; it cannot undo the press.
|
|
313
|
+
release["attempted"] = true;
|
|
314
|
+
try
|
|
315
|
+
{
|
|
316
|
+
release["response"] = await browser.CallDevToolsProtocolMethodAsync("Input.dispatchKeyEvent", upJson);
|
|
317
|
+
release["completed"] = true;
|
|
318
|
+
}
|
|
319
|
+
catch (Exception error) { releaseError = error; }
|
|
320
|
+
try
|
|
321
|
+
{
|
|
322
|
+
if (pressError != null) press["error"] = ArcaneHost.ErrorRecord(pressError, "ARCANE_APP_CONTROL_FAILED");
|
|
323
|
+
if (releaseError != null) release["error"] = ArcaneHost.ErrorRecord(releaseError, "ARCANE_APP_CONTROL_FAILED");
|
|
324
|
+
if (pressError != null && releaseError != null)
|
|
325
|
+
throw new AggregateException("The key press and release failed.", pressError, releaseError);
|
|
326
|
+
if (pressError != null) throw new IOException("The key press failed.", pressError);
|
|
327
|
+
if (releaseError != null) throw new IOException("The key release failed.", releaseError);
|
|
328
|
+
RequireAppControlDocument(selectedGeneration, selectedDocument);
|
|
329
|
+
result["actual"] = await ReadAppControlDocumentAsync("view", new Dictionary<string, object>(), selectedDocument);
|
|
330
|
+
return result;
|
|
331
|
+
}
|
|
332
|
+
catch (Exception error)
|
|
333
|
+
{
|
|
334
|
+
throw AppControlOutcomeError("The app key operation did not complete normally.", error, result);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
finally { appControlKeyOwner.Release(); }
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
private void RequireAppControlDocument(long selectedGeneration, string selectedDocument)
|
|
341
|
+
{
|
|
342
|
+
if (!AppControlDocumentReady() || appControlGeneration != selectedGeneration || appControlDocument != selectedDocument)
|
|
343
|
+
throw new InvalidOperationException("The selected app document has been replaced or is no longer ready.");
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
private void RequireAppControlNormalWindow()
|
|
347
|
+
{
|
|
348
|
+
if (WindowState == FormWindowState.Normal) return;
|
|
349
|
+
InvalidOperationException error = new InvalidOperationException("App-control resize requires a Normal window.");
|
|
350
|
+
error.Data["reason"] = "unsupported-window-state";
|
|
351
|
+
error.Data["supportedWindowState"] = "Normal";
|
|
352
|
+
error.Data["window"] = AppControlWindow();
|
|
353
|
+
throw error;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
private static int AppControlDimension(Dictionary<string, object> parameters, string name)
|
|
357
|
+
{
|
|
358
|
+
object value;
|
|
359
|
+
if (!parameters.TryGetValue(name, out value)
|
|
360
|
+
|| !(value is int || value is long || value is decimal || value is double))
|
|
361
|
+
throw new ArgumentException(name + " must be a positive integral native client dimension.");
|
|
362
|
+
double dimension = Convert.ToDouble(value);
|
|
363
|
+
if (Double.IsNaN(dimension) || dimension < 1 || dimension > Int32.MaxValue || dimension != Math.Truncate(dimension)
|
|
364
|
+
|| (value is decimal && (decimal)value != Decimal.Truncate((decimal)value)))
|
|
365
|
+
throw new ArgumentException(name + " must be a positive integral native client dimension.");
|
|
366
|
+
return (int)dimension;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
private async Task<object> AppControlResizeAsync(Dictionary<string, object> parameters,
|
|
370
|
+
long selectedGeneration, string selectedDocument)
|
|
371
|
+
{
|
|
372
|
+
int width = AppControlDimension(parameters, "width");
|
|
373
|
+
int height = AppControlDimension(parameters, "height");
|
|
374
|
+
// This check and immediate previous snapshot run in the same UI turn
|
|
375
|
+
// as the setter. Never normalize a maximized/minimized window.
|
|
376
|
+
RequireAppControlNormalWindow();
|
|
377
|
+
Dictionary<string, object> result = new Dictionary<string, object>
|
|
378
|
+
{
|
|
379
|
+
{ "requested", new Dictionary<string, object> { { "width", width }, { "height", height } } },
|
|
380
|
+
{ "previous", AppControlWindow() }, { "resizeAttempted", false }, { "resizeCompleted", false },
|
|
381
|
+
{ "documentGeneration", selectedGeneration },
|
|
382
|
+
{ "url", webView.Source == null ? null : webView.Source.AbsoluteUri }
|
|
383
|
+
};
|
|
384
|
+
try
|
|
385
|
+
{
|
|
386
|
+
result["resizeAttempted"] = true;
|
|
387
|
+
ClientSize = new Size(width, height);
|
|
388
|
+
result["resizeCompleted"] = true;
|
|
389
|
+
result["actual"] = AppControlWindow();
|
|
390
|
+
Dictionary<string, object> view = await ReadAppControlDocumentAsync("view", new Dictionary<string, object>(), selectedDocument);
|
|
391
|
+
result["viewport"] = view["viewport"];
|
|
392
|
+
return result;
|
|
393
|
+
}
|
|
394
|
+
catch (Exception error)
|
|
395
|
+
{
|
|
396
|
+
if (!result.ContainsKey("actual")) result["actual"] = AppControlWindow();
|
|
397
|
+
throw AppControlOutcomeError("The app resize operation did not complete normally.", error, result);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
private static IOException AppControlOutcomeError(string message, Exception cause, Dictionary<string, object> result)
|
|
402
|
+
{
|
|
403
|
+
IOException error = new IOException(message, cause);
|
|
404
|
+
error.Data["result"] = result;
|
|
405
|
+
return error;
|
|
406
|
+
}
|
|
407
|
+
|
|
223
408
|
private sealed class AppControlRequest
|
|
224
409
|
{
|
|
225
410
|
private readonly ArcaneHostForm window;
|
|
@@ -137,7 +137,10 @@ export function createCoreRuntimeConnection({
|
|
|
137
137
|
try {
|
|
138
138
|
return await runtime.handle(
|
|
139
139
|
{...frame, id: record.internalId},
|
|
140
|
-
|
|
140
|
+
{
|
|
141
|
+
contextRequestId: preserveContextRequestId ? record.id : undefined,
|
|
142
|
+
contextClientRequestId: record.id
|
|
143
|
+
}
|
|
141
144
|
);
|
|
142
145
|
} finally {
|
|
143
146
|
releaseRequest();
|