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,254 +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
|
-
* provider-key-mirror.js — make a provider key saved in the DESKTOP visible to
|
|
10
|
-
* every other host on the same machine.
|
|
11
|
-
*
|
|
12
|
-
* The desktop's Provider settings rows persist into its own encrypted settings
|
|
13
|
-
* store (`desktop/lib/settings.js` -> `settings.json` under the app's userData
|
|
14
|
-
* dir). That store is private to this app: `aegiscode` (the CLI) and the MCP
|
|
15
|
-
* plugin never read it, and they cannot — it is an encrypted Electron store in
|
|
16
|
-
* another process's data directory. So a key added in the desktop was invisible
|
|
17
|
-
* to the CLI on the next run: same machine, same provider, key asked for twice.
|
|
18
|
-
*
|
|
19
|
-
* The fix is not to teach every host to open an encrypted Electron store. It is
|
|
20
|
-
* to write the key where all four hosts already look at start-up —
|
|
21
|
-
* `~/.aegiscode/.env`, through the ONE shared writer (`client/env-file.js`
|
|
22
|
-
* `setEnvValue`). After this, "add your provider key in the app" and "put it in
|
|
23
|
-
* ~/.aegiscode/.env" are the same instruction, which is the point.
|
|
24
|
-
*
|
|
25
|
-
* Policy, deliberately narrow:
|
|
26
|
-
*
|
|
27
|
-
* - PROVIDER keys only. The AEGIS *account* key is already shared through
|
|
28
|
-
* `credentials.json` (`client/credentials.js`), which the CLI, the MCP
|
|
29
|
-
* plugin and the desktop all read. A second copy in the env file would be a
|
|
30
|
-
* second authority that can silently disagree with the first — exactly the
|
|
31
|
-
* "which one won?" failure env-file.js refuses for duplicate lines. So
|
|
32
|
-
* `setAegisKey()` is deliberately NOT wrapped.
|
|
33
|
-
* - The variable name comes from `envFile.envVarFor(provider)`, the same
|
|
34
|
-
* mapping the CLI's model picker uses (`keyForModelId`), and must end in
|
|
35
|
-
* `_API_KEY`. A name we cannot map is a name we do not invent: an
|
|
36
|
-
* unrecognised provider leaves the file untouched rather than growing an
|
|
37
|
-
* unrelated variable.
|
|
38
|
-
* - Removing a key only touches the file if the file actually carries that
|
|
39
|
-
* variable, so "Remove" on a provider the file never knew about does not
|
|
40
|
-
* add a stray `OPENAI_API_KEY=` line to a hand-written file.
|
|
41
|
-
* - A failure to mirror is REPORTED and swallowed. The key IS saved (the
|
|
42
|
-
* encrypted store has it); only the cross-host copy failed, and refusing
|
|
43
|
-
* the whole save over that would throw away the key the user just typed.
|
|
44
|
-
*/
|
|
45
|
-
|
|
46
|
-
/** Reserved namespaces (`__aegis`, `__quickLauncher`, …) are the store's own
|
|
47
|
-
* bookkeeping, not providers — never a key variable. */
|
|
48
|
-
const RESERVED_PREFIX = '__';
|
|
49
|
-
|
|
50
|
-
/** Every provider key variable in this product family ends this way. */
|
|
51
|
-
const KEY_VAR_SUFFIX = /_API_KEY$/;
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Write one provider key into the shared env file.
|
|
55
|
-
*
|
|
56
|
-
* @param {object} envFile client/env-file.js (injected: this module must stay
|
|
57
|
-
* unit-testable without touching the real home dir)
|
|
58
|
-
* @param {string} provider provider id, e.g. 'openai'
|
|
59
|
-
* @param {string} key the key; '' means "clear it"
|
|
60
|
-
* @param {object} [opts] `onWarn(message)`, `envOpts` forwarded to env-file
|
|
61
|
-
* (its `{ file, env }` seam, used by tests)
|
|
62
|
-
* @returns {object|null} env-file's own result, or null when nothing was
|
|
63
|
-
* mirrored (no mapping, reserved row, absent variable)
|
|
64
|
-
*/
|
|
65
|
-
function mirrorProviderKeyToEnvFile(envFile, provider, key, opts = {}) {
|
|
66
|
-
const warn = typeof opts.onWarn === 'function' ? opts.onWarn : () => {};
|
|
67
|
-
const envOpts = opts.envOpts || {};
|
|
68
|
-
if (!envFile || typeof envFile.setEnvValue !== 'function') return null;
|
|
69
|
-
if (typeof provider !== 'string' || !provider) return null;
|
|
70
|
-
if (provider.startsWith(RESERVED_PREFIX)) return null;
|
|
71
|
-
|
|
72
|
-
const name = typeof envFile.envVarFor === 'function' ? envFile.envVarFor(provider) : '';
|
|
73
|
-
if (!name || !KEY_VAR_SUFFIX.test(name)) return null;
|
|
74
|
-
|
|
75
|
-
try {
|
|
76
|
-
if (key) return envFile.setEnvValue(name, key, envOpts);
|
|
77
|
-
const status =
|
|
78
|
-
typeof envFile.keyStatus === 'function' ? envFile.keyStatus(name, envOpts) : null;
|
|
79
|
-
if (!status || !status.inFile) return null;
|
|
80
|
-
if (typeof envFile.clearEnvValue === 'function') {
|
|
81
|
-
return envFile.clearEnvValue(name, envOpts);
|
|
82
|
-
}
|
|
83
|
-
// An older vendored env-file without a remover. Writing an empty value is
|
|
84
|
-
// NOT a removal — `setEnvValue` refuses an empty value outright, so that
|
|
85
|
-
// call returned an error and left the stale line in place. Unset the live
|
|
86
|
-
// variable and say plainly that the file still carries it.
|
|
87
|
-
if (envOpts.env) delete envOpts.env[name];
|
|
88
|
-
else delete process.env[name];
|
|
89
|
-
warn(
|
|
90
|
-
`removed ${provider}'s key in the app, but ${name} is still in the shared env file — `
|
|
91
|
-
+ 'this build has no clearEnvValue; delete the line by hand',
|
|
92
|
-
);
|
|
93
|
-
return null;
|
|
94
|
-
} catch (e) {
|
|
95
|
-
warn(`could not mirror ${provider}'s key into the shared env file: ${e && e.message}`);
|
|
96
|
-
return null;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Copy every provider key the store already holds into the shared env file.
|
|
102
|
-
*
|
|
103
|
-
* `wrapProviderKeyMirror` above is a WRITE-TIME hook: it mirrors a key at the
|
|
104
|
-
* moment it is saved. That leaves a real gap the hook cannot close, because a
|
|
105
|
-
* key saved before the hook existed never fires it — an install whose provider
|
|
106
|
-
* row predates this module (or any row written while the wrapper was not
|
|
107
|
-
* installed, e.g. a direct settings write from a test or a repair script) keeps
|
|
108
|
-
* its key in the encrypted store alone, and the CLI never sees it. A stored key
|
|
109
|
-
* is not a mirrored key; only a fresh Save used to make them the same thing.
|
|
110
|
-
*
|
|
111
|
-
* The same gap swallows a "blank = keep" save: `renderer/app.js` saves a
|
|
112
|
-
* provider row with `key` undefined when the key field is left empty (that
|
|
113
|
-
* means "keep the stored one" — see the wrapper's comment), so the wrapper
|
|
114
|
-
* correctly mirrors nothing and the row's real key stays host-local for ever.
|
|
115
|
-
*
|
|
116
|
-
* So this runs ONCE at start-up and reconciles the two stores by reading, not
|
|
117
|
-
* by writing: for every non-reserved row that HAS a stored key whose variable
|
|
118
|
-
* the shared file does not already carry, write it. It is:
|
|
119
|
-
*
|
|
120
|
-
* - idempotent — a second call on an already-synced machine mirrors nothing
|
|
121
|
-
* (the `keyStatus(...).inFile` check is the guard), and it never rewrites a
|
|
122
|
-
* variable that is already present, so a hand-edited `.env` keeps its own
|
|
123
|
-
* spelling, comments and line order;
|
|
124
|
-
* - one-directional — store -> file only. The file is never read back into
|
|
125
|
-
* the encrypted store, so this cannot become a merge loop or overwrite a
|
|
126
|
-
* newer key the CLI wrote (which the desktop would then be second-guessing);
|
|
127
|
-
* - non-fatal — a row that cannot be decrypted or written is REPORTED and
|
|
128
|
-
* skipped. A start-up reconcile must never keep the window from opening.
|
|
129
|
-
*
|
|
130
|
-
* Must run after app 'ready': `safeStorage` (which decrypts the stored key) is
|
|
131
|
-
* not usable before then.
|
|
132
|
-
*
|
|
133
|
-
* @param {object} store a settings store (needs `list`, and `rawKey` for the
|
|
134
|
-
* decrypted value — the exact pair settings.js exposes)
|
|
135
|
-
* @param {object} envFile client/env-file.js
|
|
136
|
-
* @param {object} [opts] `onWarn`, `envOpts` (see above)
|
|
137
|
-
* @returns {{mirrored:string[], skipped:string[], failed:string[]}} what
|
|
138
|
-
* happened per provider, so a caller/test can assert rather than guess
|
|
139
|
-
*/
|
|
140
|
-
function backfillProviderKeys(store, envFile, opts = {}) {
|
|
141
|
-
const warn = typeof opts.onWarn === 'function' ? opts.onWarn : () => {};
|
|
142
|
-
const envOpts = opts.envOpts || {};
|
|
143
|
-
const out = { mirrored: [], skipped: [], failed: [] };
|
|
144
|
-
if (!store || typeof store.list !== 'function') return out;
|
|
145
|
-
if (!envFile || typeof envFile.envVarFor !== 'function') return out;
|
|
146
|
-
|
|
147
|
-
let rows = [];
|
|
148
|
-
try {
|
|
149
|
-
rows = store.list() || [];
|
|
150
|
-
} catch (e) {
|
|
151
|
-
warn(`could not read the provider store to backfill the shared env file: ${e && e.message}`);
|
|
152
|
-
return out;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
for (const row of rows) {
|
|
156
|
-
const provider = row && row.provider;
|
|
157
|
-
// Same filters as the write-time mirror: a reserved row is the store's own
|
|
158
|
-
// bookkeeping, and an unmappable name is a name we do not invent.
|
|
159
|
-
if (typeof provider !== 'string' || !provider) continue;
|
|
160
|
-
if (provider.startsWith(RESERVED_PREFIX)) continue;
|
|
161
|
-
|
|
162
|
-
const name = envFile.envVarFor(provider);
|
|
163
|
-
if (!name || !KEY_VAR_SUFFIX.test(name)) {
|
|
164
|
-
out.skipped.push(provider);
|
|
165
|
-
continue;
|
|
166
|
-
}
|
|
167
|
-
// A name the env file itself refuses (a production secret, not a host key)
|
|
168
|
-
// is not a failure to report — `setEnvValue` would reject it by design.
|
|
169
|
-
if (typeof envFile.isForeignSecret === 'function' && envFile.isForeignSecret(name)) {
|
|
170
|
-
out.skipped.push(provider);
|
|
171
|
-
continue;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// Already shared? Then there is nothing to do, and — importantly — nothing
|
|
175
|
-
// to overwrite. This is the idempotence guard.
|
|
176
|
-
let status = null;
|
|
177
|
-
try {
|
|
178
|
-
status = typeof envFile.keyStatus === 'function' ? envFile.keyStatus(name, envOpts) : null;
|
|
179
|
-
} catch { /* unreadable file is "not in the file"; the write below reports it */ }
|
|
180
|
-
if (status && status.inFile) {
|
|
181
|
-
out.skipped.push(provider);
|
|
182
|
-
continue;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// `row.configured` is the store's own "a key is stored" flag (settings.js
|
|
186
|
-
// get() derives it from the decrypted value), so a base-URL-only row is
|
|
187
|
-
// skipped here rather than being asked for a key it never had.
|
|
188
|
-
if (row.configured === false) {
|
|
189
|
-
out.skipped.push(provider);
|
|
190
|
-
continue;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
let key = '';
|
|
194
|
-
try {
|
|
195
|
-
key = typeof store.rawKey === 'function' ? store.rawKey(provider) : '';
|
|
196
|
-
} catch (e) {
|
|
197
|
-
warn(`could not decrypt ${provider}'s stored key for the shared env file: ${e && e.message}`);
|
|
198
|
-
out.failed.push(provider);
|
|
199
|
-
continue;
|
|
200
|
-
}
|
|
201
|
-
if (!key) {
|
|
202
|
-
out.skipped.push(provider);
|
|
203
|
-
continue;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
const res = mirrorProviderKeyToEnvFile(envFile, provider, key, opts);
|
|
207
|
-
if (res && res.ok) out.mirrored.push(provider);
|
|
208
|
-
else out.failed.push(provider);
|
|
209
|
-
}
|
|
210
|
-
return out;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Wrap a provider-settings store so every key saved or removed through it is
|
|
215
|
-
* also written to the shared env file.
|
|
216
|
-
*
|
|
217
|
-
* Wrapping the store (rather than each caller) is the whole point: the Settings
|
|
218
|
-
* pane, the welcome flow's provider rows and any future caller all funnel
|
|
219
|
-
* through `store.set` / `store.remove`, so there is exactly one place that can
|
|
220
|
-
* forget to mirror — and this is it.
|
|
221
|
-
*
|
|
222
|
-
* @param {object} store a settings store (settings.get/set/remove/list)
|
|
223
|
-
* @param {object} envFile client/env-file.js
|
|
224
|
-
* @param {object} [opts] `onWarn`, `envOpts` (see above)
|
|
225
|
-
* @returns {object} the same store, wrapped
|
|
226
|
-
*/
|
|
227
|
-
function wrapProviderKeyMirror(store, envFile, opts = {}) {
|
|
228
|
-
if (!store || typeof store.set !== 'function') return store;
|
|
229
|
-
const mirror = (provider, key) => mirrorProviderKeyToEnvFile(envFile, provider, key, opts);
|
|
230
|
-
|
|
231
|
-
const rawSet = store.set.bind(store);
|
|
232
|
-
const rawRemove = typeof store.remove === 'function' ? store.remove.bind(store) : null;
|
|
233
|
-
|
|
234
|
-
store.set = (provider, cfg) => {
|
|
235
|
-
const res = rawSet(provider, cfg);
|
|
236
|
-
// A row with no key is a base-URL/local-daemon row: there is no secret to
|
|
237
|
-
// share, and mirroring would write an empty variable into a file every host
|
|
238
|
-
// sources. `cfg.key` undefined OR empty both mean "no key supplied"
|
|
239
|
-
// (a blank field in the pane means "keep the stored one").
|
|
240
|
-
if (cfg && typeof cfg.key === 'string' && cfg.key) mirror(provider, cfg.key);
|
|
241
|
-
return res;
|
|
242
|
-
};
|
|
243
|
-
|
|
244
|
-
if (rawRemove) {
|
|
245
|
-
store.remove = (provider) => {
|
|
246
|
-
const res = rawRemove(provider);
|
|
247
|
-
mirror(provider, '');
|
|
248
|
-
return res;
|
|
249
|
-
};
|
|
250
|
-
}
|
|
251
|
-
return store;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
module.exports = { mirrorProviderKeyToEnvFile, wrapProviderKeyMirror, backfillProviderKeys };
|
|
3
|
+
"use strict";const RESERVED_PREFIX="__",KEY_VAR_SUFFIX=/_API_KEY$/;function mirrorProviderKeyToEnvFile(e,r,n,t={}){const o="function"==typeof t.onWarn?t.onWarn:()=>{},i=t.envOpts||{};if(!e||"function"!=typeof e.setEnvValue)return null;if("string"!=typeof r||!r)return null;if(r.startsWith(RESERVED_PREFIX))return null;const s="function"==typeof e.envVarFor?e.envVarFor(r):"";if(!s||!KEY_VAR_SUFFIX.test(s))return null;try{if(n)return e.setEnvValue(s,n,i);const t="function"==typeof e.keyStatus?e.keyStatus(s,i):null;return t&&t.inFile?"function"==typeof e.clearEnvValue?e.clearEnvValue(s,i):(i.env?delete i.env[s]:delete process.env[s],o(`removed ${r}'s key in the app, but ${s} is still in the shared env file — this build has no clearEnvValue; delete the line by hand`),null):null}catch(e){return o(`could not mirror ${r}'s key into the shared env file: ${e&&e.message}`),null}}function backfillProviderKeys(e,r,n={}){const t="function"==typeof n.onWarn?n.onWarn:()=>{},o=n.envOpts||{},i={mirrored:[],skipped:[],failed:[]};if(!e||"function"!=typeof e.list)return i;if(!r||"function"!=typeof r.envVarFor)return i;let s=[];try{s=e.list()||[]}catch(e){return t(`could not read the provider store to backfill the shared env file: ${e&&e.message}`),i}for(const u of s){const s=u&&u.provider;if("string"!=typeof s||!s)continue;if(s.startsWith(RESERVED_PREFIX))continue;const l=r.envVarFor(s);if(!l||!KEY_VAR_SUFFIX.test(l)){i.skipped.push(s);continue}if("function"==typeof r.isForeignSecret&&r.isForeignSecret(l)){i.skipped.push(s);continue}let f=null;try{f="function"==typeof r.keyStatus?r.keyStatus(l,o):null}catch{}if(f&&f.inFile){i.skipped.push(s);continue}if(!1===u.configured){i.skipped.push(s);continue}let c="";try{c="function"==typeof e.rawKey?e.rawKey(s):""}catch(e){t(`could not decrypt ${s}'s stored key for the shared env file: ${e&&e.message}`),i.failed.push(s);continue}if(!c){i.skipped.push(s);continue}const a=mirrorProviderKeyToEnvFile(r,s,c,n);a&&a.ok?i.mirrored.push(s):i.failed.push(s)}return i}function wrapProviderKeyMirror(e,r,n={}){if(!e||"function"!=typeof e.set)return e;const t=(e,t)=>mirrorProviderKeyToEnvFile(r,e,t,n),o=e.set.bind(e),i="function"==typeof e.remove?e.remove.bind(e):null;return e.set=(e,r)=>{const n=o(e,r);return r&&"string"==typeof r.key&&r.key&&t(e,r.key),n},i&&(e.remove=e=>{const r=i(e);return t(e,""),r}),e}module.exports={mirrorProviderKeyToEnvFile:mirrorProviderKeyToEnvFile,wrapProviderKeyMirror:wrapProviderKeyMirror,backfillProviderKeys:backfillProviderKeys};
|
package/lib/quick-launcher.js
CHANGED
|
@@ -1,86 +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
|
-
* quick-launcher.js — pure geometry + gating logic for the global quick-
|
|
10
|
-
* launcher popup (native desktop plumbing). Mirrors desktop/lib/window-state.js:
|
|
11
|
-
* no Electron import, so both functions unit-test without the Electron binary.
|
|
12
|
-
* main.js wires the result into a BrowserWindow + globalShortcut; this module
|
|
13
|
-
* only decides where the window should sit and whether the shortcut should be
|
|
14
|
-
* registered at all.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* A global shortcut is a machine-wide hook — every packaged build ships it,
|
|
19
|
-
* but a `npm start` dev run must NOT silently start grabbing a hotkey that
|
|
20
|
-
* might collide with whatever else the developer has running, unless they've
|
|
21
|
-
* explicitly opted in via the settings flag.
|
|
22
|
-
*/
|
|
23
|
-
function shouldEnableGlobalShortcut({ isPackaged, enabled } = {}) {
|
|
24
|
-
return Boolean(isPackaged) || Boolean(enabled);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Where the launcher window should land: centred on the cursor's current
|
|
29
|
-
* position, clamped so the whole window stays within that cursor's display
|
|
30
|
-
* work area (never straddling into an unplugged second monitor, never
|
|
31
|
-
* hanging off a screen edge). Falls back to centring on the first available
|
|
32
|
-
* display — or the given fallback rectangle — when there is no usable cursor
|
|
33
|
-
* point or display list (e.g. a headless test).
|
|
34
|
-
*
|
|
35
|
-
* `cursor` / `displays` take the plain shapes electron.screen already
|
|
36
|
-
* returns (`{x,y}` / `{workArea:{x,y,width,height}}`), so this stays pure and
|
|
37
|
-
* Electron-free for testing, exactly like window-state.js's clampToDisplay.
|
|
38
|
-
*/
|
|
39
|
-
function computeQuickLauncherBounds({ cursor, displays, size, fallback } = {}) {
|
|
40
|
-
const width = Math.max(1, (size && size.width) || 560);
|
|
41
|
-
const height = Math.max(1, (size && size.height) || 320);
|
|
42
|
-
const list = Array.isArray(displays) ? displays : [];
|
|
43
|
-
const point =
|
|
44
|
-
cursor && Number.isFinite(cursor.x) && Number.isFinite(cursor.y) ? cursor : null;
|
|
45
|
-
|
|
46
|
-
const containing = point
|
|
47
|
-
? list.find((d) => {
|
|
48
|
-
const wa = d && d.workArea;
|
|
49
|
-
return (
|
|
50
|
-
wa &&
|
|
51
|
-
point.x >= wa.x &&
|
|
52
|
-
point.x < wa.x + wa.width &&
|
|
53
|
-
point.y >= wa.y &&
|
|
54
|
-
point.y < wa.y + wa.height
|
|
55
|
-
);
|
|
56
|
-
})
|
|
57
|
-
: null;
|
|
58
|
-
|
|
59
|
-
const target = containing || list[0] || null;
|
|
60
|
-
if (!target || !target.workArea) {
|
|
61
|
-
return {
|
|
62
|
-
x: (fallback && fallback.x) || 0,
|
|
63
|
-
y: (fallback && fallback.y) || 0,
|
|
64
|
-
width,
|
|
65
|
-
height,
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const wa = target.workArea;
|
|
70
|
-
let x;
|
|
71
|
-
let y;
|
|
72
|
-
if (point) {
|
|
73
|
-
x = Math.round(point.x - width / 2);
|
|
74
|
-
y = Math.round(point.y - height / 2);
|
|
75
|
-
} else {
|
|
76
|
-
x = Math.round(wa.x + (wa.width - width) / 2);
|
|
77
|
-
y = Math.round(wa.y + (wa.height - height) / 2);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
x = Math.min(Math.max(x, wa.x), Math.max(wa.x, wa.x + wa.width - width));
|
|
81
|
-
y = Math.min(Math.max(y, wa.y), Math.max(wa.y, wa.y + wa.height - height));
|
|
82
|
-
|
|
83
|
-
return { x, y, width, height };
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
module.exports = { shouldEnableGlobalShortcut, computeQuickLauncherBounds };
|
|
3
|
+
"use strict";function shouldEnableGlobalShortcut({isPackaged:t,enabled:a}={}){return Boolean(t)||Boolean(a)}function computeQuickLauncherBounds({cursor:t,displays:a,size:h,fallback:e}={}){const u=Math.max(1,h&&h.width||560),n=Math.max(1,h&&h.height||320),o=Array.isArray(a)?a:[],r=t&&Number.isFinite(t.x)&&Number.isFinite(t.y)?t:null,i=r?o.find((t=>{const a=t&&t.workArea;return a&&r.x>=a.x&&r.x<a.x+a.width&&r.y>=a.y&&r.y<a.y+a.height})):null,l=i||o[0]||null;if(!l||!l.workArea)return{x:e&&e.x||0,y:e&&e.y||0,width:u,height:n};const c=l.workArea;let d,s;return r?(d=Math.round(r.x-u/2),s=Math.round(r.y-n/2)):(d=Math.round(c.x+(c.width-u)/2),s=Math.round(c.y+(c.height-n)/2)),d=Math.min(Math.max(d,c.x),Math.max(c.x,c.x+c.width-u)),s=Math.min(Math.max(s,c.y),Math.max(c.y,c.y+c.height-n)),{x:d,y:s,width:u,height:n}}module.exports={shouldEnableGlobalShortcut:shouldEnableGlobalShortcut,computeQuickLauncherBounds:computeQuickLauncherBounds};
|