aegis-desktop 1.0.13 → 1.0.14
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/google-connect.js +68 -10
- package/lib/local/engine.js +74 -1
- package/lib/local/entitlement.js +7 -1
- package/main.js +261 -122
- package/package.json +1 -4
- package/preload.js +13 -5
- package/renderer/app.js +102 -2
- package/renderer/fingerprint.js +17 -1
- package/vendor/aegis.js +128 -3
- package/vendor/drive-connect.js +28 -3
- package/vendor/google-signin.js +12 -4
- package/vendor/model-discovery.js +43 -0
package/main.js
CHANGED
|
@@ -292,6 +292,12 @@ const MENU_EXPORT_JSON_CHANNEL = `${IPC_PREFIX}menuExportJson`;
|
|
|
292
292
|
* as the channels above: main has no panel state, it just asks the renderer to
|
|
293
293
|
* open the sheet it already owns. */
|
|
294
294
|
const MENU_FINGERPRINT_CHANNEL = `${IPC_PREFIX}menuFingerprint`;
|
|
295
|
+
/** Account > Google Drive Backup… — opens the same φ(α) sheet as the item
|
|
296
|
+
* above, scrolled straight to the Drive row. Only on the menu once an AEGIS
|
|
297
|
+
* account key exists (buildAppMenu's `enabled`, rebuilt on accountKey's
|
|
298
|
+
* onChange): signing in with Google is itself how an anonymous install gets
|
|
299
|
+
* one, so the item stays but disabled until that first key lands. */
|
|
300
|
+
const MENU_GOOGLE_DRIVE_CHANNEL = `${IPC_PREFIX}menuGoogleDrive`;
|
|
295
301
|
|
|
296
302
|
/**
|
|
297
303
|
* Main -> renderer push for a resolved aegis:// deep link (D? deep linking).
|
|
@@ -413,6 +419,54 @@ function errorText(err) {
|
|
|
413
419
|
return err && err.message ? err.message : String(err);
|
|
414
420
|
}
|
|
415
421
|
|
|
422
|
+
/** Is this rejection the user pressing stop?
|
|
423
|
+
*
|
|
424
|
+
* `engine.cancel()` aborts the session's AbortController, and the in-flight
|
|
425
|
+
* transport turn rejects with `DOMException [AbortError]: This operation was
|
|
426
|
+
* aborted`. That is the app working exactly as asked — but `ipcMain.handle`
|
|
427
|
+
* prints EVERY rejection with a full stack ("Error occurred in handler for
|
|
428
|
+
* 'model:chat':"), so a deliberate Ctrl-X was the loudest thing in the
|
|
429
|
+
* terminal and read like a crash. The renderer cannot tell it either: an
|
|
430
|
+
* abort that crosses IPC loses `name`/`code` and arrives as a bare message,
|
|
431
|
+
* which is why the classification is made HERE, where the object is intact,
|
|
432
|
+
* and handed on as data (see the `aborted` resolution in registerModelIpc).
|
|
433
|
+
*
|
|
434
|
+
* Evidence only — `aegisCancelled`, `name`, `code` — and never the message.
|
|
435
|
+
*
|
|
436
|
+
* The first version also matched any message containing "abort", as the
|
|
437
|
+
* renderer's `isCancellation` backstop does. That is wrong on this side of the
|
|
438
|
+
* boundary: the renderer has a per-turn `userStopped` flag to corroborate a
|
|
439
|
+
* fuzzy match against, and main has no such notion, so the message WAS the
|
|
440
|
+
* whole signal. A genuine failure saying "connection aborted by peer" or
|
|
441
|
+
* "upstream aborted the connection" would have been resolved as a clean stop
|
|
442
|
+
* and quietly discarded, since this branch rethrows everything else. The bit
|
|
443
|
+
* that cannot lie is stamped on the error by the engine at the point it knows
|
|
444
|
+
* for certain — its own controller's signal read aborted — which no upstream
|
|
445
|
+
* cause can fake. `name`/`code` are kept because a DOMException raised by
|
|
446
|
+
* `AbortController.abort()` itself carries them reliably.
|
|
447
|
+
*/
|
|
448
|
+
function isAbortShapedError(err) {
|
|
449
|
+
if (!err || typeof err !== 'object') return false;
|
|
450
|
+
if (err.aegisCancelled === true) return true;
|
|
451
|
+
return err.name === 'AbortError' || err.code === 'ABORT_ERR';
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/** The resolved stand-in for a stopped turn.
|
|
455
|
+
*
|
|
456
|
+
* Same shape as the `bestEffortFailed` payload below, so every existing
|
|
457
|
+
* `data.choices[0].message.content` reader keeps working, plus the
|
|
458
|
+
* `aborted` discriminator the renderer keys its salvage path off.
|
|
459
|
+
*/
|
|
460
|
+
function abortedTurnPayload(err) {
|
|
461
|
+
return {
|
|
462
|
+
aborted: true,
|
|
463
|
+
status: 0,
|
|
464
|
+
code: 'ABORT_ERR',
|
|
465
|
+
message: errorText(err),
|
|
466
|
+
choices: [{ message: { content: '' } }],
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
|
|
416
470
|
/**
|
|
417
471
|
* Precise classification of a cloud-sync refusal — the desktop port of
|
|
418
472
|
* `describeError()` in cli/src/cloudsync.js, so both hosts name a 402/401 the
|
|
@@ -2074,6 +2128,21 @@ function registerModelIpc(ipcMain, engine, sessionsDir, aegis, onReplyFinished,
|
|
|
2074
2128
|
if (name === 'chat') {
|
|
2075
2129
|
ipcMain.handle(`${MODEL_PREFIX}${name}`, (event, payload) => {
|
|
2076
2130
|
const opts = { ...(payload || {}) };
|
|
2131
|
+
// A background dispatch the user never typed — the welcome panel's
|
|
2132
|
+
// greeting, a discovery lane — is allowed to fail quietly. A REJECTED
|
|
2133
|
+
// ipcMain.handle is printed by Electron itself ("Error occurred in
|
|
2134
|
+
// handler for 'model:chat':" plus the whole stack), and the renderer
|
|
2135
|
+
// that catches it is another process, so a greeting that timed out on
|
|
2136
|
+
// boot put a provider stack trace in the terminal as the first thing
|
|
2137
|
+
// the app said. Nothing was lost: those lanes already treat "no answer"
|
|
2138
|
+
// as "keep what is on screen". So the flag turns the rejection into a
|
|
2139
|
+
// resolved classified payload (the renderer gets MORE than it used to —
|
|
2140
|
+
// status and code, which `ipcRenderer.invoke` would have flattened to a
|
|
2141
|
+
// message string anyway) and the terminal gets one line, not a stack.
|
|
2142
|
+
const bestEffort = opts.bestEffort === true;
|
|
2143
|
+
// …and it never reaches the wire: this is a host-side instruction, not
|
|
2144
|
+
// a provider field, and the engine forwards what it recognises.
|
|
2145
|
+
delete opts.bestEffort;
|
|
2077
2146
|
// The rung every renderer sends is low|medium|high; this is the one
|
|
2078
2147
|
// place a stale window can still hand us the retired `auto` (a page
|
|
2079
2148
|
// loaded before the select lost that row, or a renderer that forgot to
|
|
@@ -2099,10 +2168,59 @@ function registerModelIpc(ipcMain, engine, sessionsDir, aegis, onReplyFinished,
|
|
|
2099
2168
|
sender.send(CHAT_DELTA_CHANNEL, taggedChunk(chunk, opts.sessionId));
|
|
2100
2169
|
}
|
|
2101
2170
|
};
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2171
|
+
const lane = opts.class || 'model';
|
|
2172
|
+
const turn = handler({ ...opts, onStream: forward });
|
|
2173
|
+
if (!bestEffort) {
|
|
2174
|
+
// A user-pressed stop must not be printed as an app error. The
|
|
2175
|
+
// rejection itself is correct — it is how the turn unwinds — so the
|
|
2176
|
+
// catch is added AFTER withReplyNotify, which means the success and
|
|
2177
|
+
// "reply ready" notification paths are untouched and only the
|
|
2178
|
+
// rejection is reclassified. Anything that is not an abort rethrows
|
|
2179
|
+
// verbatim: a genuine provider failure still reaches the renderer as
|
|
2180
|
+
// a rejection, and still prints, because that is information.
|
|
2181
|
+
//
|
|
2182
|
+
// Resolved rather than rethrown because `ipcMain.handle` prints what
|
|
2183
|
+
// it rejects and no flag suppresses that. The renderer turns this
|
|
2184
|
+
// payload back into an AbortError (see `models.chat` in app.js), so
|
|
2185
|
+
// `isCancellation` → `salvageTurn` still runs and a half-written
|
|
2186
|
+
// answer is kept and labelled "stopped by you" — the same outcome as
|
|
2187
|
+
// before, minus the stack trace.
|
|
2188
|
+
return withReplyNotify(turn, event, onReplyFinished).catch((err) => {
|
|
2189
|
+
if (!isAbortShapedError(err)) throw err;
|
|
2190
|
+
console.warn(
|
|
2191
|
+
`[model] ${lane} turn stopped before it finished: ${errorText(err)} [ABORT_ERR]`
|
|
2192
|
+
);
|
|
2193
|
+
return abortedTurnPayload(err);
|
|
2194
|
+
});
|
|
2195
|
+
}
|
|
2196
|
+
return turn.then(
|
|
2197
|
+
(result) => {
|
|
2198
|
+
if (onReplyFinished) onReplyFinished(event, result);
|
|
2199
|
+
return result;
|
|
2200
|
+
},
|
|
2201
|
+
(err) => {
|
|
2202
|
+
// Classified, not verbatim: the status and code are what tell an
|
|
2203
|
+
// operator which failure this was (0/timeout, 402 balance, 401
|
|
2204
|
+
// key), and neither survives the IPC boundary unless it is stated
|
|
2205
|
+
// here. The stack does not survive anything useful.
|
|
2206
|
+
const status = err && typeof err.status === 'number' ? err.status : 0;
|
|
2207
|
+
const code = (err && err.code) || '';
|
|
2208
|
+
console.warn(
|
|
2209
|
+
`[model] background ${lane} turn produced no answer: ${errorText(err)}` +
|
|
2210
|
+
`${status ? ` (status ${status})` : ''}${code ? ` [${code}]` : ''}`
|
|
2211
|
+
);
|
|
2212
|
+
// Resolved, so Electron does not print the rejection and the lane
|
|
2213
|
+
// that asked can read "nothing came back" without a try/catch.
|
|
2214
|
+
// `bestEffortFailed` is the discriminator; the empty content keeps
|
|
2215
|
+
// every existing `data.choices[0].message.content` reader working.
|
|
2216
|
+
return {
|
|
2217
|
+
bestEffortFailed: true,
|
|
2218
|
+
status,
|
|
2219
|
+
code: code || undefined,
|
|
2220
|
+
message: errorText(err),
|
|
2221
|
+
choices: [{ message: { content: '' } }],
|
|
2222
|
+
};
|
|
2223
|
+
}
|
|
2106
2224
|
);
|
|
2107
2225
|
});
|
|
2108
2226
|
continue;
|
|
@@ -2723,35 +2841,39 @@ const ACCOUNT_STATUS_CHANNEL = `${IPC_PREFIX}accountStatusChanged`;
|
|
|
2723
2841
|
const UPDATE_CHECK_INTERVAL_MS = 4 * 60 * 60 * 1000;
|
|
2724
2842
|
|
|
2725
2843
|
/**
|
|
2726
|
-
*
|
|
2727
|
-
*
|
|
2728
|
-
*
|
|
2844
|
+
* The npm registry is the ONLY update channel, packaged build included.
|
|
2845
|
+
*
|
|
2846
|
+
* There used to be a second one: electron-updater reading GitHub Releases via
|
|
2847
|
+
* electron-builder.yml's `publish:` feed. That feed never existed. The repo
|
|
2848
|
+
* named there (aegiscloud-devs/aegiscode-plugin) is private and has no
|
|
2849
|
+
* published release, so every launch of a packaged app failed its check with
|
|
2729
2850
|
*
|
|
2730
|
-
*
|
|
2731
|
-
*
|
|
2732
|
-
* calling checkForUpdates() there would just throw on every launch. Rather
|
|
2733
|
-
* than special-case that error, `isPackaged: false` skips wiring the real
|
|
2734
|
-
* autoUpdater entirely and every method resolves a harmless 'disabled'
|
|
2735
|
-
* state — dev never touches the network or the update machinery.
|
|
2851
|
+
* 404 "method: GET url: .../releases.atom Please double check that your
|
|
2852
|
+
* authentication token is correct..."
|
|
2736
2853
|
*
|
|
2737
|
-
*
|
|
2738
|
-
*
|
|
2739
|
-
*
|
|
2740
|
-
*
|
|
2741
|
-
*
|
|
2742
|
-
*
|
|
2854
|
+
* — GitHub answers 404 rather than 403 to an unauthenticated request on a
|
|
2855
|
+
* private repo, and electron-updater reports every non-200 as an auth problem,
|
|
2856
|
+
* so the banner blamed the user's token for a destination that was never
|
|
2857
|
+
* public and never had a release. Meanwhile aegis-desktop ships to npm on
|
|
2858
|
+
* every release. So the packaged path now uses the same npm manager as every
|
|
2859
|
+
* other install, and `publish:`/electron-updater are gone rather than left
|
|
2860
|
+
* pointing at a feed nobody can read.
|
|
2861
|
+
*
|
|
2862
|
+
* Checking happens automatically on ready and on the interval below; nothing
|
|
2863
|
+
* here installs or restarts on its own. `download()` runs only on the banner's
|
|
2864
|
+
* explicit click, and `quitAndInstall()` only on "Restart to install".
|
|
2743
2865
|
*/
|
|
2744
2866
|
|
|
2745
2867
|
/**
|
|
2746
|
-
* Update state backed by the npm registry
|
|
2747
|
-
*
|
|
2868
|
+
* Update state backed by the npm registry — the channel for EVERY install,
|
|
2869
|
+
* packaged AppImage/deb/NSIS/dmg included.
|
|
2748
2870
|
*
|
|
2749
|
-
*
|
|
2750
|
-
*
|
|
2751
|
-
*
|
|
2752
|
-
*
|
|
2753
|
-
*
|
|
2754
|
-
*
|
|
2871
|
+
* Keeps the state shape the IPC dispatch and the renderer banner consume
|
|
2872
|
+
* (status/check/download/quitAndInstall/start), so neither needed a branch
|
|
2873
|
+
* when the packaged path stopped using electron-updater. Which installer owns
|
|
2874
|
+
* this copy is `detectInstallMethod`'s answer, and the command shown is that
|
|
2875
|
+
* plan's `instruction` — an AppImage or a global npm install must not be told
|
|
2876
|
+
* to run a command that writes a different copy.
|
|
2755
2877
|
*/
|
|
2756
2878
|
/**
|
|
2757
2879
|
* How long to wait before re-checking after a FAILED background check: 1m, 5m,
|
|
@@ -2792,6 +2914,13 @@ function describeRegistryFailure(reason, status, pkg) {
|
|
|
2792
2914
|
|
|
2793
2915
|
function createNpmUpdateManager({
|
|
2794
2916
|
onStatus,
|
|
2917
|
+
// A packaged build (AppImage/deb/NSIS/dmg) is NOT the npm copy. `npm i -g`
|
|
2918
|
+
// from inside one writes a SECOND install beside the running binary and the
|
|
2919
|
+
// relaunch then starts the unchanged original — a button that appears to work
|
|
2920
|
+
// and changes nothing. The version news still comes from npm (there is no
|
|
2921
|
+
// other channel), but the packaged case names the install instead of
|
|
2922
|
+
// pretending to apply it.
|
|
2923
|
+
packaged = false,
|
|
2795
2924
|
now = () => Date.now(),
|
|
2796
2925
|
fetchImpl,
|
|
2797
2926
|
setTimeoutImpl = setTimeout,
|
|
@@ -2915,8 +3044,8 @@ function createNpmUpdateManager({
|
|
|
2915
3044
|
*
|
|
2916
3045
|
* `intervalMs: 0` is deliberate: the *schedule* stays this manager's (it
|
|
2917
3046
|
* polls every `UPDATE_CHECK_INTERVAL_MS` and drives its retry ladder off the
|
|
2918
|
-
* result), but the *check* is not re-implemented here. The desktop
|
|
2919
|
-
*
|
|
3047
|
+
* result), but the *check* is not re-implemented here. The desktop's applier
|
|
3048
|
+
* is the npm install above; it has no version fetch of its own.
|
|
2920
3049
|
*/
|
|
2921
3050
|
async function fetchDetailed() {
|
|
2922
3051
|
if (fetchImpl) return fetchImpl({ pkg: PKG });
|
|
@@ -2998,11 +3127,22 @@ function createNpmUpdateManager({
|
|
|
2998
3127
|
retryInMs: 0,
|
|
2999
3128
|
plan,
|
|
3000
3129
|
method,
|
|
3001
|
-
|
|
3130
|
+
// A packaged build is not the npm copy, so it must not be told to run
|
|
3131
|
+
// `npm i -g` — that writes a second install and the relaunch starts
|
|
3132
|
+
// the unchanged original. It gets the same version news, addressed to
|
|
3133
|
+
// the install that actually owns it.
|
|
3134
|
+
command: packaged
|
|
3135
|
+
? `${res.latest} is published — reinstall AEGIS Desktop from the installer to pick it up`
|
|
3136
|
+
: (plan && plan.instruction) || `npm i -g ${PKG}@latest`,
|
|
3137
|
+
// Whether `command` is something to TYPE or a sentence to READ. The
|
|
3138
|
+
// banner otherwise prefixes "run:" to it, which is right for
|
|
3139
|
+
// `npm i -g aegis-desktop@latest` and wrong for a channel with no
|
|
3140
|
+
// command at all (packaged, npx, a source checkout).
|
|
3141
|
+
commandIsSentence: packaged || Boolean(plan && !plan.apply),
|
|
3002
3142
|
// A channel with an `apply` argv has something this process can run;
|
|
3003
|
-
// `npx
|
|
3004
|
-
// silently does nothing is worse than the command alone.
|
|
3005
|
-
canDownload: Boolean(plan && plan.apply),
|
|
3143
|
+
// `npx`, a source checkout and a packaged build do not, and offering
|
|
3144
|
+
// a button that silently does nothing is worse than the command alone.
|
|
3145
|
+
canDownload: Boolean(plan && plan.apply) && !packaged,
|
|
3006
3146
|
});
|
|
3007
3147
|
}
|
|
3008
3148
|
|
|
@@ -3080,11 +3220,12 @@ function createNpmUpdateManager({
|
|
|
3080
3220
|
* timer), and it still does not hot-swap the running process: after a
|
|
3081
3221
|
* successful install the honest state is 'downloaded' — "restart to install"
|
|
3082
3222
|
* — because the new files are on disk and this process is still the old
|
|
3083
|
-
* ones.
|
|
3084
|
-
*
|
|
3223
|
+
* ones. Packaged and npm installs share that promise, so the banner needs
|
|
3224
|
+
* no branch.
|
|
3085
3225
|
*/
|
|
3086
3226
|
download: async () => {
|
|
3087
|
-
|
|
3227
|
+
// Nothing to run for a packaged build — see the `packaged` option above.
|
|
3228
|
+
const target = !packaged && state.plan && state.plan.apply;
|
|
3088
3229
|
if (!target) {
|
|
3089
3230
|
// Nothing to install on this channel (npx fetches fresh every run; a
|
|
3090
3231
|
// checkout belongs to its git remote). Say who owns it instead of
|
|
@@ -3151,7 +3292,7 @@ function createNpmUpdateManager({
|
|
|
3151
3292
|
};
|
|
3152
3293
|
}
|
|
3153
3294
|
|
|
3154
|
-
function createUpdateManager({
|
|
3295
|
+
function createUpdateManager({ isPackaged, onStatus }) {
|
|
3155
3296
|
let state = { status: 'idle', version: null, error: null, canDownload: false };
|
|
3156
3297
|
|
|
3157
3298
|
function setState(patch) {
|
|
@@ -3159,76 +3300,16 @@ function createUpdateManager({ autoUpdater, isPackaged, onStatus }) {
|
|
|
3159
3300
|
if (onStatus) onStatus(state);
|
|
3160
3301
|
}
|
|
3161
3302
|
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
// feed the packaged path depends on does not exist, while the registry we
|
|
3173
|
-
// actually publish to does.
|
|
3174
|
-
//
|
|
3175
|
-
// npm is therefore the real update channel. Checking it costs one request
|
|
3176
|
-
// a day, reports the version and the exact command, and deliberately does
|
|
3177
|
-
// NOT self-install: replacing your own running process mid-session is a
|
|
3178
|
-
// different promise from telling you an update exists.
|
|
3179
|
-
return createNpmUpdateManager({ onStatus: setState });
|
|
3180
|
-
}
|
|
3181
|
-
|
|
3182
|
-
autoUpdater.autoDownload = false;
|
|
3183
|
-
autoUpdater.autoInstallOnAppQuit = false;
|
|
3184
|
-
|
|
3185
|
-
autoUpdater.on('checking-for-update', () => setState({ status: 'checking', error: null }));
|
|
3186
|
-
autoUpdater.on('update-available', (info) =>
|
|
3187
|
-
setState({ status: 'available', version: info && info.version, error: null, canDownload: true })
|
|
3188
|
-
);
|
|
3189
|
-
autoUpdater.on('update-not-available', () => setState({ status: 'up-to-date', error: null }));
|
|
3190
|
-
autoUpdater.on('download-progress', (progress) =>
|
|
3191
|
-
setState({ status: 'downloading', progress: progress && progress.percent })
|
|
3192
|
-
);
|
|
3193
|
-
autoUpdater.on('update-downloaded', (info) =>
|
|
3194
|
-
setState({ status: 'downloaded', version: info && info.version, error: null })
|
|
3195
|
-
);
|
|
3196
|
-
// Any failure — offline, a malformed feed, a 404 on latest.yml — lands
|
|
3197
|
-
// here instead of an unhandled rejection, since electron-updater emits
|
|
3198
|
-
// 'error' for background failures that occur outside the promise a
|
|
3199
|
-
// check()/download() call is awaiting.
|
|
3200
|
-
autoUpdater.on('error', (err) => setState({ status: 'error', error: errorText(err) }));
|
|
3201
|
-
|
|
3202
|
-
async function check() {
|
|
3203
|
-
try {
|
|
3204
|
-
await autoUpdater.checkForUpdates();
|
|
3205
|
-
} catch (err) {
|
|
3206
|
-
setState({ status: 'error', error: errorText(err) });
|
|
3207
|
-
}
|
|
3208
|
-
return state;
|
|
3209
|
-
}
|
|
3210
|
-
|
|
3211
|
-
async function download() {
|
|
3212
|
-
try {
|
|
3213
|
-
await autoUpdater.downloadUpdate();
|
|
3214
|
-
} catch (err) {
|
|
3215
|
-
setState({ status: 'error', error: errorText(err) });
|
|
3216
|
-
}
|
|
3217
|
-
return state;
|
|
3218
|
-
}
|
|
3219
|
-
|
|
3220
|
-
function quitAndInstall() {
|
|
3221
|
-
autoUpdater.quitAndInstall();
|
|
3222
|
-
}
|
|
3223
|
-
|
|
3224
|
-
function start() {
|
|
3225
|
-
check();
|
|
3226
|
-
const timer = setInterval(check, UPDATE_CHECK_INTERVAL_MS);
|
|
3227
|
-
// Never keep the process alive solely to poll for updates.
|
|
3228
|
-
if (timer.unref) timer.unref();
|
|
3229
|
-
}
|
|
3230
|
-
|
|
3231
|
-
return { status: () => state, check, download, quitAndInstall, start };
|
|
3303
|
+
// ONE channel for every install shape: packaged or `npm i -g`, AppImage, deb,
|
|
3304
|
+
// NSIS, dmg. There is no second applier any more — see this section's header
|
|
3305
|
+
// for the 404 the GitHub Releases feed produced on every packaged launch.
|
|
3306
|
+
//
|
|
3307
|
+
// The old branch here existed only because electron-updater needed a
|
|
3308
|
+
// build-time app-update.yml and a non-packaged run had none. npm needs
|
|
3309
|
+
// neither, so there is nothing left to branch on: the npm manager reads the
|
|
3310
|
+
// registry, reports the version, and names the command that updates THIS
|
|
3311
|
+
// copy (its install method is detected, not assumed).
|
|
3312
|
+
return createNpmUpdateManager({ onStatus: setState, packaged: Boolean(isPackaged) });
|
|
3232
3313
|
}
|
|
3233
3314
|
|
|
3234
3315
|
/** Pure mapping: IPC payload -> update-manager call, same shape as
|
|
@@ -3298,7 +3379,7 @@ function sendDeepLinkToWindow(win, parsed) {
|
|
|
3298
3379
|
* hours, and an About item (mac gets one for free in the app submenu; other
|
|
3299
3380
|
* platforms get a Help menu — role 'about' shows app.setAboutPanelOptions()
|
|
3300
3381
|
* cross-platform since Electron 15). */
|
|
3301
|
-
function buildAppMenu({ app, Menu, BrowserWindow, updateManager }) {
|
|
3382
|
+
function buildAppMenu({ app, Menu, BrowserWindow, updateManager, loggedIn }) {
|
|
3302
3383
|
const isMac = process.platform === 'darwin';
|
|
3303
3384
|
|
|
3304
3385
|
app.setAboutPanelOptions({
|
|
@@ -3341,6 +3422,11 @@ function buildAppMenu({ app, Menu, BrowserWindow, updateManager }) {
|
|
|
3341
3422
|
label: 'Authorship Panel…',
|
|
3342
3423
|
click: () => sendToFocusedWindow(BrowserWindow, MENU_FINGERPRINT_CHANNEL),
|
|
3343
3424
|
};
|
|
3425
|
+
const googleDriveItem = {
|
|
3426
|
+
label: 'Google Drive Backup…',
|
|
3427
|
+
enabled: Boolean(loggedIn),
|
|
3428
|
+
click: () => sendToFocusedWindow(BrowserWindow, MENU_GOOGLE_DRIVE_CHANNEL),
|
|
3429
|
+
};
|
|
3344
3430
|
|
|
3345
3431
|
const template = [
|
|
3346
3432
|
...(isMac
|
|
@@ -3392,6 +3478,7 @@ function buildAppMenu({ app, Menu, BrowserWindow, updateManager }) {
|
|
|
3392
3478
|
searchItem,
|
|
3393
3479
|
{ type: 'separator' },
|
|
3394
3480
|
fingerprintItem,
|
|
3481
|
+
googleDriveItem,
|
|
3395
3482
|
{ type: 'separator' },
|
|
3396
3483
|
{ role: 'reload', accelerator: 'CmdOrCtrl+R' },
|
|
3397
3484
|
{ role: 'forceReload' },
|
|
@@ -3441,6 +3528,60 @@ function bootstrap() {
|
|
|
3441
3528
|
app.commandLine.appendSwitch('disable-software-rasterizer');
|
|
3442
3529
|
app.commandLine.appendSwitch('disable-gpu-compositing');
|
|
3443
3530
|
|
|
3531
|
+
// Chromium asks xdg-desktop-portal for the desktop accent colour on Linux so
|
|
3532
|
+
// form controls can match the system palette. On this Cinnamon session the
|
|
3533
|
+
// portal's appearance backend does not answer that request the way Chromium
|
|
3534
|
+
// expects, so Chromium cancels it and the portal framework prints to stderr:
|
|
3535
|
+
//
|
|
3536
|
+
// [<pid>:<date>:ERROR:components/dbus/xdg/request.cc(173)]
|
|
3537
|
+
// Request ended (non-user cancelled).
|
|
3538
|
+
//
|
|
3539
|
+
// Harmless — Chromium falls back to its default accent — but it lands in the
|
|
3540
|
+
// terminal of every `npm start` next to real errors, and reads like an app
|
|
3541
|
+
// failure. This app renders its own theme tokens and never consults the
|
|
3542
|
+
// system accent colour (no nativeTheme/accentColor reference in main.js,
|
|
3543
|
+
// preload.js, renderer/ or lib/), so that lookup is pure cost here.
|
|
3544
|
+
//
|
|
3545
|
+
// Measured on this Electron build (44.8.0) via dbus-monitor: the switch is
|
|
3546
|
+
// honoured and halves startup portal traffic — org.freedesktop.portal.
|
|
3547
|
+
// Settings Reads go 4 -> 2 and accent-color calls 2 -> 0 per launch. The
|
|
3548
|
+
// surviving Reads are color-scheme, which Electron 44 exposes no gate for;
|
|
3549
|
+
// left alone deliberately, since the window chrome follows light/dark.
|
|
3550
|
+
app.commandLine.appendSwitch('disable-features', 'UsePortalAccentColor');
|
|
3551
|
+
|
|
3552
|
+
// The X11 clipboard advertises its drag-and-drop target formats by looking
|
|
3553
|
+
// each MIME atom up in Chromium's atom cache, and Chromium logs an ERROR for
|
|
3554
|
+
// every atom it has to mint on the spot:
|
|
3555
|
+
//
|
|
3556
|
+
// [<pid>:<date>:ERROR:ui/gfx/x/atom_cache.cc(236)]
|
|
3557
|
+
// Add application/vnd.portal.filetransfer to kAtomsToCache
|
|
3558
|
+
// [<pid>:<date>:ERROR:ui/gfx/x/atom_cache.cc(236)]
|
|
3559
|
+
// Add application/vnd.portal.files to kAtomsToCache
|
|
3560
|
+
//
|
|
3561
|
+
// They fire the first time you CUT (Ctrl-X) or drag a selection, because the
|
|
3562
|
+
// initial atom table covers copy but not the portal file-transfer pair, and
|
|
3563
|
+
// they are printed at ERROR level even though the behaviour is correct: the
|
|
3564
|
+
// atoms get added and the clipboard works.
|
|
3565
|
+
//
|
|
3566
|
+
// Measured on this Electron build (44.8.0): no feature gate suppresses these
|
|
3567
|
+
// — `--disable-features=XdgFileTransferPortal` left them at 2, and adding
|
|
3568
|
+
// UsePortalAccentColor above also left them at 2. Only the log level does
|
|
3569
|
+
// (2 -> 0 at `--log-level=3`). So this is a logging decision, not a feature
|
|
3570
|
+
// one, and it is deliberately the LAST such switch: `3` = FATAL, which
|
|
3571
|
+
// silences ERROR-level chatter like the above while still printing anything
|
|
3572
|
+
// that actually kills the process.
|
|
3573
|
+
//
|
|
3574
|
+
// Never applied over a level the user chose: `--log-level` on the command
|
|
3575
|
+
// line, or ELECTRON_ENABLE_LOGGING (which asks for the verbose output this
|
|
3576
|
+
// would undo), wins. Neither is something this app sets for itself, so a
|
|
3577
|
+
// developer debugging Chromium still sees everything.
|
|
3578
|
+
const argvLogLevel = process.argv.some((a) =>
|
|
3579
|
+
/^--(log-level|enable-logging)/.test(a)
|
|
3580
|
+
);
|
|
3581
|
+
if (!argvLogLevel && !process.env.ELECTRON_ENABLE_LOGGING) {
|
|
3582
|
+
app.commandLine.appendSwitch('log-level', '3');
|
|
3583
|
+
}
|
|
3584
|
+
|
|
3444
3585
|
// A second launch (double-clicking the icon again, `aegis` from a second
|
|
3445
3586
|
// terminal, …) must focus the existing window, not spawn a second process
|
|
3446
3587
|
// fighting the first over the same on-disk settings/session/queue files —
|
|
@@ -4012,18 +4153,6 @@ function bootstrap() {
|
|
|
4012
4153
|
});
|
|
4013
4154
|
});
|
|
4014
4155
|
|
|
4015
|
-
// electron-updater touches the network and expects a build-time
|
|
4016
|
-
// app-update.yml that only exists in a packaged app; requiring it is safe
|
|
4017
|
-
// either way, but it's wrapped defensively so a version mismatch or a
|
|
4018
|
-
// missing optional native dep degrades to "updates disabled" instead of
|
|
4019
|
-
// taking the whole app down.
|
|
4020
|
-
let realAutoUpdater = null;
|
|
4021
|
-
try {
|
|
4022
|
-
realAutoUpdater = require('electron-updater').autoUpdater;
|
|
4023
|
-
} catch {
|
|
4024
|
-
realAutoUpdater = null;
|
|
4025
|
-
}
|
|
4026
|
-
|
|
4027
4156
|
const openWindows = new Set();
|
|
4028
4157
|
function broadcastUpdateStatus(state) {
|
|
4029
4158
|
for (const win of openWindows) {
|
|
@@ -4034,10 +4163,19 @@ function bootstrap() {
|
|
|
4034
4163
|
for (const win of openWindows) {
|
|
4035
4164
|
if (!win.isDestroyed()) win.webContents.send(ACCOUNT_STATUS_CHANNEL, state);
|
|
4036
4165
|
}
|
|
4166
|
+
// The Google Drive menu item is enabled/disabled on login state (see
|
|
4167
|
+
// buildAppMenu) — native menus don't repaint themselves, so every path
|
|
4168
|
+
// that can change aegis.apiKey (register-device, Google sign-in) has to
|
|
4169
|
+
// rebuild through here, the one place both already call.
|
|
4170
|
+
rebuildAppMenu();
|
|
4171
|
+
}
|
|
4172
|
+
function rebuildAppMenu() {
|
|
4173
|
+
Menu.setApplicationMenu(
|
|
4174
|
+
buildAppMenu({ app, Menu, BrowserWindow, updateManager, loggedIn: Boolean(aegis.apiKey) })
|
|
4175
|
+
);
|
|
4037
4176
|
}
|
|
4038
4177
|
|
|
4039
4178
|
const updateManager = createUpdateManager({
|
|
4040
|
-
autoUpdater: realAutoUpdater,
|
|
4041
4179
|
isPackaged: app.isPackaged,
|
|
4042
4180
|
onStatus: broadcastUpdateStatus,
|
|
4043
4181
|
});
|
|
@@ -4059,7 +4197,7 @@ function bootstrap() {
|
|
|
4059
4197
|
(opts) => dialog.showSaveDialog(BrowserWindow.getFocusedWindow(), opts),
|
|
4060
4198
|
(filePath, content) => fs.promises.writeFile(filePath, content, 'utf8')
|
|
4061
4199
|
);
|
|
4062
|
-
|
|
4200
|
+
rebuildAppMenu();
|
|
4063
4201
|
|
|
4064
4202
|
// The local file this window is allowed to be at — used both to restore a
|
|
4065
4203
|
// saved position/reload and to recognise (and block) any navigation away
|
|
@@ -4337,6 +4475,7 @@ module.exports = {
|
|
|
4337
4475
|
MENU_EXPORT_MARKDOWN_CHANNEL,
|
|
4338
4476
|
MENU_EXPORT_JSON_CHANNEL,
|
|
4339
4477
|
MENU_FINGERPRINT_CHANNEL,
|
|
4478
|
+
MENU_GOOGLE_DRIVE_CHANNEL,
|
|
4340
4479
|
DEEP_LINK_CHANNEL,
|
|
4341
4480
|
UPDATE_STATUS_CHANNEL,
|
|
4342
4481
|
UPDATE_CHECK_INTERVAL_MS,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aegis-desktop",
|
|
3
3
|
"productName": "AEGIS Desktop",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.14",
|
|
5
5
|
"description": "Thin Electron host for AEGIS — a local chat UI over the shared client/aegis.js transport. Ships transport + UI only; engine logic stays server-side.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "AEGIS Code",
|
|
@@ -64,9 +64,6 @@
|
|
|
64
64
|
"engines": {
|
|
65
65
|
"node": ">=22.0.0"
|
|
66
66
|
},
|
|
67
|
-
"dependencies": {
|
|
68
|
-
"electron-updater": "^6.8.9"
|
|
69
|
-
},
|
|
70
67
|
"devDependencies": {
|
|
71
68
|
"electron-builder": "^26.15.3",
|
|
72
69
|
"electron": "^44.0.0"
|
package/preload.js
CHANGED
|
@@ -33,6 +33,7 @@ const MENU_SEARCH_CHANNEL = `${IPC_PREFIX}menuSearch`;
|
|
|
33
33
|
const MENU_EXPORT_MARKDOWN_CHANNEL = `${IPC_PREFIX}menuExportMarkdown`;
|
|
34
34
|
const MENU_EXPORT_JSON_CHANNEL = `${IPC_PREFIX}menuExportJson`;
|
|
35
35
|
const MENU_FINGERPRINT_CHANNEL = `${IPC_PREFIX}menuFingerprint`;
|
|
36
|
+
const MENU_GOOGLE_DRIVE_CHANNEL = `${IPC_PREFIX}menuGoogleDrive`;
|
|
36
37
|
const DEEP_LINK_CHANNEL = `${IPC_PREFIX}deepLink`;
|
|
37
38
|
const QUICK_LAUNCHER_PUSH_CHANNEL = `${IPC_PREFIX}quickLauncherPush`;
|
|
38
39
|
|
|
@@ -260,11 +261,10 @@ const api = {
|
|
|
260
261
|
getMemoryPersist: () => invoke('memoryPersist.get'),
|
|
261
262
|
setMemoryPersist: (enabled) => invoke('memoryPersist.set', { enabled: Boolean(enabled) }),
|
|
262
263
|
|
|
263
|
-
// Auto-update (
|
|
264
|
-
//
|
|
265
|
-
//
|
|
266
|
-
//
|
|
267
|
-
// ever be called from an explicit user action (the banner's "Restart to
|
|
264
|
+
// Auto-update (npm registry — see main.js createUpdateManager). check and
|
|
265
|
+
// download resolve the same status shape the push channel delivers: download
|
|
266
|
+
// installs on an explicit click and only then, and quitAndInstallUpdate must
|
|
267
|
+
// only ever be called from an explicit user action (the banner's "Restart to
|
|
268
268
|
// install" button) — main.js never restarts on its own.
|
|
269
269
|
checkForUpdates: () => invoke('checkForUpdates'),
|
|
270
270
|
downloadUpdate: () => invoke('downloadUpdate'),
|
|
@@ -329,6 +329,14 @@ const api = {
|
|
|
329
329
|
ipcRenderer.on(MENU_FINGERPRINT_CHANNEL, listener);
|
|
330
330
|
return () => ipcRenderer.removeListener(MENU_FINGERPRINT_CHANNEL, listener);
|
|
331
331
|
},
|
|
332
|
+
// Account > Google Drive Backup… — same sheet as Authorship Panel above,
|
|
333
|
+
// opened straight to the Drive row instead of leaving the Google sign-in
|
|
334
|
+
// buttons to be found by scrolling past the unrelated φ(α) key controls.
|
|
335
|
+
onMenuGoogleDrive: (onTrigger) => {
|
|
336
|
+
const listener = () => onTrigger();
|
|
337
|
+
ipcRenderer.on(MENU_GOOGLE_DRIVE_CHANNEL, listener);
|
|
338
|
+
return () => ipcRenderer.removeListener(MENU_GOOGLE_DRIVE_CHANNEL, listener);
|
|
339
|
+
},
|
|
332
340
|
onMenuExportJson: (onTrigger) => {
|
|
333
341
|
const listener = () => onTrigger();
|
|
334
342
|
ipcRenderer.on(MENU_EXPORT_JSON_CHANNEL, listener);
|