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
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# Native application control
|
|
2
|
+
|
|
3
|
+
`connectAppControl` from `arcane-os/core/app-control` connects to one explicitly
|
|
4
|
+
selected running application window. The Windows WebView2 host provides document
|
|
5
|
+
inspection, a PNG of its rendered viewport, and targeted DOM actions through its
|
|
6
|
+
own local named pipe. The application keeps its existing Core child, profile,
|
|
7
|
+
origin and window lifecycle. These operations do not activate the desktop window
|
|
8
|
+
or send global keyboard or pointer input.
|
|
9
|
+
|
|
10
|
+
The Node client uses the SDK's existing `arcane/1` framing and local pipe/socket
|
|
11
|
+
transport on Windows, Linux and macOS. The native window adapter described here
|
|
12
|
+
is Windows WebView2; macOS, Linux and Android hosts do not yet expose these
|
|
13
|
+
operations. An installed generic browser tool does not automatically discover
|
|
14
|
+
this endpoint. A caller must use this public client or CLI explicitly.
|
|
15
|
+
|
|
16
|
+
## Select the running instance
|
|
17
|
+
|
|
18
|
+
Add an explicit endpoint to the application's existing native launch context:
|
|
19
|
+
|
|
20
|
+
```json
|
|
21
|
+
{
|
|
22
|
+
"native": {
|
|
23
|
+
"launchContext": {
|
|
24
|
+
"appControl": {
|
|
25
|
+
"endpoint": "\\\\.\\pipe\\arcane-pm-control"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The same `appControl` record can be supplied in the launch JSON already selected
|
|
33
|
+
by `--arcane-launch-config`. That record is merged by the existing launcher; it
|
|
34
|
+
does not select a different profile or Core topology. Omission starts the usual
|
|
35
|
+
application without a control listener. Each simultaneous application instance
|
|
36
|
+
needs its own endpoint. An occupied endpoint reports the actual startup failure;
|
|
37
|
+
the host does not silently attach to another window or choose another name.
|
|
38
|
+
|
|
39
|
+
Use a matching published Windows host asset when assembling the application.
|
|
40
|
+
The host producer generates `arcane-app-control.js` from the canonical standalone
|
|
41
|
+
DOM operation function and includes it beside `Arcane.exe`. Direct C# composition
|
|
42
|
+
uses `ArcaneHostOptions.ApplicationId`, `AppControlEndpoint`, and
|
|
43
|
+
`AppControlSource` with the same generated source. Ordinary composition leaves
|
|
44
|
+
the two control options unset.
|
|
45
|
+
|
|
46
|
+
## Public client
|
|
47
|
+
|
|
48
|
+
```js
|
|
49
|
+
import {connectAppControl} from 'arcane-os/core/app-control';
|
|
50
|
+
|
|
51
|
+
const app = await connectAppControl({
|
|
52
|
+
endpoint: String.raw`\\.\pipe\arcane-pm-control`
|
|
53
|
+
});
|
|
54
|
+
try {
|
|
55
|
+
const page = await app.inspect();
|
|
56
|
+
await app.act({
|
|
57
|
+
documentGeneration: page.documentGeneration,
|
|
58
|
+
selector: 'a[data-route="connections"]',
|
|
59
|
+
action: 'click'
|
|
60
|
+
});
|
|
61
|
+
const contents = await app.inspect({selector: '#pm-content'});
|
|
62
|
+
console.log(contents);
|
|
63
|
+
} finally {
|
|
64
|
+
await app.close();
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
This example selects an existing navigation link. Its returned action result
|
|
69
|
+
describes the immediate DOM operation. Inspect the resulting page to establish
|
|
70
|
+
the application outcome; a click alone does not establish an asynchronous save,
|
|
71
|
+
connection or other business result.
|
|
72
|
+
|
|
73
|
+
| Member | Contract |
|
|
74
|
+
| --- | --- |
|
|
75
|
+
| `connectAppControl({endpoint, signal?, onError?})` | Connect once to the explicit running host. It never launches an app, creates Core, changes a profile or reconnects automatically. |
|
|
76
|
+
| `status(options?)` | Returns app identity, platform/host, current URL, document generation, readiness, window title/state/client dimensions and available operations. Readiness describes the document, not models or application services. |
|
|
77
|
+
| `inspect(parameters={}, options?)` | Optional `shadowPath` selects an open shadow root. Optional CSS `selector` selects zero or more elements within the selected root; omission selects the whole root. Optional `documentGeneration` selects the expected document. Returns complete HTML/text, live control state and DOM-derived role/name hints. |
|
|
78
|
+
| `capture(parameters={}, options?)` | Returns `{mimeType:'image/png', data, documentGeneration, url}`. `data` contains the complete base64 PNG. Optional `documentGeneration` selects the expected document. |
|
|
79
|
+
| `act(parameters, options?)` | Requires `documentGeneration`, one unambiguous CSS `selector` within the root selected by optional `shadowPath`, and the action fields below. |
|
|
80
|
+
| `close()` | Closes this caller's connection and cancels its pending requests. It never closes the app or Core. Returns the connection's completion promise. |
|
|
81
|
+
| `closed` | Connection lifetime promise; actual transport failures remain observable. |
|
|
82
|
+
| `endpoint` | The caller-selected local endpoint. |
|
|
83
|
+
|
|
84
|
+
Each operation's separate `options` uses the existing Core client request
|
|
85
|
+
contract, including `signal` and `timeoutMs`. Connection cancellation closes only
|
|
86
|
+
that connection. Cancellation before UI dispatch prevents execution. Once a
|
|
87
|
+
script or capture has been dispatched, cancellation cannot reverse its effects;
|
|
88
|
+
the host still observes its completion. A disconnected caller's completed result
|
|
89
|
+
is retained in the host diagnostics instead of being represented as delivered.
|
|
90
|
+
|
|
91
|
+
## Document operations
|
|
92
|
+
|
|
93
|
+
Inspection defaults to the top-level light DOM. Its result contains `scope`, `title`,
|
|
94
|
+
`readyState`, `doctype`, `roots`, `controls`, `frames`, `shadowHosts`,
|
|
95
|
+
`activeElement`, `scrollingElement`, `selection`, `documentGeneration`, and
|
|
96
|
+
`url`. Each root retains its complete `html` and `text`; controls retain their
|
|
97
|
+
attributes and current form, selection, disabled, scrolling and related state.
|
|
98
|
+
Generated CSS selectors are useful for the inspected document; a later DOM edit
|
|
99
|
+
can change their meaning. Application-authored stable selectors are preferable
|
|
100
|
+
when available. Component replacement or mutation can make a selector stale
|
|
101
|
+
without changing the native document generation.
|
|
102
|
+
|
|
103
|
+
### Open shadow roots
|
|
104
|
+
|
|
105
|
+
`inspect` and `act` accept `shadowPath`, an ordered array of CSS host selectors.
|
|
106
|
+
The first selector resolves inside the top-level document; each later selector
|
|
107
|
+
resolves inside the preceding host's open shadow root. Every host selector must
|
|
108
|
+
match exactly one element. The operation's `selector` then resolves only inside
|
|
109
|
+
the final root. An omitted or empty `shadowPath` retains the ordinary light-DOM
|
|
110
|
+
scope. Selecting a root never recursively enters its nested shadow roots.
|
|
111
|
+
|
|
112
|
+
```js
|
|
113
|
+
const shadowPath = ['#appearance-panel', 'theme-switcher'];
|
|
114
|
+
const panel = await app.inspect({shadowPath});
|
|
115
|
+
await app.act({
|
|
116
|
+
documentGeneration: panel.documentGeneration,
|
|
117
|
+
shadowPath,
|
|
118
|
+
selector: 'button[data-theme="dark"]',
|
|
119
|
+
action: 'click'
|
|
120
|
+
});
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Shadow inspection returns `scope:'open-shadow-root'` and the selected
|
|
124
|
+
`shadowPath`. Its element selectors are local to that root. With no `selector`,
|
|
125
|
+
`roots` contains one complete root record with `selector:null`, its full
|
|
126
|
+
`innerHTML` as `html`, its full `textContent` as `text`, and `state:null`; a
|
|
127
|
+
ShadowRoot itself is not an element or an action target. With a selector, roots
|
|
128
|
+
retain their element HTML and state. Controls, frames and open-shadow hosts are
|
|
129
|
+
described within this selected tree. Each `shadowHosts` descriptor retains its
|
|
130
|
+
local `selector` and `mode`, and adds the full `shadowPath` needed to inspect
|
|
131
|
+
that host's shadow root, including nested hosts. Pass that path to the next
|
|
132
|
+
inspection; pair selectors from its result with that same path for actions.
|
|
133
|
+
|
|
134
|
+
`activeElement` comes from the selected root. `scrollingElement` is `null` for a
|
|
135
|
+
shadow-root scope because the root has no document scrolling element; scroll
|
|
136
|
+
actions still target elements. `selection.text`, `title`, `readyState`, `doctype`
|
|
137
|
+
and native URL/generation remain document-level information. Role/name label
|
|
138
|
+
references resolve within the element's own tree. Inspection follows the DOM
|
|
139
|
+
tree, not the composed rendering tree: slot-assigned light-DOM nodes remain in
|
|
140
|
+
their own containing DOM tree and are not duplicated inside the selected shadow
|
|
141
|
+
root.
|
|
142
|
+
|
|
143
|
+
A missing or ambiguous host produces document error `ARCANE_APP_CONTROL_TARGET_COUNT`
|
|
144
|
+
with the complete path, zero-based `index`, `selector` and match count. Invalid
|
|
145
|
+
path or CSS input reports its actual error before any action. A host without an
|
|
146
|
+
accessible open root produces document error `ARCANE_APP_CONTROL_UNSUPPORTED`
|
|
147
|
+
with the path and failing step. The Windows client rejects these document failures
|
|
148
|
+
with top-level `error.code === 'ARCANE_APP_CONTROL_FAILED'`; the original document
|
|
149
|
+
error, including its `code` and `details`, is available at
|
|
150
|
+
`error.data.find(entry => entry.key === 'documentError').value`.
|
|
151
|
+
Closed roots cannot be discovered, entered, or distinguished from
|
|
152
|
+
absent roots through this API. No closed-root interception is installed.
|
|
153
|
+
Shadow actions add the selected `shadowPath` to their result and retain the
|
|
154
|
+
actual target state after dispatch, including disconnection by the action.
|
|
155
|
+
|
|
156
|
+
Role/name hints are derived from DOM attributes and labels, not a native
|
|
157
|
+
accessibility-tree computation. Frame documents and unselected shadow-root
|
|
158
|
+
contents are outside the selected scope. Frame elements and open-shadow hosts
|
|
159
|
+
are described; closed shadow roots cannot be discovered. Capture is the current rendered
|
|
160
|
+
viewport, not an offscreen full-document rendering. Covered and minimized-window
|
|
161
|
+
capture behavior requires execution evidence in the selected environment.
|
|
162
|
+
|
|
163
|
+
| Action | Parameters and immediate behavior |
|
|
164
|
+
| --- | --- |
|
|
165
|
+
| `click` | Calls the uniquely selected element's DOM `click()` method. |
|
|
166
|
+
| `fill` | Exact string `value` replaces an input, textarea or contenteditable value; input/change events are dispatched. Read-only, disabled and non-text input controls report their actual unsupported state. |
|
|
167
|
+
| `select` | Exact `value` string or `values` string array selects native `<select>` options and dispatches input/change. Multiple values require a multiple-select control. |
|
|
168
|
+
| `scroll` | Absolute numeric `left`, `top`, or both in CSS pixels; the omitted axis retains its position. Returns the resulting scroll state, including browser clamping. |
|
|
169
|
+
|
|
170
|
+
DOM actions use programmatic, untrusted events. They do not supply physical user
|
|
171
|
+
activation for browser features that require it. Results describe the actual
|
|
172
|
+
immediate state, including browser value sanitization; input is not silently
|
|
173
|
+
rewritten by the SDK.
|
|
174
|
+
|
|
175
|
+
The host publishes a new generation when an actual replacement document becomes
|
|
176
|
+
ready. A creation-time document marker is also checked inside the executing
|
|
177
|
+
script so a queued action cannot run against a successor page. During navigation,
|
|
178
|
+
operations report that the document is not ready. A change during completion
|
|
179
|
+
reports the actual result with that failure; actions are never retried. These
|
|
180
|
+
markers are transient document lifecycle state and are not written to saved data.
|
|
181
|
+
|
|
182
|
+
## CLI
|
|
183
|
+
|
|
184
|
+
```powershell
|
|
185
|
+
arcane app-control status --endpoint '\\.\pipe\arcane-pm-control'
|
|
186
|
+
arcane app-control inspect --endpoint '\\.\pipe\arcane-pm-control' --selector '#pm-content'
|
|
187
|
+
arcane app-control inspect --endpoint '\\.\pipe\arcane-pm-control' --request '.arcane\inspection.json'
|
|
188
|
+
arcane app-control capture --endpoint '\\.\pipe\arcane-pm-control' --output '.arcane\view.png'
|
|
189
|
+
arcane app-control act --endpoint '\\.\pipe\arcane-pm-control' --request '.arcane\action.json'
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
The action file is the complete action parameter object, including the actual
|
|
193
|
+
`documentGeneration` returned by the preceding status or inspection. Inspection
|
|
194
|
+
also accepts `--request` for a complete parameter object, such as
|
|
195
|
+
`{"shadowPath":["#appearance-panel","theme-switcher"]}` or the same object with
|
|
196
|
+
`selector` and `documentGeneration`. Inspection's `--request` and `--selector`
|
|
197
|
+
options are mutually exclusive. The CLI reads the selected file completely.
|
|
198
|
+
Paths are relative to the current directory. Capture
|
|
199
|
+
writes the PNG to the explicitly selected file and prints its metadata. The other
|
|
200
|
+
operations print complete JSON. `app-control --help` describes this command's
|
|
201
|
+
options; other Arcane commands retain their existing global output-format option.
|
|
202
|
+
|
|
203
|
+
Pipe startup, framing, document, image and output-file errors remain observable.
|
|
204
|
+
Host shutdown stops new control work and observes accepted operations alongside
|
|
205
|
+
the existing Core drain. It does not wait for desktop input or leave an
|
|
206
|
+
independent controller process running.
|
|
@@ -372,6 +372,26 @@ reach developer diagnostics and a concise recovery status stays outside the
|
|
|
372
372
|
transcript. Destruction cancels pending preparation, while an already requested
|
|
373
373
|
print retains its snapshot through `afterprint`.
|
|
374
374
|
|
|
375
|
+
Markdown images with local `arcane-media:` references saved through
|
|
376
|
+
[`MarkdownMedia`](runtime-modules.md#markdownmediajs) display automatically in
|
|
377
|
+
new messages, live response chunks, final responses, and restored history.
|
|
378
|
+
Chat resolves references through the existing application-scoped DBOPFS owner
|
|
379
|
+
while preserving the complete original Markdown, model payloads, saved history,
|
|
380
|
+
image descriptions, and ordinary image URLs. Text appears immediately; local
|
|
381
|
+
image reads and decoding proceed independently without delaying streaming or
|
|
382
|
+
session readiness. Failed images retain complete developer diagnostics and show
|
|
383
|
+
a concise message beside that card; successful sibling images remain visible.
|
|
384
|
+
|
|
385
|
+
Each rendered card owns its display URLs. Re-rendering, history replacement,
|
|
386
|
+
transient-message removal, and destruction cancel obsolete display work and
|
|
387
|
+
release those URLs without deleting stored images. Back/Forward Cache retains
|
|
388
|
+
the existing component lifecycle. Explicit `print()` awaits the current images;
|
|
389
|
+
native Print reports images that are still loading or failed instead of claiming
|
|
390
|
+
they are ready. A requested print snapshot retains its image URLs across later
|
|
391
|
+
message changes or component destruction until `afterprint` releases them.
|
|
392
|
+
Applications continue to own image-record names, associations, and cleanup;
|
|
393
|
+
Chat introduces no renderer callback or additional storage API.
|
|
394
|
+
|
|
375
395
|
Before any session binding begins, callback-mode `sessionStatus` follows the
|
|
376
396
|
selected sticky LLM role and the existing application-owned `modelName` label
|
|
377
397
|
(or the selected model ID). Assigning `modelName` refreshes that status even
|
|
@@ -58,6 +58,7 @@ runtime layouts.
|
|
|
58
58
|
| `arcane-os/core/development` | App-service composition and document-owned Core transport during source development, independently of local AI. |
|
|
59
59
|
| `arcane-os/core/stdio` | Framed Node stdio transport with graceful runtime drain. |
|
|
60
60
|
| `arcane-os/core/host` | Node-only reusable Core host lifecycle, app-selected native launch defaults and shared native location resolution. |
|
|
61
|
+
| `arcane-os/core/app-control` | Explicit connection to an existing native window for background document inspection, capture and DOM actions. |
|
|
61
62
|
| `arcane-os/codex/app-server` | Explicit native Codex App Server session, complete live turns, server requests and recovery. |
|
|
62
63
|
| `arcane-os/mcp/stdio` | Node MCP STDIO server for app-owned tools and static resources. |
|
|
63
64
|
| `arcane-os/core/repositories` | Native persistent ArcaneData paths, connected working repositories and scoped non-secret Git identity configuration. |
|
|
@@ -428,6 +429,7 @@ The remaining data-only subpaths are eight JSON Schemas and package metadata.
|
|
|
428
429
|
| `startCoreHost()` | function | `arcane-os/core/host` | Native Core host composition | Node with readable input and writable output streams |
|
|
429
430
|
| `startSharedCoreHost()` | function | `arcane-os/core/host` | Shared Core host | Node local IPC on Windows, Linux and macOS |
|
|
430
431
|
| `connectSharedCoreHost()` | function | `arcane-os/core/host` | Shared Core host | Node local IPC and optional explicit headless startup |
|
|
432
|
+
| `connectAppControl()` | function | `arcane-os/core/app-control` | Native application control | Node local IPC; current window adapter is Windows WebView2 |
|
|
431
433
|
| `runSharedCoreHost()` | function | `arcane-os/core/host` | Shared Core host | Node headless entry and optional parent startup IPC |
|
|
432
434
|
| `startSharedCoreBridge()` | function | `arcane-os/core/host` | Shared Core host | Node readable/writable streams and local IPC |
|
|
433
435
|
| `readCoreLaunchContext()` | function | `arcane-os/core/host` | Native Core host composition | Node; combines packaged defaults, host state and explicitly selected launch JSON |
|
|
@@ -8886,6 +8888,34 @@ try {
|
|
|
8886
8888
|
}
|
|
8887
8889
|
```
|
|
8888
8890
|
|
|
8891
|
+
## connectAppControl()
|
|
8892
|
+
|
|
8893
|
+
```text
|
|
8894
|
+
connectAppControl({endpoint,signal,onError}={})
|
|
8895
|
+
```
|
|
8896
|
+
|
|
8897
|
+
Connects once to the explicit running native window and resolves
|
|
8898
|
+
`{endpoint,closed,status,inspect,capture,act,close}`. The Windows host selects its
|
|
8899
|
+
own listener through `native.launchContext.appControl.endpoint` and preserves the
|
|
8900
|
+
existing Core child, origin, profile and application lifetime. The client uses
|
|
8901
|
+
portable Node local IPC; other native host adapters do not yet implement these
|
|
8902
|
+
document operations. No application is launched, no desktop window is activated,
|
|
8903
|
+
and no automatic reconnect or action retry occurs.
|
|
8904
|
+
|
|
8905
|
+
`status(options)` reports current app/window/document readiness.
|
|
8906
|
+
`inspect(parameters={},options)` returns complete selected DOM HTML/text
|
|
8907
|
+
and live control state. Inspection and actions accept optional `shadowPath`, an
|
|
8908
|
+
ordered array of unique CSS host selectors through open shadow roots; their
|
|
8909
|
+
`selector` resolves inside the final root. Omission retains top-level light DOM.
|
|
8910
|
+
Closed roots remain unavailable. `capture(parameters={},options)` returns a full base64 PNG
|
|
8911
|
+
of the rendered viewport. `act(parameters,options)` requires a returned
|
|
8912
|
+
`documentGeneration`, one CSS selector, and `click`, `fill`, `select` or `scroll`
|
|
8913
|
+
fields. Options preserve the Core client signal/timeout contract. `close()`
|
|
8914
|
+
cancels this connection's requests and returns `closed`, without closing the app.
|
|
8915
|
+
Document changes and actual errors remain observable; a dispatched action is
|
|
8916
|
+
not reversed by later cancellation. Full parameter/result, CLI and platform
|
|
8917
|
+
contracts are in [native application control](native-app-control.md).
|
|
8918
|
+
|
|
8889
8919
|
## runSharedCoreHost()
|
|
8890
8920
|
|
|
8891
8921
|
Accepts the same options as `startSharedCoreHost(options)` and adds the headless
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "arcane-os",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.83.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",
|
|
@@ -62,6 +62,7 @@
|
|
|
62
62
|
"./core/development": "./src/core/development.mjs",
|
|
63
63
|
"./core/stdio": "./src/core/stdio.mjs",
|
|
64
64
|
"./core/host": "./src/core/host.mjs",
|
|
65
|
+
"./core/app-control": "./src/core/app-control.mjs",
|
|
65
66
|
"./codex/app-server": "./src/codex/app-server.mjs",
|
|
66
67
|
"./mcp/stdio": "./src/mcp/stdio-server.mjs",
|
|
67
68
|
"./core/repositories": "./src/core/repositories.mjs",
|
|
@@ -886,6 +886,7 @@
|
|
|
886
886
|
const aiActivationButton=shadowRoot.querySelector('#ai_activation_button');
|
|
887
887
|
const [
|
|
888
888
|
{default:MD},
|
|
889
|
+
{hydrateMarkdownMedia},
|
|
889
890
|
{default:FileEntity},
|
|
890
891
|
{ConversationSubmissionBarrier,requireConversationTimeboxDelivery},
|
|
891
892
|
{requestAIRuntimeIntent,subscribeAIRuntimeState},
|
|
@@ -893,6 +894,7 @@
|
|
|
893
894
|
{getBrowserDeviceClass}
|
|
894
895
|
]=await Promise.all([
|
|
895
896
|
import('../modules/MD.js'),
|
|
897
|
+
import('../modules/MarkdownMedia.js'),
|
|
896
898
|
import('../entities/File.js'),
|
|
897
899
|
import('../modules/ConversationTimebox.js'),
|
|
898
900
|
import('../modules/AIRuntimeState.js'),
|
|
@@ -946,15 +948,19 @@
|
|
|
946
948
|
let destroyed=false;
|
|
947
949
|
let recognitionDraft=null;
|
|
948
950
|
let writingRecognitionDraft=false;
|
|
951
|
+
const transcriptMedia=new Map();
|
|
952
|
+
let printMediaRetains=null;
|
|
949
953
|
const printViewPromise=import('../modules/PrintView.js').then(
|
|
950
954
|
function registerChatPrint({createPrintView}){
|
|
951
955
|
if(destroyed) return null;
|
|
952
956
|
return createPrintView({
|
|
953
957
|
host,
|
|
954
|
-
content:
|
|
958
|
+
content:renderedChatPrintContent,
|
|
955
959
|
title:function chatPrintTitle(){return host.printTitle||'Conversation';},
|
|
956
960
|
active:function chatPrintActive(){return !destroyed&&chatOutput.children.length>0;},
|
|
957
961
|
signal:aiRuntimeStateAbortController.signal,
|
|
962
|
+
retain:retainChatPrintMedia,
|
|
963
|
+
prepare:prepareChatPrintMedia,
|
|
958
964
|
onError:reportChatPrintFailure
|
|
959
965
|
});
|
|
960
966
|
}
|
|
@@ -1190,6 +1196,7 @@
|
|
|
1190
1196
|
for(const item of record.removedNodes) {
|
|
1191
1197
|
if(item.localName === 'li' && item.parentElement !== chatOutput) {
|
|
1192
1198
|
transcriptResizeObserver?.unobserve(item);
|
|
1199
|
+
releaseTranscriptMessageMedia(item);
|
|
1193
1200
|
}
|
|
1194
1201
|
}
|
|
1195
1202
|
for(const item of record.addedNodes) {
|
|
@@ -1427,14 +1434,72 @@
|
|
|
1427
1434
|
return entry;
|
|
1428
1435
|
}
|
|
1429
1436
|
|
|
1437
|
+
function renderTranscriptMarkdown(target,content){
|
|
1438
|
+
if(destroyed) return null;
|
|
1439
|
+
const template=target.ownerDocument.createElement('template');
|
|
1440
|
+
template.innerHTML=new MD(content).rendered;
|
|
1441
|
+
const pending=[...template.content.querySelectorAll('img')].some(
|
|
1442
|
+
function localChatImage(image){
|
|
1443
|
+
return image.getAttribute('src')?.startsWith('arcane-media:');
|
|
1444
|
+
}
|
|
1445
|
+
);
|
|
1446
|
+
// Remove local-reference src values while detached; text renders now,
|
|
1447
|
+
// and the shared media owner observes independent storage and decoding.
|
|
1448
|
+
const handle=hydrateMarkdownMedia(template.content,{
|
|
1449
|
+
signal:aiRuntimeStateAbortController.signal
|
|
1450
|
+
});
|
|
1451
|
+
releaseTranscriptMarkdown(target);
|
|
1452
|
+
const media={handle,pending,error:null,status:null,ready:null};
|
|
1453
|
+
transcriptMedia.set(target,media);
|
|
1454
|
+
if(printMediaRetains){
|
|
1455
|
+
printMediaRetains.add(handle.retain());
|
|
1456
|
+
}
|
|
1457
|
+
target.raw=content;
|
|
1458
|
+
target.replaceChildren(template.content);
|
|
1459
|
+
media.ready=handle.ready.then(function transcriptImagesReady(){
|
|
1460
|
+
media.pending=false;
|
|
1461
|
+
},function transcriptImagesFailed(error){
|
|
1462
|
+
media.pending=false;
|
|
1463
|
+
media.error=error;
|
|
1464
|
+
if(destroyed||transcriptMedia.get(target)!==media) return;
|
|
1465
|
+
arcaneLogging.error('Unable to display saved Chat images.',error);
|
|
1466
|
+
const status=target.ownerDocument.createElement('p');
|
|
1467
|
+
status.className='message_media_status';
|
|
1468
|
+
status.setAttribute('role','status');
|
|
1469
|
+
status.textContent='Some saved images could not be displayed.';
|
|
1470
|
+
media.status=status;
|
|
1471
|
+
target.after(status);
|
|
1472
|
+
scrollTranscriptToBottom();
|
|
1473
|
+
});
|
|
1474
|
+
return media;
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
function releaseTranscriptMarkdown(target){
|
|
1478
|
+
const media=transcriptMedia.get(target);
|
|
1479
|
+
if(!media) return;
|
|
1480
|
+
transcriptMedia.delete(target);
|
|
1481
|
+
media.status?.remove();
|
|
1482
|
+
media.handle.destroy();
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
function releaseTranscriptMessageMedia(item){
|
|
1486
|
+
const markdown=item?.querySelector('.markdown');
|
|
1487
|
+
if(markdown) releaseTranscriptMarkdown(markdown);
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
function removeTranscriptMessage(item){
|
|
1491
|
+
if(!item) return;
|
|
1492
|
+
releaseTranscriptMessageMedia(item);
|
|
1493
|
+
item.remove();
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1430
1496
|
function setTranscriptMessageContent(item,content,timestamp){
|
|
1431
1497
|
if(!is.string(content)){
|
|
1432
1498
|
throw new TypeError('Chat session message content must be a string.');
|
|
1433
1499
|
}
|
|
1434
1500
|
item.querySelector('.thinking')?.remove();
|
|
1435
1501
|
const markdown=item.querySelector('.markdown');
|
|
1436
|
-
markdown
|
|
1437
|
-
markdown.innerHTML=new MD(content).rendered;
|
|
1502
|
+
renderTranscriptMarkdown(markdown,content);
|
|
1438
1503
|
if(timestamp!==undefined){
|
|
1439
1504
|
item.querySelector('.message_timestamp')?.replaceWith(
|
|
1440
1505
|
transcriptTime(timestamp)
|
|
@@ -1495,12 +1560,11 @@
|
|
|
1495
1560
|
header.append(heading);
|
|
1496
1561
|
const markdown=chatOutput.ownerDocument.createElement('div');
|
|
1497
1562
|
markdown.className='markdown';
|
|
1498
|
-
markdown.raw=content;
|
|
1499
|
-
markdown.innerHTML=new MD(content).rendered;
|
|
1500
1563
|
item.append(header,markdown,transcriptTime(timestamp));
|
|
1501
1564
|
for(const call of normalizeVisibleToolCalls(toolCalls)){
|
|
1502
1565
|
appendVisibleToolCall(item,call);
|
|
1503
1566
|
}
|
|
1567
|
+
renderTranscriptMarkdown(markdown,content);
|
|
1504
1568
|
return item;
|
|
1505
1569
|
}
|
|
1506
1570
|
|
|
@@ -1642,6 +1706,9 @@
|
|
|
1642
1706
|
setPendingStructuralToolCalls(
|
|
1643
1707
|
actionable?[...restoredPendingToolCalls.values()]:[]
|
|
1644
1708
|
);
|
|
1709
|
+
for(const item of chatOutput.children){
|
|
1710
|
+
releaseTranscriptMessageMedia(item);
|
|
1711
|
+
}
|
|
1645
1712
|
chatOutput.replaceChildren(fragment);
|
|
1646
1713
|
transcriptFollowing = true;
|
|
1647
1714
|
transcriptScrollTop = chatOutput.scrollTop;
|
|
@@ -3034,9 +3101,7 @@
|
|
|
3034
3101
|
thinkingHTML.remove();
|
|
3035
3102
|
}
|
|
3036
3103
|
|
|
3037
|
-
target.raw
|
|
3038
|
-
|
|
3039
|
-
target.innerHTML=new MD(target.raw).rendered;
|
|
3104
|
+
renderTranscriptMarkdown(target,target.raw?target.raw+text:text);
|
|
3040
3105
|
|
|
3041
3106
|
scrollTranscriptToBottom();
|
|
3042
3107
|
}
|
|
@@ -3393,8 +3458,7 @@
|
|
|
3393
3458
|
message.querySelector('.thinking')?.remove();
|
|
3394
3459
|
const markdown=message.querySelector('.markdown');
|
|
3395
3460
|
const text=visibleErrorMessage(error,'The AI request failed.');
|
|
3396
|
-
markdown
|
|
3397
|
-
markdown.innerHTML=new MD(text).rendered;
|
|
3461
|
+
renderTranscriptMarkdown(markdown,text);
|
|
3398
3462
|
scrollTranscriptToBottom();
|
|
3399
3463
|
return true;
|
|
3400
3464
|
}
|
|
@@ -3421,8 +3485,8 @@
|
|
|
3421
3485
|
const response=[...chatOutput.children].find(
|
|
3422
3486
|
candidate=>candidate.id===`message-${messageId}`
|
|
3423
3487
|
)||null;
|
|
3424
|
-
response
|
|
3425
|
-
for(const request of requestMessages) request
|
|
3488
|
+
removeTranscriptMessage(response);
|
|
3489
|
+
for(const request of requestMessages) removeTranscriptMessage(request);
|
|
3426
3490
|
const existing=textArea.value;
|
|
3427
3491
|
if(existing!==text){
|
|
3428
3492
|
textArea.value=existing
|
|
@@ -3500,7 +3564,7 @@
|
|
|
3500
3564
|
function removeTransientSessionCards(){
|
|
3501
3565
|
if(retainTurn||!transientCards.length) return;
|
|
3502
3566
|
// The submitter supplies only nodes created for this submission.
|
|
3503
|
-
for(const card of transientCards) card
|
|
3567
|
+
for(const card of transientCards) removeTranscriptMessage(card);
|
|
3504
3568
|
transientCards.length=0;
|
|
3505
3569
|
scrollTranscriptToBottom();
|
|
3506
3570
|
}
|
|
@@ -3675,9 +3739,9 @@
|
|
|
3675
3739
|
const response=[...chatOutput.children].find(
|
|
3676
3740
|
candidate=>candidate.id===`message-${messageId}`
|
|
3677
3741
|
)||null;
|
|
3678
|
-
response
|
|
3742
|
+
removeTranscriptMessage(response);
|
|
3679
3743
|
for(const requestMessage of requestMessages){
|
|
3680
|
-
requestMessage
|
|
3744
|
+
removeTranscriptMessage(requestMessage);
|
|
3681
3745
|
}
|
|
3682
3746
|
scrollTranscriptToBottom();
|
|
3683
3747
|
}else if(retainTurn){
|
|
@@ -3692,7 +3756,7 @@
|
|
|
3692
3756
|
arcaneLogging.error('Arcane chat request failed.',error);
|
|
3693
3757
|
if(sessionRequestMessages.every(message=>message.role==='tool')){
|
|
3694
3758
|
for(const requestMessage of requestMessages){
|
|
3695
|
-
requestMessage
|
|
3759
|
+
removeTranscriptMessage(requestMessage);
|
|
3696
3760
|
}
|
|
3697
3761
|
}
|
|
3698
3762
|
if(retainTurn) renderSessionMessageFailure(messageId,error);
|
|
@@ -4136,11 +4200,64 @@
|
|
|
4136
4200
|
return message;
|
|
4137
4201
|
}
|
|
4138
4202
|
|
|
4203
|
+
function renderedChatPrintContent(){
|
|
4204
|
+
for(const media of transcriptMedia.values()){
|
|
4205
|
+
if(media.pending){
|
|
4206
|
+
const error=new Error('Saved images are still loading. Print again when they finish.');
|
|
4207
|
+
error.code='ARCANE_MARKDOWN_MEDIA_PENDING';
|
|
4208
|
+
throw error;
|
|
4209
|
+
}
|
|
4210
|
+
if(media.error) throw media.error;
|
|
4211
|
+
}
|
|
4212
|
+
return chatOutput;
|
|
4213
|
+
}
|
|
4214
|
+
|
|
4215
|
+
function retainChatPrintMedia(){
|
|
4216
|
+
renderedChatPrintContent();
|
|
4217
|
+
const releases=new Set();
|
|
4218
|
+
for(const media of transcriptMedia.values()){
|
|
4219
|
+
releases.add(media.handle.retain());
|
|
4220
|
+
}
|
|
4221
|
+
printMediaRetains=releases;
|
|
4222
|
+
return function releaseChatPrintMedia(){
|
|
4223
|
+
if(printMediaRetains===releases){
|
|
4224
|
+
printMediaRetains=null;
|
|
4225
|
+
}
|
|
4226
|
+
for(const release of releases) release();
|
|
4227
|
+
releases.clear();
|
|
4228
|
+
};
|
|
4229
|
+
}
|
|
4230
|
+
|
|
4231
|
+
async function prepareChatPrintMedia(signal){
|
|
4232
|
+
let cancel;
|
|
4233
|
+
const cancelled=new Promise(function waitForChatPrintCancellation(resolve){cancel=resolve;});
|
|
4234
|
+
signal?.addEventListener('abort',cancel,{once:true});
|
|
4235
|
+
try{
|
|
4236
|
+
while(!destroyed&&!signal?.aborted){
|
|
4237
|
+
const selected=[...transcriptMedia];
|
|
4238
|
+
await Promise.race([
|
|
4239
|
+
Promise.all(selected.map(function readyChatMedia([,media]){return media.ready;})),
|
|
4240
|
+
cancelled
|
|
4241
|
+
]);
|
|
4242
|
+
if(destroyed||signal?.aborted) return false;
|
|
4243
|
+
if(selected.length!==transcriptMedia.size||selected.some(
|
|
4244
|
+
function replacedChatMedia([target,media]){return transcriptMedia.get(target)!==media;}
|
|
4245
|
+
)) continue;
|
|
4246
|
+
renderedChatPrintContent();
|
|
4247
|
+
return true;
|
|
4248
|
+
}
|
|
4249
|
+
return false;
|
|
4250
|
+
}finally{
|
|
4251
|
+
signal?.removeEventListener('abort',cancel);
|
|
4252
|
+
}
|
|
4253
|
+
}
|
|
4254
|
+
|
|
4139
4255
|
async function print(){
|
|
4140
4256
|
if(destroyed) return false;
|
|
4141
4257
|
try{
|
|
4142
4258
|
const view=await printViewPromise;
|
|
4143
|
-
|
|
4259
|
+
if(destroyed||!view||!await prepareChatPrintMedia()) return false;
|
|
4260
|
+
return await view.print();
|
|
4144
4261
|
}catch(error){
|
|
4145
4262
|
reportChatPrintFailure(error);
|
|
4146
4263
|
throw error;
|
|
@@ -4150,7 +4267,9 @@
|
|
|
4150
4267
|
function reportChatPrintFailure(error){
|
|
4151
4268
|
arcaneLogging.error('Unable to print the conversation.',error);
|
|
4152
4269
|
if(!destroyed){
|
|
4153
|
-
setSessionStatus('error','
|
|
4270
|
+
setSessionStatus('error',error?.code==='ARCANE_MARKDOWN_MEDIA_PENDING'
|
|
4271
|
+
?'Saved images are still loading. Print again when they finish.'
|
|
4272
|
+
:'Unable to open print preview. Please try again.');
|
|
4154
4273
|
}
|
|
4155
4274
|
}
|
|
4156
4275
|
|
|
@@ -4235,6 +4354,9 @@
|
|
|
4235
4354
|
ownership.abort(destroyReason);
|
|
4236
4355
|
}
|
|
4237
4356
|
aiRuntimeStateAbortController.abort(destroyReason);
|
|
4357
|
+
for(const target of transcriptMedia.keys()){
|
|
4358
|
+
releaseTranscriptMarkdown(target);
|
|
4359
|
+
}
|
|
4238
4360
|
aiActivationController.destroy();
|
|
4239
4361
|
window.removeEventListener('pagehide',handlePageHide);
|
|
4240
4362
|
window.removeEventListener('pageshow',handlePageShow);
|