arcane-os 0.81.0 → 0.83.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 +33 -0
- package/README.md +6 -1
- package/browser-runtime/core/app-control.mjs +488 -0
- package/docs/reference/cli.md +12 -1
- package/docs/reference/core-native-packaging.md +5 -0
- package/docs/reference/inventory/package-api.json +16 -2
- package/docs/reference/inventory/runtime-components.json +2 -1
- package/docs/reference/native-app-control.md +206 -0
- package/docs/reference/runtime-components.md +20 -0
- package/docs/reference/sdk-api.md +30 -0
- package/package.json +2 -1
- package/runtime/arcane/components/chat.html +140 -18
- package/src/cli/app-control.mjs +111 -0
- package/src/cli/main.mjs +5 -0
- package/src/core/app-control.mjs +23 -0
- package/src/core/hosts/windows/ArcaneAppControl.cs +525 -0
- package/src/core/hosts/windows/ArcaneCoreProcess.cs +2 -63
- package/src/core/hosts/windows/ArcaneFrameTransport.cs +80 -0
- package/src/core/hosts/windows/ArcaneHost.cs +25 -3
- package/src/core/hosts/windows/ArcaneLauncher.cs +5 -0
- package/src/core/hosts/windows/ArcaneWindowControl.cs +248 -0
- package/src/core/shared-host.mjs +7 -91
- package/src/core/socket-connection.mjs +109 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.83.0
|
|
4
|
+
|
|
5
|
+
- Add optional `shadowPath` to native app-control inspection and actions. Follow
|
|
6
|
+
an explicit ordered path through open shadow hosts, then inspect complete
|
|
7
|
+
content or operate a selector within that root. Nested host records expose
|
|
8
|
+
their full paths; omitted or empty paths preserve existing light-DOM calls.
|
|
9
|
+
- Accept a complete inspection JSON object through `arcane app-control inspect
|
|
10
|
+
--request`, including shadow paths and document generation. Existing selector,
|
|
11
|
+
action, capture and connection lifecycle behavior remains unchanged.
|
|
12
|
+
- Publish the matching Windows helper with the native host archive. Closed
|
|
13
|
+
shadow roots and frame documents remain outside this capability. Preserve
|
|
14
|
+
Core, profiles, full payloads and existing dependencies. Package verification
|
|
15
|
+
is separate from actual built-application acceptance.
|
|
16
|
+
|
|
17
|
+
## 0.82.0
|
|
18
|
+
|
|
19
|
+
- Add `connectAppControl` from `arcane-os/core/app-control` and the
|
|
20
|
+
`arcane app-control` CLI for an explicitly selected running Windows WebView2
|
|
21
|
+
application. Inspect complete document content and live controls, capture the
|
|
22
|
+
rendered viewport, and perform targeted click, fill, select and scroll actions
|
|
23
|
+
without activating the desktop window or injecting global input. Preserve the
|
|
24
|
+
application's existing Core, profile and lifecycle; closing the client closes
|
|
25
|
+
only its connection. The matching Windows host archive includes the generated
|
|
26
|
+
DOM operation source. Other native host adapters remain unavailable.
|
|
27
|
+
- Render stored `arcane-media:` images in shared Chat through MarkdownMedia,
|
|
28
|
+
including restored history, live chunks and final responses. Preserve complete
|
|
29
|
+
original Markdown and immediate text rendering. Release obsolete display URLs
|
|
30
|
+
on replacement, removal and destruction; retain requested print snapshots
|
|
31
|
+
through `afterprint` and await current images for explicit printing.
|
|
32
|
+
- Keep dependencies, saved-data formats, model and speech routing unchanged.
|
|
33
|
+
Package verification and host compilation do not establish actual built-app,
|
|
34
|
+
Core/profile, covered-window capture or model behavior.
|
|
35
|
+
|
|
3
36
|
## 0.81.0
|
|
4
37
|
|
|
5
38
|
- Add per-activation native decision `family`, `model`, `revision`, and `dtype`
|
package/README.md
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
# Arcane OS SDK
|
|
4
4
|
|
|
5
|
+
[](https://github.com/TheWizardNexus/arcane-os-sdk/actions/workflows/check.yml?query=branch%3Amain)
|
|
6
|
+
[](https://www.npmjs.com/package/arcane-os)
|
|
7
|
+
[](https://www.npmjs.com/package/arcane-os)
|
|
8
|
+
[](LICENSE)
|
|
9
|
+
|
|
5
10
|
<p align="center">
|
|
6
11
|
<strong>Build, test, package, and manage Arcane applications inside or outside Arcane OS.</strong><br>
|
|
7
12
|
Keep proprietary source in its own repository while using the same headless workflow for apps and shared runtime work in the Arcane checkout.
|
|
@@ -20,7 +25,7 @@ selected SDK projection or an explicit live SDK source mount. Both profiles
|
|
|
20
25
|
share the theme, packaging, event, cancellation,
|
|
21
26
|
and browser run contracts while retaining their selected application layout.
|
|
22
27
|
|
|
23
|
-
This checkout defines the `0.
|
|
28
|
+
This checkout defines the `0.83.0` SDK contract. Development applications declare
|
|
24
29
|
`arcane-os` as `latest` and retain their lockfile. Exact SDK pins begin when Roshi
|
|
25
30
|
designates a production build. Follow [Consumer updates](docs/consumer-updates.md)
|
|
26
31
|
to refresh and compare the installed SDK and lockfile with the published channel;
|
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runs inside the existing app document. The native host serializes this named
|
|
3
|
+
* function with toString(), so all helpers must remain inside its lexical scope.
|
|
4
|
+
*
|
|
5
|
+
* inspect({selector?,shadowPath?}) reads the top-level light DOM or the open
|
|
6
|
+
* shadow root selected by an ordered array of unique host selectors. Omission
|
|
7
|
+
* of selector selects that whole root; a CSS selector may select any number of
|
|
8
|
+
* elements within it. HTML, textContent, attributes and live control state are
|
|
9
|
+
* returned completely. Frame documents and unselected shadow-root contents stay
|
|
10
|
+
* outside this scope. Closed shadow roots cannot be discovered or entered.
|
|
11
|
+
* domRole/domName are DOM-derived hints, not an accessibility-tree computation.
|
|
12
|
+
*
|
|
13
|
+
* act({action,selector,shadowPath?,...}) requires one current CSS match in the
|
|
14
|
+
* selected root. fill uses an exact
|
|
15
|
+
* string value; select uses either value:string or values:string[]. scroll uses
|
|
16
|
+
* absolute left/top CSS-pixel offsets, preserving any omitted axis. Actions use
|
|
17
|
+
* DOM APIs and untrusted events without focus or desktop input. Results describe
|
|
18
|
+
* immediate DOM state, including any browser value sanitization or scroll
|
|
19
|
+
* clamping, not completion of asynchronous application work.
|
|
20
|
+
*
|
|
21
|
+
* The native host owns document-generation checks, scheduling and cancellation.
|
|
22
|
+
* This function retains no state, retries no action and returns a JSON envelope
|
|
23
|
+
* because ExecuteScriptAsync otherwise loses uncaught JavaScript exceptions.
|
|
24
|
+
*/
|
|
25
|
+
export function runAppControl(operation, params = {}) {
|
|
26
|
+
let actionAttempted = false;
|
|
27
|
+
const eventsDispatched = [];
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
if (params === null || typeof params !== 'object' || Array.isArray(params)) {
|
|
31
|
+
fail('INVALID_ARGUMENT', 'App-control parameters must be an object.');
|
|
32
|
+
}
|
|
33
|
+
if (operation === 'inspect') {
|
|
34
|
+
return {ok: true, result: inspectDocument()};
|
|
35
|
+
}
|
|
36
|
+
if (operation === 'act') {
|
|
37
|
+
return {ok: true, result: actOnElement()};
|
|
38
|
+
}
|
|
39
|
+
fail('INVALID_ARGUMENT', 'Expected the inspect or act operation.');
|
|
40
|
+
} catch (error) {
|
|
41
|
+
return {
|
|
42
|
+
ok: false,
|
|
43
|
+
error: {
|
|
44
|
+
name: error?.name ?? 'Error',
|
|
45
|
+
message: error?.message ?? String(error),
|
|
46
|
+
stack: error?.stack ?? null,
|
|
47
|
+
code: error?.code ?? 'ARCANE_APP_CONTROL_FAILED',
|
|
48
|
+
details: error?.details ?? null,
|
|
49
|
+
actionAttempted,
|
|
50
|
+
eventsDispatched
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function fail(code, message, details = null) {
|
|
56
|
+
const error = new Error(message);
|
|
57
|
+
error.code = code;
|
|
58
|
+
error.details = details;
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function queryElements(selector, root = document) {
|
|
63
|
+
if (typeof selector !== 'string') {
|
|
64
|
+
fail('INVALID_ARGUMENT', 'selector must be a CSS selector string.');
|
|
65
|
+
}
|
|
66
|
+
return Array.from(root.querySelectorAll(selector));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function selectedRoot() {
|
|
70
|
+
const shadowPath = params.shadowPath === undefined ? [] : params.shadowPath;
|
|
71
|
+
if (!Array.isArray(shadowPath)) {
|
|
72
|
+
fail('INVALID_ARGUMENT', 'shadowPath must be an array of CSS host selectors.');
|
|
73
|
+
}
|
|
74
|
+
let root = document;
|
|
75
|
+
for (let index = 0; index < shadowPath.length; index += 1) {
|
|
76
|
+
const selector = shadowPath[index];
|
|
77
|
+
let hosts;
|
|
78
|
+
try {
|
|
79
|
+
hosts = queryElements(selector, root);
|
|
80
|
+
} catch (error) {
|
|
81
|
+
error.details = {shadowPath, index, selector};
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
if (hosts.length !== 1) {
|
|
85
|
+
fail(
|
|
86
|
+
'ARCANE_APP_CONTROL_TARGET_COUNT',
|
|
87
|
+
'Each shadowPath selector requires exactly one matching host.',
|
|
88
|
+
{shadowPath, index, selector, matches: hosts.length}
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
const shadowRoot = hosts[0].shadowRoot;
|
|
92
|
+
if (!shadowRoot) {
|
|
93
|
+
fail(
|
|
94
|
+
'ARCANE_APP_CONTROL_UNSUPPORTED',
|
|
95
|
+
'The selected host has no accessible open shadow root. Closed and absent roots cannot be distinguished.',
|
|
96
|
+
{shadowPath, index, selector}
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
root = shadowRoot;
|
|
100
|
+
}
|
|
101
|
+
return {root, shadowPath};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function inspectDocument() {
|
|
105
|
+
const {root, shadowPath} = selectedRoot();
|
|
106
|
+
const isDocument = root === document;
|
|
107
|
+
const roots = params.selector === undefined
|
|
108
|
+
? (isDocument ? (document.documentElement ? [document.documentElement] : []) : [root])
|
|
109
|
+
: queryElements(params.selector, root);
|
|
110
|
+
const rootSet = new Set(roots);
|
|
111
|
+
const elements = Array.from(root.querySelectorAll('*'));
|
|
112
|
+
const idCounts = new Map();
|
|
113
|
+
const childCounts = new Map();
|
|
114
|
+
const childIndexes = new Map();
|
|
115
|
+
const scope = new Set();
|
|
116
|
+
const selectors = new Map();
|
|
117
|
+
const controls = [];
|
|
118
|
+
const frames = [];
|
|
119
|
+
const shadowHosts = [];
|
|
120
|
+
|
|
121
|
+
// One traversal establishes selector paths and scope for all records.
|
|
122
|
+
for (const element of elements) {
|
|
123
|
+
if (element.id) {
|
|
124
|
+
idCounts.set(element.id, (idCounts.get(element.id) ?? 0) + 1);
|
|
125
|
+
}
|
|
126
|
+
const parent = element.parentElement;
|
|
127
|
+
const index = (childCounts.get(parent) ?? 0) + 1;
|
|
128
|
+
childCounts.set(parent, index);
|
|
129
|
+
childIndexes.set(element, index);
|
|
130
|
+
if (rootSet.has(element) || rootSet.has(element.parentNode) || scope.has(parent)) {
|
|
131
|
+
scope.add(element);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
for (const element of elements) {
|
|
136
|
+
if (!scope.has(element)) continue;
|
|
137
|
+
if (element.matches('input,textarea,select,button,option,optgroup,fieldset,output,progress,meter,details,summary,a[href],area[href],audio,video,[contenteditable],[role],[tabindex]')) {
|
|
138
|
+
controls.push(describeElement(element, uniqueSelector(element)));
|
|
139
|
+
}
|
|
140
|
+
if (element.localName === 'iframe' || element.localName === 'frame') {
|
|
141
|
+
frames.push(describeElement(element, uniqueSelector(element)));
|
|
142
|
+
}
|
|
143
|
+
if (element.shadowRoot) {
|
|
144
|
+
shadowHosts.push(
|
|
145
|
+
{
|
|
146
|
+
selector: uniqueSelector(element),
|
|
147
|
+
mode: element.shadowRoot.mode,
|
|
148
|
+
shadowPath: [...shadowPath, uniqueSelector(element)]
|
|
149
|
+
}
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
scope: isDocument ? 'top-level-light-dom' : 'open-shadow-root',
|
|
156
|
+
...(isDocument ? {} : {shadowPath}),
|
|
157
|
+
title: document.title,
|
|
158
|
+
readyState: document.readyState,
|
|
159
|
+
doctype: document.doctype
|
|
160
|
+
? new XMLSerializer().serializeToString(document.doctype)
|
|
161
|
+
: null,
|
|
162
|
+
roots: roots.map(describeRoot),
|
|
163
|
+
controls,
|
|
164
|
+
frames,
|
|
165
|
+
shadowHosts,
|
|
166
|
+
activeElement: root.activeElement
|
|
167
|
+
? describeElement(root.activeElement, uniqueSelector(root.activeElement))
|
|
168
|
+
: null,
|
|
169
|
+
scrollingElement: isDocument && document.scrollingElement
|
|
170
|
+
? uniqueSelector(document.scrollingElement)
|
|
171
|
+
: null,
|
|
172
|
+
selection: {text: document.getSelection()?.toString() ?? ''}
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
function describeRoot(element) {
|
|
176
|
+
if (element === root && !isDocument) {
|
|
177
|
+
return {selector: null, html: root.innerHTML, text: root.textContent, state: null};
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
selector: uniqueSelector(element),
|
|
181
|
+
html: element.outerHTML,
|
|
182
|
+
text: element.textContent,
|
|
183
|
+
state: readState(element)
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function uniqueSelector(element) {
|
|
188
|
+
if (!element.isConnected || element.getRootNode() !== root) return null;
|
|
189
|
+
const path = [];
|
|
190
|
+
let current = element;
|
|
191
|
+
let prefix = '';
|
|
192
|
+
while (current) {
|
|
193
|
+
if (selectors.has(current)) {
|
|
194
|
+
prefix = selectors.get(current);
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
if (current.id && !current.id.includes('\0') && idCounts.get(current.id) === 1) {
|
|
198
|
+
prefix = `#${CSS.escape(current.id)}`;
|
|
199
|
+
selectors.set(current, prefix);
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
path.push(current);
|
|
203
|
+
current = current.parentElement;
|
|
204
|
+
}
|
|
205
|
+
for (let index = path.length - 1; index >= 0; index -= 1) {
|
|
206
|
+
const part = path[index];
|
|
207
|
+
const segment = `*:nth-child(${childIndexes.get(part)})`;
|
|
208
|
+
// A ShadowRoot is not an element matched by :root or :scope.
|
|
209
|
+
// The parent exclusion anchors its top-level child in this tree.
|
|
210
|
+
prefix = prefix ? `${prefix} > ${segment}`
|
|
211
|
+
: isDocument ? ':root' : `${segment}:not(* > *)`;
|
|
212
|
+
selectors.set(part, prefix);
|
|
213
|
+
}
|
|
214
|
+
return prefix;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function describeElement(element, selector) {
|
|
219
|
+
const attributeEntries = [];
|
|
220
|
+
for (const attribute of element.attributes) {
|
|
221
|
+
attributeEntries.push(
|
|
222
|
+
[attribute.name, attribute.value]
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
selector,
|
|
227
|
+
tagName: element.localName,
|
|
228
|
+
attributes: Object.fromEntries(attributeEntries),
|
|
229
|
+
domRole: element.getAttribute('role'),
|
|
230
|
+
domName: readDOMName(element),
|
|
231
|
+
state: readState(element)
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function readDOMName(element) {
|
|
236
|
+
const labelledBy = element.getAttribute('aria-labelledby');
|
|
237
|
+
if (labelledBy) {
|
|
238
|
+
const labels = [];
|
|
239
|
+
for (const id of labelledBy.split(/\s+/u)) {
|
|
240
|
+
const label = element.getRootNode().getElementById?.(id);
|
|
241
|
+
if (label) labels.push(label.textContent);
|
|
242
|
+
}
|
|
243
|
+
if (labels.length) return {source: 'aria-labelledby', value: labels.join(' ')};
|
|
244
|
+
}
|
|
245
|
+
if (element.hasAttribute('aria-label')) {
|
|
246
|
+
return {source: 'aria-label', value: element.getAttribute('aria-label')};
|
|
247
|
+
}
|
|
248
|
+
if (element.labels?.length) {
|
|
249
|
+
const labels = [];
|
|
250
|
+
for (const label of element.labels) labels.push(label.textContent);
|
|
251
|
+
return {source: 'labels', value: labels.join(' ')};
|
|
252
|
+
}
|
|
253
|
+
if (element.hasAttribute('alt')) {
|
|
254
|
+
return {source: 'alt', value: element.getAttribute('alt')};
|
|
255
|
+
}
|
|
256
|
+
if (element.localName === 'input' && ['button', 'submit', 'reset'].includes(element.type)) {
|
|
257
|
+
return {source: 'value', value: element.value};
|
|
258
|
+
}
|
|
259
|
+
return {source: 'textContent', value: element.textContent};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function readState(element) {
|
|
263
|
+
const state = {
|
|
264
|
+
connected: element.isConnected,
|
|
265
|
+
contentEditable: element.isContentEditable === true,
|
|
266
|
+
effectivelyDisabled: element.matches(':disabled'),
|
|
267
|
+
scrollLeft: element.scrollLeft,
|
|
268
|
+
scrollTop: element.scrollTop,
|
|
269
|
+
scrollWidth: element.scrollWidth,
|
|
270
|
+
scrollHeight: element.scrollHeight,
|
|
271
|
+
clientWidth: element.clientWidth,
|
|
272
|
+
clientHeight: element.clientHeight
|
|
273
|
+
};
|
|
274
|
+
for (const field of [
|
|
275
|
+
'value', 'type', 'checked', 'indeterminate', 'disabled', 'readOnly',
|
|
276
|
+
'required', 'multiple', 'selected', 'selectedIndex', 'hidden', 'open',
|
|
277
|
+
'tabIndex', 'selectionStart', 'selectionEnd', 'selectionDirection',
|
|
278
|
+
'willValidate', 'validationMessage', 'paused', 'ended', 'currentTime',
|
|
279
|
+
'muted', 'volume', 'playbackRate'
|
|
280
|
+
]) {
|
|
281
|
+
const value = element[field];
|
|
282
|
+
if (value === null || ['string', 'boolean', 'number'].includes(typeof value)) {
|
|
283
|
+
state[field] = value;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (element.isContentEditable) state.editableText = element.textContent;
|
|
287
|
+
if (element.validity) {
|
|
288
|
+
state.validity = {};
|
|
289
|
+
for (const field of [
|
|
290
|
+
'badInput', 'customError', 'patternMismatch', 'rangeOverflow',
|
|
291
|
+
'rangeUnderflow', 'stepMismatch', 'tooLong', 'tooShort',
|
|
292
|
+
'typeMismatch', 'valid', 'valueMissing'
|
|
293
|
+
]) {
|
|
294
|
+
state.validity[field] = element.validity[field];
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (element.localName === 'select') {
|
|
298
|
+
state.options = [];
|
|
299
|
+
for (const option of element.options) {
|
|
300
|
+
state.options.push(
|
|
301
|
+
{
|
|
302
|
+
value: option.value,
|
|
303
|
+
text: option.text,
|
|
304
|
+
label: option.label,
|
|
305
|
+
selected: option.selected,
|
|
306
|
+
disabled: option.disabled,
|
|
307
|
+
effectivelyDisabled: option.matches(':disabled')
|
|
308
|
+
}
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (element.localName === 'input' && element.type === 'file') {
|
|
313
|
+
state.files = [];
|
|
314
|
+
for (const file of element.files) {
|
|
315
|
+
state.files.push(
|
|
316
|
+
{name: file.name, type: file.type, lastModified: file.lastModified}
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return state;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function actOnElement() {
|
|
324
|
+
const {root, shadowPath} = selectedRoot();
|
|
325
|
+
const targets = queryElements(params.selector, root);
|
|
326
|
+
if (targets.length !== 1) {
|
|
327
|
+
fail(
|
|
328
|
+
'ARCANE_APP_CONTROL_TARGET_COUNT',
|
|
329
|
+
'An action requires exactly one matching element.',
|
|
330
|
+
{matches: targets.length}
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
const target = targets[0];
|
|
334
|
+
const action = params.action;
|
|
335
|
+
if (!['click', 'fill', 'select', 'scroll'].includes(action)) {
|
|
336
|
+
fail('INVALID_ARGUMENT', 'Expected a click, fill, select or scroll action.');
|
|
337
|
+
}
|
|
338
|
+
if (action !== 'scroll' && target.matches(':disabled')) {
|
|
339
|
+
fail('ARCANE_APP_CONTROL_DISABLED', 'The selected control is disabled.');
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
let method;
|
|
343
|
+
if (action === 'click') {
|
|
344
|
+
if (typeof target.click !== 'function') {
|
|
345
|
+
fail('ARCANE_APP_CONTROL_UNSUPPORTED', 'The selected element does not provide a DOM click method.');
|
|
346
|
+
}
|
|
347
|
+
actionAttempted = true;
|
|
348
|
+
target.click();
|
|
349
|
+
method = 'click';
|
|
350
|
+
} else if (action === 'fill') {
|
|
351
|
+
fillElement(target);
|
|
352
|
+
method = 'fill';
|
|
353
|
+
} else if (action === 'select') {
|
|
354
|
+
selectOptions(target);
|
|
355
|
+
method = 'select';
|
|
356
|
+
} else {
|
|
357
|
+
if (params.left === undefined && params.top === undefined) {
|
|
358
|
+
fail('INVALID_ARGUMENT', 'scroll requires left, top or both absolute offsets.');
|
|
359
|
+
}
|
|
360
|
+
for (const field of ['left', 'top']) {
|
|
361
|
+
if (params[field] !== undefined && (typeof params[field] !== 'number' || !Number.isFinite(params[field]))) {
|
|
362
|
+
fail('INVALID_ARGUMENT', `${field} must be a finite number.`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
actionAttempted = true;
|
|
366
|
+
target.scrollTo(
|
|
367
|
+
{
|
|
368
|
+
left: params.left === undefined ? target.scrollLeft : params.left,
|
|
369
|
+
top: params.top === undefined ? target.scrollTop : params.top,
|
|
370
|
+
behavior: 'instant'
|
|
371
|
+
}
|
|
372
|
+
);
|
|
373
|
+
method = 'scrollTo';
|
|
374
|
+
}
|
|
375
|
+
return {
|
|
376
|
+
action,
|
|
377
|
+
method,
|
|
378
|
+
invoked: true,
|
|
379
|
+
...(root === document ? {} : {shadowPath}),
|
|
380
|
+
eventsDispatched,
|
|
381
|
+
target: describeElement(target, params.selector)
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function fillElement(target) {
|
|
386
|
+
if (typeof params.value !== 'string') {
|
|
387
|
+
fail('INVALID_ARGUMENT', 'fill requires an exact string value.');
|
|
388
|
+
}
|
|
389
|
+
if (target.readOnly) fail('ARCANE_APP_CONTROL_READ_ONLY', 'The selected control is read-only.');
|
|
390
|
+
let prototype;
|
|
391
|
+
if (target.localName === 'textarea') {
|
|
392
|
+
prototype = HTMLTextAreaElement.prototype;
|
|
393
|
+
} else if (target.localName === 'input') {
|
|
394
|
+
if (['file', 'checkbox', 'radio', 'button', 'submit', 'reset', 'image'].includes(target.type)) {
|
|
395
|
+
fail('ARCANE_APP_CONTROL_UNSUPPORTED', 'The selected input type does not support filling text.');
|
|
396
|
+
}
|
|
397
|
+
prototype = HTMLInputElement.prototype;
|
|
398
|
+
} else if (!target.isContentEditable) {
|
|
399
|
+
fail('ARCANE_APP_CONTROL_UNSUPPORTED', 'fill requires an input, textarea or contenteditable element.');
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
actionAttempted = true;
|
|
403
|
+
if (prototype) {
|
|
404
|
+
Object.getOwnPropertyDescriptor(prototype, 'value').set.call(target, params.value);
|
|
405
|
+
} else {
|
|
406
|
+
Object.getOwnPropertyDescriptor(Node.prototype, 'textContent').set.call(target, params.value);
|
|
407
|
+
}
|
|
408
|
+
dispatchInputAndChange(target, params.value, true);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function selectOptions(target) {
|
|
412
|
+
if (target.localName !== 'select') {
|
|
413
|
+
fail('ARCANE_APP_CONTROL_UNSUPPORTED', 'select requires a native select element.');
|
|
414
|
+
}
|
|
415
|
+
if ((params.value === undefined) === (params.values === undefined)) {
|
|
416
|
+
fail('INVALID_ARGUMENT', 'select requires either value or values.');
|
|
417
|
+
}
|
|
418
|
+
const values = params.values === undefined ? [params.value] : params.values;
|
|
419
|
+
if (!Array.isArray(values)) fail('INVALID_ARGUMENT', 'values must be an array of exact strings.');
|
|
420
|
+
for (const value of values) {
|
|
421
|
+
if (typeof value !== 'string') fail('INVALID_ARGUMENT', 'Every selected value must be an exact string.');
|
|
422
|
+
}
|
|
423
|
+
if (!target.multiple && values.length > 1) {
|
|
424
|
+
fail('INVALID_ARGUMENT', 'A single-select control accepts at most one selected value.');
|
|
425
|
+
}
|
|
426
|
+
const optionsByValue = new Map();
|
|
427
|
+
for (const option of target.options) {
|
|
428
|
+
if (!optionsByValue.has(option.value)) {
|
|
429
|
+
optionsByValue.set(
|
|
430
|
+
option.value,
|
|
431
|
+
[]
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
optionsByValue.get(option.value).push(option);
|
|
435
|
+
}
|
|
436
|
+
const selectedOptions = new Set();
|
|
437
|
+
for (const value of values) {
|
|
438
|
+
const options = optionsByValue.get(value);
|
|
439
|
+
if (!options) {
|
|
440
|
+
fail(
|
|
441
|
+
'ARCANE_APP_CONTROL_OPTION_MISSING',
|
|
442
|
+
'No option has the requested value.',
|
|
443
|
+
{value}
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
for (const option of options) {
|
|
447
|
+
if (option.matches(':disabled')) {
|
|
448
|
+
fail(
|
|
449
|
+
'ARCANE_APP_CONTROL_DISABLED',
|
|
450
|
+
'A requested option is disabled.',
|
|
451
|
+
{value}
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
selectedOptions.add(option);
|
|
455
|
+
if (!target.multiple) break;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
actionAttempted = true;
|
|
459
|
+
const setter = Object.getOwnPropertyDescriptor(HTMLOptionElement.prototype, 'selected').set;
|
|
460
|
+
for (const option of target.options) {
|
|
461
|
+
setter.call(option, selectedOptions.has(option));
|
|
462
|
+
}
|
|
463
|
+
if (selectedOptions.size === 0) {
|
|
464
|
+
Object.getOwnPropertyDescriptor(HTMLSelectElement.prototype, 'selectedIndex').set.call(target, -1);
|
|
465
|
+
}
|
|
466
|
+
dispatchInputAndChange(target, null, false);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function dispatchInputAndChange(target, data, textInput) {
|
|
470
|
+
const input = textInput
|
|
471
|
+
? new InputEvent(
|
|
472
|
+
'input',
|
|
473
|
+
{bubbles: true, composed: true, inputType: 'insertReplacementText', data}
|
|
474
|
+
)
|
|
475
|
+
: new Event(
|
|
476
|
+
'input',
|
|
477
|
+
{bubbles: true, composed: true}
|
|
478
|
+
);
|
|
479
|
+
target.dispatchEvent(input);
|
|
480
|
+
eventsDispatched.push('input');
|
|
481
|
+
const change = new Event(
|
|
482
|
+
'change',
|
|
483
|
+
{bubbles: true}
|
|
484
|
+
);
|
|
485
|
+
target.dispatchEvent(change);
|
|
486
|
+
eventsDispatched.push('change');
|
|
487
|
+
}
|
|
488
|
+
}
|
package/docs/reference/cli.md
CHANGED
|
@@ -9,6 +9,16 @@ work, emits progress or heartbeat records, observes cancellation where safe,
|
|
|
9
9
|
and exits nonzero on failure. Machine output is defined by
|
|
10
10
|
`arcane-cli-events/1`.
|
|
11
11
|
|
|
12
|
+
`app-control` is the dedicated native-window client described in
|
|
13
|
+
[native application control](native-app-control.md). It writes a running JSON
|
|
14
|
+
acknowledgement to stderr, then complete result JSON to stdout, or complete error
|
|
15
|
+
JSON to stderr and a nonzero exit. Capture writes its image to the explicit
|
|
16
|
+
`--output` filename and returns metadata. Its options are scoped to that command;
|
|
17
|
+
the general output-format parser below applies to the other commands.
|
|
18
|
+
Inspection accepts either `--selector` or `--request <json-file>`; the latter
|
|
19
|
+
reads the complete inspection object, including an explicit `shadowPath` through
|
|
20
|
+
open shadow roots. Actions continue to use `--request` for their complete object.
|
|
21
|
+
|
|
12
22
|
## Command inventory
|
|
13
23
|
|
|
14
24
|
| Command | Scope and result |
|
|
@@ -31,6 +41,7 @@ and exits nonzero on failure. Machine output is defined by
|
|
|
31
41
|
| `arcane run` | Serves an existing browser release, or packages, plans, builds, and launches one paired native artifact. |
|
|
32
42
|
| `arcane update-check` | Performs one explicit, read-only npm dist-tag query for the installed SDK version. |
|
|
33
43
|
| `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. |
|
|
34
45
|
| `arcane repo status\|pull\|push` | Runs one selected repository operation for the current app workspace. |
|
|
35
46
|
| `arcane mail key set\|status\|delete` | Manages one server-only Resend API-key profile in `.arcane.env.json`. |
|
|
36
47
|
| `arcane mail send` | Performs one explicit, idempotency-keyed Resend attempt from a complete JSON report on redirected stdin. |
|
|
@@ -69,7 +80,7 @@ meaning and cardinality rules:
|
|
|
69
80
|
| `--allow-to` | optional comma-separated addresses | `mail serve` |
|
|
70
81
|
| `--report-key` | nonempty string | `mail send`; caller-owned stable Resend idempotency key, forwarded unchanged |
|
|
71
82
|
| `--request-timeout` | optional integer from 1 through 2147483647 milliseconds | `mail send`, `mail serve` |
|
|
72
|
-
| `--output` | `human`, `json`, `ndjson` |
|
|
83
|
+
| `--output` | `human`, `json`, `ndjson` | General toolchain invocations; the final occurrence wins. `app-control capture` instead uses its command-specific output filename. |
|
|
73
84
|
| `--git` | flag | `new` |
|
|
74
85
|
| `--skip-tests` | flag | `check --scope app` |
|
|
75
86
|
| `--dry-run` | flag | `package`; parser-supported on `build` with the boundary below |
|
|
@@ -602,6 +602,11 @@ prevent the underlying browser-process exit, restart the application, or change
|
|
|
602
602
|
its profile and saved data.
|
|
603
603
|
|
|
604
604
|
`ArcaneLauncher.cs` supplies the executable entry point for that composition.
|
|
605
|
+
An explicit `native.launchContext.appControl.endpoint` enables the current
|
|
606
|
+
Windows window's background document-control listener. The public client and
|
|
607
|
+
CLI inspect, capture and act on that existing WebView without selecting a desktop
|
|
608
|
+
window or changing its Core/profile. See [native application control](native-app-control.md)
|
|
609
|
+
for launch configuration, document generations, cancellation and platform scope.
|
|
605
610
|
`tools/build-core-windows-host.mjs` builds the selected SDK host output from the
|
|
606
611
|
approved WebView2 package, existing .NET Framework compiler and selected Node
|
|
607
612
|
SEA output. It retains complete compiler diagnostics and produces a fresh
|
|
@@ -1,17 +1,31 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"sdkVersion": "0.
|
|
3
|
+
"sdkVersion": "0.82.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",
|
|
7
7
|
"moduleSystem": "ESM"
|
|
8
8
|
},
|
|
9
|
-
"memberCount":
|
|
9
|
+
"memberCount": 302,
|
|
10
10
|
"runtimeSubpathPatterns": {
|
|
11
11
|
"arcane-os/modules/*": "./runtime/arcane/modules/*",
|
|
12
12
|
"arcane-os/entities/*": "./runtime/arcane/entities/*"
|
|
13
13
|
},
|
|
14
14
|
"members": [
|
|
15
|
+
{
|
|
16
|
+
"id": "core-app-control:connectAppControl",
|
|
17
|
+
"name": "connectAppControl",
|
|
18
|
+
"displayName": "connectAppControl()",
|
|
19
|
+
"kind": "function",
|
|
20
|
+
"signature": "connectAppControl({endpoint,signal,onError}={})",
|
|
21
|
+
"entrypoints": ["arcane-os/core/app-control"],
|
|
22
|
+
"primaryImport": "arcane-os/core/app-control",
|
|
23
|
+
"group": "Native application control",
|
|
24
|
+
"summary": "Connects to one explicit running native window for background document inspection, viewport capture and targeted DOM actions.",
|
|
25
|
+
"availability": "Node local IPC on Windows, Linux and macOS; the current window adapter is Windows WebView2, with other native adapters unavailable",
|
|
26
|
+
"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.",
|
|
27
|
+
"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."
|
|
28
|
+
},
|
|
15
29
|
{
|
|
16
30
|
"id": "local-ai-execution-devices:createExecutionDeviceCatalog",
|
|
17
31
|
"name": "createExecutionDeviceCatalog",
|
|
@@ -194,13 +194,14 @@
|
|
|
194
194
|
"slots": [],
|
|
195
195
|
"dependencies": [
|
|
196
196
|
"MD.js",
|
|
197
|
+
"MarkdownMedia.js",
|
|
197
198
|
"File.js",
|
|
198
199
|
"ConversationTimebox.js",
|
|
199
200
|
"AIRuntimeState.js"
|
|
200
201
|
],
|
|
201
202
|
"availability": "Browser and supported native WebViews",
|
|
202
203
|
"transport": "HTMLImport + DOM; injected Arcane/provider modules where listed",
|
|
203
|
-
"normalization": "Default completed-transcription submit or opt-in transcriptionMode=draft, complete appendDraft text with preserved existing selection, native interim-range ownership, and rendered conversation print/printTitle are normalized. No-retention submissions stay outside transcript, recurring context, memory, and DBOPFS. UI/runtime state, honest model progress, complete timestamped transcript including ui_hidden stored records, exact saved nonblank assistant display names with AI fallback rather than current-name inference, live session name capture with host.aiName display fallback, user-facing structural arguments.message, nonblank all-ID tool-result restoration, collapsed complete raw inspection, ordered parallel-call replay validation, per-choice streamed/terminal complete-envelope correlation, complete nonstructural stream rendering, automatic repeated-formatting-mark removal from only the outbound speech-input copy while displayed and stored Markdown stays exact, generic visible failure outcomes with complete console diagnostics, atomic executed/declined/cancelled/not-executed result batches with one continuation, plural pending-call reload recovery, reentrant terminal-event ownership, and BFCache-preserving page lifecycle are normalized; destroy aborts observation and returns true once/false thereafter; AI/storage/media behavior remains mixed"
|
|
204
|
+
"normalization": "Default completed-transcription submit or opt-in transcriptionMode=draft, complete appendDraft text with preserved existing selection, native interim-range ownership, and rendered conversation print/printTitle are normalized. Local MarkdownMedia images hydrate independently in restored, new, streamed and final content without changing original Markdown or delaying text. Per-render ownership releases obsolete display URLs on replacement, removal and destruction; requested print snapshots retain them through afterprint, while explicit print awaits current image readiness. No-retention submissions stay outside transcript, recurring context, memory, and DBOPFS. UI/runtime state, honest model progress, complete timestamped transcript including ui_hidden stored records, exact saved nonblank assistant display names with AI fallback rather than current-name inference, live session name capture with host.aiName display fallback, user-facing structural arguments.message, nonblank all-ID tool-result restoration, collapsed complete raw inspection, ordered parallel-call replay validation, per-choice streamed/terminal complete-envelope correlation, complete nonstructural stream rendering, automatic repeated-formatting-mark removal from only the outbound speech-input copy while displayed and stored Markdown stays exact, generic visible failure outcomes with complete console diagnostics, atomic executed/declined/cancelled/not-executed result batches with one continuation, plural pending-call reload recovery, reentrant terminal-event ownership, and BFCache-preserving page lifecycle are normalized; destroy aborts observation and returns true once/false thereafter; AI/storage/media behavior remains mixed"
|
|
204
205
|
},
|
|
205
206
|
{
|
|
206
207
|
"file": "runtime/arcane/components/conversation-view.html",
|