aegis-desktop 0.8.8 → 0.8.9

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.
Files changed (45) hide show
  1. package/README.md +6 -0
  2. package/lib/local/engine.js +37 -31
  3. package/package.json +5 -4
  4. package/renderer/fonts/4iCs6KVjbNBYlgoKcQ72j00.woff2 +0 -0
  5. package/renderer/fonts/4iCs6KVjbNBYlgoKcg72j00.woff2 +0 -0
  6. package/renderer/fonts/4iCs6KVjbNBYlgoKcw72j00.woff2 +0 -0
  7. package/renderer/fonts/4iCs6KVjbNBYlgoKew72j00.woff2 +0 -0
  8. package/renderer/fonts/4iCs6KVjbNBYlgoKfA72j00.woff2 +0 -0
  9. package/renderer/fonts/4iCs6KVjbNBYlgoKfw72.woff2 +0 -0
  10. package/renderer/fonts/4iCu6KVjbNBYlgoKej70l0k.woff2 +0 -0
  11. package/renderer/fonts/4iCu6KVjbNBYlgoKej73l0mwFg.woff2 +0 -0
  12. package/renderer/fonts/4iCu6KVjbNBYlgoKej74l0mwFg.woff2 +0 -0
  13. package/renderer/fonts/4iCu6KVjbNBYlgoKej75l0mwFg.woff2 +0 -0
  14. package/renderer/fonts/4iCu6KVjbNBYlgoKej76l0mwFg.woff2 +0 -0
  15. package/renderer/fonts/4iCu6KVjbNBYlgoKej7wl0mwFg.woff2 +0 -0
  16. package/renderer/fonts/4iCv6KVjbNBYlgoCjC3js2yNL4U.woff2 +0 -0
  17. package/renderer/fonts/4iCv6KVjbNBYlgoCjC3jsGyN.woff2 +0 -0
  18. package/renderer/fonts/4iCv6KVjbNBYlgoCjC3jtGyNL4U.woff2 +0 -0
  19. package/renderer/fonts/4iCv6KVjbNBYlgoCjC3jvGyNL4U.woff2 +0 -0
  20. package/renderer/fonts/4iCv6KVjbNBYlgoCjC3jvWyNL4U.woff2 +0 -0
  21. package/renderer/fonts/4iCv6KVjbNBYlgoCjC3jvmyNL4U.woff2 +0 -0
  22. package/renderer/fonts/4iCv6KVjbNBYlgoCxCvjs2yNL4U.woff2 +0 -0
  23. package/renderer/fonts/4iCv6KVjbNBYlgoCxCvjsGyN.woff2 +0 -0
  24. package/renderer/fonts/4iCv6KVjbNBYlgoCxCvjtGyNL4U.woff2 +0 -0
  25. package/renderer/fonts/4iCv6KVjbNBYlgoCxCvjvGyNL4U.woff2 +0 -0
  26. package/renderer/fonts/4iCv6KVjbNBYlgoCxCvjvWyNL4U.woff2 +0 -0
  27. package/renderer/fonts/4iCv6KVjbNBYlgoCxCvjvmyNL4U.woff2 +0 -0
  28. package/renderer/fonts/8vIH7w4qzmVxm25L9Hz_.woff2 +0 -0
  29. package/renderer/fonts/8vIH7w4qzmVxm2BL9A.woff2 +0 -0
  30. package/renderer/fonts/8vIH7w4qzmVxm2NL9Hz_.woff2 +0 -0
  31. package/renderer/fonts/NOTICE.md +57 -0
  32. package/renderer/fonts/aFTR7PB1QTsUX8KYvrGyEY2tbZX9.woff2 +0 -0
  33. package/renderer/fonts/aFTR7PB1QTsUX8KYvrGyEYOtbQ.woff2 +0 -0
  34. package/renderer/fonts/aFTR7PB1QTsUX8KYvumzEY2tbZX9.woff2 +0 -0
  35. package/renderer/fonts/aFTR7PB1QTsUX8KYvumzEYOtbQ.woff2 +0 -0
  36. package/renderer/fonts/aFTU7PB1QTsUX8KYthSQBLyM.woff2 +0 -0
  37. package/renderer/fonts/aFTU7PB1QTsUX8KYthqQBA.woff2 +0 -0
  38. package/renderer/fonts/aFTW7PB1QTsUX8KYth-gBqSI.woff2 +0 -0
  39. package/renderer/fonts/aFTW7PB1QTsUX8KYth-gCKSIUKw.woff2 +0 -0
  40. package/renderer/fonts/fonts.css +368 -0
  41. package/renderer/index.html +6 -7
  42. package/renderer/quick.css +28 -3
  43. package/renderer/quick.html +4 -1
  44. package/renderer/style.css +14 -2
  45. package/vendor/brain-catalog.js +113 -0
