aegis-desktop 1.0.10 → 1.0.13
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/lib/auto.js +11 -7
- package/lib/fingerprint.js +76 -17
- package/lib/google-connect.js +171 -0
- package/lib/local/claude-pro.js +134 -11
- package/lib/local/engine.js +111 -26
- package/lib/local/prompt.js +67 -37
- package/lib/local/tools.js +49 -13
- package/lib/settings.js +65 -24
- package/main.js +270 -36
- package/package.json +5 -2
- package/preload.js +45 -12
- package/renderer/app.js +691 -22
- package/renderer/budget.js +18 -9
- package/renderer/companion.js +225 -0
- package/renderer/fingerprint.css +16 -0
- package/renderer/fingerprint.js +136 -2
- package/renderer/index.html +302 -121
- package/renderer/provider-toolbar.js +136 -0
- package/renderer/providers.js +261 -0
- package/renderer/style.css +547 -171
- package/vendor/aegis.js +169 -3
- package/vendor/credentials.js +9 -2
- package/vendor/drive-connect.js +87 -0
- package/vendor/drive-upload.js +176 -0
- package/vendor/google-signin.js +127 -0
- package/vendor/record-vault.js +431 -113
- package/vendor/update.js +366 -8
package/renderer/budget.js
CHANGED
|
@@ -89,16 +89,25 @@ const DEEPSEEK_REASONING_MODEL_RE = /^deepseek-(v4(\.\d+)?-(flash|pro)|flash|pro
|
|
|
89
89
|
const EFFORT_TOKEN_BUDGET = { low: 8192, medium: 16384, high: 32768 };
|
|
90
90
|
|
|
91
91
|
/**
|
|
92
|
-
* DeepSeek native reasoning level per config rung.
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
92
|
+
* DeepSeek native reasoning level per config rung.
|
|
93
|
+
*
|
|
94
|
+
* The field is `effort`, not `reasoning_effort`. Measured live 2026-10-21 on the
|
|
95
|
+
* surface this client actually reaches (the relay's
|
|
96
|
+
* `https://api.deepseek.com/anthropic` transport), interleaved paired samples,
|
|
97
|
+
* n=12, output tokens: absent 818 · low 772 · medium 836 · high 818 · max 1494.
|
|
98
|
+
* Only `max` separates from sending nothing, and `GET /models` reports
|
|
99
|
+
* `supported_levels: ["low","high","max"]` on both live ids — so `medium` is
|
|
100
|
+
* not a level these models have, and it is folded upward here rather than sent
|
|
101
|
+
* to be absorbed into the vendor's default (a 200 that changes nothing, which
|
|
102
|
+
* is the one failure this knob cannot surface). `max` stays unreachable: it is
|
|
103
|
+
* not a rung of this knob.
|
|
104
|
+
*
|
|
105
|
+
* Mirrors DEEPSEEK_EFFORT in desktop/lib/local/engine.js — carried here so the
|
|
106
|
+
* no-drift guarantee for DEEPSEEK_REASONING_MODEL_RE and EFFORT_TOKEN_BUDGET
|
|
107
|
+
* extends to the wire knob too. That guard was missing, which is how the two
|
|
108
|
+
* copies agreed on a field name that reached no vendor at all.
|
|
100
109
|
*/
|
|
101
|
-
const DEEPSEEK_EFFORT = { low: 'low', medium: '
|
|
110
|
+
const DEEPSEEK_EFFORT = { low: 'low', medium: 'high', high: 'high' };
|
|
102
111
|
|
|
103
112
|
/**
|
|
104
113
|
* There is no longer a "class that requires a stated max_tokens".
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Copyright (C) 2026 Niklas Borneklint — AEGIS Code (aegiscode) / Aegiscloud.
|
|
3
|
+
// AEGIS Code (aegiscode) — source: https://github.com/aegisinfo/aegiscode-plugin
|
|
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
|
+
* companion.js — the one greeting the app writes to you, once a day.
|
|
10
|
+
*
|
|
11
|
+
* WHAT THIS IS NOT. It is not a notification, a pop-up, a badge, or anything
|
|
12
|
+
* that interrupts. It is one line in the sidebar that changes at most once per
|
|
13
|
+
* local day and can be dismissed for that day. A "daily reminder" that steals
|
|
14
|
+
* focus is a nag; the whole value here is that it is quiet enough to read.
|
|
15
|
+
*
|
|
16
|
+
* THE RULE THAT SHAPES EVERY MESSAGE: it may only say things this process
|
|
17
|
+
* actually knows. There is no engagement metric in this file, no invented
|
|
18
|
+
* streak, no "you usually work on Tuesdays". Every candidate message reads its
|
|
19
|
+
* facts from the `facts` object the caller passes, and a message whose facts
|
|
20
|
+
* are absent is not a candidate at all — it is never shown with a guess in the
|
|
21
|
+
* blank. That is why `pickMessage` returns `{ kind, ... }` rather than a
|
|
22
|
+
* pre-baked string: the copy is chosen by which facts exist, and each branch
|
|
23
|
+
* says exactly which ones it used.
|
|
24
|
+
*
|
|
25
|
+
* Determinism matters for the daily behaviour: the same local day and the same
|
|
26
|
+
* facts must select the same message, or a re-render (which happens on every
|
|
27
|
+
* status read) would flip the suggestion while the user is looking at it. The
|
|
28
|
+
* rotation between equally-valid tips is therefore seeded from the day key,
|
|
29
|
+
* never from Math.random().
|
|
30
|
+
*
|
|
31
|
+
* Pure: no DOM, no Electron, no fs, no clock of its own — `now` is passed in.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The local calendar day, as YYYY-MM-DD.
|
|
36
|
+
*
|
|
37
|
+
* Local, not UTC, on purpose: "a daily reminder" means the user's day. A UTC
|
|
38
|
+
* key would roll the message over at 01:00 or 02:00 for most of Europe, which
|
|
39
|
+
* is the middle of the evening for the people most likely to be running this.
|
|
40
|
+
*/
|
|
41
|
+
function dayKey(now) {
|
|
42
|
+
const d = now instanceof Date ? now : new Date(now == null ? Date.now() : now);
|
|
43
|
+
if (Number.isNaN(d.getTime())) return '';
|
|
44
|
+
const p = (n) => String(n).padStart(2, '0');
|
|
45
|
+
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Whole local days between two timestamps; null when either is unusable. */
|
|
49
|
+
function daysBetween(a, b) {
|
|
50
|
+
const t1 = a instanceof Date ? a.getTime() : Number(a);
|
|
51
|
+
const t2 = b instanceof Date ? b.getTime() : Number(b);
|
|
52
|
+
if (!Number.isFinite(t1) || !Number.isFinite(t2)) return null;
|
|
53
|
+
const d1 = new Date(t1); d1.setHours(0, 0, 0, 0);
|
|
54
|
+
const d2 = new Date(t2); d2.setHours(0, 0, 0, 0);
|
|
55
|
+
return Math.round((d2.getTime() - d1.getTime()) / 86400000);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Stable small hash, so tip rotation is deterministic per day. */
|
|
59
|
+
function hash(s) {
|
|
60
|
+
let h = 2166136261;
|
|
61
|
+
const str = String(s);
|
|
62
|
+
for (let i = 0; i < str.length; i += 1) {
|
|
63
|
+
h ^= str.charCodeAt(i);
|
|
64
|
+
h = Math.imul(h, 16777619);
|
|
65
|
+
}
|
|
66
|
+
return h >>> 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Tips that are TRUE OF THIS BUILD — each one names a real affordance. They are
|
|
71
|
+
* the fallback rotation once nothing needs attention; none of them is a "come
|
|
72
|
+
* back and use the app more" nudge, because that would be the app asking for
|
|
73
|
+
* the user's time rather than giving them something back.
|
|
74
|
+
*/
|
|
75
|
+
const TIPS = Object.freeze([
|
|
76
|
+
{ id: 'effort', text: 'Effort sizes the call — low for quick edits, high when the answer has to be complete.' },
|
|
77
|
+
{ id: 'tools', text: 'Turn on "confirm before running tools" in Tool approvals to see every command before it runs.' },
|
|
78
|
+
{ id: 'quick', text: 'The Quick Launcher shortcut opens a prompt from anywhere — no need to switch windows.' },
|
|
79
|
+
{ id: 'sessions', text: 'Sessions → Export writes the thread out as a file you keep, not something held in the app.' },
|
|
80
|
+
{ id: 'queue', text: 'Queued tasks run on their own class — they can stay on Aegis Cloud while you work locally.' },
|
|
81
|
+
{ id: 'memory', text: 'Memory entries are yours: the inspector shows the source and tags for every one.' },
|
|
82
|
+
{ id: 'local', text: 'Local model needs no key and no network — useful for anything you would rather not send anywhere.' },
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Choose today's message.
|
|
87
|
+
*
|
|
88
|
+
* `facts` (all optional; each one absent merely removes its branch):
|
|
89
|
+
* keyConfigured {boolean} an AEGIS API key exists on this machine
|
|
90
|
+
* modelCount {number} how many models the current class listed
|
|
91
|
+
* lastTurnAt {number} epoch ms of the last finished turn, if any
|
|
92
|
+
* firstSeenAt {number} epoch ms this install first ran
|
|
93
|
+
* queuePending {number} unfinished queued tasks
|
|
94
|
+
* memoryCount {number} memory entries visible in the inspector
|
|
95
|
+
* providerNeedsKey {boolean} byok class has models but no provider key
|
|
96
|
+
* updatePending {string} version string of a downloaded update, if any
|
|
97
|
+
*
|
|
98
|
+
* Returns `{ id, kind, text, usedFacts }` — `kind` is 'action' when the message
|
|
99
|
+
* points at something concrete to do and 'tip' when it is just the rotation.
|
|
100
|
+
*/
|
|
101
|
+
function pickMessage(facts, now) {
|
|
102
|
+
const f = facts && typeof facts === 'object' ? facts : {};
|
|
103
|
+
const day = dayKey(now);
|
|
104
|
+
const used = [];
|
|
105
|
+
const pick = (id, kind, text, keys) => ({
|
|
106
|
+
id,
|
|
107
|
+
kind,
|
|
108
|
+
text,
|
|
109
|
+
day,
|
|
110
|
+
usedFacts: keys.filter((k) => f[k] !== undefined && f[k] !== null),
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// 1. An update that is downloaded and waiting is the single most actionable
|
|
114
|
+
// thing the app can know: it is finished work sitting idle until restart.
|
|
115
|
+
if (typeof f.updatePending === 'string' && f.updatePending) {
|
|
116
|
+
return pick(
|
|
117
|
+
'update-ready',
|
|
118
|
+
'action',
|
|
119
|
+
`Update ${f.updatePending} is downloaded — restart when it suits you, it installs on quit.`,
|
|
120
|
+
['updatePending'],
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 2. No key at all: the app can barely do anything, and the remedy is one
|
|
125
|
+
// paste. Not an error message — a reminder is allowed to be the first
|
|
126
|
+
// place the user hears it, so it is phrased as an invitation.
|
|
127
|
+
if (f.keyConfigured === false) {
|
|
128
|
+
return pick(
|
|
129
|
+
'connect',
|
|
130
|
+
'action',
|
|
131
|
+
'Paste your AEGIS API key in Status to run turns on Aegis Cloud — or pick Local model below for no key at all.',
|
|
132
|
+
['keyConfigured'],
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 3. A provider class chosen with models listed but no provider key: the user
|
|
137
|
+
// has already walked to the door and has not opened it.
|
|
138
|
+
if (f.providerNeedsKey === true) {
|
|
139
|
+
return pick(
|
|
140
|
+
'provider-key',
|
|
141
|
+
'action',
|
|
142
|
+
'Your provider class has models listed but no key saved — add one in Provider settings to use them.',
|
|
143
|
+
['providerNeedsKey'],
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// 4. Queued work still waiting. Counted, never estimated: "3 queued" is a
|
|
148
|
+
// fact, "your queue is busy" would be a guess about progress.
|
|
149
|
+
if (typeof f.queuePending === 'number' && f.queuePending > 0) {
|
|
150
|
+
return pick(
|
|
151
|
+
'queue',
|
|
152
|
+
'action',
|
|
153
|
+
`${f.queuePending} queued task${f.queuePending === 1 ? '' : 's'} still waiting — the queue card runs ${f.queuePending === 1 ? 'it' : 'them'} on its own class.`,
|
|
154
|
+
['queuePending'],
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// 4b. Memory this install already holds. Documented in the facts list from
|
|
159
|
+
// the day this file was written, but with no branch behind it — so a user
|
|
160
|
+
// with twenty saved entries was told nothing about them and got a generic
|
|
161
|
+
// tip instead. Counted exactly, and it points at the inspector, which is
|
|
162
|
+
// the only place in the app where the provenance of each entry is shown.
|
|
163
|
+
if (typeof f.memoryCount === 'number' && f.memoryCount > 0) {
|
|
164
|
+
return pick(
|
|
165
|
+
'memory-held',
|
|
166
|
+
'action',
|
|
167
|
+
`${f.memoryCount} memory entr${f.memoryCount === 1 ? 'y is' : 'ies are'} already saved here — the inspector shows the source and tags for every one.`,
|
|
168
|
+
['memoryCount'],
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// 5. First-run, no key required path: say what the app is, once. Only on the
|
|
173
|
+
// FIRST local day, so it does not become a permanent fixture.
|
|
174
|
+
const age = daysBetween(f.firstSeenAt, now);
|
|
175
|
+
if (age === 0 && f.lastTurnAt == null) {
|
|
176
|
+
return pick(
|
|
177
|
+
'first-day',
|
|
178
|
+
'tip',
|
|
179
|
+
'First day. Pick a class and a model in the Model card, then send a turn — everything else here is optional.',
|
|
180
|
+
['firstSeenAt', 'lastTurnAt'],
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// 6. A gap after real use. Reported only when there is a genuine last turn,
|
|
185
|
+
// and only past a threshold where the observation is worth stating; below
|
|
186
|
+
// it, nothing is said at all (fall through to the rotation).
|
|
187
|
+
const since = daysBetween(f.lastTurnAt, now);
|
|
188
|
+
if (since != null && since >= 3) {
|
|
189
|
+
return pick(
|
|
190
|
+
'returning',
|
|
191
|
+
'tip',
|
|
192
|
+
'Last turn was a few days ago — your sessions and queued tasks are still here.',
|
|
193
|
+
['lastTurnAt'],
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// 7. Nothing needs attention: rotate one true tip, seeded by the day so it is
|
|
198
|
+
// stable for the whole day and different tomorrow.
|
|
199
|
+
const tip = TIPS[hash(day) % TIPS.length];
|
|
200
|
+
return pick(`tip-${tip.id}`, 'tip', tip.text, ['tipRotation']);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** True when this message was already dismissed today. */
|
|
204
|
+
function dismissedToday(dismissed, now) {
|
|
205
|
+
const d = dismissed && typeof dismissed === 'object' ? dismissed : null;
|
|
206
|
+
if (!d) return false;
|
|
207
|
+
return d.day === dayKey(now);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
211
|
+
module.exports = { pickMessage, dayKey, daysBetween, dismissedToday, TIPS };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Also a page global, because index.html loads this file as a classic script
|
|
215
|
+
// (`<script src="companion.js">`) and app.js reads window.aegisCompanion. The
|
|
216
|
+
// module branch above is for Node (test/renderer-providers.test.mjs requires
|
|
217
|
+
// the file directly); the two are mutually exclusive in practice — a classic
|
|
218
|
+
// script has no `module` — so this cannot clobber the module export.
|
|
219
|
+
if (typeof window !== 'undefined') {
|
|
220
|
+
(window.aegisCompanion = window.aegisCompanion || {}).pickMessage = pickMessage;
|
|
221
|
+
window.aegisCompanion.dayKey = dayKey;
|
|
222
|
+
window.aegisCompanion.daysBetween = daysBetween;
|
|
223
|
+
window.aegisCompanion.dismissedToday = dismissedToday;
|
|
224
|
+
window.aegisCompanion.TIPS = TIPS;
|
|
225
|
+
}
|
package/renderer/fingerprint.css
CHANGED
|
@@ -54,6 +54,22 @@
|
|
|
54
54
|
display: none;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
/* Which of the three destinations a publish/backup goes to. A row of
|
|
58
|
+
buttons, not a <select>: `aria-pressed` on the active one is the whole
|
|
59
|
+
state fingerprint.js's paintVaultBackend() paints, and three named options
|
|
60
|
+
read faster than a dropdown you have to open to see the current one. */
|
|
61
|
+
.fp-vault-backend {
|
|
62
|
+
display: flex;
|
|
63
|
+
flex-wrap: wrap;
|
|
64
|
+
gap: 6px;
|
|
65
|
+
padding: 8px 16px 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.fp-backend-btn[aria-pressed='true'] {
|
|
69
|
+
border-color: var(--teal);
|
|
70
|
+
color: var(--teal);
|
|
71
|
+
}
|
|
72
|
+
|
|
57
73
|
.fp-controls {
|
|
58
74
|
display: flex;
|
|
59
75
|
flex-direction: column;
|
package/renderer/fingerprint.js
CHANGED
|
@@ -57,6 +57,9 @@
|
|
|
57
57
|
keyTag: 'fp-key-tag',
|
|
58
58
|
keyWarning: 'fp-key-warning',
|
|
59
59
|
vaultTarget: 'fp-vault-target',
|
|
60
|
+
vaultBackendDrive: 'fp-vault-backend-drive',
|
|
61
|
+
vaultBackendCloud: 'fp-vault-backend-aegiscloud',
|
|
62
|
+
vaultBackendOff: 'fp-vault-backend-off',
|
|
60
63
|
label: 'fp-label',
|
|
61
64
|
publishOnMint: 'fp-publish-on-mint',
|
|
62
65
|
autoSeal: 'fp-auto-seal',
|
|
@@ -75,6 +78,10 @@
|
|
|
75
78
|
audit: 'fp-audit',
|
|
76
79
|
vaultList: 'fp-vault-list',
|
|
77
80
|
out: 'fp-out',
|
|
81
|
+
driveStatus: 'fp-drive-status',
|
|
82
|
+
driveSignIn: 'fp-drive-signin',
|
|
83
|
+
driveConnect: 'fp-drive-connect',
|
|
84
|
+
driveSyncNow: 'fp-drive-sync',
|
|
78
85
|
};
|
|
79
86
|
|
|
80
87
|
// ------------------------------------------------------------- formatting
|
|
@@ -136,6 +143,23 @@
|
|
|
136
143
|
});
|
|
137
144
|
}
|
|
138
145
|
|
|
146
|
+
/**
|
|
147
|
+
* The one-line summary of the Google Drive card — `desktop/lib/
|
|
148
|
+
* google-connect.js`'s `status()` shape, `{signedIn, connected, email}`.
|
|
149
|
+
*
|
|
150
|
+
* Three distinct states, because "not connected" means something different
|
|
151
|
+
* depending on whether this machine has signed in with Google at all:
|
|
152
|
+
* connecting Drive before signing in attaches the tokens to a different
|
|
153
|
+
* account than the one this machine polls (see google-connect.js's header
|
|
154
|
+
* comment), so the line has to say WHICH step is next, not just "no".
|
|
155
|
+
*/
|
|
156
|
+
function driveLine(status) {
|
|
157
|
+
if (!status || status.ok === false) return (status && status.error) || 'unavailable';
|
|
158
|
+
if (!status.signedIn) return 'not signed in — sign in with Google first';
|
|
159
|
+
if (!status.connected) return 'signed in · Google Drive not connected yet';
|
|
160
|
+
return `Google Drive connected${status.email ? ` · ${status.email}` : ''}`;
|
|
161
|
+
}
|
|
162
|
+
|
|
139
163
|
/** The anchor answer, kept as three separate claims on purpose: "the file is
|
|
140
164
|
* gone", "the file was replaced" and "the key that sealed it has moved on"
|
|
141
165
|
* are three different situations, and one boolean hides the interesting one
|
|
@@ -276,7 +300,19 @@
|
|
|
276
300
|
warn.hidden = !w;
|
|
277
301
|
}
|
|
278
302
|
if (vt && s && s.vault) {
|
|
279
|
-
|
|
303
|
+
// The default backend is the account (`aegiscloud`), where "gdrive" is
|
|
304
|
+
// not where anything goes — printing `gdrive:AEGIS-AGPL/records` for a
|
|
305
|
+
// cloud publish would name a folder the record never touches. Say the
|
|
306
|
+
// backend for that case, and keep the remembered rclone target for the
|
|
307
|
+
// case where it IS a remote. 'off' names neither — nothing is resolved
|
|
308
|
+
// behind it (desktop/lib/fingerprint.js's status() short-circuits
|
|
309
|
+
// before ever calling vaultTarget()), so the line says so plainly
|
|
310
|
+
// rather than printing the leftover "–:–" two dashes would read as.
|
|
311
|
+
vt.textContent = s.vault.backend === 'off'
|
|
312
|
+
? 'nothing — sync is off'
|
|
313
|
+
: s.vault.backend === 'aegiscloud'
|
|
314
|
+
? `aegiscloud:${dash(s.vault.recordsDir)}`
|
|
315
|
+
: `${dash(s.vault.remote)}:${dash(s.vault.recordsDir || s.vault.root)}`;
|
|
280
316
|
} else if (vt) {
|
|
281
317
|
vt.textContent = '–';
|
|
282
318
|
}
|
|
@@ -287,6 +323,90 @@
|
|
|
287
323
|
if (kg) kg.textContent = s && s.configured ? 'Replace key…' : 'Mint key';
|
|
288
324
|
}
|
|
289
325
|
|
|
326
|
+
/**
|
|
327
|
+
* The three backend buttons' pressed state.
|
|
328
|
+
*
|
|
329
|
+
* Deliberately read from `getVaultBackend` (the preference the user set),
|
|
330
|
+
* NOT from `status().vault.backend` (the resolved record-vault.js
|
|
331
|
+
* backend id): the preference vocabulary is 'aegiscloud' | 'drive' | 'off',
|
|
332
|
+
* the resolved one is 'aegiscloud' | 'rclone' | 'off' — a 'drive'
|
|
333
|
+
* preference resolves to 'rclone', so pressing "Google Drive" from status
|
|
334
|
+
* alone would never read back as pressed.
|
|
335
|
+
*/
|
|
336
|
+
function paintVaultBackend(backend) {
|
|
337
|
+
const map = {
|
|
338
|
+
drive: el(IDS.vaultBackendDrive),
|
|
339
|
+
aegiscloud: el(IDS.vaultBackendCloud),
|
|
340
|
+
off: el(IDS.vaultBackendOff),
|
|
341
|
+
};
|
|
342
|
+
for (const [key, btn] of Object.entries(map)) {
|
|
343
|
+
if (btn) btn.setAttribute('aria-pressed', String(key === backend));
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async function loadVaultBackend() {
|
|
348
|
+
const r = await call('getVaultBackend');
|
|
349
|
+
state.vaultBackend = r && r.backend ? r.backend : 'aegiscloud';
|
|
350
|
+
paintVaultBackend(state.vaultBackend);
|
|
351
|
+
return state.vaultBackend;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const doSetVaultBackend = (backend) =>
|
|
355
|
+
action(`vault: ${backend === 'drive' ? 'Google Drive' : backend === 'off' ? 'no sync' : 'AE'}`, async () => {
|
|
356
|
+
const r = await call('setVaultBackend', backend);
|
|
357
|
+
return r && r.backend ? { ok: true, ...r } : { ok: false, error: 'could not save that' };
|
|
358
|
+
}, {
|
|
359
|
+
// The "publishes to" line names the backend too, so both it and the
|
|
360
|
+
// three buttons must repaint the moment the preference changes — not
|
|
361
|
+
// just on the next open().
|
|
362
|
+
after: (r) => (r && r.ok ? refresh() : null),
|
|
363
|
+
say: (r) => (r.ok ? `vault backend set to ${dash(r.backend)}` : `could not save that: ${r.error}`),
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Google Drive — memory backup (desktop/lib/google-connect.js, exposed
|
|
368
|
+
* over `aegis.driveSignIn`/`driveConnect`/`driveStatus`/`driveSyncMemory`).
|
|
369
|
+
*
|
|
370
|
+
* Distinct from the vault backend buttons above: those choose where a
|
|
371
|
+
* SEALED φ(α) RECORD is published (rclone or the account vault). This is
|
|
372
|
+
* the account's AEGIS CLOUD MEMORY, backed up to the user's own Drive
|
|
373
|
+
* through real Google OAuth — unrelated data, unrelated credential.
|
|
374
|
+
*/
|
|
375
|
+
function paintDrive(status) {
|
|
376
|
+
const line = el(IDS.driveStatus);
|
|
377
|
+
if (line) line.textContent = driveLine(status);
|
|
378
|
+
const connectBtn = el(IDS.driveConnect);
|
|
379
|
+
if (connectBtn) connectBtn.disabled = Boolean(status && !status.signedIn);
|
|
380
|
+
const syncBtn = el(IDS.driveSyncNow);
|
|
381
|
+
if (syncBtn) syncBtn.disabled = Boolean(!status || !status.connected);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
async function loadDriveStatus() {
|
|
385
|
+
const r = await call('driveStatus');
|
|
386
|
+
state.drive = r;
|
|
387
|
+
paintDrive(r);
|
|
388
|
+
return r;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const doDriveSignIn = () =>
|
|
392
|
+
action('sign in with Google', () => call('driveSignIn'), {
|
|
393
|
+
after: (r) => (r && r.ok ? loadDriveStatus() : null),
|
|
394
|
+
say: (r) => (r.ok ? `signed in${r.username ? ` as ${r.username}` : ''}` : `sign-in failed: ${r.error}`),
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
const doDriveConnect = () =>
|
|
398
|
+
action('connect Google Drive', () => call('driveConnect'), {
|
|
399
|
+
after: (r) => (r && r.ok ? loadDriveStatus() : null),
|
|
400
|
+
say: (r) => (r.ok ? `Google Drive connected${r.email ? ` · ${r.email}` : ''}` : `could not connect: ${r.error}`),
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
const doDriveSyncNow = () =>
|
|
404
|
+
action('back up memory to Drive', () => call('driveSyncMemory'), {
|
|
405
|
+
say: (r) => (r.ok
|
|
406
|
+
? `backed up ${r.count} memory entr${r.count === 1 ? 'y' : 'ies'}${r.webViewLink ? ` → ${r.webViewLink}` : ''}`
|
|
407
|
+
: `backup failed: ${r.error}`),
|
|
408
|
+
});
|
|
409
|
+
|
|
290
410
|
function paintVault(list) {
|
|
291
411
|
const ul = el(IDS.vaultList);
|
|
292
412
|
if (!ul) return;
|
|
@@ -312,6 +432,8 @@
|
|
|
312
432
|
state.status = await call('fingerprintStatus');
|
|
313
433
|
paintStatus();
|
|
314
434
|
await loadAutoMint();
|
|
435
|
+
await loadVaultBackend();
|
|
436
|
+
await loadDriveStatus();
|
|
315
437
|
return state.status;
|
|
316
438
|
}
|
|
317
439
|
|
|
@@ -547,6 +669,12 @@
|
|
|
547
669
|
bind(IDS.backup, 'click', doBackup);
|
|
548
670
|
bind(IDS.vault, 'click', doVault);
|
|
549
671
|
bind(IDS.audit, 'click', doAudit);
|
|
672
|
+
bind(IDS.vaultBackendDrive, 'click', () => doSetVaultBackend('drive'));
|
|
673
|
+
bind(IDS.vaultBackendCloud, 'click', () => doSetVaultBackend('aegiscloud'));
|
|
674
|
+
bind(IDS.vaultBackendOff, 'click', () => doSetVaultBackend('off'));
|
|
675
|
+
bind(IDS.driveSignIn, 'click', doDriveSignIn);
|
|
676
|
+
bind(IDS.driveConnect, 'click', doDriveConnect);
|
|
677
|
+
bind(IDS.driveSyncNow, 'click', doDriveSyncNow);
|
|
550
678
|
// The auto-seal switch is the ONE control on this panel that can file a
|
|
551
679
|
// record with nobody watching, so it is the one that must repaint from
|
|
552
680
|
// main's answer rather than from the checkbox the user just clicked.
|
|
@@ -567,7 +695,12 @@
|
|
|
567
695
|
state,
|
|
568
696
|
// Exposed for the test and for app.js's on-demand repaint. Every one is
|
|
569
697
|
// idempotent, so calling them from a console is safe.
|
|
570
|
-
actions: {
|
|
698
|
+
actions: {
|
|
699
|
+
doKeygen, doSeal, doSealNow, doVerify, doPublish, doAnchor, doBackup, doVault, doAudit,
|
|
700
|
+
doSetVaultBackend, doDriveSignIn, doDriveConnect, doDriveSyncNow,
|
|
701
|
+
},
|
|
702
|
+
loadVaultBackend,
|
|
703
|
+
loadDriveStatus,
|
|
571
704
|
};
|
|
572
705
|
}
|
|
573
706
|
|
|
@@ -583,6 +716,7 @@
|
|
|
583
716
|
auditVerdict,
|
|
584
717
|
sealLine,
|
|
585
718
|
autoMintLine,
|
|
719
|
+
driveLine,
|
|
586
720
|
dash,
|
|
587
721
|
};
|
|
588
722
|
});
|