arcane-os 0.2.2 → 0.3.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 +29 -0
- package/README.md +8 -8
- package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +29 -22
- package/browser-runtime/ai/ARCANE_AI_BROWSER_SPEECH_COMPONENTS.json +203 -0
- package/browser-runtime/ai/browser-kokoro-worker.mjs +11 -2
- package/browser-runtime/ai/browser-speech-artifacts.mjs +3230 -397
- package/browser-runtime/ai/browser-speech-providers.mjs +1141 -157
- package/browser-runtime/ai/browser-speech.mjs +2 -0
- package/browser-runtime/ai/browser-whisper-worker.mjs +11 -2
- package/browser-runtime/ai/model-controller.mjs +285 -95
- package/browser-runtime/ai/speech-worker-client.mjs +247 -32
- package/browser-runtime/ai/speech-worker-runtime.mjs +2310 -167
- package/browser-runtime/event-manager.mjs +1097 -1
- package/docs/architecture.md +2 -2
- package/docs/event-manager.md +155 -27
- package/docs/reference/README.md +27 -27
- package/docs/reference/ai/browser-speech-package-authority.json +835 -0
- package/docs/reference/ai/browser-speech.md +1162 -246
- package/docs/reference/ai/browser-wasm.md +18 -7
- package/docs/reference/availability-and-normalization.md +6 -3
- package/docs/reference/behavioral-testing.md +29 -6
- package/docs/reference/cli.md +117 -9
- package/docs/reference/core/arcane-ai-contracts.md +1 -1
- package/docs/reference/event-manager.md +577 -32
- package/docs/reference/inventory/package-api.json +478 -2
- package/docs/reference/inventory/runtime-components.json +108 -44
- package/docs/reference/inventory/runtime-modules.json +131 -53
- package/docs/reference/mail.md +316 -0
- package/docs/reference/protocols.md +157 -43
- package/docs/reference/runtime-components.md +258 -83
- package/docs/reference/runtime-modules.md +613 -77
- package/docs/reference/sdk-api.md +1014 -25
- package/package.json +5 -4
- package/runtime/ARCANE_RUNTIME_RELEASE.json +145 -140
- package/runtime/arcane/components/app-bar.html +34 -13
- package/runtime/arcane/components/assistant-panel.html +110 -57
- package/runtime/arcane/components/calculator.html +7 -4
- package/runtime/arcane/components/chart.html +58 -17
- package/runtime/arcane/components/chat.html +606 -136
- package/runtime/arcane/components/conversation-view.html +13 -6
- package/runtime/arcane/components/dashboard-config.html +96 -59
- package/runtime/arcane/components/data-maintenance.html +69 -14
- package/runtime/arcane/components/data-view.html +53 -7
- package/runtime/arcane/components/directory-picker.html +118 -32
- package/runtime/arcane/components/document-inspector.html +47 -10
- package/runtime/arcane/components/file-drop.html +81 -35
- package/runtime/arcane/components/file-inspector.html +72 -22
- package/runtime/arcane/components/file-manager.html +374 -79
- package/runtime/arcane/components/integration-settings.html +12 -5
- package/runtime/arcane/components/local-ai-status.html +48 -19
- package/runtime/arcane/components/markdown-document.html +161 -68
- package/runtime/arcane/components/markdown-editor.html +110 -33
- package/runtime/arcane/components/media-embed.html +8 -5
- package/runtime/arcane/components/modal.html +15 -5
- package/runtime/arcane/components/output-panel.html +28 -23
- package/runtime/arcane/components/preferences-form.html +22 -4
- package/runtime/arcane/components/record-timeline.html +18 -2
- package/runtime/arcane/components/relationship-board.html +23 -3
- package/runtime/arcane/components/screen-capture.html +10 -4
- package/runtime/arcane/components/source-code-viewer.html +76 -9
- package/runtime/arcane/components/source-explanation.html +23 -3
- package/runtime/arcane/components/speech.html +462 -384
- package/runtime/arcane/components/summary-strip.html +22 -11
- package/runtime/arcane/components/table.html +39 -21
- package/runtime/arcane/components/task-progress.html +79 -21
- package/runtime/arcane/components/terminal-workspace.html +7 -4
- package/runtime/arcane/components/theme-editor.html +7 -3
- package/runtime/arcane/components/unified-inbox.html +9 -4
- package/runtime/arcane/components/voice-transcription.html +639 -98
- package/runtime/arcane/components/weather-widget.html +5 -3
- package/runtime/arcane/components/web-navigator.html +48 -8
- package/runtime/arcane/entities/Chat.js +1 -1
- package/runtime/arcane/entities/User.js +110 -23
- package/runtime/arcane/modules/AI.js +2109 -130
- package/runtime/arcane/modules/AIProviderRuntime.js +720 -13
- package/runtime/arcane/modules/AIRuntimeState.js +109 -52
- package/runtime/arcane/modules/ApiModelDatabase.js +390 -17
- package/runtime/arcane/modules/BrowserTestSuite.js +205 -28
- package/runtime/arcane/modules/CalculatorEngine.js +63 -3
- package/runtime/arcane/modules/CommunicationAppController.js +588 -28
- package/runtime/arcane/modules/CommunicationHub.js +590 -10
- package/runtime/arcane/modules/ComponentContracts.js +470 -0
- package/runtime/arcane/modules/ConversationTimebox.js +152 -33
- package/runtime/arcane/modules/DBLS.js +40 -7
- package/runtime/arcane/modules/DBOPFS.js +35 -11
- package/runtime/arcane/modules/DataMaintenance.js +12 -2
- package/runtime/arcane/modules/Errors.js +65 -7
- package/runtime/arcane/modules/HTMLImport.js +198 -14
- package/runtime/arcane/modules/LocalAIReadinessController.js +208 -29
- package/runtime/arcane/modules/Mail.js +738 -115
- package/runtime/arcane/modules/MailOutbox.mjs +1395 -0
- package/runtime/arcane/modules/MailTransport.mjs +197 -39
- package/runtime/arcane/modules/Ollama.js +36 -1
- package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +583 -7
- package/runtime/arcane/modules/PreferenceStore.js +367 -33
- package/runtime/arcane/modules/RecordReviewStore.js +322 -23
- package/runtime/arcane/modules/ScreenCapture.js +1397 -15
- package/runtime/arcane/modules/SpeechPlayback.js +438 -41
- package/runtime/arcane/modules/TerminalClient.js +277 -12
- package/runtime/arcane/modules/ThemeBootstrap.js +80 -6
- package/runtime/arcane/modules/ThemeManager.js +39 -7
- package/runtime/arcane/modules/TimeGuard.js +131 -20
- package/runtime/arcane/modules/WaitForComponent.js +386 -33
- package/schemas/arcane-lock.schema.json +2 -2
- package/src/cli/main.mjs +435 -9
- package/src/event-manager.mjs +1097 -1
- package/src/import-map.mjs +21 -3
- package/src/index.mjs +13 -0
- package/src/installed-sdk-runtime.mjs +112 -0
- package/src/mail-api.mjs +22 -0
- package/src/mail-credentials.mjs +667 -0
- package/src/mail-server.mjs +1769 -0
- package/src/mail.mjs +261 -0
- package/src/sdk-browser-runtime.mjs +85 -41
- package/src/testing-loader.mjs +7 -0
- package/src/toolchain.mjs +3 -0
- package/src/workspace.mjs +1 -1
|
@@ -1,52 +1,317 @@
|
|
|
1
|
+
import {createArcaneEventSource} from 'arcane-os/event-manager';
|
|
1
2
|
import TerminalSession from '../entities/TerminalSession.js';
|
|
2
3
|
|
|
4
|
+
export const TERMINAL_CLIENT_EVENT_TYPES=Object.freeze({
|
|
5
|
+
sessionStarted:'terminal-session',
|
|
6
|
+
sessionExited:'terminal-exit',
|
|
7
|
+
sessionError:'terminal-error',
|
|
8
|
+
sessionOutput:'terminal-output'
|
|
9
|
+
});
|
|
10
|
+
export const TERMINAL_CLIENT_ERROR_CODES=Object.freeze({
|
|
11
|
+
capabilityUnavailable:'ARCANE_TERMINAL_CAPABILITY_UNAVAILABLE',
|
|
12
|
+
disposed:'ARCANE_TERMINAL_CLIENT_DISPOSED',
|
|
13
|
+
hostEventSubscriptionInvalid:'ARCANE_TERMINAL_HOST_EVENT_SUBSCRIPTION_INVALID',
|
|
14
|
+
hostEventUnsubscribeRejected:'ARCANE_TERMINAL_HOST_EVENT_UNSUBSCRIBE_REJECTED',
|
|
15
|
+
eventTypeInvalid:'ARCANE_TERMINAL_CLIENT_EVENT_TYPE_INVALID',
|
|
16
|
+
staleSessionCleanupRejected:'ARCANE_TERMINAL_STALE_SESSION_CLEANUP_REJECTED',
|
|
17
|
+
sessionHostError:'ARCANE_TERMINAL_SESSION_HOST_ERROR'
|
|
18
|
+
});
|
|
19
|
+
export const TERMINAL_CLIENT_REASONS=Object.freeze({
|
|
20
|
+
capabilityUnavailable:'terminal-capability-unavailable',
|
|
21
|
+
disposed:'terminal-client-disposed',
|
|
22
|
+
hostEventSubscriptionInvalid:'terminal-host-event-subscription-invalid',
|
|
23
|
+
hostEventUnsubscribeRejected:'terminal-host-event-unsubscribe-rejected',
|
|
24
|
+
eventTypeInvalid:'terminal-client-event-type-invalid',
|
|
25
|
+
staleSessionCleanupRejected:'terminal-stale-session-cleanup-rejected',
|
|
26
|
+
sessionStarted:'terminal-session-started',
|
|
27
|
+
sessionExited:'terminal-session-exited',
|
|
28
|
+
sessionHostErrorReceived:'terminal-session-host-error-received',
|
|
29
|
+
sessionOutputReceived:'terminal-session-output-received'
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
function terminalClientError(code,reason,message,cause){
|
|
33
|
+
const error=cause===undefined
|
|
34
|
+
?new Error(message)
|
|
35
|
+
:new Error(message,{cause});
|
|
36
|
+
error.code=code;
|
|
37
|
+
error.reason=reason;
|
|
38
|
+
return error;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function terminalEventReason(type){
|
|
42
|
+
if(type==='session')return TERMINAL_CLIENT_REASONS.sessionStarted;
|
|
43
|
+
if(type==='exit')return TERMINAL_CLIENT_REASONS.sessionExited;
|
|
44
|
+
if(type==='error')return TERMINAL_CLIENT_REASONS.sessionHostErrorReceived;
|
|
45
|
+
return TERMINAL_CLIENT_REASONS.sessionOutputReceived;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function terminalPublicDetail(detail,type,reason){
|
|
49
|
+
const id=detail?.session?.id??detail?.sessionId;
|
|
50
|
+
const output=typeof detail?.data==='string'?detail.data:'';
|
|
51
|
+
const code=type==='error'
|
|
52
|
+
?typeof detail?.code==='string'&&detail.code
|
|
53
|
+
?detail.code
|
|
54
|
+
:TERMINAL_CLIENT_ERROR_CODES.sessionHostError
|
|
55
|
+
:null;
|
|
56
|
+
return Object.freeze({
|
|
57
|
+
...(typeof id==='string'&&id?{id}:{}),
|
|
58
|
+
...(typeof detail?.stream==='string'?{stream:detail.stream}:{}),
|
|
59
|
+
...(output?{byteCount:new TextEncoder().encode(output).byteLength}:{}),
|
|
60
|
+
...(typeof detail?.session?.state==='string'?{status:detail.session.state}:{}),
|
|
61
|
+
...(code?{code}:{}),
|
|
62
|
+
reason
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
3
66
|
export default class TerminalClient extends EventTarget{
|
|
67
|
+
#destroyed=false;
|
|
68
|
+
#events;
|
|
69
|
+
#generation=0;
|
|
70
|
+
#operationSequence=0;
|
|
4
71
|
constructor(api=globalThis.Arcane?.terminal){
|
|
5
72
|
super();
|
|
6
73
|
this.api=api||null;
|
|
7
74
|
this.sessions=new Map();
|
|
8
75
|
this.unsubscribe=[];
|
|
76
|
+
this.#events=createArcaneEventSource(this,{
|
|
77
|
+
source:'terminal-client',
|
|
78
|
+
eventTypes:Object.freeze(
|
|
79
|
+
Object.values(TERMINAL_CLIENT_EVENT_TYPES)
|
|
80
|
+
)
|
|
81
|
+
});
|
|
9
82
|
const events=globalThis.Arcane?.events;
|
|
10
83
|
if(events?.on){
|
|
11
|
-
|
|
12
|
-
|
|
84
|
+
const client=this;
|
|
85
|
+
try{
|
|
86
|
+
for(const type of [
|
|
87
|
+
'terminal.output',
|
|
88
|
+
'terminal.exit',
|
|
89
|
+
'terminal.error'
|
|
90
|
+
]){
|
|
91
|
+
function receiveTerminalHostEvent(data){
|
|
92
|
+
client.receive(type,data);
|
|
93
|
+
}
|
|
94
|
+
const unsubscribe=events.on(
|
|
95
|
+
type,
|
|
96
|
+
receiveTerminalHostEvent
|
|
97
|
+
);
|
|
98
|
+
if(typeof unsubscribe!=='function'
|
|
99
|
+
&&typeof unsubscribe?.dispose!=='function'){
|
|
100
|
+
throw terminalClientError(
|
|
101
|
+
TERMINAL_CLIENT_ERROR_CODES.hostEventSubscriptionInvalid,
|
|
102
|
+
TERMINAL_CLIENT_REASONS.hostEventSubscriptionInvalid,
|
|
103
|
+
`Arcane.events.on(${type}) must return an unsubscribe function or disposable handle.`
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
this.unsubscribe.push(unsubscribe);
|
|
107
|
+
}
|
|
108
|
+
}catch(cause){
|
|
109
|
+
this.#releaseHostSubscriptions();
|
|
110
|
+
this.#events.dispose();
|
|
111
|
+
if(cause?.code===TERMINAL_CLIENT_ERROR_CODES.hostEventSubscriptionInvalid){
|
|
112
|
+
throw cause;
|
|
113
|
+
}
|
|
114
|
+
throw terminalClientError(
|
|
115
|
+
TERMINAL_CLIENT_ERROR_CODES.hostEventSubscriptionInvalid,
|
|
116
|
+
TERMINAL_CLIENT_REASONS.hostEventSubscriptionInvalid,
|
|
117
|
+
'Terminal host event subscription was rejected.',
|
|
118
|
+
cause
|
|
119
|
+
);
|
|
13
120
|
}
|
|
14
121
|
}
|
|
15
122
|
}
|
|
16
123
|
|
|
17
|
-
|
|
124
|
+
addEventListener(type,listener,options){return this.#events.addEventListener(type,listener,options);}
|
|
125
|
+
removeEventListener(type,listener,options){return this.#events.removeEventListener(type,listener,options);}
|
|
126
|
+
on(type,listener,options){return this.#events.on(type,listener,options);}
|
|
127
|
+
subscribe(type,listener,options){return this.#events.subscribe(type,listener,options);}
|
|
128
|
+
dispatchEvent(value){return this.#events.dispatchEvent(value);}
|
|
129
|
+
|
|
130
|
+
get available(){
|
|
131
|
+
return !this.#destroyed
|
|
132
|
+
&&Boolean(this.api?.start&&this.api?.write&&this.api?.close);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
#disposedError(){
|
|
136
|
+
return terminalClientError(
|
|
137
|
+
TERMINAL_CLIENT_ERROR_CODES.disposed,
|
|
138
|
+
TERMINAL_CLIENT_REASONS.disposed,
|
|
139
|
+
'The terminal client has been disposed.'
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
#assertOpen(){
|
|
144
|
+
if(this.#destroyed)throw this.#disposedError();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
#assertCapability(method){
|
|
148
|
+
this.#assertOpen();
|
|
149
|
+
if(typeof this.api?.[method]!=='function'){
|
|
150
|
+
throw terminalClientError(
|
|
151
|
+
TERMINAL_CLIENT_ERROR_CODES.capabilityUnavailable,
|
|
152
|
+
TERMINAL_CLIENT_REASONS.capabilityUnavailable,
|
|
153
|
+
`The Arcane native terminal ${method} capability is unavailable. Open Terminal from the installed Arcane shell.`
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
#assertCurrent(generation){
|
|
159
|
+
if(this.#destroyed||generation!==this.#generation){
|
|
160
|
+
throw this.#disposedError();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
#releaseHostSubscriptions(){
|
|
165
|
+
let firstError=null;
|
|
166
|
+
for(const subscription of this.unsubscribe){
|
|
167
|
+
try{
|
|
168
|
+
if(typeof subscription==='function')subscription();
|
|
169
|
+
else subscription.dispose();
|
|
170
|
+
}catch(error){
|
|
171
|
+
firstError??=error;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
this.unsubscribe=[];
|
|
175
|
+
return firstError;
|
|
176
|
+
}
|
|
18
177
|
|
|
19
178
|
async start(options={}){
|
|
20
|
-
|
|
179
|
+
this.#assertCapability('start');
|
|
180
|
+
const generation=this.#generation;
|
|
21
181
|
const result=await this.api.start(options);
|
|
182
|
+
if(this.#destroyed||generation!==this.#generation){
|
|
183
|
+
let cleanupError=null;
|
|
184
|
+
if(typeof result?.id==='string'&&typeof this.api?.close==='function'){
|
|
185
|
+
try{
|
|
186
|
+
await this.api.close(result.id);
|
|
187
|
+
}catch(error){
|
|
188
|
+
cleanupError=error;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if(cleanupError){
|
|
192
|
+
throw terminalClientError(
|
|
193
|
+
TERMINAL_CLIENT_ERROR_CODES.staleSessionCleanupRejected,
|
|
194
|
+
TERMINAL_CLIENT_REASONS.staleSessionCleanupRejected,
|
|
195
|
+
'The terminal client was disposed and cleanup of its newly started host session was rejected.',
|
|
196
|
+
cleanupError
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
throw this.#disposedError();
|
|
200
|
+
}
|
|
22
201
|
const session=new TerminalSession({...result,state:'running'});
|
|
23
202
|
this.sessions.set(session.id,session);
|
|
24
203
|
this.emit('session',{session});
|
|
25
204
|
return session;
|
|
26
205
|
}
|
|
27
206
|
|
|
28
|
-
async write(id,data){
|
|
29
|
-
|
|
30
|
-
|
|
207
|
+
async write(id,data){
|
|
208
|
+
this.#assertCapability('write');
|
|
209
|
+
TerminalSession.id(id);
|
|
210
|
+
const generation=this.#generation;
|
|
211
|
+
const result=await this.api.write(id,String(data??''));
|
|
212
|
+
this.#assertCurrent(generation);
|
|
213
|
+
return result;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async resize(id,columns,rows){
|
|
217
|
+
this.#assertCapability('resize');
|
|
218
|
+
TerminalSession.id(id);
|
|
219
|
+
const generation=this.#generation;
|
|
220
|
+
const result=await this.api.resize(id,Number(columns),Number(rows));
|
|
221
|
+
this.#assertCurrent(generation);
|
|
222
|
+
return result;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async signal(id,signal='interrupt'){
|
|
226
|
+
this.#assertCapability('signal');
|
|
227
|
+
TerminalSession.id(id);
|
|
228
|
+
const generation=this.#generation;
|
|
229
|
+
const result=await this.api.signal(id,String(signal));
|
|
230
|
+
this.#assertCurrent(generation);
|
|
231
|
+
return result;
|
|
232
|
+
}
|
|
233
|
+
|
|
31
234
|
async close(id){
|
|
235
|
+
this.#assertCapability('close');
|
|
32
236
|
TerminalSession.id(id);
|
|
237
|
+
const generation=this.#generation;
|
|
33
238
|
const result=await this.api.close(id);
|
|
239
|
+
this.#assertCurrent(generation);
|
|
34
240
|
const session=this.sessions.get(id);
|
|
35
241
|
if(session) this.sessions.set(id,session.with({state:'closed'}));
|
|
36
242
|
return result;
|
|
37
243
|
}
|
|
38
244
|
|
|
39
245
|
receive(type,data={}){
|
|
246
|
+
if(this.#destroyed)return false;
|
|
40
247
|
const id=String(data.sessionId||'');
|
|
41
|
-
if(!id||!this.sessions.has(id))
|
|
248
|
+
if(!id||!this.sessions.has(id))return false;
|
|
42
249
|
if(type==='terminal.exit'){
|
|
43
250
|
const session=this.sessions.get(id).with({state:'exited'});
|
|
44
251
|
this.sessions.set(id,session);
|
|
45
252
|
this.emit('exit',{...data,session});
|
|
46
|
-
}else if(type==='terminal.error')
|
|
47
|
-
|
|
253
|
+
}else if(type==='terminal.error'){
|
|
254
|
+
this.emit('error',data);
|
|
255
|
+
}else{
|
|
256
|
+
this.emit('output',data);
|
|
257
|
+
}
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
emit(type,detail){
|
|
262
|
+
this.#assertOpen();
|
|
263
|
+
if(!['session','exit','error','output'].includes(type)){
|
|
264
|
+
throw terminalClientError(
|
|
265
|
+
TERMINAL_CLIENT_ERROR_CODES.eventTypeInvalid,
|
|
266
|
+
TERMINAL_CLIENT_REASONS.eventTypeInvalid,
|
|
267
|
+
'Terminal client events must be session, exit, error, or output.'
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
const eventType=TERMINAL_CLIENT_EVENT_TYPES[
|
|
271
|
+
type==='session'
|
|
272
|
+
?'sessionStarted'
|
|
273
|
+
:type==='exit'
|
|
274
|
+
?'sessionExited'
|
|
275
|
+
:type==='error'
|
|
276
|
+
?'sessionError'
|
|
277
|
+
:'sessionOutput'
|
|
278
|
+
];
|
|
279
|
+
const reason=terminalEventReason(type);
|
|
280
|
+
this.#operationSequence+=1;
|
|
281
|
+
const operationId=`${this.#events.instanceId}:${type}:${this.#operationSequence.toString(36)}`;
|
|
282
|
+
const compatibilityDetail=Object.freeze({
|
|
283
|
+
...detail,
|
|
284
|
+
operationId,
|
|
285
|
+
reason
|
|
286
|
+
});
|
|
287
|
+
return this.#events.dispatch(
|
|
288
|
+
eventType,
|
|
289
|
+
compatibilityDetail,
|
|
290
|
+
{
|
|
291
|
+
operationId,
|
|
292
|
+
publicDetail:terminalPublicDetail(detail,type,reason)
|
|
293
|
+
}
|
|
294
|
+
).occurrence;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
destroy(){
|
|
298
|
+
if(this.#destroyed)return false;
|
|
299
|
+
this.#destroyed=true;
|
|
300
|
+
this.#generation+=1;
|
|
301
|
+
const unsubscribeError=this.#releaseHostSubscriptions();
|
|
302
|
+
this.#events.dispose();
|
|
303
|
+
if(unsubscribeError){
|
|
304
|
+
throw terminalClientError(
|
|
305
|
+
TERMINAL_CLIENT_ERROR_CODES.hostEventUnsubscribeRejected,
|
|
306
|
+
TERMINAL_CLIENT_REASONS.hostEventUnsubscribeRejected,
|
|
307
|
+
'A terminal host event subscription rejected cleanup.',
|
|
308
|
+
unsubscribeError
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
return true;
|
|
48
312
|
}
|
|
49
313
|
|
|
50
|
-
|
|
51
|
-
|
|
314
|
+
dispose(){
|
|
315
|
+
return this.destroy();
|
|
316
|
+
}
|
|
52
317
|
}
|
|
@@ -1,11 +1,83 @@
|
|
|
1
1
|
import {loadAndApplyTheme} from './ThemeManager.js';
|
|
2
|
+
import {createArcaneEventSource} from 'arcane-os/event-manager';
|
|
2
3
|
|
|
3
4
|
const sharedKey='arcaneThemeReady';
|
|
4
5
|
const listenerKey='arcaneThemeAppearanceListener';
|
|
6
|
+
const THEME_BOOTSTRAP_EVENT_OWNER=Object.freeze({});
|
|
7
|
+
let themeBootstrapOperationSequence=0;
|
|
8
|
+
const themeBootstrapEvents=createArcaneEventSource(
|
|
9
|
+
THEME_BOOTSTRAP_EVENT_OWNER,
|
|
10
|
+
{
|
|
11
|
+
source:'theme-bootstrap',
|
|
12
|
+
eventTypes:Object.freeze(['appearance.changed'])
|
|
13
|
+
}
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
function installAppearanceListener(ready){
|
|
17
|
+
const hostEvents=globalThis.Arcane?.events;
|
|
18
|
+
if(typeof hostEvents?.on!=='function'||globalThis[listenerKey]){
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let active=true;
|
|
23
|
+
const hostUnsubscribe=hostEvents.on(
|
|
24
|
+
'appearance.changed',
|
|
25
|
+
function forwardAppearanceChange(detail={}){
|
|
26
|
+
themeBootstrapOperationSequence+=1;
|
|
27
|
+
const forwarded=Object.freeze({
|
|
28
|
+
scheme:typeof detail?.scheme==='string'?detail.scheme:null,
|
|
29
|
+
effectiveScheme:typeof detail?.effectiveScheme==='string'
|
|
30
|
+
?detail.effectiveScheme
|
|
31
|
+
:null,
|
|
32
|
+
source:typeof detail?.source==='string'?detail.source:null,
|
|
33
|
+
reason:'host-appearance-changed'
|
|
34
|
+
});
|
|
35
|
+
themeBootstrapEvents.dispatch(
|
|
36
|
+
'appearance.changed',
|
|
37
|
+
forwarded,
|
|
38
|
+
{
|
|
39
|
+
operationId:`theme-bootstrap-${themeBootstrapEvents.instanceId}-${themeBootstrapOperationSequence}`,
|
|
40
|
+
publicDetail:forwarded
|
|
41
|
+
}
|
|
42
|
+
);
|
|
43
|
+
Promise.resolve(ready).then(function reloadTheme(result){
|
|
44
|
+
return result?.manager?.load?.();
|
|
45
|
+
}).catch(function reportThemeReloadFailure(error){
|
|
46
|
+
console.warn(
|
|
47
|
+
'[Arcane theme] Unable to apply the changed host appearance.',
|
|
48
|
+
error
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
);
|
|
53
|
+
const dispose=function disposeArcaneThemeAppearanceListener(){
|
|
54
|
+
if(!active){
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
active=false;
|
|
58
|
+
if(typeof hostUnsubscribe==='function'){
|
|
59
|
+
hostUnsubscribe();
|
|
60
|
+
}
|
|
61
|
+
if(globalThis[listenerKey]===dispose){
|
|
62
|
+
delete globalThis[listenerKey];
|
|
63
|
+
}
|
|
64
|
+
return true;
|
|
65
|
+
};
|
|
66
|
+
Object.defineProperty(dispose,'dispose',{
|
|
67
|
+
value:dispose,
|
|
68
|
+
enumerable:false,
|
|
69
|
+
configurable:false,
|
|
70
|
+
writable:false
|
|
71
|
+
});
|
|
72
|
+
globalThis[listenerKey]=dispose;
|
|
73
|
+
}
|
|
5
74
|
|
|
6
75
|
export function bootstrapArcaneTheme(options={}){
|
|
7
76
|
const useSharedPromise=Object.keys(options).length===0;
|
|
8
|
-
if(useSharedPromise&&globalThis[sharedKey])
|
|
77
|
+
if(useSharedPromise&&globalThis[sharedKey]){
|
|
78
|
+
installAppearanceListener(globalThis[sharedKey]);
|
|
79
|
+
return globalThis[sharedKey];
|
|
80
|
+
}
|
|
9
81
|
|
|
10
82
|
const ready=loadAndApplyTheme(options).catch(error=>{
|
|
11
83
|
console.warn('[Arcane theme] Unable to load the saved appearance; using the system theme.',error);
|
|
@@ -13,14 +85,16 @@ export function bootstrapArcaneTheme(options={}){
|
|
|
13
85
|
});
|
|
14
86
|
|
|
15
87
|
if(useSharedPromise) globalThis[sharedKey]=ready;
|
|
16
|
-
if(useSharedPromise
|
|
17
|
-
|
|
18
|
-
const result=await ready;
|
|
19
|
-
if(result?.manager) await result.manager.load();
|
|
20
|
-
});
|
|
88
|
+
if(useSharedPromise){
|
|
89
|
+
installAppearanceListener(ready);
|
|
21
90
|
}
|
|
22
91
|
return ready;
|
|
23
92
|
}
|
|
24
93
|
|
|
94
|
+
export function disposeArcaneThemeBootstrap(){
|
|
95
|
+
const dispose=globalThis[listenerKey];
|
|
96
|
+
return typeof dispose==='function'?dispose():false;
|
|
97
|
+
}
|
|
98
|
+
|
|
25
99
|
export const arcaneThemeReady=bootstrapArcaneTheme();
|
|
26
100
|
export default arcaneThemeReady;
|
|
@@ -2,6 +2,10 @@ import Theme,{arcaneDarkThemeTokens,arcaneLightThemeTokens} from '../entities/Th
|
|
|
2
2
|
import PreferenceStore from './PreferenceStore.js';
|
|
3
3
|
import {applyAppearancePreferences,createAppearancePreferenceStore} from './AppearancePreferences.js';
|
|
4
4
|
import SystemAppearance from './SystemAppearance.js';
|
|
5
|
+
import {
|
|
6
|
+
createArcaneEventSource,
|
|
7
|
+
projectArcaneDOMEvent
|
|
8
|
+
} from 'arcane-os/event-manager';
|
|
5
9
|
|
|
6
10
|
const skinSchema=Object.freeze([
|
|
7
11
|
{key:'appearance.activeSkin',type:'text',defaultValue:''},
|
|
@@ -9,6 +13,9 @@ const skinSchema=Object.freeze([
|
|
|
9
13
|
]);
|
|
10
14
|
|
|
11
15
|
export default class ThemeManager{
|
|
16
|
+
#events;
|
|
17
|
+
#operationSequence=0;
|
|
18
|
+
|
|
12
19
|
constructor({appearanceStore=null,skinStore=null,systemAppearance=null,root=null}={}){
|
|
13
20
|
this.appearanceStore=appearanceStore||createAppearancePreferenceStore();
|
|
14
21
|
this.skinStore=skinStore||new PreferenceStore({namespace:'arcane',schema:skinSchema});
|
|
@@ -17,6 +24,10 @@ export default class ThemeManager{
|
|
|
17
24
|
this.appearance=this.appearanceStore.defaults();
|
|
18
25
|
this.skinState=this.skinStore.defaults();
|
|
19
26
|
this.customTheme=null;
|
|
27
|
+
this.#events=createArcaneEventSource(this,{
|
|
28
|
+
source:'theme-manager',
|
|
29
|
+
eventTypes:Object.freeze(['arcane-theme-change'])
|
|
30
|
+
});
|
|
20
31
|
}
|
|
21
32
|
|
|
22
33
|
async load(){
|
|
@@ -53,7 +64,7 @@ export default class ThemeManager{
|
|
|
53
64
|
]);
|
|
54
65
|
this.appearance={...this.appearance,'appearance.colorScheme':normalized};
|
|
55
66
|
this.skinState={...this.skinState,'appearance.activeSkin':''};
|
|
56
|
-
this.apply();await this.syncSystemAppearance();this.emit();
|
|
67
|
+
this.apply();await this.syncSystemAppearance();this.emit('color-scheme-changed');
|
|
57
68
|
return this.current();
|
|
58
69
|
}
|
|
59
70
|
|
|
@@ -67,7 +78,7 @@ export default class ThemeManager{
|
|
|
67
78
|
this.customTheme=theme;
|
|
68
79
|
this.skinState={...this.skinState,'appearance.customSkin':JSON.stringify(theme),'appearance.activeSkin':'custom'};
|
|
69
80
|
this.appearance={...this.appearance,'appearance.colorScheme':theme.scheme};
|
|
70
|
-
this.apply();await this.syncSystemAppearance();this.emit();
|
|
81
|
+
this.apply();await this.syncSystemAppearance();this.emit('custom-theme-activated');
|
|
71
82
|
return this.current();
|
|
72
83
|
}
|
|
73
84
|
|
|
@@ -80,7 +91,7 @@ export default class ThemeManager{
|
|
|
80
91
|
]);
|
|
81
92
|
this.skinState={...this.skinState,'appearance.activeSkin':'custom'};
|
|
82
93
|
this.appearance={...this.appearance,'appearance.colorScheme':this.customTheme.scheme};
|
|
83
|
-
this.apply();await this.syncSystemAppearance();this.emit();
|
|
94
|
+
this.apply();await this.syncSystemAppearance();this.emit('custom-theme-activated');
|
|
84
95
|
return this.current();
|
|
85
96
|
}
|
|
86
97
|
|
|
@@ -97,7 +108,7 @@ export default class ThemeManager{
|
|
|
97
108
|
]);
|
|
98
109
|
this.customTheme=null;
|
|
99
110
|
this.skinState={...this.skinState,'appearance.customSkin':'','appearance.activeSkin':''};
|
|
100
|
-
this.apply();await this.syncSystemAppearance();this.emit();
|
|
111
|
+
this.apply();await this.syncSystemAppearance();this.emit('custom-theme-reset');
|
|
101
112
|
return this.current();
|
|
102
113
|
}
|
|
103
114
|
|
|
@@ -112,10 +123,31 @@ export default class ThemeManager{
|
|
|
112
123
|
});
|
|
113
124
|
}
|
|
114
125
|
|
|
115
|
-
emit(){
|
|
116
|
-
|
|
117
|
-
|
|
126
|
+
emit(reason='theme-state-applied'){
|
|
127
|
+
const state=this.current();
|
|
128
|
+
const detail=Object.freeze({...state,reason});
|
|
129
|
+
this.#operationSequence+=1;
|
|
130
|
+
const {occurrence}=this.#events.dispatch(
|
|
131
|
+
'arcane-theme-change',
|
|
132
|
+
detail,
|
|
133
|
+
{
|
|
134
|
+
operationId:`theme-manager-${this.#events.instanceId}-${this.#operationSequence}`,
|
|
135
|
+
publicDetail:Object.freeze({mode:detail.mode,reason})
|
|
136
|
+
}
|
|
137
|
+
);
|
|
138
|
+
if(typeof globalThis.CustomEvent==='function'
|
|
139
|
+
&&typeof globalThis.dispatchEvent==='function'){
|
|
140
|
+
projectArcaneDOMEvent(globalThis,occurrence);
|
|
118
141
|
}
|
|
142
|
+
return occurrence;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
dispose(){
|
|
146
|
+
return this.#events.dispose();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
destroy(){
|
|
150
|
+
return this.dispose();
|
|
119
151
|
}
|
|
120
152
|
}
|
|
121
153
|
|