aegis-desktop 0.8.2 → 0.8.4
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/main.js +66 -4
- package/package.json +1 -1
- package/renderer/app.js +41 -6
package/main.js
CHANGED
|
@@ -508,6 +508,50 @@ function isSafeExternalUrl(url) {
|
|
|
508
508
|
}
|
|
509
509
|
}
|
|
510
510
|
|
|
511
|
+
/**
|
|
512
|
+
* Whether this machine holds a provider route of the user's OWN — the "bring
|
|
513
|
+
* your own key" half of the welcome block's two pitches.
|
|
514
|
+
*
|
|
515
|
+
* The AEGIS Cloud key is `aegis.apiKey` and is reported separately as
|
|
516
|
+
* `keyConfigured`; a BYOK provider row never appears there (it is written by
|
|
517
|
+
* the Provider settings card through settings.set(provider, {baseURL, key})),
|
|
518
|
+
* so it needs its own reader. A row counts once it carries either a key (the
|
|
519
|
+
* pasted-provider case) or a baseURL (the point-it-at-a-local-model case) —
|
|
520
|
+
* both are the user having answered the question the block was asking.
|
|
521
|
+
*
|
|
522
|
+
* `settings.list()` goes through `get()`, which is the same reader the IPC
|
|
523
|
+
* bridge uses to answer the renderer — it never puts the plaintext key on a
|
|
524
|
+
* row (that would put it one `list()` call away from crossing IPC), only the
|
|
525
|
+
* `configured` boolean it already derived from the key. Reading `row.key`
|
|
526
|
+
* here was always reading a field `get()` doesn't emit, so a pasted-key-only
|
|
527
|
+
* provider row (no baseURL — the common case: OpenAI, Anthropic, DeepSeek)
|
|
528
|
+
* never counted and the block kept nagging a user who had already connected
|
|
529
|
+
* one. Verified live: `store.set('openai', { key })` then `list()` — the row
|
|
530
|
+
* carries `configured: true` and no `key` property at all.
|
|
531
|
+
*
|
|
532
|
+
* Reserved namespaces are excluded for the same reason createModelDispatch
|
|
533
|
+
* filters them: they are the store's own bookkeeping (the AEGIS key, the
|
|
534
|
+
* memory-persist preference), not a provider the user configured.
|
|
535
|
+
*
|
|
536
|
+
* Failure reads as "no", which keeps the block visible — the same
|
|
537
|
+
* fail-toward-showing-it bias the CLI's connectNeeded() documents. Showing a
|
|
538
|
+
* block the user has outgrown is a nag; hiding one a new install needed is a
|
|
539
|
+
* dead end, and only the latter loses the user.
|
|
540
|
+
*/
|
|
541
|
+
function providerRouteConfigured(engine) {
|
|
542
|
+
try {
|
|
543
|
+
const list = engine && engine.settings && engine.settings.list;
|
|
544
|
+
if (typeof list !== 'function') return false;
|
|
545
|
+
return (list.call(engine.settings) || []).some((row) => {
|
|
546
|
+
if (!row || !row.provider || isReservedNamespace(row.provider)) return false;
|
|
547
|
+
const baseURL = typeof row.baseURL === 'string' ? row.baseURL.trim() : '';
|
|
548
|
+
return Boolean(row.configured || baseURL);
|
|
549
|
+
});
|
|
550
|
+
} catch {
|
|
551
|
+
return false;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
511
555
|
/**
|
|
512
556
|
* Pure mapping: IPC payload -> shared-client call. No Electron types here, so
|
|
513
557
|
* tests can drive it with a stub client and a fake ipcMain. `openExternal` is
|
|
@@ -515,7 +559,17 @@ function isSafeExternalUrl(url) {
|
|
|
515
559
|
* reject), injected by bootstrap() so this module still needs no `electron`
|
|
516
560
|
* import to stay unit-testable.
|
|
517
561
|
*/
|
|
518
|
-
function createIpcDispatch(aegis, dir, persistApiKey, openExternal) {
|
|
562
|
+
function createIpcDispatch(aegis, dir, persistApiKey, openExternal, providerConfigured) {
|
|
563
|
+
// `providerConfigured` is an injected thunk (bootstrap() passes one that reads
|
|
564
|
+
// the settings store) so this module keeps its no-Electron, unit-testable
|
|
565
|
+
// shape; absent in tests and older call sites, where it reads as "no".
|
|
566
|
+
const hasOwnProviderKey = () => {
|
|
567
|
+
try {
|
|
568
|
+
return Boolean(typeof providerConfigured === 'function' && providerConfigured());
|
|
569
|
+
} catch {
|
|
570
|
+
return false;
|
|
571
|
+
}
|
|
572
|
+
};
|
|
519
573
|
const openExternalFn = openExternal || (() => Promise.reject(new Error('no opener configured')));
|
|
520
574
|
const dispatch = {
|
|
521
575
|
status: () => ({
|
|
@@ -524,6 +578,12 @@ function createIpcDispatch(aegis, dir, persistApiKey, openExternal) {
|
|
|
524
578
|
apiBase: aegis.apiBase,
|
|
525
579
|
keyConfigured: Boolean(aegis.apiKey),
|
|
526
580
|
keyMask: maskKey(aegis.apiKey),
|
|
581
|
+
// The welcome block pitches TWO routes (bring your own key, or AEGIS
|
|
582
|
+
// Cloud) and must retire once the user has taken either one. A BYOK
|
|
583
|
+
// provider key never touches aegis.apiKey, so without this the block
|
|
584
|
+
// kept re-pitching "bring your own key" at someone who just did — see
|
|
585
|
+
// applyWelcomeConnect() in renderer/app.js.
|
|
586
|
+
providerConfigured: hasOwnProviderKey(),
|
|
527
587
|
}),
|
|
528
588
|
|
|
529
589
|
// In-app API key entry (plan rebuild): replace the live client key and
|
|
@@ -628,8 +688,8 @@ function withReplyNotify(promise, event, onReplyFinished) {
|
|
|
628
688
|
* tests, where the safe no-op default in createIpcDispatch takes over.
|
|
629
689
|
* `onReplyFinished` is likewise bootstrap()-only: it fires the native
|
|
630
690
|
* "reply ready" notification when the window is unfocused/hidden. */
|
|
631
|
-
function registerIpc(ipcMain, aegis, dir, persistApiKey, openExternal, onReplyFinished) {
|
|
632
|
-
const dispatch = createIpcDispatch(aegis, dir, persistApiKey, openExternal);
|
|
691
|
+
function registerIpc(ipcMain, aegis, dir, persistApiKey, openExternal, onReplyFinished, providerConfigured) {
|
|
692
|
+
const dispatch = createIpcDispatch(aegis, dir, persistApiKey, openExternal, providerConfigured);
|
|
633
693
|
for (const [name, handler] of Object.entries(dispatch)) {
|
|
634
694
|
if (name === 'chatCompletion') {
|
|
635
695
|
// Streaming render (D2.1): when the renderer asks for stream, SSE deltas
|
|
@@ -2378,7 +2438,8 @@ function bootstrap() {
|
|
|
2378
2438
|
dataDir,
|
|
2379
2439
|
persistApiKey,
|
|
2380
2440
|
(url) => shell.openExternal(url),
|
|
2381
|
-
notifyReplyIfUnfocused
|
|
2441
|
+
notifyReplyIfUnfocused,
|
|
2442
|
+
() => providerRouteConfigured(engine)
|
|
2382
2443
|
);
|
|
2383
2444
|
registerModelIpc(
|
|
2384
2445
|
ipcMain,
|
|
@@ -2755,6 +2816,7 @@ if (electron && electron.app) {
|
|
|
2755
2816
|
module.exports = {
|
|
2756
2817
|
createIpcDispatch,
|
|
2757
2818
|
registerIpc,
|
|
2819
|
+
providerRouteConfigured,
|
|
2758
2820
|
IPC_PREFIX,
|
|
2759
2821
|
MODEL_PREFIX,
|
|
2760
2822
|
SYNC_PREFIX,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aegis-desktop",
|
|
3
3
|
"productName": "AEGIS Desktop",
|
|
4
|
-
"version": "0.8.
|
|
4
|
+
"version": "0.8.4",
|
|
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",
|
package/renderer/app.js
CHANGED
|
@@ -193,7 +193,24 @@ const LOCAL_DEFAULT_BASE = 'http://localhost:11434';
|
|
|
193
193
|
// and the catalog is key-gated, so this is the first thing a new install needs;
|
|
194
194
|
// it lives here rather than inline so the Model hint and any future "connect"
|
|
195
195
|
// affordance cannot drift to two different pages.
|
|
196
|
-
|
|
196
|
+
// The short deep link, not the homepage. `/key` is an aegis1 route that
|
|
197
|
+
// expands server-side into the register panel plus the page that shows the
|
|
198
|
+
// key, with the channel UTMs attached:
|
|
199
|
+
//
|
|
200
|
+
// https://aegiscloud.org/key?s=desktop&c=key_prompt
|
|
201
|
+
// -> /login?next=%2Fapi-keys&utm_source=desktop&utm_medium=app&utm_campaign=key_prompt#register
|
|
202
|
+
//
|
|
203
|
+
// The bare homepage was the bug: no register panel, no route to the key page,
|
|
204
|
+
// and no UTM, so the click was filed as 'direct' and the most intent-rich
|
|
205
|
+
// moment in a fresh install was unattributable. `s=desktop` must stay on
|
|
206
|
+
// aegis1's SIGNUP_SOURCES or this channel reads as dead in the funnel.
|
|
207
|
+
// KEEP IN SYNC with the CLI's twin (cli/src/signup.js, SIGNUP_SOURCE) —
|
|
208
|
+
// desktop/test/renderer-key-link.test.mjs fails if the two diverge.
|
|
209
|
+
const GET_AEGIS_KEY_URL = 'https://aegiscloud.org/key?s=desktop&c=key_prompt';
|
|
210
|
+
// Where an upgrade prompt sends someone when the server supplies no
|
|
211
|
+
// upgradeUrl of its own. Tagged for the same reason as the line above: a bare
|
|
212
|
+
// /subscribe click cannot be credited to the desktop app.
|
|
213
|
+
const AEGIS_SUBSCRIBE_URL = 'https://aegiscloud.org/subscribe?utm_source=desktop&utm_medium=app';
|
|
197
214
|
|
|
198
215
|
let pendingEl = null;
|
|
199
216
|
let pendingSessionId = null;
|
|
@@ -588,13 +605,26 @@ function setConn(ok, text) {
|
|
|
588
605
|
* trip on a guaranteed 401 (aegis1 login_required). */
|
|
589
606
|
let keyConfigured = null;
|
|
590
607
|
|
|
608
|
+
/** True when the user has configured a route of their own — a BYOK provider
|
|
609
|
+
* key or a local model base URL — as reported by the main process
|
|
610
|
+
* (main.js providerRouteConfigured(), shipped in the status payload as
|
|
611
|
+
* `providerConfigured`). This is NOT the same fact as keyConfigured: BYOK
|
|
612
|
+
* keys go to the provider-settings store and never touch aegis.apiKey, so
|
|
613
|
+
* without this the connect block kept pitching "Bring your own key" at the
|
|
614
|
+
* person who had just brought one. Defaults to false so an unknown state
|
|
615
|
+
* keeps the block visible, the same fail-toward-showing-it bias the main
|
|
616
|
+
* process and the CLI both document. */
|
|
617
|
+
let providerConfigured = false;
|
|
618
|
+
|
|
591
619
|
function renderStatus(s) {
|
|
592
620
|
if (!s) {
|
|
593
621
|
keyConfigured = null;
|
|
622
|
+
providerConfigured = false;
|
|
594
623
|
setConn(false, 'IPC unavailable');
|
|
595
624
|
return;
|
|
596
625
|
}
|
|
597
626
|
keyConfigured = Boolean(s.keyConfigured);
|
|
627
|
+
providerConfigured = Boolean(s.providerConfigured);
|
|
598
628
|
els.app.textContent = s.appVersion ? `v${s.appVersion}` : '–';
|
|
599
629
|
els.client.textContent = s.clientVersion || '–';
|
|
600
630
|
els.base.textContent = s.apiBase || '–';
|
|
@@ -724,7 +754,7 @@ function renderBillingError(res, kind) {
|
|
|
724
754
|
// wipe it, and an href left in text is not clickable.
|
|
725
755
|
els.billingHint.textContent = `free plan cap reached — ${what} lifts it. `;
|
|
726
756
|
const a = document.createElement('a');
|
|
727
|
-
a.href = res.upgrade.url ||
|
|
757
|
+
a.href = res.upgrade.url || AEGIS_SUBSCRIBE_URL;
|
|
728
758
|
a.target = '_blank';
|
|
729
759
|
a.rel = 'noreferrer noopener';
|
|
730
760
|
a.textContent = 'See plans →';
|
|
@@ -1401,7 +1431,7 @@ async function fetchMemory(query, limit) {
|
|
|
1401
1431
|
entries: [],
|
|
1402
1432
|
error: '',
|
|
1403
1433
|
upgrade: {
|
|
1404
|
-
url: (err.data && err.data.upgradeUrl) ||
|
|
1434
|
+
url: (err.data && err.data.upgradeUrl) || AEGIS_SUBSCRIBE_URL,
|
|
1405
1435
|
used: err.data && (err.data.tokensUsed != null ? err.data.tokensUsed : err.data.sessionsUsed),
|
|
1406
1436
|
limit: err.data && (err.data.tokenLimit != null ? err.data.tokenLimit : err.data.freeSessionLimit),
|
|
1407
1437
|
},
|
|
@@ -1440,7 +1470,7 @@ function capNotice(el, upgrade, prefix, cta) {
|
|
|
1440
1470
|
`${prefix || 'sync limit reached'} — ${used} of ${cap} tokens synced. ` +
|
|
1441
1471
|
'Nothing was lost. ';
|
|
1442
1472
|
const a = document.createElement('a');
|
|
1443
|
-
a.href = upgrade.url ||
|
|
1473
|
+
a.href = upgrade.url || AEGIS_SUBSCRIBE_URL;
|
|
1444
1474
|
a.target = '_blank';
|
|
1445
1475
|
a.rel = 'noreferrer noopener';
|
|
1446
1476
|
a.textContent = cta || 'Upgrade to keep saving →';
|
|
@@ -1459,7 +1489,7 @@ function renderMemoryResults(entries, error, upgrade) {
|
|
|
1459
1489
|
const li = document.createElement('li');
|
|
1460
1490
|
li.className = 'empty mem-side-upgrade';
|
|
1461
1491
|
const a = document.createElement('a');
|
|
1462
|
-
a.href = upgrade.url ||
|
|
1492
|
+
a.href = upgrade.url || AEGIS_SUBSCRIBE_URL;
|
|
1463
1493
|
a.target = '_blank';
|
|
1464
1494
|
a.rel = 'noreferrer noopener';
|
|
1465
1495
|
a.textContent = 'Free plan limit reached — upgrade to read memory →';
|
|
@@ -1918,10 +1948,15 @@ function applyWelcomeConnect() {
|
|
|
1918
1948
|
} catch {
|
|
1919
1949
|
/* storage disabled — treat as not dismissed */
|
|
1920
1950
|
}
|
|
1951
|
+
// Both connect routes retire the block: the AEGIS Cloud key (keyConfigured)
|
|
1952
|
+
// and a provider route of the user's own (providerConfigured — a BYOK key or
|
|
1953
|
+
// a local base URL, which never touch aegis.apiKey). A panel that keeps
|
|
1954
|
+
// advertising "Bring your own key" to someone who already did is a nag.
|
|
1955
|
+
//
|
|
1921
1956
|
// keyConfigured is null until status is read, so an unknown state keeps the
|
|
1922
1957
|
// block visible: on a cold boot the user has no key far more often than not,
|
|
1923
1958
|
// and renderStatus() calls this again the moment the truth arrives.
|
|
1924
|
-
box.hidden = dismissed || keyConfigured === true;
|
|
1959
|
+
box.hidden = dismissed || keyConfigured === true || providerConfigured === true;
|
|
1925
1960
|
}
|
|
1926
1961
|
|
|
1927
1962
|
/** Scroll a sidebar card into view and mark it, so a welcome click has a
|