package/README.md CHANGED
@@ -326,3 +326,9 @@ host over the same engine.
326
326
  [`aegis-desktop`](https://www.npmjs.com/package/aegis-desktop) is built from a
327
327
  `git subtree split` of it — same code, published without the rest of the
328
328
  monorepo. Edits land here and are synced out; nothing is authored elsewhere.
329
+
330
+ ---
331
+
332
+ Built by **Niklas Borneklint** — [aegiscloud.org](https://aegiscloud.org) · [@aegisinfo](https://github.com/aegisinfo)
333
+
334
+ Part of the ÆGIS ecosystem.
@@ -76,6 +76,34 @@ try {
76
76
  }
77
77
  }
78
78
 
79
+ /**
80
+ * The shared pooled-brain selection rule (`client/brain-catalog.js`), resolved
81
+ * the same two ways as every other shared module: repo-relative from a source
82
+ * checkout, and this app's own staged tree (`desktop/vendor/brain-catalog.js`,
83
+ * flattened by scripts/predist.mjs because a packaged app cannot reach outside
84
+ * its own directory). The CLI resolves the same file its own way, through
85
+ * `cli/src/sharedpaths.js`. Both hosts must agree on which entry stands for the
86
+ * pool, so there is deliberately no fallback implementation here — a silently
87
+ * divergent local copy is the exact bug this module removes. When neither
88
+ * layout has it, require() throws at load time naming the file, rather than the
89
+ * host quietly offering a different model list than its sibling.
90
+ */
91
+ function requireSharedBrain() {
92
+ const candidates = [
93
+ () => require('../../../client/brain-catalog.js'),
94
+ () => require('../../vendor/brain-catalog.js'),
95
+ ];
96
+ let last = null;
97
+ for (const load of candidates) {
98
+ try {
99
+ return load();
100
+ } catch (err) {
101
+ last = err;
102
+ }
103
+ }
104
+ throw last;
105
+ }
106
+
79
107
  /**
80
108
  * How long a turn waits for the working-tree lock before running anyway.
81
109
  *
@@ -254,38 +282,16 @@ function normalizeCatalog(models) {
254
282
  * The pool already auto-routes across whichever providers are live, so the
255
283
  * desktop dropdown offers exactly one entry for the "aegis" class: the
256
284
  * collapsed "Nexus" brain — never the raw provider list.
285
+ *
286
+ * The selection rule itself now lives in `client/brain-catalog.js`, shared with
287
+ * the CLI: this host and the terminal were offering the same account two
288
+ * different model lists (one collapsed entry vs. every distinct provider id),
289
+ * and two copies of the rule is precisely how that drift happened. Resolved the
290
+ * two ways this repo resolves every shared module — repo-relative in a
291
+ * checkout, and this app's staged vendor/ tree — with the selection itself
292
+ * unchanged.
257
293
  */
258
- const NEXUS_BRAIN_IDS = Object.freeze(['nexus-brain', 'aegis-brain']);
259
- const NEXUS_LABEL = 'Nexus';
260
-
261
- /**
262
- * Pick the one entry standing in for the pooled brain. The server serves
263
- * `nexus-brain` as the canonical tier (`hidden: false`, no `alias_of`) and
264
- * marks every other spelling — `aegis-brain` and the `-smart`/`-neo` tiers —
265
- * as `hidden: true, alias_of: "nexus-brain"`. Prefer the canonical id so the
266
- * request travels on the name the server owns; fall back to the alias, then to
267
- * any tier whose `alias_of` names the brain, so a renamed or trimmed catalog
268
- * still resolves to something selectable instead of silently emptying the
269
- * dropdown (the previous fixed-id lookup returned [] if `aegis-brain` was ever
270
- * retired, leaving the class with no model to choose).
271
- */
272
- function selectBrainEntry(models) {
273
- return (
274
- models.find((m) => NEXUS_BRAIN_IDS.includes(m.id) && !m.hidden && m.alias_of === undefined)
275
- || models.find((m) => NEXUS_BRAIN_IDS.includes(m.id))
276
- || models.find((m) => m.alias_of && NEXUS_BRAIN_IDS.includes(m.alias_of))
277
- || null
278
- );
279
- }
280
-
281
- function filterAegisCatalog(models) {
282
- const nexus = selectBrainEntry(models);
283
- if (!nexus) return [];
284
- // Drop the alias bookkeeping: this entry *is* the selection, so the renderer
285
- // must never treat it as a hidden alias and filter it back out.
286
- const { hidden, alias_of, ...rest } = nexus;
287
- return [{ ...rest, label: NEXUS_LABEL }];
288
- }
294
+ const { filterAegisCatalog } = requireSharedBrain();
289
295
 
290
296
  // ── Agent-loop helpers ──────────────────────────────────────────────────────
291
297
 
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "aegis-desktop",
3
3
  "productName": "AEGIS Desktop",
4
- "version": "0.8.8",
5
- "description": "Thin Electron host for AEGIS \u2014 a local chat UI over the shared client/aegis.js transport. Ships transport + UI only; engine logic stays server-side.",
4
+ "version": "0.8.9",
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",
8
8
  "email": "nborneklint@gmail.com"
@@ -32,7 +32,7 @@
32
32
  "demo": "node scripts/record-demo-gif.mjs",
33
33
  "dist": "npm run predist && electron-builder",
34
34
  "dist:dir": "npm run predist && electron-builder --dir",
35
- "check": "node --check main.js && node --check preload.js && node --check renderer/app.js && node --check renderer/quick.js && node --check renderer/budget.js && node --check renderer/usage.js && node --check renderer/stream-policy.js && node --check renderer/transcript-view.js && node --check renderer/diff.js && node --check renderer/diffview.js && node --check renderer/markdown.js && node --check renderer/vendor/aegis-highlight.js && node --check scripts/predist.mjs && node --check scripts/generate-icon.mjs && node --check lib/local/local.js && node --check lib/local/context.js && node --check lib/local/engine.js && node --check lib/local/session-rounds.js && node --check lib/local/tools.js && node --check lib/local/prompt.js && node --check lib/local/shell.js && node --check lib/local/agents.js && node --check lib/local/queue.js && node --check lib/local/autonomous.js && node --check lib/settings.js && node --check lib/sync/sessions.js && node --check lib/sync/memory-queue.js && node --check lib/window-state.js && node --check lib/deep-link.js && node --check lib/quick-launcher.js && node --check lib/avatar/tiers.js && node --check lib/avatar/xp.js && node --check lib/avatar/level.js && node --check lib/avatar/events.js && node --check lib/avatar/persona.js && node --check bin/aegis.js",
35
+ "check": "node --check main.js && node --check preload.js && node --check renderer/app.js && node --check renderer/quick.js && node --check renderer/budget.js && node --check renderer/usage.js && node --check renderer/stream-policy.js && node --check renderer/transcript-view.js && node --check renderer/diff.js && node --check renderer/diffview.js && node --check renderer/markdown.js && node --check renderer/vendor/aegis-highlight.js && node --check scripts/predist.mjs && node --check scripts/generate-icon.mjs && node --check scripts/bundle-fonts.mjs && node --check lib/local/local.js && node --check lib/local/context.js && node --check lib/local/engine.js && node --check lib/local/session-rounds.js && node --check lib/local/tools.js && node --check lib/local/prompt.js && node --check lib/local/shell.js && node --check lib/local/agents.js && node --check lib/local/queue.js && node --check lib/local/autonomous.js && node --check lib/settings.js && node --check lib/sync/sessions.js && node --check lib/sync/memory-queue.js && node --check lib/window-state.js && node --check lib/deep-link.js && node --check lib/quick-launcher.js && node --check lib/avatar/tiers.js && node --check lib/avatar/xp.js && node --check lib/avatar/level.js && node --check lib/avatar/events.js && node --check lib/avatar/persona.js && node --check bin/aegis.js",
36
36
  "test:shell": "node ../test/desktop-shell.mjs",
37
37
  "test:model": "node ../test/model-dispatch.mjs",
38
38
  "test:budget": "node ../test/budget.test.mjs",
@@ -52,7 +52,8 @@
52
52
  "test:avatar-level": "node test/avatar-level.test.mjs",
53
53
  "test:avatar-events": "node test/avatar-events.test.mjs",
54
54
  "test:avatar-tiers": "node test/avatar-tiers.test.mjs",
55
- "test:avatar-persona": "node test/avatar-persona.test.mjs"
55
+ "test:avatar-persona": "node test/avatar-persona.test.mjs",
56
+ "test:renderer-fonts": "node test/renderer-fonts.test.mjs"
56
57
  },
