arcane-os 0.81.0 → 0.82.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 +19 -0
- package/README.md +1 -1
- package/browser-runtime/core/app-control.mjs +439 -0
- package/docs/reference/cli.md +9 -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 +147 -0
- package/docs/reference/runtime-components.md +20 -0
- package/docs/reference/sdk-api.md +27 -0
- package/package.json +2 -1
- package/runtime/arcane/components/chat.html +140 -18
- package/src/cli/app-control.mjs +106 -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,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.82.0
|
|
4
|
+
|
|
5
|
+
- Add `connectAppControl` from `arcane-os/core/app-control` and the
|
|
6
|
+
`arcane app-control` CLI for an explicitly selected running Windows WebView2
|
|
7
|
+
application. Inspect complete document content and live controls, capture the
|
|
8
|
+
rendered viewport, and perform targeted click, fill, select and scroll actions
|
|
9
|
+
without activating the desktop window or injecting global input. Preserve the
|
|
10
|
+
application's existing Core, profile and lifecycle; closing the client closes
|
|
11
|
+
only its connection. The matching Windows host archive includes the generated
|
|
12
|
+
DOM operation source. Other native host adapters remain unavailable.
|
|
13
|
+
- Render stored `arcane-media:` images in shared Chat through MarkdownMedia,
|
|
14
|
+
including restored history, live chunks and final responses. Preserve complete
|
|
15
|
+
original Markdown and immediate text rendering. Release obsolete display URLs
|
|
16
|
+
on replacement, removal and destruction; retain requested print snapshots
|
|
17
|
+
through `afterprint` and await current images for explicit printing.
|
|
18
|
+
- Keep dependencies, saved-data formats, model and speech routing unchanged.
|
|
19
|
+
Package verification and host compilation do not establish actual built-app,
|
|
20
|
+
Core/profile, covered-window capture or model behavior.
|
|
21
|
+
|
|
3
22
|
## 0.81.0
|
|
4
23
|
|
|
5
24
|
- Add per-activation native decision `family`, `model`, `revision`, and `dtype`
|
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ selected SDK projection or an explicit live SDK source mount. Both profiles
|
|
|
20
20
|
share the theme, packaging, event, cancellation,
|
|
21
21
|
and browser run contracts while retaining their selected application layout.
|
|
22
22
|
|
|
23
|
-
This checkout defines the `0.
|
|
23
|
+
This checkout defines the `0.82.0` SDK contract. Development applications declare
|
|
24
24
|
`arcane-os` as `latest` and retain their lockfile. Exact SDK pins begin when Roshi
|
|
25
25
|
designates a production build. Follow [Consumer updates](docs/consumer-updates.md)
|
|
26
26
|
to refresh and compare the installed SDK and lockfile with the published channel;
|
|
@@ -0,0 +1,439 @@
|
|
|
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?}) reads the top-level light DOM. Omission selects the whole
|
|
6
|
+
* document; a CSS selector may select any number of roots. HTML, textContent,
|
|
7
|
+
* attributes and live control state are returned completely. Frame documents
|
|
8
|
+
* and shadow-root contents are outside this CSS scope; frame elements and open
|
|
9
|
+
* shadow hosts are identified only. Closed shadow roots cannot be discovered.
|
|
10
|
+
* domRole/domName are DOM-derived hints, not an accessibility-tree computation.
|
|
11
|
+
*
|
|
12
|
+
* act({action,selector,...}) requires one current CSS match. fill uses an exact
|
|
13
|
+
* string value; select uses either value:string or values:string[]. scroll uses
|
|
14
|
+
* absolute left/top CSS-pixel offsets, preserving any omitted axis. Actions use
|
|
15
|
+
* DOM APIs and untrusted events without focus or desktop input. Results describe
|
|
16
|
+
* immediate DOM state, including any browser value sanitization or scroll
|
|
17
|
+
* clamping, not completion of asynchronous application work.
|
|
18
|
+
*
|
|
19
|
+
* The native host owns document-generation checks, scheduling and cancellation.
|
|
20
|
+
* This function retains no state, retries no action and returns a JSON envelope
|
|
21
|
+
* because ExecuteScriptAsync otherwise loses uncaught JavaScript exceptions.
|
|
22
|
+
*/
|
|
23
|
+
export function runAppControl(operation, params = {}) {
|
|
24
|
+
let actionAttempted = false;
|
|
25
|
+
const eventsDispatched = [];
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
if (params === null || typeof params !== 'object' || Array.isArray(params)) {
|
|
29
|
+
fail('INVALID_ARGUMENT', 'App-control parameters must be an object.');
|
|
30
|
+
}
|
|
31
|
+
if (operation === 'inspect') {
|
|
32
|
+
return {ok: true, result: inspectDocument()};
|
|
33
|
+
}
|
|
34
|
+
if (operation === 'act') {
|
|
35
|
+
return {ok: true, result: actOnElement()};
|
|
36
|
+
}
|
|
37
|
+
fail('INVALID_ARGUMENT', 'Expected the inspect or act operation.');
|
|
38
|
+
} catch (error) {
|
|
39
|
+
return {
|
|
40
|
+
ok: false,
|
|
41
|
+
error: {
|
|
42
|
+
name: error?.name ?? 'Error',
|
|
43
|
+
message: error?.message ?? String(error),
|
|
44
|
+
stack: error?.stack ?? null,
|
|
45
|
+
code: error?.code ?? 'ARCANE_APP_CONTROL_FAILED',
|
|
46
|
+
details: error?.details ?? null,
|
|
47
|
+
actionAttempted,
|
|
48
|
+
eventsDispatched
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function fail(code, message, details = null) {
|
|
54
|
+
const error = new Error(message);
|
|
55
|
+
error.code = code;
|
|
56
|
+
error.details = details;
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function queryElements(selector) {
|
|
61
|
+
if (typeof selector !== 'string') {
|
|
62
|
+
fail('INVALID_ARGUMENT', 'selector must be a CSS selector string.');
|
|
63
|
+
}
|
|
64
|
+
return Array.from(document.querySelectorAll(selector));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function inspectDocument() {
|
|
68
|
+
const roots = params.selector === undefined
|
|
69
|
+
? (document.documentElement ? [document.documentElement] : [])
|
|
70
|
+
: queryElements(params.selector);
|
|
71
|
+
const rootSet = new Set(roots);
|
|
72
|
+
const elements = Array.from(document.querySelectorAll('*'));
|
|
73
|
+
const idCounts = new Map();
|
|
74
|
+
const childCounts = new Map();
|
|
75
|
+
const childIndexes = new Map();
|
|
76
|
+
const scope = new Set();
|
|
77
|
+
const selectors = new Map();
|
|
78
|
+
const controls = [];
|
|
79
|
+
const frames = [];
|
|
80
|
+
const shadowHosts = [];
|
|
81
|
+
|
|
82
|
+
// One traversal establishes selector paths and scope for all records.
|
|
83
|
+
for (const element of elements) {
|
|
84
|
+
if (element.id) {
|
|
85
|
+
idCounts.set(element.id, (idCounts.get(element.id) ?? 0) + 1);
|
|
86
|
+
}
|
|
87
|
+
const parent = element.parentElement;
|
|
88
|
+
const index = (childCounts.get(parent) ?? 0) + 1;
|
|
89
|
+
childCounts.set(parent, index);
|
|
90
|
+
childIndexes.set(element, index);
|
|
91
|
+
if (rootSet.has(element) || scope.has(parent)) {
|
|
92
|
+
scope.add(element);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
for (const element of elements) {
|
|
97
|
+
if (!scope.has(element)) continue;
|
|
98
|
+
if (element.matches('input,textarea,select,button,option,optgroup,fieldset,output,progress,meter,details,summary,a[href],area[href],audio,video,[contenteditable],[role],[tabindex]')) {
|
|
99
|
+
controls.push(describeElement(element, uniqueSelector(element)));
|
|
100
|
+
}
|
|
101
|
+
if (element.localName === 'iframe' || element.localName === 'frame') {
|
|
102
|
+
frames.push(describeElement(element, uniqueSelector(element)));
|
|
103
|
+
}
|
|
104
|
+
if (element.shadowRoot) {
|
|
105
|
+
shadowHosts.push(
|
|
106
|
+
{
|
|
107
|
+
selector: uniqueSelector(element),
|
|
108
|
+
mode: element.shadowRoot.mode
|
|
109
|
+
}
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
scope: 'top-level-light-dom',
|
|
116
|
+
title: document.title,
|
|
117
|
+
readyState: document.readyState,
|
|
118
|
+
doctype: document.doctype
|
|
119
|
+
? new XMLSerializer().serializeToString(document.doctype)
|
|
120
|
+
: null,
|
|
121
|
+
roots: roots.map(describeRoot),
|
|
122
|
+
controls,
|
|
123
|
+
frames,
|
|
124
|
+
shadowHosts,
|
|
125
|
+
activeElement: document.activeElement
|
|
126
|
+
? describeElement(document.activeElement, uniqueSelector(document.activeElement))
|
|
127
|
+
: null,
|
|
128
|
+
scrollingElement: document.scrollingElement
|
|
129
|
+
? uniqueSelector(document.scrollingElement)
|
|
130
|
+
: null,
|
|
131
|
+
selection: {text: document.getSelection()?.toString() ?? ''}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
function describeRoot(element) {
|
|
135
|
+
return {
|
|
136
|
+
selector: uniqueSelector(element),
|
|
137
|
+
html: element.outerHTML,
|
|
138
|
+
text: element.textContent,
|
|
139
|
+
state: readState(element)
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function uniqueSelector(element) {
|
|
144
|
+
if (!element.isConnected || element.getRootNode() !== document) return null;
|
|
145
|
+
const path = [];
|
|
146
|
+
let current = element;
|
|
147
|
+
let prefix = '';
|
|
148
|
+
while (current) {
|
|
149
|
+
if (selectors.has(current)) {
|
|
150
|
+
prefix = selectors.get(current);
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
if (current.id && !current.id.includes('\0') && idCounts.get(current.id) === 1) {
|
|
154
|
+
prefix = `#${CSS.escape(current.id)}`;
|
|
155
|
+
selectors.set(current, prefix);
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
path.push(current);
|
|
159
|
+
current = current.parentElement;
|
|
160
|
+
}
|
|
161
|
+
for (let index = path.length - 1; index >= 0; index -= 1) {
|
|
162
|
+
const part = path[index];
|
|
163
|
+
const segment = `*:nth-child(${childIndexes.get(part)})`;
|
|
164
|
+
prefix = prefix ? `${prefix} > ${segment}` : ':root';
|
|
165
|
+
selectors.set(part, prefix);
|
|
166
|
+
}
|
|
167
|
+
return prefix;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function describeElement(element, selector) {
|
|
172
|
+
const attributeEntries = [];
|
|
173
|
+
for (const attribute of element.attributes) {
|
|
174
|
+
attributeEntries.push(
|
|
175
|
+
[attribute.name, attribute.value]
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
selector,
|
|
180
|
+
tagName: element.localName,
|
|
181
|
+
attributes: Object.fromEntries(attributeEntries),
|
|
182
|
+
domRole: element.getAttribute('role'),
|
|
183
|
+
domName: readDOMName(element),
|
|
184
|
+
state: readState(element)
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function readDOMName(element) {
|
|
189
|
+
const labelledBy = element.getAttribute('aria-labelledby');
|
|
190
|
+
if (labelledBy) {
|
|
191
|
+
const labels = [];
|
|
192
|
+
for (const id of labelledBy.split(/\s+/u)) {
|
|
193
|
+
const label = document.getElementById(id);
|
|
194
|
+
if (label) labels.push(label.textContent);
|
|
195
|
+
}
|
|
196
|
+
if (labels.length) return {source: 'aria-labelledby', value: labels.join(' ')};
|
|
197
|
+
}
|
|
198
|
+
if (element.hasAttribute('aria-label')) {
|
|
199
|
+
return {source: 'aria-label', value: element.getAttribute('aria-label')};
|
|
200
|
+
}
|
|
201
|
+
if (element.labels?.length) {
|
|
202
|
+
const labels = [];
|
|
203
|
+
for (const label of element.labels) labels.push(label.textContent);
|
|
204
|
+
return {source: 'labels', value: labels.join(' ')};
|
|
205
|
+
}
|
|
206
|
+
if (element.hasAttribute('alt')) {
|
|
207
|
+
return {source: 'alt', value: element.getAttribute('alt')};
|
|
208
|
+
}
|
|
209
|
+
if (element.localName === 'input' && ['button', 'submit', 'reset'].includes(element.type)) {
|
|
210
|
+
return {source: 'value', value: element.value};
|
|
211
|
+
}
|
|
212
|
+
return {source: 'textContent', value: element.textContent};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function readState(element) {
|
|
216
|
+
const state = {
|
|
217
|
+
connected: element.isConnected,
|
|
218
|
+
contentEditable: element.isContentEditable === true,
|
|
219
|
+
effectivelyDisabled: element.matches(':disabled'),
|
|
220
|
+
scrollLeft: element.scrollLeft,
|
|
221
|
+
scrollTop: element.scrollTop,
|
|
222
|
+
scrollWidth: element.scrollWidth,
|
|
223
|
+
scrollHeight: element.scrollHeight,
|
|
224
|
+
clientWidth: element.clientWidth,
|
|
225
|
+
clientHeight: element.clientHeight
|
|
226
|
+
};
|
|
227
|
+
for (const field of [
|
|
228
|
+
'value', 'type', 'checked', 'indeterminate', 'disabled', 'readOnly',
|
|
229
|
+
'required', 'multiple', 'selected', 'selectedIndex', 'hidden', 'open',
|
|
230
|
+
'tabIndex', 'selectionStart', 'selectionEnd', 'selectionDirection',
|
|
231
|
+
'willValidate', 'validationMessage', 'paused', 'ended', 'currentTime',
|
|
232
|
+
'muted', 'volume', 'playbackRate'
|
|
233
|
+
]) {
|
|
234
|
+
const value = element[field];
|
|
235
|
+
if (value === null || ['string', 'boolean', 'number'].includes(typeof value)) {
|
|
236
|
+
state[field] = value;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (element.isContentEditable) state.editableText = element.textContent;
|
|
240
|
+
if (element.validity) {
|
|
241
|
+
state.validity = {};
|
|
242
|
+
for (const field of [
|
|
243
|
+
'badInput', 'customError', 'patternMismatch', 'rangeOverflow',
|
|
244
|
+
'rangeUnderflow', 'stepMismatch', 'tooLong', 'tooShort',
|
|
245
|
+
'typeMismatch', 'valid', 'valueMissing'
|
|
246
|
+
]) {
|
|
247
|
+
state.validity[field] = element.validity[field];
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (element.localName === 'select') {
|
|
251
|
+
state.options = [];
|
|
252
|
+
for (const option of element.options) {
|
|
253
|
+
state.options.push(
|
|
254
|
+
{
|
|
255
|
+
value: option.value,
|
|
256
|
+
text: option.text,
|
|
257
|
+
label: option.label,
|
|
258
|
+
selected: option.selected,
|
|
259
|
+
disabled: option.disabled,
|
|
260
|
+
effectivelyDisabled: option.matches(':disabled')
|
|
261
|
+
}
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (element.localName === 'input' && element.type === 'file') {
|
|
266
|
+
state.files = [];
|
|
267
|
+
for (const file of element.files) {
|
|
268
|
+
state.files.push(
|
|
269
|
+
{name: file.name, type: file.type, lastModified: file.lastModified}
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return state;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function actOnElement() {
|
|
277
|
+
const targets = queryElements(params.selector);
|
|
278
|
+
if (targets.length !== 1) {
|
|
279
|
+
fail(
|
|
280
|
+
'ARCANE_APP_CONTROL_TARGET_COUNT',
|
|
281
|
+
'An action requires exactly one matching element.',
|
|
282
|
+
{matches: targets.length}
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
const target = targets[0];
|
|
286
|
+
const action = params.action;
|
|
287
|
+
if (!['click', 'fill', 'select', 'scroll'].includes(action)) {
|
|
288
|
+
fail('INVALID_ARGUMENT', 'Expected a click, fill, select or scroll action.');
|
|
289
|
+
}
|
|
290
|
+
if (action !== 'scroll' && target.matches(':disabled')) {
|
|
291
|
+
fail('ARCANE_APP_CONTROL_DISABLED', 'The selected control is disabled.');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
let method;
|
|
295
|
+
if (action === 'click') {
|
|
296
|
+
if (typeof target.click !== 'function') {
|
|
297
|
+
fail('ARCANE_APP_CONTROL_UNSUPPORTED', 'The selected element does not provide a DOM click method.');
|
|
298
|
+
}
|
|
299
|
+
actionAttempted = true;
|
|
300
|
+
target.click();
|
|
301
|
+
method = 'click';
|
|
302
|
+
} else if (action === 'fill') {
|
|
303
|
+
fillElement(target);
|
|
304
|
+
method = 'fill';
|
|
305
|
+
} else if (action === 'select') {
|
|
306
|
+
selectOptions(target);
|
|
307
|
+
method = 'select';
|
|
308
|
+
} else {
|
|
309
|
+
if (params.left === undefined && params.top === undefined) {
|
|
310
|
+
fail('INVALID_ARGUMENT', 'scroll requires left, top or both absolute offsets.');
|
|
311
|
+
}
|
|
312
|
+
for (const field of ['left', 'top']) {
|
|
313
|
+
if (params[field] !== undefined && (typeof params[field] !== 'number' || !Number.isFinite(params[field]))) {
|
|
314
|
+
fail('INVALID_ARGUMENT', `${field} must be a finite number.`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
actionAttempted = true;
|
|
318
|
+
target.scrollTo(
|
|
319
|
+
{
|
|
320
|
+
left: params.left === undefined ? target.scrollLeft : params.left,
|
|
321
|
+
top: params.top === undefined ? target.scrollTop : params.top,
|
|
322
|
+
behavior: 'instant'
|
|
323
|
+
}
|
|
324
|
+
);
|
|
325
|
+
method = 'scrollTo';
|
|
326
|
+
}
|
|
327
|
+
return {
|
|
328
|
+
action,
|
|
329
|
+
method,
|
|
330
|
+
invoked: true,
|
|
331
|
+
eventsDispatched,
|
|
332
|
+
target: describeElement(target, params.selector)
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function fillElement(target) {
|
|
337
|
+
if (typeof params.value !== 'string') {
|
|
338
|
+
fail('INVALID_ARGUMENT', 'fill requires an exact string value.');
|
|
339
|
+
}
|
|
340
|
+
if (target.readOnly) fail('ARCANE_APP_CONTROL_READ_ONLY', 'The selected control is read-only.');
|
|
341
|
+
let prototype;
|
|
342
|
+
if (target.localName === 'textarea') {
|
|
343
|
+
prototype = HTMLTextAreaElement.prototype;
|
|
344
|
+
} else if (target.localName === 'input') {
|
|
345
|
+
if (['file', 'checkbox', 'radio', 'button', 'submit', 'reset', 'image'].includes(target.type)) {
|
|
346
|
+
fail('ARCANE_APP_CONTROL_UNSUPPORTED', 'The selected input type does not support filling text.');
|
|
347
|
+
}
|
|
348
|
+
prototype = HTMLInputElement.prototype;
|
|
349
|
+
} else if (!target.isContentEditable) {
|
|
350
|
+
fail('ARCANE_APP_CONTROL_UNSUPPORTED', 'fill requires an input, textarea or contenteditable element.');
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
actionAttempted = true;
|
|
354
|
+
if (prototype) {
|
|
355
|
+
Object.getOwnPropertyDescriptor(prototype, 'value').set.call(target, params.value);
|
|
356
|
+
} else {
|
|
357
|
+
Object.getOwnPropertyDescriptor(Node.prototype, 'textContent').set.call(target, params.value);
|
|
358
|
+
}
|
|
359
|
+
dispatchInputAndChange(target, params.value, true);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function selectOptions(target) {
|
|
363
|
+
if (target.localName !== 'select') {
|
|
364
|
+
fail('ARCANE_APP_CONTROL_UNSUPPORTED', 'select requires a native select element.');
|
|
365
|
+
}
|
|
366
|
+
if ((params.value === undefined) === (params.values === undefined)) {
|
|
367
|
+
fail('INVALID_ARGUMENT', 'select requires either value or values.');
|
|
368
|
+
}
|
|
369
|
+
const values = params.values === undefined ? [params.value] : params.values;
|
|
370
|
+
if (!Array.isArray(values)) fail('INVALID_ARGUMENT', 'values must be an array of exact strings.');
|
|
371
|
+
for (const value of values) {
|
|
372
|
+
if (typeof value !== 'string') fail('INVALID_ARGUMENT', 'Every selected value must be an exact string.');
|
|
373
|
+
}
|
|
374
|
+
if (!target.multiple && values.length > 1) {
|
|
375
|
+
fail('INVALID_ARGUMENT', 'A single-select control accepts at most one selected value.');
|
|
376
|
+
}
|
|
377
|
+
const optionsByValue = new Map();
|
|
378
|
+
for (const option of target.options) {
|
|
379
|
+
if (!optionsByValue.has(option.value)) {
|
|
380
|
+
optionsByValue.set(
|
|
381
|
+
option.value,
|
|
382
|
+
[]
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
optionsByValue.get(option.value).push(option);
|
|
386
|
+
}
|
|
387
|
+
const selectedOptions = new Set();
|
|
388
|
+
for (const value of values) {
|
|
389
|
+
const options = optionsByValue.get(value);
|
|
390
|
+
if (!options) {
|
|
391
|
+
fail(
|
|
392
|
+
'ARCANE_APP_CONTROL_OPTION_MISSING',
|
|
393
|
+
'No option has the requested value.',
|
|
394
|
+
{value}
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
for (const option of options) {
|
|
398
|
+
if (option.matches(':disabled')) {
|
|
399
|
+
fail(
|
|
400
|
+
'ARCANE_APP_CONTROL_DISABLED',
|
|
401
|
+
'A requested option is disabled.',
|
|
402
|
+
{value}
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
selectedOptions.add(option);
|
|
406
|
+
if (!target.multiple) break;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
actionAttempted = true;
|
|
410
|
+
const setter = Object.getOwnPropertyDescriptor(HTMLOptionElement.prototype, 'selected').set;
|
|
411
|
+
for (const option of target.options) {
|
|
412
|
+
setter.call(option, selectedOptions.has(option));
|
|
413
|
+
}
|
|
414
|
+
if (selectedOptions.size === 0) {
|
|
415
|
+
Object.getOwnPropertyDescriptor(HTMLSelectElement.prototype, 'selectedIndex').set.call(target, -1);
|
|
416
|
+
}
|
|
417
|
+
dispatchInputAndChange(target, null, false);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function dispatchInputAndChange(target, data, textInput) {
|
|
421
|
+
const input = textInput
|
|
422
|
+
? new InputEvent(
|
|
423
|
+
'input',
|
|
424
|
+
{bubbles: true, composed: true, inputType: 'insertReplacementText', data}
|
|
425
|
+
)
|
|
426
|
+
: new Event(
|
|
427
|
+
'input',
|
|
428
|
+
{bubbles: true, composed: true}
|
|
429
|
+
);
|
|
430
|
+
target.dispatchEvent(input);
|
|
431
|
+
eventsDispatched.push('input');
|
|
432
|
+
const change = new Event(
|
|
433
|
+
'change',
|
|
434
|
+
{bubbles: true}
|
|
435
|
+
);
|
|
436
|
+
target.dispatchEvent(change);
|
|
437
|
+
eventsDispatched.push('change');
|
|
438
|
+
}
|
|
439
|
+
}
|
package/docs/reference/cli.md
CHANGED
|
@@ -9,6 +9,13 @@ 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
|
+
|
|
12
19
|
## Command inventory
|
|
13
20
|
|
|
14
21
|
| Command | Scope and result |
|
|
@@ -31,6 +38,7 @@ and exits nonzero on failure. Machine output is defined by
|
|
|
31
38
|
| `arcane run` | Serves an existing browser release, or packages, plans, builds, and launches one paired native artifact. |
|
|
32
39
|
| `arcane update-check` | Performs one explicit, read-only npm dist-tag query for the installed SDK version. |
|
|
33
40
|
| `arcane targets` | Lists target ids, declared status, formats, architectures, signing profiles, methods, and pairing reason. |
|
|
41
|
+
| `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
42
|
| `arcane repo status\|pull\|push` | Runs one selected repository operation for the current app workspace. |
|
|
35
43
|
| `arcane mail key set\|status\|delete` | Manages one server-only Resend API-key profile in `.arcane.env.json`. |
|
|
36
44
|
| `arcane mail send` | Performs one explicit, idempotency-keyed Resend attempt from a complete JSON report on redirected stdin. |
|
|
@@ -69,7 +77,7 @@ meaning and cardinality rules:
|
|
|
69
77
|
| `--allow-to` | optional comma-separated addresses | `mail serve` |
|
|
70
78
|
| `--report-key` | nonempty string | `mail send`; caller-owned stable Resend idempotency key, forwarded unchanged |
|
|
71
79
|
| `--request-timeout` | optional integer from 1 through 2147483647 milliseconds | `mail send`, `mail serve` |
|
|
72
|
-
| `--output` | `human`, `json`, `ndjson` |
|
|
80
|
+
| `--output` | `human`, `json`, `ndjson` | General toolchain invocations; the final occurrence wins. `app-control capture` instead uses its command-specific output filename. |
|
|
73
81
|
| `--git` | flag | `new` |
|
|
74
82
|
| `--skip-tests` | flag | `check --scope app` |
|
|
75
83
|
| `--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.81.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 top-level light-DOM HTML/text and live control state with DOM-derived role/name hints; frames and open shadow hosts are descriptors only. 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. 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",
|