aegis-desktop 1.0.13 → 1.0.15
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/SOURCE-OFFER.txt +9 -0
- package/bin/aegis.js +1 -32
- package/lib/account-key.js +1 -56
- package/lib/auto.js +1 -430
- package/lib/avatar/companion.js +1 -698
- package/lib/avatar/cosmetics.js +1 -532
- package/lib/avatar/events.js +1 -344
- package/lib/avatar/holder-turn.js +1 -350
- package/lib/avatar/holders.js +1 -612
- package/lib/avatar/identity.js +1 -641
- package/lib/avatar/level.js +1 -368
- package/lib/avatar/mirror.js +1 -524
- package/lib/avatar/persona.js +1 -388
- package/lib/avatar/profile.js +1 -977
- package/lib/avatar/register.js +1 -307
- package/lib/avatar/store.js +1 -472
- package/lib/avatar/tiers.js +1 -38
- package/lib/avatar/transfer.js +1 -415
- package/lib/avatar/turn.js +1 -216
- package/lib/avatar/voice.js +1 -328
- package/lib/avatar/xp.js +1 -404
- package/lib/deep-link.js +1 -120
- package/lib/fingerprint.js +2 -347
- package/lib/google-connect.js +1 -169
- package/lib/local/agents.js +1 -105
- package/lib/local/autonomous.js +1 -1045
- package/lib/local/claude-pro.js +1 -993
- package/lib/local/context.js +1 -95
- package/lib/local/engine.js +1 -3228
- package/lib/local/entitlement.js +1 -654
- package/lib/local/git-scope.js +1 -228
- package/lib/local/local.js +1 -2135
- package/lib/local/prompt.js +1 -275
- package/lib/local/queue.js +1 -560
- package/lib/local/session-rounds.js +1 -208
- package/lib/local/shell.js +1 -680
- package/lib/local/telemetry.js +1 -470
- package/lib/local/tools.js +1 -1238
- package/lib/local/turn-guard.js +1 -300
- package/lib/local/worktree-lock.js +1 -235
- package/lib/provider-key-mirror.js +1 -252
- package/lib/quick-launcher.js +1 -84
- package/lib/settings.js +1 -890
- package/lib/sync/memory-queue.js +1 -60
- package/lib/sync/persist-gate.js +1 -148
- package/lib/sync/sessions.js +1 -106
- package/lib/window-state.js +1 -66
- package/main.js +3 -4390
- package/package.json +3 -5
- package/preload.js +3 -628
- package/renderer/app.js +1 -6560
- package/renderer/avatar/assemble.js +1 -335
- package/renderer/avatar/avatar.js +1 -380
- package/renderer/avatar/cards.js +1 -141
- package/renderer/avatar/hud.js +1 -206
- package/renderer/avatar/machine.js +1 -213
- package/renderer/avatar/pane.js +1 -439
- package/renderer/avatar/parts.js +1 -437
- package/renderer/budget.js +1 -168
- package/renderer/companion.js +1 -223
- package/renderer/diff.js +1 -359
- package/renderer/diffview.js +1 -336
- package/renderer/fingerprint.js +3 -720
- package/renderer/markdown.js +1 -168
- package/renderer/provider-toolbar.js +1 -134
- package/renderer/providers.js +1 -259
- package/renderer/quick.js +1 -191
- package/renderer/stream-policy.js +1 -306
- package/renderer/transcript-view.js +1 -323
- package/renderer/usage.js +1 -1239
- package/vendor/aegis.js +1 -2054
- package/vendor/attest.js +1 -442
- package/vendor/authorship-key.js +1 -410
- package/vendor/auto-provision.js +1 -134
- package/vendor/brain-catalog.js +1 -268
- package/vendor/byok-catalog.js +1 -230
- package/vendor/credentials.js +1 -494
- package/vendor/device-fingerprint.js +1 -141
- package/vendor/drive-connect.js +1 -85
- package/vendor/drive-upload.js +1 -174
- package/vendor/env-file.js +1 -1273
- package/vendor/fingerprint.js +1 -1064
- package/vendor/foreign-memory.js +1 -669
- package/vendor/google-signin.js +1 -125
- package/vendor/hooks.js +1 -430
- package/vendor/model-discovery.js +1 -330
- package/vendor/platform.js +1 -937
- package/vendor/prefs.js +1 -351
- package/vendor/presence-constants.js +1 -107
- package/vendor/presence.js +1 -438
- package/vendor/record-vault.js +3 -1055
- package/vendor/session-store.js +1 -548
- package/vendor/topup.js +1 -297
- package/vendor/touch.js +1 -458
- package/vendor/update.js +1 -704
- package/vendor/user-models.js +1 -381
|
@@ -1,382 +1,3 @@
|
|
|
1
1
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
2
|
// Copyright (C) 2026 Niklas Borneklint — AEGIS Code (aegiscode) / Aegiscloud.
|
|
3
|
-
|
|
4
|
-
// Conveying a modified copy, or running one as a network service, owes the
|
|
5
|
-
// corresponding source under this licence (§5/§13) — see LICENSING.md.
|
|
6
|
-
'use strict';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* avatar.js — the mount: stage, HUD, pane, and the rules that keep the avatar
|
|
10
|
-
* out of everything else's way.
|
|
11
|
-
*
|
|
12
|
-
* The three modules beside this one are pure or nearly so (`parts.js` is data,
|
|
13
|
-
* `assemble.js` builds a string, `hud.js` builds a view model, `machine.js`
|
|
14
|
-
* owns the face's state). This file is where they meet the document, and that
|
|
15
|
-
* makes it the only place the phase's non-functional promises can be kept or
|
|
16
|
-
* broken:
|
|
17
|
-
*
|
|
18
|
-
* 1. **Never blocks input.** No key listener is registered anywhere in
|
|
19
|
-
* `renderer/avatar/`, so the avatar cannot consume a keystroke — Escape
|
|
20
|
-
* still reaches the interrupt handler and typing still reaches the
|
|
21
|
-
* composer, whatever the avatar is doing. Painting is coalesced to at most
|
|
22
|
-
* one write per animation frame (`schedule()`), so a stream that emits
|
|
23
|
-
* fifty deltas in a frame still costs one repaint, and every entry point is
|
|
24
|
-
* wrapped: a throw inside the avatar is swallowed and reported, never
|
|
25
|
-
* propagated into the chat path. `desktop/test/electron-smoke-main.js`
|
|
26
|
-
* proves the two behaviours this protects (scroll-hold and interrupt) with
|
|
27
|
-
* the avatar mounted and streaming.
|
|
28
|
-
*
|
|
29
|
-
* 2. **Never claims a level it was not given.** There is no arithmetic on XP
|
|
30
|
-
* in this file. It renders whatever projection main hands it, and when main
|
|
31
|
-
* has nothing to hand it it says `level unknown` (see hud.js).
|
|
32
|
-
*
|
|
33
|
-
* 3. **Accessible by construction.** The SVG is `aria-hidden` +
|
|
34
|
-
* `focusable="false"` (the assembler sets it), the layer that matters is a
|
|
35
|
-
* `role="status"` text readout beside it, the XP bar is `aria-hidden` and
|
|
36
|
-
* its numbers live in that readout, and `prefers-reduced-motion` is read
|
|
37
|
-
* from the OS and forced through `persona.appliesMotion` — the same
|
|
38
|
-
* function the storage layer uses, so a persona cannot opt out of the
|
|
39
|
-
* accessibility preference.
|
|
40
|
-
*
|
|
41
|
-
* Source resolution is deliberately forward-compatible: `window.avatar` is
|
|
42
|
-
* used when it exists, and `preview()` exists so the
|
|
43
|
-
* harnesses (and the customization pane, which previews edits before anything
|
|
44
|
-
* is written) can drive the avatar without it. Both are one code path — the
|
|
45
|
-
* preview state is an override layer over the real one, and it is labelled as
|
|
46
|
-
* such in the readout's own vocabulary (`source`), never presented as truth.
|
|
47
|
-
*/
|
|
48
|
-
|
|
49
|
-
const pkg = typeof window !== 'undefined' && window.AegisAvatarAssemble
|
|
50
|
-
? {
|
|
51
|
-
parts: window.AegisAvatarParts,
|
|
52
|
-
assemble: window.AegisAvatarAssemble.assemble,
|
|
53
|
-
hud: window.AegisAvatarHud,
|
|
54
|
-
machine: window.AegisAvatarMachine,
|
|
55
|
-
pane: window.AegisAvatarPane,
|
|
56
|
-
persona: window.AegisAvatarPersona,
|
|
57
|
-
}
|
|
58
|
-
: typeof module !== 'undefined' && module.exports
|
|
59
|
-
? {
|
|
60
|
-
parts: require('./parts.js'),
|
|
61
|
-
assemble: require('./assemble.js').assemble,
|
|
62
|
-
hud: require('./hud.js'),
|
|
63
|
-
machine: require('./machine.js'),
|
|
64
|
-
pane: require('./pane.js'),
|
|
65
|
-
persona: require('../../lib/avatar/persona.js'),
|
|
66
|
-
}
|
|
67
|
-
: null;
|
|
68
|
-
|
|
69
|
-
if (!pkg) throw new Error('avatar/avatar.js: the avatar modules must be loaded first');
|
|
70
|
-
|
|
71
|
-
/** How long a quiet `idle` state sits before the face dozes off. */
|
|
72
|
-
const IDLE_TIMEOUT_MS = 5 * 60 * 1000;
|
|
73
|
-
/** The clock the idle timeout is evaluated on. Not a frame loop — see §6. */
|
|
74
|
-
const TICK_INTERVAL_MS = 30000;
|
|
75
|
-
|
|
76
|
-
function safe(fn, fallback) {
|
|
77
|
-
try {
|
|
78
|
-
return fn();
|
|
79
|
-
} catch {
|
|
80
|
-
return fallback;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* The data source. `window.avatar` is the preload bridge the host will expose
|
|
86
|
-
* (`state()` returning `{ persona, projection, capabilities, warnings }`); when
|
|
87
|
-
* it is missing the avatar renders the honest unknown state rather than
|
|
88
|
-
* inventing numbers.
|
|
89
|
-
*/
|
|
90
|
-
function resolveSource(win) {
|
|
91
|
-
const bridge = win && win.avatar;
|
|
92
|
-
if (bridge && typeof bridge.state === 'function') {
|
|
93
|
-
return {
|
|
94
|
-
kind: 'main',
|
|
95
|
-
reason: '',
|
|
96
|
-
state: () => Promise.resolve(bridge.state()),
|
|
97
|
-
set: typeof bridge.set === 'function' ? (patch) => Promise.resolve(bridge.set(patch)) : null,
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
return {
|
|
101
|
-
kind: 'none',
|
|
102
|
-
reason: 'main has no avatar surface yet',
|
|
103
|
-
state: () => Promise.resolve(null),
|
|
104
|
-
set: null,
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Mount the avatar.
|
|
110
|
-
*
|
|
111
|
-
* @param {object} input
|
|
112
|
-
* @param {object} input.doc
|
|
113
|
-
* @param {object} input.win
|
|
114
|
-
* @param {object} input.els element handles (see ELEMENT_IDS in app.js); any
|
|
115
|
-
* missing handle is skipped rather than thrown on
|
|
116
|
-
* @param {object} [input.source] override the bridge lookup (tests)
|
|
117
|
-
* @param {object} [input.persona] starting persona
|
|
118
|
-
* @param {object} [input.projection] starting level projection
|
|
119
|
-
* @param {object} [input.capabilities] starting capabilities
|
|
120
|
-
* @param {number} [input.idleTimeoutMs]
|
|
121
|
-
*/
|
|
122
|
-
function mountAvatar(input = {}) {
|
|
123
|
-
const doc = input.doc;
|
|
124
|
-
const win = input.win || (typeof window !== 'undefined' ? window : null);
|
|
125
|
-
if (!doc || typeof doc.createElement !== 'function') {
|
|
126
|
-
throw new Error('mountAvatar: a document is required');
|
|
127
|
-
}
|
|
128
|
-
const els = input.els || {};
|
|
129
|
-
const source = input.source || resolveSource(win);
|
|
130
|
-
|
|
131
|
-
const signals = pkg.machine.createAvatarSignals({ idleTimeoutMs: input.idleTimeoutMs || IDLE_TIMEOUT_MS });
|
|
132
|
-
|
|
133
|
-
let persona = input.persona && typeof input.persona === 'object'
|
|
134
|
-
? input.persona
|
|
135
|
-
: pkg.persona
|
|
136
|
-
? pkg.persona.defaultPersona()
|
|
137
|
-
: pkg.parts.defaultPersona();
|
|
138
|
-
let projection = input.projection && typeof input.projection === 'object' ? input.projection : null;
|
|
139
|
-
let capabilities = input.capabilities && typeof input.capabilities === 'object' ? input.capabilities : null;
|
|
140
|
-
let reason = input.reason || source.reason || '';
|
|
141
|
-
|
|
142
|
-
const overrides = { persona: null, projection: null, capabilities: null, reason: null, active: false };
|
|
143
|
-
let reducedMotion = false;
|
|
144
|
-
let frame = null;
|
|
145
|
-
let destroyed = false;
|
|
146
|
-
let lastPainting = null;
|
|
147
|
-
let tickTimer = null;
|
|
148
|
-
|
|
149
|
-
// ---- OS preference -----------------------------------------------------
|
|
150
|
-
const media = safe(() => (win && typeof win.matchMedia === 'function'
|
|
151
|
-
? win.matchMedia('(prefers-reduced-motion: reduce)')
|
|
152
|
-
: null), null);
|
|
153
|
-
reducedMotion = Boolean(media && media.matches);
|
|
154
|
-
if (media && typeof media.addEventListener === 'function') {
|
|
155
|
-
safe(() => media.addEventListener('change', (event) => {
|
|
156
|
-
reducedMotion = Boolean(event && 'matches' in event ? event.matches : media.matches);
|
|
157
|
-
schedule();
|
|
158
|
-
}));
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// ---- the customization pane -------------------------------------------
|
|
162
|
-
const pane = safe(() => pkg.pane.buildPane({
|
|
163
|
-
doc,
|
|
164
|
-
persona,
|
|
165
|
-
capabilities,
|
|
166
|
-
onPatch: (patch) => {
|
|
167
|
-
const validated = safe(() => (pkg.persona ? pkg.persona.update(persona, patch).persona : persona), persona);
|
|
168
|
-
applyPersona(validated.persona || validated, { repaint: true });
|
|
169
|
-
return persist(patch);
|
|
170
|
-
},
|
|
171
|
-
}), null);
|
|
172
|
-
if (pane && els.paneHost && typeof els.paneHost.appendChild === 'function') {
|
|
173
|
-
els.paneHost.appendChild(pane.root);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Persist a patch through the bridge. Returns the shape the pane's status
|
|
178
|
-
* line prints, so "saved" is only ever shown for a write that really landed.
|
|
179
|
-
*/
|
|
180
|
-
function persist(patch) {
|
|
181
|
-
if (!source.set) return { ok: true, saved: false, reason: source.reason || 'no write surface' };
|
|
182
|
-
return source.set(patch).then(
|
|
183
|
-
(res) => {
|
|
184
|
-
if (res && res.ok === false) return { ok: false, reason: res.reason || 'refused' };
|
|
185
|
-
if (res && res.persona) applyPersona(res.persona, { repaint: false });
|
|
186
|
-
return { ok: true, saved: true };
|
|
187
|
-
},
|
|
188
|
-
(err) => ({ ok: false, reason: (err && err.message) || 'write failed' })
|
|
189
|
-
);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// ---- painting ----------------------------------------------------------
|
|
193
|
-
function schedule() {
|
|
194
|
-
if (destroyed) return;
|
|
195
|
-
if (frame !== null) return;
|
|
196
|
-
const raf = win && typeof win.requestAnimationFrame === 'function' ? win.requestAnimationFrame : null;
|
|
197
|
-
if (raf) {
|
|
198
|
-
frame = safe(() => raf(paint), null);
|
|
199
|
-
if (frame !== null) return;
|
|
200
|
-
}
|
|
201
|
-
// No rAF (a bare test document): coalesce on the microtask queue instead, so
|
|
202
|
-
// the "one repaint per burst" property still holds without a timer.
|
|
203
|
-
frame = 0;
|
|
204
|
-
Promise.resolve().then(() => {
|
|
205
|
-
frame = null;
|
|
206
|
-
paint();
|
|
207
|
-
});
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function effective() {
|
|
211
|
-
return {
|
|
212
|
-
persona: overrides.active && overrides.persona ? overrides.persona : persona,
|
|
213
|
-
projection: overrides.active ? overrides.projection : projection,
|
|
214
|
-
capabilities: overrides.active ? overrides.capabilities : capabilities,
|
|
215
|
-
reason: overrides.active && overrides.reason !== null ? overrides.reason : reason,
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function paint() {
|
|
220
|
-
if (destroyed) return;
|
|
221
|
-
const now = effective();
|
|
222
|
-
const assembled = safe(
|
|
223
|
-
() => pkg.assemble(now.persona, { expression: signals.expression, reducedMotion }),
|
|
224
|
-
null
|
|
225
|
-
);
|
|
226
|
-
if (!assembled) {
|
|
227
|
-
// A manifest that cannot assemble must not leave a stale face claiming to
|
|
228
|
-
// be current: blank the stage and say so in the readout.
|
|
229
|
-
if (els.stage) els.stage.innerHTML = '';
|
|
230
|
-
} else if (els.stage) {
|
|
231
|
-
// Generated markup only: ids, geometry and colour roles from the manifest.
|
|
232
|
-
// No persona text ever reaches this string (see assemble.js rule 2), so
|
|
233
|
-
// there is nothing here for a persona to inject through.
|
|
234
|
-
els.stage.innerHTML = assembled.svg;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
const model = pkg.hud.hudModel({
|
|
238
|
-
persona: now.persona,
|
|
239
|
-
projection: now.projection,
|
|
240
|
-
capabilities: now.capabilities,
|
|
241
|
-
expression: signals.expression,
|
|
242
|
-
expressionLabel: signals.label,
|
|
243
|
-
reason: now.reason,
|
|
244
|
-
});
|
|
245
|
-
safe(() => pkg.hud.renderHud(els, model));
|
|
246
|
-
if (pane && typeof pane.update === 'function') safe(() => pane.update(now.persona));
|
|
247
|
-
lastPainting = {
|
|
248
|
-
expression: assembled ? assembled.expression : null,
|
|
249
|
-
motion: assembled ? assembled.motion : null,
|
|
250
|
-
warnings: assembled ? assembled.warnings.slice() : ['assemble failed'],
|
|
251
|
-
missing: assembled ? assembled.missing.slice() : [],
|
|
252
|
-
hud: model,
|
|
253
|
-
source: overrides.active ? 'preview' : source.kind,
|
|
254
|
-
};
|
|
255
|
-
return lastPainting;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
function applyPersona(next, opts = {}) {
|
|
259
|
-
if (!next || typeof next !== 'object') return persona;
|
|
260
|
-
const validated = safe(() => (pkg.persona ? pkg.persona.validate(next).persona : next), next);
|
|
261
|
-
persona = validated && validated.persona ? validated.persona : validated;
|
|
262
|
-
if (opts.repaint !== false) schedule();
|
|
263
|
-
return persona;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// ---- reads from main ---------------------------------------------------
|
|
267
|
-
function refresh() {
|
|
268
|
-
if (destroyed) return Promise.resolve(null);
|
|
269
|
-
return Promise.resolve(safe(() => source.state(), null)).then(
|
|
270
|
-
(state) => {
|
|
271
|
-
if (destroyed || !state || typeof state !== 'object') {
|
|
272
|
-
if (!state && !reason) reason = source.reason || 'main has not published a level yet';
|
|
273
|
-
schedule();
|
|
274
|
-
return null;
|
|
275
|
-
}
|
|
276
|
-
if (state.persona) applyPersona(state.persona, { repaint: false });
|
|
277
|
-
if (state.projection && typeof state.projection === 'object') projection = state.projection;
|
|
278
|
-
else if (typeof state.level === 'number') projection = state;
|
|
279
|
-
if (state.capabilities && typeof state.capabilities === 'object') capabilities = state.capabilities;
|
|
280
|
-
reason = '';
|
|
281
|
-
schedule();
|
|
282
|
-
return state;
|
|
283
|
-
},
|
|
284
|
-
(err) => {
|
|
285
|
-
reason = `level unavailable — ${(err && err.message) || 'read failed'}`;
|
|
286
|
-
schedule();
|
|
287
|
-
return null;
|
|
288
|
-
}
|
|
289
|
-
);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// ---- the face follows the engine ---------------------------------------
|
|
293
|
-
function signal(name, meta) {
|
|
294
|
-
const out = safe(() => signals.signal(name, meta), null);
|
|
295
|
-
if (out) schedule();
|
|
296
|
-
return out;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
const api = {
|
|
300
|
-
get expression() {
|
|
301
|
-
return signals.expression;
|
|
302
|
-
},
|
|
303
|
-
get state() {
|
|
304
|
-
return signals.state;
|
|
305
|
-
},
|
|
306
|
-
get persona() {
|
|
307
|
-
return persona;
|
|
308
|
-
},
|
|
309
|
-
get painting() {
|
|
310
|
-
return lastPainting;
|
|
311
|
-
},
|
|
312
|
-
signal,
|
|
313
|
-
turnStart: (meta) => signal('turn.start', meta),
|
|
314
|
-
stream: () => signal('turn.stream'),
|
|
315
|
-
turnEnd: (meta) => signal('turn.end', meta),
|
|
316
|
-
tool: (tool) => signal(pkg.machine.signalForTool(tool), { tool: pkg.machine.toolName(tool) }),
|
|
317
|
-
approvalCard: () => signal('approval.card'),
|
|
318
|
-
approvalDecided: () => signal('approval.decided'),
|
|
319
|
-
queue: () => signal('queue.progress'),
|
|
320
|
-
syncOk: () => signal('sync.ok'),
|
|
321
|
-
syncError: (meta) => signal('sync.error', meta),
|
|
322
|
-
poke: () => signal('poke'),
|
|
323
|
-
celebrateDone: () => signal('celebrate.done'),
|
|
324
|
-
reset: () => signal('reset'),
|
|
325
|
-
tick: (now) => {
|
|
326
|
-
const out = safe(() => signals.tick(now), null);
|
|
327
|
-
if (out && out.changed) schedule();
|
|
328
|
-
return out;
|
|
329
|
-
},
|
|
330
|
-
on: (fn) => signals.on(fn),
|
|
331
|
-
setPersona: (next) => applyPersona(next),
|
|
332
|
-
/** Ask main for a fresh projection (a turn that saved memory moved XP). */
|
|
333
|
-
refresh,
|
|
334
|
-
/**
|
|
335
|
-
* The preview seam. Harnesses (the Electron smoke leg and the marketing
|
|
336
|
-
* shot driver) and the pane use it to render a persona/level this process
|
|
337
|
-
* was not handed by main, and `painting.source` reports it as `preview` so
|
|
338
|
-
* the evidence can never read as if main had published it.
|
|
339
|
-
*/
|
|
340
|
-
preview(next = {}) {
|
|
341
|
-
overrides.active = true;
|
|
342
|
-
if (next.persona) overrides.persona = next.persona;
|
|
343
|
-
if ('projection' in next) overrides.projection = next.projection;
|
|
344
|
-
if ('capabilities' in next) overrides.capabilities = next.capabilities;
|
|
345
|
-
if ('reason' in next) overrides.reason = next.reason === undefined ? null : next.reason;
|
|
346
|
-
schedule();
|
|
347
|
-
return lastPainting;
|
|
348
|
-
},
|
|
349
|
-
clearPreview() {
|
|
350
|
-
overrides.active = false;
|
|
351
|
-
schedule();
|
|
352
|
-
return lastPainting;
|
|
353
|
-
},
|
|
354
|
-
/** The readout text, for harnesses that assert on what a user would read. */
|
|
355
|
-
readout() {
|
|
356
|
-
return els.readout ? String(els.readout.textContent || '') : '';
|
|
357
|
-
},
|
|
358
|
-
destroy() {
|
|
359
|
-
destroyed = true;
|
|
360
|
-
if (tickTimer !== null) safe(() => win.clearInterval(tickTimer));
|
|
361
|
-
tickTimer = null;
|
|
362
|
-
if (pane && typeof pane.destroy === 'function') safe(() => pane.destroy());
|
|
363
|
-
if (els.stage) els.stage.innerHTML = '';
|
|
364
|
-
},
|
|
365
|
-
};
|
|
366
|
-
|
|
367
|
-
// ---- the idle clock ----------------------------------------------------
|
|
368
|
-
// One 30s interval for the doze timeout — not a frame loop. §6 of the plan
|
|
369
|
-
// spells out why: "pure SVG, no WebGL, no per-frame work". The interval only
|
|
370
|
-
// evaluates a comparison; it paints only when the machine actually changes.
|
|
371
|
-
if (win && typeof win.setInterval === 'function') {
|
|
372
|
-
tickTimer = safe(() => win.setInterval(() => api.tick(Date.now()), TICK_INTERVAL_MS), null);
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
schedule();
|
|
376
|
-
return api;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
const API = Object.freeze({ mountAvatar, resolveSource, IDLE_TIMEOUT_MS, TICK_INTERVAL_MS });
|
|
380
|
-
|
|
381
|
-
if (typeof module !== 'undefined' && module.exports) module.exports = API;
|
|
382
|
-
else if (typeof globalThis !== 'undefined') globalThis.AegisAvatar = API;
|
|
3
|
+
"use strict";const pkg="undefined"!=typeof window&&window.AegisAvatarAssemble?{parts:window.AegisAvatarParts,assemble:window.AegisAvatarAssemble.assemble,hud:window.AegisAvatarHud,machine:window.AegisAvatarMachine,pane:window.AegisAvatarPane,persona:window.AegisAvatarPersona}:"undefined"!=typeof module&&module.exports?{parts:require("./parts.js"),assemble:require("./assemble.js").assemble,hud:require("./hud.js"),machine:require("./machine.js"),pane:require("./pane.js"),persona:require("../../lib/avatar/persona.js")}:null;if(!pkg)throw new Error("avatar/avatar.js: the avatar modules must be loaded first");const IDLE_TIMEOUT_MS=3e5,TICK_INTERVAL_MS=3e4;function safe(e,n){try{return e()}catch{return n}}function resolveSource(e){const n=e&&e.avatar;return n&&"function"==typeof n.state?{kind:"main",reason:"",state:()=>Promise.resolve(n.state()),set:"function"==typeof n.set?e=>Promise.resolve(n.set(e)):null}:{kind:"none",reason:"main has no avatar surface yet",state:()=>Promise.resolve(null),set:null}}function mountAvatar(e={}){const n=e.doc,a=e.win||("undefined"!=typeof window?window:null);if(!n||"function"!=typeof n.createElement)throw new Error("mountAvatar: a document is required");const t=e.els||{},o=e.source||resolveSource(a),r=pkg.machine.createAvatarSignals({idleTimeoutMs:e.idleTimeoutMs||3e5});let s=e.persona&&"object"==typeof e.persona?e.persona:pkg.persona?pkg.persona.defaultPersona():pkg.parts.defaultPersona(),i=e.projection&&"object"==typeof e.projection?e.projection:null,l=e.capabilities&&"object"==typeof e.capabilities?e.capabilities:null,p=e.reason||o.reason||"";const u={persona:null,projection:null,capabilities:null,reason:null,active:!1};let c=!1,d=null,f=!1,m=null,v=null;const g=safe((()=>a&&"function"==typeof a.matchMedia?a.matchMedia("(prefers-reduced-motion: reduce)"):null),null);c=Boolean(g&&g.matches),g&&"function"==typeof g.addEventListener&&safe((()=>g.addEventListener("change",(e=>{c=Boolean(e&&"matches"in e?e.matches:g.matches),h()}))));const b=safe((()=>pkg.pane.buildPane({doc:n,persona:s,capabilities:l,onPatch:e=>{const n=safe((()=>pkg.persona?pkg.persona.update(s,e).persona:s),s);return y(n.persona||n,{repaint:!0}),function(e){return o.set?o.set(e).then((e=>e&&!1===e.ok?{ok:!1,reason:e.reason||"refused"}:(e&&e.persona&&y(e.persona,{repaint:!1}),{ok:!0,saved:!0})),(e=>({ok:!1,reason:e&&e.message||"write failed"}))):{ok:!0,saved:!1,reason:o.reason||"no write surface"}}(e)}})),null);function h(){if(f)return;if(null!==d)return;const e=a&&"function"==typeof a.requestAnimationFrame?a.requestAnimationFrame:null;e&&(d=safe((()=>e(w)),null),null!==d)||(d=0,Promise.resolve().then((()=>{d=null,w()})))}function w(){if(f)return;const e={persona:u.active&&u.persona?u.persona:s,projection:u.active?u.projection:i,capabilities:u.active?u.capabilities:l,reason:u.active&&null!==u.reason?u.reason:p},n=safe((()=>pkg.assemble(e.persona,{expression:r.expression,reducedMotion:c})),null);n?t.stage&&(t.stage.innerHTML=n.svg):t.stage&&(t.stage.innerHTML="");const a=pkg.hud.hudModel({persona:e.persona,projection:e.projection,capabilities:e.capabilities,expression:r.expression,expressionLabel:r.label,reason:e.reason});return safe((()=>pkg.hud.renderHud(t,a))),b&&"function"==typeof b.update&&safe((()=>b.update(e.persona))),m={expression:n?n.expression:null,motion:n?n.motion:null,warnings:n?n.warnings.slice():["assemble failed"],missing:n?n.missing.slice():[],hud:a,source:u.active?"preview":o.kind},m}function y(e,n={}){if(!e||"object"!=typeof e)return s;const a=safe((()=>pkg.persona?pkg.persona.validate(e).persona:e),e);return s=a&&a.persona?a.persona:a,!1!==n.repaint&&h(),s}function k(e,n){const a=safe((()=>r.signal(e,n)),null);return a&&h(),a}b&&t.paneHost&&"function"==typeof t.paneHost.appendChild&&t.paneHost.appendChild(b.root);const A={get expression(){return r.expression},get state(){return r.state},get persona(){return s},get painting(){return m},signal:k,turnStart:e=>k("turn.start",e),stream:()=>k("turn.stream"),turnEnd:e=>k("turn.end",e),tool:e=>k(pkg.machine.signalForTool(e),{tool:pkg.machine.toolName(e)}),approvalCard:()=>k("approval.card"),approvalDecided:()=>k("approval.decided"),queue:()=>k("queue.progress"),syncOk:()=>k("sync.ok"),syncError:e=>k("sync.error",e),poke:()=>k("poke"),celebrateDone:()=>k("celebrate.done"),reset:()=>k("reset"),tick:e=>{const n=safe((()=>r.tick(e)),null);return n&&n.changed&&h(),n},on:e=>r.on(e),setPersona:e=>y(e),refresh:function(){return f?Promise.resolve(null):Promise.resolve(safe((()=>o.state()),null)).then((e=>f||!e||"object"!=typeof e?(e||p||(p=o.reason||"main has not published a level yet"),h(),null):(e.persona&&y(e.persona,{repaint:!1}),e.projection&&"object"==typeof e.projection?i=e.projection:"number"==typeof e.level&&(i=e),e.capabilities&&"object"==typeof e.capabilities&&(l=e.capabilities),p="",h(),e)),(e=>(p=`level unavailable — ${e&&e.message||"read failed"}`,h(),null)))},preview:(e={})=>(u.active=!0,e.persona&&(u.persona=e.persona),"projection"in e&&(u.projection=e.projection),"capabilities"in e&&(u.capabilities=e.capabilities),"reason"in e&&(u.reason=void 0===e.reason?null:e.reason),h(),m),clearPreview:()=>(u.active=!1,h(),m),readout:()=>t.readout?String(t.readout.textContent||""):"",destroy(){f=!0,null!==v&&safe((()=>a.clearInterval(v))),v=null,b&&"function"==typeof b.destroy&&safe((()=>b.destroy())),t.stage&&(t.stage.innerHTML="")}};return a&&"function"==typeof a.setInterval&&(v=safe((()=>a.setInterval((()=>A.tick(Date.now())),3e4)),null)),h(),A}const API=Object.freeze({mountAvatar:mountAvatar,resolveSource:resolveSource,IDLE_TIMEOUT_MS:3e5,TICK_INTERVAL_MS:3e4});"undefined"!=typeof module&&module.exports?module.exports=API:"undefined"!=typeof globalThis&&(globalThis.AegisAvatar=API);
|
package/renderer/avatar/cards.js
CHANGED
|
@@ -1,143 +1,3 @@
|
|
|
1
1
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
2
|
// Copyright (C) 2026 Niklas Borneklint — AEGIS Code (aegiscode) / Aegiscloud.
|
|
3
|
-
|
|
4
|
-
// Conveying a modified copy, or running one as a network service, owes the
|
|
5
|
-
// corresponding source under this licence (§5/§13) — see LICENSING.md.
|
|
6
|
-
'use strict';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* cards.js — the companion card, rendered in the shape of the tool approval
|
|
10
|
-
* card.
|
|
11
|
-
*
|
|
12
|
-
* Two things make that more than a styling choice:
|
|
13
|
-
*
|
|
14
|
-
* 1. **It is genuinely the same card.** The class names, the action-row
|
|
15
|
-
* structure, the disable-on-first-click behaviour and the "what just
|
|
16
|
-
* happened" line are the approval card's, not a lookalike: a companion
|
|
17
|
-
* proposal and a tool request read as the same kind of event, because they
|
|
18
|
-
* are — an offer that does nothing until the user says yes.
|
|
19
|
-
* 2. **It presumes nothing.** The decision is sent to `onDecision(cardId,
|
|
20
|
-
* 'accept'|'dismiss')` and the card is dead afterwards. There is no
|
|
21
|
-
* "auto-accept after N seconds", no default focus that a stray Enter could
|
|
22
|
-
* resolve, and no path from rendering to acting: main's `dispatch()`
|
|
23
|
-
* (`lib/avatar/companion.js`) still refuses anything without the ticket the
|
|
24
|
-
* accept click produced. Rendering a card cannot cause an effect.
|
|
25
|
-
*
|
|
26
|
-
* Dependency-free and DOM-injectable (`doc`), so a test drives it without
|
|
27
|
-
* jsdom, exactly like `renderer/diffview.js` and `renderer/avatar/assemble.js`.
|
|
28
|
-
*/
|
|
29
|
-
|
|
30
|
-
const COMPANION_CARD_CLASS = 'companion-card';
|
|
31
|
-
const APPROVAL_CARD_CLASS = 'approval-card';
|
|
32
|
-
|
|
33
|
-
function textEl(doc, className, text) {
|
|
34
|
-
const el = doc.createElement('div');
|
|
35
|
-
el.className = className;
|
|
36
|
-
el.textContent = text;
|
|
37
|
-
return el;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Build one companion card element.
|
|
42
|
-
*
|
|
43
|
-
* @param {Document} doc the document (injected: this module touches nothing global)
|
|
44
|
-
* @param {object} card a card from `companion.createSession().consider(...)`
|
|
45
|
-
* — `{id, title, body, facts, actions, behaviour}`
|
|
46
|
-
* @param {(cardId: string, decision: 'accept'|'dismiss') => void} onDecision
|
|
47
|
-
* @returns {HTMLElement|null} null for a malformed card (never throws)
|
|
48
|
-
*/
|
|
49
|
-
function buildCompanionCard(doc, card, onDecision) {
|
|
50
|
-
if (!doc || !card || !card.id) return null;
|
|
51
|
-
|
|
52
|
-
const el = doc.createElement('div');
|
|
53
|
-
// Both classes: `.approval-card` for the shared look, `.companion-card` so a
|
|
54
|
-
// theme or a test can tell a proposal from a tool gate.
|
|
55
|
-
el.className = `${APPROVAL_CARD_CLASS} ${COMPANION_CARD_CLASS}`;
|
|
56
|
-
el.dataset.companionId = String(card.id);
|
|
57
|
-
if (card.behaviour) el.dataset.companionBehaviour = String(card.behaviour);
|
|
58
|
-
el.dataset.proactive = card.proactive ? 'true' : 'false';
|
|
59
|
-
|
|
60
|
-
el.appendChild(textEl(doc, 'approval-title', String(card.title || card.label || 'Suggestion')));
|
|
61
|
-
|
|
62
|
-
if (card.body) el.appendChild(textEl(doc, 'approval-summary', String(card.body)));
|
|
63
|
-
|
|
64
|
-
const facts = Array.isArray(card.facts) ? card.facts.filter(Boolean) : [];
|
|
65
|
-
if (facts.length) {
|
|
66
|
-
const list = doc.createElement('ul');
|
|
67
|
-
list.className = 'companion-facts';
|
|
68
|
-
for (const fact of facts.slice(0, 6)) {
|
|
69
|
-
const li = doc.createElement('li');
|
|
70
|
-
li.textContent = String(fact);
|
|
71
|
-
list.appendChild(li);
|
|
72
|
-
}
|
|
73
|
-
el.appendChild(list);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const actions = doc.createElement('div');
|
|
77
|
-
actions.className = 'approval-actions';
|
|
78
|
-
|
|
79
|
-
// Every button this card made, so resolving it can disable exactly those —
|
|
80
|
-
// no `querySelectorAll` on the container (a card is self-contained).
|
|
81
|
-
const buttons = [];
|
|
82
|
-
let resolved = false;
|
|
83
|
-
|
|
84
|
-
// No decision is sent until a click. Anything else (Enter on a focused card,
|
|
85
|
-
// a timer, a programmatic "accept") is not a path this card offers.
|
|
86
|
-
//
|
|
87
|
-
// The `resolved` flag is belt-and-braces alongside `disabled`: a browser will
|
|
88
|
-
// not fire a click on a disabled button, but the approval card's promise is
|
|
89
|
-
// that one card produces at most one decision, and that must not depend on
|
|
90
|
-
// the browser's event dispatch. main's `dispatch()` refuses the replayed
|
|
91
|
-
// ticket anyway — three independent layers, none of them trusted alone.
|
|
92
|
-
const decide = (action) => {
|
|
93
|
-
if (resolved) return;
|
|
94
|
-
resolved = true;
|
|
95
|
-
for (const btn of buttons) btn.disabled = true;
|
|
96
|
-
el.classList.add('resolved');
|
|
97
|
-
const tag = textEl(doc, 'approval-decision', String(action.doneLabel || (action.decision === 'accept' ? 'Accepted' : 'Not now')));
|
|
98
|
-
el.appendChild(tag);
|
|
99
|
-
if (typeof onDecision === 'function') onDecision(String(card.id), action.decision);
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
for (const action of Array.isArray(card.actions) ? card.actions : []) {
|
|
103
|
-
const btn = doc.createElement('button');
|
|
104
|
-
btn.type = 'button';
|
|
105
|
-
btn.className = `approval-btn ${action.decision === 'accept' ? 'allow' : 'deny'}`;
|
|
106
|
-
btn.textContent = String(action.label || action.decision);
|
|
107
|
-
btn.dataset.decision = String(action.decision);
|
|
108
|
-
btn.addEventListener('click', () => decide(action));
|
|
109
|
-
buttons.push(btn);
|
|
110
|
-
actions.appendChild(btn);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
if (!buttons.length) {
|
|
114
|
-
// A card with no actions is a bug in the caller, not something to render
|
|
115
|
-
// as a dead end.
|
|
116
|
-
return null;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
el.appendChild(actions);
|
|
120
|
-
return el;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Mount a card into `container`, reusing the approval card's `.approval-slot`
|
|
125
|
-
* so the two can never interleave in the transcript.
|
|
126
|
-
*/
|
|
127
|
-
function mountCompanionCard(doc, container, card, onDecision) {
|
|
128
|
-
if (!container || typeof container.appendChild !== 'function') return null;
|
|
129
|
-
const el = buildCompanionCard(doc, card, onDecision);
|
|
130
|
-
if (!el) return null;
|
|
131
|
-
let slot = typeof container.querySelector === 'function' ? container.querySelector('.approval-slot') : null;
|
|
132
|
-
if (!slot) {
|
|
133
|
-
slot = doc.createElement('div');
|
|
134
|
-
slot.className = 'approval-slot';
|
|
135
|
-
container.appendChild(slot);
|
|
136
|
-
}
|
|
137
|
-
slot.appendChild(el);
|
|
138
|
-
return el;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
if (typeof module !== 'undefined' && module.exports) {
|
|
142
|
-
module.exports = { buildCompanionCard, mountCompanionCard, COMPANION_CARD_CLASS, APPROVAL_CARD_CLASS };
|
|
143
|
-
}
|
|
3
|
+
"use strict";const COMPANION_CARD_CLASS="companion-card",APPROVAL_CARD_CLASS="approval-card";function textEl(t,e,n){const o=t.createElement("div");return o.className=e,o.textContent=n,o}function buildCompanionCard(t,e,n){if(!t||!e||!e.id)return null;const o=t.createElement("div");o.className=`${APPROVAL_CARD_CLASS} ${COMPANION_CARD_CLASS}`,o.dataset.companionId=String(e.id),e.behaviour&&(o.dataset.companionBehaviour=String(e.behaviour)),o.dataset.proactive=e.proactive?"true":"false",o.appendChild(textEl(t,"approval-title",String(e.title||e.label||"Suggestion"))),e.body&&o.appendChild(textEl(t,"approval-summary",String(e.body)));const a=Array.isArray(e.facts)?e.facts.filter(Boolean):[];if(a.length){const e=t.createElement("ul");e.className="companion-facts";for(const n of a.slice(0,6)){const o=t.createElement("li");o.textContent=String(n),e.appendChild(o)}o.appendChild(e)}const i=t.createElement("div");i.className="approval-actions";const l=[];let r=!1;const d=a=>{if(r)return;r=!0;for(const t of l)t.disabled=!0;o.classList.add("resolved");const i=textEl(t,"approval-decision",String(a.doneLabel||("accept"===a.decision?"Accepted":"Not now")));o.appendChild(i),"function"==typeof n&&n(String(e.id),a.decision)};for(const n of Array.isArray(e.actions)?e.actions:[]){const e=t.createElement("button");e.type="button",e.className="approval-btn "+("accept"===n.decision?"allow":"deny"),e.textContent=String(n.label||n.decision),e.dataset.decision=String(n.decision),e.addEventListener("click",(()=>d(n))),l.push(e),i.appendChild(e)}return l.length?(o.appendChild(i),o):null}function mountCompanionCard(t,e,n,o){if(!e||"function"!=typeof e.appendChild)return null;const a=buildCompanionCard(t,n,o);if(!a)return null;let i="function"==typeof e.querySelector?e.querySelector(".approval-slot"):null;return i||(i=t.createElement("div"),i.className="approval-slot",e.appendChild(i)),i.appendChild(a),a}"undefined"!=typeof module&&module.exports&&(module.exports={buildCompanionCard:buildCompanionCard,mountCompanionCard:mountCompanionCard,COMPANION_CARD_CLASS:COMPANION_CARD_CLASS,APPROVAL_CARD_CLASS:APPROVAL_CARD_CLASS});
|