57
58
  "dependencies": {
58
59
  "electron-updater": "^6.8.9"
@@ -0,0 +1,57 @@
1
+ # Vendored webfonts
2
+
3
+ Committed verbatim, like `renderer/vendor/` — no CDN and no network at run time.
4
+ Loaded by `renderer/index.html` and `renderer/quick.html` under a
5
+ `font-src 'self'` CSP.
6
+
7
+ Generated by `desktop/scripts/bundle-fonts.mjs`; do not edit the `.woff2` files
8
+ or `fonts.css` by hand. To refresh the set:
9
+
10
+ node desktop/scripts/bundle-fonts.mjs # rewrite
11
+ node desktop/scripts/bundle-fonts.mjs --check # verify against the CDN (needs network)
12
+
13
+ All three families are licensed under the **SIL Open Font License 1.1**, which
14
+ permits bundling and redistribution. The copyright lines Google Fonts serves
15
+ with them:
16
+
17
+ - `Syne` — © 2018 The Syne Project Authors
18
+ (<https://github.com/bonjour-monde/fonderie>), OFL 1.1. Display face used for
19
+ the wordmark, headings and UI chrome (`--font-display`).
20
+ - `DM Mono` — © 2020 The DM Mono Project Authors
21
+ (<https://github.com/googlefonts/dm-mono>), OFL 1.1. Monospace face for code,
22
+ diffs and metrics (`--font-mono`).
23
+ - `Ubuntu` — © 2010-2011 Canonical Ltd.
24
+ (<https://github.com/googlefonts/UbuntuFont>), OFL 1.1. Chat body text
25
+ (`--font-chat`).
26
+
27
+ The OFL is a per-file licence; the full text ships with each upstream project at
28
+ the links above and is not duplicated here.
29
+
30
+ ## Why these are vendored rather than fetched
31
+
32
+ The `<link>` to `fonts.googleapis.com` that used to sit in `index.html` made the
33
+ app's typography depend on the network and on the host. Online it drew the three
34
+ faces above. Offline — or behind a blocker, or on a machine whose font resolver
35
+ disagreed — the fallback chain `'Segoe UI', system-ui, sans-serif` resolved to a
36
+ *different* font on each platform: SF Pro on macOS, Segoe UI on Windows, whatever
37
+ fontconfig offered on Linux. Three visibly different products from one stylesheet.
38
+
39
+ Serving the same bytes from the app directory makes the rendering identical on
40
+ every platform and lets the CSP drop its external origins entirely. This is the
41
+ desktop counterpart of the terminal-side policy in `cli/src/caps.js`, which
42
+ likewise refuses to let the host change what is drawn.
43
+
44
+ Note the honest limits. These three families cover latin, latin-ext, cyrillic,
45
+ cyrillic-ext, greek and greek-ext — and **not** vietnamese: Google Fonts serves
46
+ no vietnamese subset for Syne, DM Mono or Ubuntu, and `latin-ext` stops at
47
+ U+1E9F and resumes at U+1EF2, so the core Vietnamese diacritic block
48
+ U+1EA0–U+1EF1 falls through to a host font. `desktop/scripts/bundle-fonts.mjs`
49
+ records the same gap, and `desktop/test/renderer-fonts.test.mjs` asserts it, so
50
+ the day Google adds the subset the test fails loudly instead of the gap
51
+ silently persisting.
52
+
53
+ Glyphs outside the covered set — CJK, and colour emoji in particular — are also
54
+ still drawn by a host font and remain platform-specific. `Apple Color Emoji`,
55
+ `Segoe UI Emoji` and `Noto Color Emoji` are not interchangeable and are not
56
+ redistributable here, so that divergence cannot be removed by bundling; it is
57
+ inherent to the platform.
@@ -0,0 +1,368 @@
1
+ /* Vendored webfonts — DO NOT EDIT BY HAND.
2
+ *
3
+ * Generated by desktop/scripts/bundle-fonts.mjs from the Google Fonts CSS for
4
+ * Syne (400/600/700/800), DM Mono (300/400/500 + italic 400) and Ubuntu
5
+ * (400/500/700 + italic 400) — the exact families renderer/index.html used to
6
+ * request from fonts.googleapis.com at run time.
7
+ *
8
+ * WHY THIS IS VENDORED. A CDN <link> made the app's type depend on the network
9
+ * and on the host: online it drew Syne/DM Mono/Ubuntu, offline (or behind a
10
+ * blocker) it silently fell through to 'Segoe UI' on Windows, system-ui/SF on
11
+ * macOS and whatever fontconfig produced on Linux — three different-looking
12
+ * apps from one stylesheet. Serving the same bytes from the app directory makes
13
+ * the rendering identical everywhere and offline-capable. Licences are OFL 1.1;
14
+ * see NOTICE.md in this directory.
15
+ */
16
+
17
+ @font-face {
18
+ font-family: 'DM Mono';
19
+ font-style: italic;
20
+ font-weight: 400;
21
+ font-display: swap;
22
+ src: url(./aFTW7PB1QTsUX8KYth-gCKSIUKw.woff2) format('woff2');
23
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
24
+ }
25
+ @font-face {
26
+ font-family: 'DM Mono';
27
+ font-style: italic;
28
+ font-weight: 400;
29
+ font-display: swap;
30
+ src: url(./aFTW7PB1QTsUX8KYth-gBqSI.woff2) format('woff2');
31
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
32
+ }
33
+ @font-face {
34
+ font-family: 'DM Mono';
35
+ font-style: normal;
36
+ font-weight: 300;
37
+ font-display: swap;
38
+ src: url(./aFTR7PB1QTsUX8KYvrGyEY2tbZX9.woff2) format('woff2');
39
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
40
+ }
41
+ @font-face {
42
+ font-family: 'DM Mono';
43
+ font-style: normal;
44
+ font-weight: 300;
45
+ font-display: swap;
46
+ src: url(./aFTR7PB1QTsUX8KYvrGyEYOtbQ.woff2) format('woff2');
47
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
48
+ }
49
+ @font-face {
50
+ font-family: 'DM Mono';
51
+ font-style: normal;
52
+ font-weight: 400;
53
+ font-display: swap;
54
+ src: url(./aFTU7PB1QTsUX8KYthSQBLyM.woff2) format('woff2');
55
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
56
+ }
57
+ @font-face {
58
+ font-family: 'DM Mono';
59
+ font-style: normal;
60
+ font-weight: 400;
61
+ font-display: swap;
62
+ src: url(./aFTU7PB1QTsUX8KYthqQBA.woff2) format('woff2');
63
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
64
+ }
65
+ @font-face {
66
+ font-family: 'DM Mono';
67
+ font-style: normal;
68
+ font-weight: 500;
69
+ font-display: swap;
70
+ src: url(./aFTR7PB1QTsUX8KYvumzEY2tbZX9.woff2) format('woff2');
71
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
72
+ }
73
+ @font-face {
74
+ font-family: 'DM Mono';
75
+ font-style: normal;
76
+ font-weight: 500;
77
+ font-display: swap;
78
+ src: url(./aFTR7PB1QTsUX8KYvumzEYOtbQ.woff2) format('woff2');
79
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
80
+ }
81
+ @font-face {
82
+ font-family: 'Syne';
83
+ font-style: normal;
84
+ font-weight: 400;
85
+ font-display: swap;
86
+ src: url(./8vIH7w4qzmVxm2NL9Hz_.woff2) format('woff2');
87
+ unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
88
+ }
89
+ @font-face {
90
+ font-family: 'Syne';
91
+ font-style: normal;
92
+ font-weight: 400;
93
+ font-display: swap;
94
+ src: url(./8vIH7w4qzmVxm25L9Hz_.woff2) format('woff2');
95
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
96
+ }
97
+ @font-face {
98
+ font-family: 'Syne';
99
+ font-style: normal;
100
+ font-weight: 400;
101
+ font-display: swap;
102
+ src: url(./8vIH7w4qzmVxm2BL9A.woff2) format('woff2');
103
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
104
+ }
105
+ @font-face {
106
+ font-family: 'Syne';
107
+ font-style: normal;
108
+ font-weight: 600;
109
+ font-display: swap;
110
+ src: url(./8vIH7w4qzmVxm2NL9Hz_.woff2) format('woff2');
111
+ unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
112
+ }
113
+ @font-face {
114
+ font-family: 'Syne';
115
+ font-style: normal;
116
+ font-weight: 600;
117
+ font-display: swap;
118
+ src: url(./8vIH7w4qzmVxm25L9Hz_.woff2) format('woff2');
119
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
120
+ }
121
+ @font-face {
122
+ font-family: 'Syne';
123
+ font-style: normal;
124
+ font-weight: 600;
125
+ font-display: swap;
126
+ src: url(./8vIH7w4qzmVxm2BL9A.woff2) format('woff2');
127
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
128
+ }
129
+ @font-face {
130
+ font-family: 'Syne';
131
+ font-style: normal;
132
+ font-weight: 700;
133
+ font-display: swap;
134
+ src: url(./8vIH7w4qzmVxm2NL9Hz_.woff2) format('woff2');
135
+ unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
136
+ }
137
+ @font-face {
138
+ font-family: 'Syne';
139
+ font-style: normal;
140
+ font-weight: 700;
141
+ font-display: swap;
142
+ src: url(./8vIH7w4qzmVxm25L9Hz_.woff2) format('woff2');
143
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
144
+ }
145
+ @font-face {
146
+ font-family: 'Syne';
147
+ font-style: normal;
148
+ font-weight: 700;
149
+ font-display: swap;
150
+ src: url(./8vIH7w4qzmVxm2BL9A.woff2) format('woff2');
151
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
152
+ }
153
+ @font-face {
154
+ font-family: 'Syne';
155
+ font-style: normal;
156
+ font-weight: 800;
157
+ font-display: swap;
158
+ src: url(./8vIH7w4qzmVxm2NL9Hz_.woff2) format('woff2');
159
+ unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
160
+ }
161
+ @font-face {
162
+ font-family: 'Syne';
163
+ font-style: normal;
164
+ font-weight: 800;
165
+ font-display: swap;
166
+ src: url(./8vIH7w4qzmVxm25L9Hz_.woff2) format('woff2');
167
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
168
+ }
169
+ @font-face {
170
+ font-family: 'Syne';
171
+ font-style: normal;
172
+ font-weight: 800;
173
+ font-display: swap;
174
+ src: url(./8vIH7w4qzmVxm2BL9A.woff2) format('woff2');
175
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
176
+ }
177
+ @font-face {
178
+ font-family: 'Ubuntu';
179
+ font-style: italic;
180
+ font-weight: 400;
181
+ font-display: swap;
182
+ src: url(./4iCu6KVjbNBYlgoKej75l0mwFg.woff2) format('woff2');
183
+ unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
184
+ }
185
+ @font-face {
186
+ font-family: 'Ubuntu';
187
+ font-style: italic;
188
+ font-weight: 400;
189
+ font-display: swap;
190
+ src: url(./4iCu6KVjbNBYlgoKej7wl0mwFg.woff2) format('woff2');
191
+ unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
192
+ }
193
+ @font-face {
194
+ font-family: 'Ubuntu';
195
+ font-style: italic;
196
+ font-weight: 400;
197
+ font-display: swap;
198
+ src: url(./4iCu6KVjbNBYlgoKej74l0mwFg.woff2) format('woff2');
199
+ unicode-range: U+1F00-1FFF;
200
+ }
201
+ @font-face {
202
+ font-family: 'Ubuntu';
203
+ font-style: italic;
204
+ font-weight: 400;
205
+ font-display: swap;
206
+ src: url(./4iCu6KVjbNBYlgoKej73l0mwFg.woff2) format('woff2');
207
+ unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
208
+ }
209
+ @font-face {
210
+ font-family: 'Ubuntu';
211
+ font-style: italic;
212
+ font-weight: 400;
213
+ font-display: swap;
214
+ src: url(./4iCu6KVjbNBYlgoKej76l0mwFg.woff2) format('woff2');
215
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
216
+ }
217
+ @font-face {
218
+ font-family: 'Ubuntu';
219
+ font-style: italic;
220
+ font-weight: 400;
221
+ font-display: swap;
222
+ src: url(./4iCu6KVjbNBYlgoKej70l0k.woff2) format('woff2');
223
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
224
+ }
225
+ @font-face {
226
+ font-family: 'Ubuntu';
227
+ font-style: normal;
228
+ font-weight: 400;
229
+ font-display: swap;
230
+ src: url(./4iCs6KVjbNBYlgoKcg72j00.woff2) format('woff2');
231
+ unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
232
+ }
233
+ @font-face {
234
+ font-family: 'Ubuntu';
235
+ font-style: normal;
236
+ font-weight: 400;
237
+ font-display: swap;
238
+ src: url(./4iCs6KVjbNBYlgoKew72j00.woff2) format('woff2');
239
+ unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
240
+ }
241
+ @font-face {
242
+ font-family: 'Ubuntu';
243
+ font-style: normal;
244
+ font-weight: 400;
245
+ font-display: swap;
246
+ src: url(./4iCs6KVjbNBYlgoKcw72j00.woff2) format('woff2');
247
+ unicode-range: U+1F00-1FFF;
248
+ }
249
+ @font-face {
250
+ font-family: 'Ubuntu';
251
+ font-style: normal;
252
+ font-weight: 400;
253
+ font-display: swap;
254
+ src: url(./4iCs6KVjbNBYlgoKfA72j00.woff2) format('woff2');
255
+ unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
256
+ }
257
+ @font-face {
258
+ font-family: 'Ubuntu';
259
+ font-style: normal;
260
+ font-weight: 400;
261
+ font-display: swap;
262
+ src: url(./4iCs6KVjbNBYlgoKcQ72j00.woff2) format('woff2');
263
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
264
+ }
265
+ @font-face {
266
+ font-family: 'Ubuntu';
267
+ font-style: normal;
268
+ font-weight: 400;
269
+ font-display: swap;
270
+ src: url(./4iCs6KVjbNBYlgoKfw72.woff2) format('woff2');
271
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
272
+ }
273
+ @font-face {
274
+ font-family: 'Ubuntu';
275
+ font-style: normal;
276
+ font-weight: 500;
277
+ font-display: swap;
278
+ src: url(./4iCv6KVjbNBYlgoCjC3jvWyNL4U.woff2) format('woff2');
279
+ unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
280
+ }
281
+ @font-face {
282
+ font-family: 'Ubuntu';
283
+ font-style: normal;
284
+ font-weight: 500;
285
+ font-display: swap;
286
+ src: url(./4iCv6KVjbNBYlgoCjC3jtGyNL4U.woff2) format('woff2');
287
+ unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
288
+ }
289
+ @font-face {
290
+ font-family: 'Ubuntu';
291
+ font-style: normal;
292
+ font-weight: 500;
293
+ font-display: swap;
294
+ src: url(./4iCv6KVjbNBYlgoCjC3jvGyNL4U.woff2) format('woff2');
295
+ unicode-range: U+1F00-1FFF;
296
+ }
297
+ @font-face {
298
+ font-family: 'Ubuntu';
299
+ font-style: normal;
300
+ font-weight: 500;
301
+ font-display: swap;
302
+ src: url(./4iCv6KVjbNBYlgoCjC3js2yNL4U.woff2) format('woff2');
303
+ unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
304
+ }
305
+ @font-face {
306
+ font-family: 'Ubuntu';
307
+ font-style: normal;
308
+ font-weight: 500;
309
+ font-display: swap;
310
+ src: url(./4iCv6KVjbNBYlgoCjC3jvmyNL4U.woff2) format('woff2');
311
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
312
+ }
313
+ @font-face {
314
+ font-family: 'Ubuntu';
315
+ font-style: normal;
316
+ font-weight: 500;
317
+ font-display: swap;
318
+ src: url(./4iCv6KVjbNBYlgoCjC3jsGyN.woff2) format('woff2');
319
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
320
+ }
321
+ @font-face {
322
+ font-family: 'Ubuntu';
323
+ font-style: normal;
324
+ font-weight: 700;
325
+ font-display: swap;
326
+ src: url(./4iCv6KVjbNBYlgoCxCvjvWyNL4U.woff2) format('woff2');
327
+ unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
328
+ }
329
+ @font-face {
330
+ font-family: 'Ubuntu';
331
+ font-style: normal;
332
+ font-weight: 700;
333
+ font-display: swap;
334
+ src: url(./4iCv6KVjbNBYlgoCxCvjtGyNL4U.woff2) format('woff2');
335
+ unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
336
+ }
337
+ @font-face {
338
+ font-family: 'Ubuntu';
339
+ font-style: normal;
340
+ font-weight: 700;
341
+ font-display: swap;
342
+ src: url(./4iCv6KVjbNBYlgoCxCvjvGyNL4U.woff2) format('woff2');
343
+ unicode-range: U+1F00-1FFF;
344
+ }
345
+ @font-face {
346
+ font-family: 'Ubuntu';
347
+ font-style: normal;
348
+ font-weight: 700;
349
+ font-display: swap;
350
+ src: url(./4iCv6KVjbNBYlgoCxCvjs2yNL4U.woff2) format('woff2');
351
+ unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
352
+ }
353
+ @font-face {
354
+ font-family: 'Ubuntu';
355
+ font-style: normal;
356
+ font-weight: 700;
357
+ font-display: swap;
358
+ src: url(./4iCv6KVjbNBYlgoCxCvjvmyNL4U.woff2) format('woff2');
359
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
360
+ }
361
+ @font-face {
362
+ font-family: 'Ubuntu';
363
+ font-style: normal;
364
+ font-weight: 700;
365
+ font-display: swap;
366
+ src: url(./4iCv6KVjbNBYlgoCxCvjsGyN.woff2) format('woff2');
367
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
368
+ }
@@ -2,17 +2,16 @@
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="utf-8" />
5
+ <!-- No external origins. The UI fonts are vendored in ./fonts/, so the window
6
+ draws the same faces on every platform with the network down; see
7
+ fonts/NOTICE.md. `font-src 'self'` is what allows them, and is why the
8
+ old fonts.googleapis.com / fonts.gstatic.com allowances are gone. -->
5
9
  <meta
6
10
  http-equiv="Content-Security-Policy"
7
- content="default-src 'none'; script-src 'self'; style-src 'self' https://fonts.googleapis.com; font-src https://fonts.gstatic.com; img-src 'self' data:"
11
+ content="default-src 'none'; script-src 'self'; style-src 'self'; font-src 'self'; img-src 'self' data:"
8
12
  />
9
13
  <title>AEGIS Desktop</title>
10
- <link rel="preconnect" href="https://fonts.googleapis.com" />
11
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
12
- <link
13
- href="https://fonts.googleapis.com/css2?family=DM+Mono:ital,wght@0,300;0,400;0,500;1,400&family=Syne:wght@400;600;700;800&family=Ubuntu:ital,wght@0,400;0,500;0,700;1,400&display=swap"
14
- rel="stylesheet"
15
- />
14
+ <link rel="stylesheet" href="fonts/fonts.css" />
16
15
  <link rel="stylesheet" href="style.css" />
17
16
  </head>
18
17
  <body>
@@ -6,6 +6,18 @@
6
6
  --text2: #7e9ab8;
7
7
  --teal: #00e5c0;
8
8
  --red: #ff1744;
9
+
10
+ /* Same deterministic stacks as style.css, and for the same reason: the three
11
+ named families are vendored in renderer/fonts/ (loaded by quick.html before
12
+ this file), so the launcher resolves to the same bytes on every platform.
13
+
14
+ This file is `-apple-system, 'Segoe UI', system-ui` no more. As the *only*
15
+ stack it made the launcher the last place in the app where the host still
16
+ decided what was drawn — SF Pro on macOS, Segoe UI on Windows, fontconfig's
17
+ pick on Linux — which is exactly the divergence the vendoring removes. */
18
+ --font-display: 'Syne', sans-serif;
19
+ --font-mono: 'DM Mono', monospace;
20
+ --font-chat: 'Ubuntu', sans-serif;
9
21
  }
10
22
 
11
23
  * {
@@ -21,7 +33,7 @@ body {
21
33
  margin: 0;
22
34
  background: transparent;
23
35
  color: var(--text);
24
- font: 14px/1.4 -apple-system, 'Segoe UI', system-ui, sans-serif;
36
+ font: 14px/1.4 var(--font-chat);
25
37
  }
26
38
 
27
39
  .ql-shell {
@@ -45,13 +57,20 @@ body {
45
57
  }
46
58
 
47
59
  .ql-badge {
60
+ font-family: var(--font-display);
48
61
  font-weight: 700;
49
62
  letter-spacing: 0.04em;
50
63
  color: var(--teal);
51
64
  }
52
65
 
53
- .ql-model {
54
- color: var(--text2);
66
+ /* Form controls do NOT inherit type from their parent — browsers give them the
67
+ platform widget font — so each one here names its family. Left implicit, the
68
+ launcher's own controls were the last place the host decided what was drawn. */
69
+ .ql-model-select,
70
+ .ql-stop,
71
+ .ql-push,
72
+ .ql-prompt {
73
+ font-family: var(--font-chat);
55
74
  }
56
75
 
57
76
  .ql-model-select {
@@ -63,6 +82,11 @@ body {
63
82
  padding: 2px 4px;
64
83
  }
65
84
 
85
+ .ql-model {
86
+ color: var(--text2);
87
+ font-family: var(--font-mono);
88
+ }
89
+
66
90
  .ql-spacer {
67
91
  flex: 1 1 auto;
68
92
  }
@@ -121,6 +145,7 @@ body {
121
145
  display: flex;
122
146
  align-items: center;
123
147
  gap: 8px;
148
+ font-family: var(--font-mono);
124
149
  font-size: 11px;
125
150
  color: var(--text2);
126
151
  }
@@ -2,11 +2,14 @@
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="utf-8" />
5
+ <!-- Same policy as index.html: no external origins, fonts vendored locally so
6
+ the quick bar looks identical on every platform. See fonts/NOTICE.md. -->
5
7
  <meta
6
8
  http-equiv="Content-Security-Policy"
7
- content="default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self' data:"
9
+ content="default-src 'none'; script-src 'self'; style-src 'self'; font-src 'self'; img-src 'self' data:"
8
10
  />
9
11
  <title>AEGIS Quick Launcher</title>
12
+ <link rel="stylesheet" href="fonts/fonts.css" />
10
13
  <link rel="stylesheet" href="quick.css" />
11
14
  </head>
12
15
  <body>
@@ -20,9 +20,21 @@
20
20
  --user: var(--purple);
21
21
  --assistant: var(--surface2);
22
22
 
23
- --font-display: 'Syne', 'Segoe UI', system-ui, sans-serif;
23
+ /* Deterministic stacks. All three named families are vendored in
24
+ renderer/fonts/ (see fonts/NOTICE.md), so they resolve to the same bytes on
25
+ every platform and with the network down — the desktop counterpart of the
26
+ terminal policy in cli/src/caps.js.
27
+
28
+ 'Segoe UI' and system-ui were removed from the display/chat stacks on
29
+ purpose. As *first fallbacks* they converted "webfont didn't load" into a
30
+ platform-specific font — Segoe UI on Windows, SF Pro on macOS, fontconfig's
31
+ pick on Linux — which is how one stylesheet produced three different-looking
32
+ apps. Only glyphs the vendored families do not cover (CJK, colour emoji)
33
+ now reach the generic keyword; that residue is a host font by definition
34
+ and cannot be bundled away. */
35
+ --font-display: 'Syne', sans-serif;
24
36
  --font-mono: 'DM Mono', monospace;
25
- --font-chat: 'Ubuntu', 'Segoe UI', system-ui, sans-serif;
37
+ --font-chat: 'Ubuntu', sans-serif;
26
38
  }
27
39
 
28
40
  * {
@@ -0,0 +1,113 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * The pooled-brain entry, defined ONCE for every host.
5
+ *
6
+ * The platform's `/api/v1/models` payload lists the brain tier under five
7
+ * spellings (`nexus-brain` canonical; `aegis-brain`, `nexus-brain-smart`,
8
+ * `aegis-brain-smart`, `nexus-brain-neo` each sent as `hidden: true,
9
+ * alias_of: "nexus-brain"`). A host that showed all five painted five models
10
+ * where the server routes one, and a host that showed none (or a different one
11
+ * per release) left users comparing two clients that disagree about the same
12
+ * account.
13
+ *
14
+ * This module exists because that disagreement was real: the desktop collapsed
15
+ * the class to a single "Nexus" entry (desktop/lib/local/engine.js) while the
16
+ * CLI kept every distinct id, so the same account saw a different model list in
17
+ * each host. Two copies of the selection rule is the drift; one shared function
18
+ * is the fix, which is why both hosts require THIS file — the desktop from
19
+ * `desktop/lib/local/engine.js` (repo-relative `client/brain-catalog.js`, or
20
+ * `desktop/vendor/brain-catalog.js` in the packaged app) and the CLI from
21
+ * `cli/src/models.js` (through `src/sharedpaths.js`: repo-relative
22
+ * `client/brain-catalog.js`, or `vendor/client/brain-catalog.js` installed).
23
+ *
24
+ * The two hosts do NOT hand this module the same entry shape and must not have
25
+ * to: the desktop passes the relay's raw payload (`hidden`, `alias_of`), the
26
+ * CLI passes its own normalised entries (`hidden`, `aliasOf`). So the readers
27
+ * below accept either spelling — one rule, two shapes — instead of each host
28
+ * keeping a private copy of the rule to match its own shape, which is the drift
29
+ * this module exists to end.
30
+ *
31
+ * Pure: no I/O, no node builtins, no dependencies — safe to require from a
32
+ * renderer, a CLI start-up path, or an MCP host.
33
+ */
34
+
35
+ /** Every id the server uses for the pooled brain, canonical spelling first. */
36
+ const NEXUS_BRAIN_IDS = Object.freeze(['nexus-brain', 'aegis-brain']);
37
+
38
+ /** The name users see for the collapsed entry. */
39
+ const NEXUS_LABEL = 'Nexus';
40
+
41
+ /**
42
+ * The id this entry is an alias OF, or `null`. Reads the relay's `alias_of` and
43
+ * the CLI's normalised `aliasOf`, so callers may pass either shape.
44
+ */
45
+ function aliasOfEntry(m) {
46
+ if (!m || typeof m !== 'object') return null;
47
+ for (const value of [m.alias_of, m.aliasOf]) {
48
+ if (typeof value === 'string' && value.trim()) return value.trim();
49
+ }
50
+ return null;
51
+ }
52
+
53
+ /** Whether the entry is marked display-hidden, in either shape. */
54
+ function isHiddenEntry(m) {
55
+ return Boolean(m) && m.hidden === true;
56
+ }
57
+
58
+ /**
59
+ * Pick the one entry standing in for the pooled brain. The server serves
60
+ * `nexus-brain` as the canonical tier (`hidden: false`, no `alias_of`) and
61
+ * marks every other spelling — `aegis-brain` and the `-smart`/`-neo` tiers —
62
+ * as `hidden: true, alias_of: "nexus-brain"`. Prefer the canonical id so the
63
+ * request travels on the name the server owns; fall back to the alias, then to
64
+ * any tier whose `alias_of` names the brain, so a renamed or trimmed catalog
65
+ * still resolves to something selectable instead of silently emptying the
66
+ * dropdown (the previous fixed-id lookup returned [] if `aegis-brain` was ever
67
+ * retired, leaving the class with no model to choose).
68
+ *
69
+ * @param {Array<object>} models normalized or raw catalog entries.
70
+ * @returns {object|null}
71
+ */
72
+ function selectBrainEntry(models) {
73
+ const list = Array.isArray(models) ? models.filter(Boolean) : [];
74
+ return (
75
+ list.find((m) => NEXUS_BRAIN_IDS.includes(m.id) && !isHiddenEntry(m) && aliasOfEntry(m) === null)
76
+ || list.find((m) => NEXUS_BRAIN_IDS.includes(m.id))
77
+ || list.find((m) => NEXUS_BRAIN_IDS.includes(aliasOfEntry(m)))
78
+ || null
79
+ );
80
+ }
81
+
82
+ /**
83
+ * The Aegis-class catalog as a host must OFFER it: exactly one entry, the
84
+ * collapsed brain — never the raw provider list, and never the alias tiers.
85
+ *
86
+ * Offering this shape is not the same as accepting only this shape: the ids the
87
+ * server advertises still route (a pin on `deepseek` still runs), so callers
88
+ * keep the full catalog for validation and use this for what they show.
89
+ *
90
+ * Returns `[]` when the payload names no brain tier at all — which is the
91
+ * caller's signal that this is not a pooled-brain catalog (the BYOK listing is
92
+ * per-provider models and must be offered in full, not collapsed to nothing).
93
+ *
94
+ * @param {Array<object>} models the server's catalog (any shape).
95
+ * @returns {Array<object>} `[]` when the payload names no brain tier at all.
96
+ */
97
+ function filterAegisCatalog(models) {
98
+ const nexus = selectBrainEntry(models);
99
+ if (!nexus) return [];
100
+ // Drop the alias bookkeeping in BOTH spellings: this entry *is* the
101
+ // selection, so a renderer must never treat it as a hidden alias and filter
102
+ // it back out — which is exactly what happens if `aliasOf` survives here and
103
+ // the consumer re-runs its own "drop aliases" pass over the result.
104
+ const { hidden, alias_of, aliasOf, ...rest } = nexus;
105
+ return [{ ...rest, label: NEXUS_LABEL }];
106
+ }
107
+
108
+ module.exports = {
109
+ NEXUS_BRAIN_IDS,
110
+ NEXUS_LABEL,
111
+ selectBrainEntry,
112
+ filterAegisCatalog,
113
+ };