aegis-desktop 0.8.0 → 0.8.2
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/README.md +42 -11
- package/build/icon.icns +0 -0
- package/build/icon.ico +0 -0
- package/build/icon.png +0 -0
- package/build/icons/1024x1024.png +0 -0
- package/build/icons/128x128.png +0 -0
- package/build/icons/16x16.png +0 -0
- package/build/icons/256x256.png +0 -0
- package/build/icons/32x32.png +0 -0
- package/build/icons/48x48.png +0 -0
- package/build/icons/512x512.png +0 -0
- package/build/icons/64x64.png +0 -0
- package/main.js +19 -0
- package/package.json +7 -3
- package/renderer/app.js +147 -0
- package/renderer/index.html +74 -0
- package/renderer/style.css +216 -0
- package/vendor/aegis.js +45 -2
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# AEGIS Desktop
|
|
2
2
|
|
|
3
|
+

|
|
4
|
+
|
|
3
5
|
A standalone Electron chat app over the [AEGIS](https://aegiscloud.org) API,
|
|
4
6
|
with an **agentic tool loop**: the model can read, write, and edit files,
|
|
5
7
|
list directories, glob, grep, run shell commands in a persistent session, and
|
|
@@ -8,8 +10,8 @@ delegate whole sub-tasks to subagents. It does **not** require Claude Code.
|
|
|
8
10
|
## Install
|
|
9
11
|
|
|
10
12
|
Download a build from the
|
|
11
|
-
[releases page](https://github.com/
|
|
12
|
-
install from npm:
|
|
13
|
+
[releases page](https://github.com/aegisinfo/aegiscode-plugin/releases) — the
|
|
14
|
+
repo the release workflow publishes binaries to — or install from npm:
|
|
13
15
|
|
|
14
16
|
```bash
|
|
15
17
|
npm install -g aegis-desktop # requires Node 18+
|
|
@@ -25,8 +27,10 @@ Either way, on first launch:
|
|
|
25
27
|
3. Pick a model class from the picker at the bottom of the composer.
|
|
26
28
|
|
|
27
29
|
The published package is [`aegis-desktop`](https://www.npmjs.com/package/aegis-desktop)
|
|
28
|
-
on npm; this repo is its source.
|
|
29
|
-
the pooled margin, or the BYOK handling fee on top of your own
|
|
30
|
+
on npm; this repo is its source. Two of the three classes bill your AEGIS
|
|
31
|
+
account — either the pooled margin, or the BYOK handling fee on top of your own
|
|
32
|
+
provider key; the free **Local** class runs on your own hardware and bills
|
|
33
|
+
nobody.
|
|
30
34
|
|
|
31
35
|
## Using it
|
|
32
36
|
|
|
@@ -49,18 +53,25 @@ with **Settings → "Confirm before running tools"** — on by default.
|
|
|
49
53
|
|
|
50
54
|
## Model classes
|
|
51
55
|
|
|
52
|
-
Pick
|
|
53
|
-
|
|
56
|
+
Pick from three routes on the model-class picker, switchable mid-conversation
|
|
57
|
+
with context intact:
|
|
54
58
|
|
|
55
59
|
| Class | Transport | Key held in | Billed? |
|
|
56
60
|
|---|---|---|---|
|
|
57
61
|
| **Aegis Cloud** | `aegiscloud.org` — one entry, **Nexus**; the pool auto-routes across whichever providers are live | main process | yes — pooled margin |
|
|
58
62
|
| **Bring your own key** | your provider key, relayed by AEGIS — see below | main process | yes — flat AEGIS handling fee |
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
63
|
+
| **Local** | a model on your own machine or network — your Ollama daemon at `localhost:11434` by default | n/a — no key | no — free, no AEGIS account needed |
|
|
64
|
+
|
|
65
|
+
Get a free AEGIS key at **https://aegiscloud.org** for the Cloud and BYOK
|
|
66
|
+
classes. The generic OpenAI-/Anthropic-compatible direct-dial lanes that used
|
|
67
|
+
to live here were removed for good — those providers are reachable only
|
|
68
|
+
through **Bring your own key**, which bills the handling fee. **Local** is
|
|
69
|
+
narrower: it points at a model on your own machine or your own network, and the
|
|
70
|
+
endpoint check (`desktop/lib/local/local.js`) refuses anything public —
|
|
71
|
+
fail-closed, run when the base URL is saved and again before a request is
|
|
72
|
+
dialled. Your Ollama daemon at `localhost:11434` is the default, and any
|
|
73
|
+
OpenAI-compatible server on that box — llama.cpp, LM Studio, vLLM — works by
|
|
74
|
+
pointing the base URL at it. So there's no vendor to bill and no key to hold.
|
|
64
75
|
|
|
65
76
|
### Bring your own key (BYOK)
|
|
66
77
|
|
|
@@ -94,6 +105,26 @@ Note that a **BYOK turn is single-shot** — the relay takes no `tools` paramete
|
|
|
94
105
|
so the agentic tool loop below is off for these models. Use the **Aegis Cloud**
|
|
95
106
|
class when you want file and shell access.
|
|
96
107
|
|
|
108
|
+
## Screenshots
|
|
109
|
+
|
|
110
|
+
Captured from the real app, not mocked up: the stills come from
|
|
111
|
+
`scripts/capture-marketing-shots.mjs` and the demo at the top of this page from
|
|
112
|
+
`scripts/record-demo-gif.mjs`. Both drive the actual renderer against a local
|
|
113
|
+
stub and refuse to publish an asset that fails their assertions.
|
|
114
|
+
|
|
115
|
+
**The model-class picker** — all three routes, switchable mid-conversation:
|
|
116
|
+
|
|
117
|
+

|
|
118
|
+
|
|
119
|
+
**A completed answer** in the transcript:
|
|
120
|
+
|
|
121
|
+

|
|
122
|
+
|
|
123
|
+
**The tool-call approval card** with a proposed edit — what blocks `exec`,
|
|
124
|
+
`writeFile` and `editFile` until you allow, allow for the session, or deny:
|
|
125
|
+
|
|
126
|
+

|
|
127
|
+
|
|
97
128
|
## Tools available to the model
|
|
98
129
|
|
|
99
130
|
| Tool | What it does |
|
package/build/icon.icns
ADDED
|
Binary file
|
package/build/icon.ico
ADDED
|
Binary file
|
package/build/icon.png
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/main.js
CHANGED
|
@@ -480,6 +480,22 @@ async function importForeignMemory(aegis, dir, payload) {
|
|
|
480
480
|
};
|
|
481
481
|
}
|
|
482
482
|
|
|
483
|
+
/** Where the packaged/dev build keeps its icon files. Packaged: shipped via
|
|
484
|
+
* electron-builder.yml's `extraResources` into resourcesPath/icons/ (icons
|
|
485
|
+
* live outside asar, since app.icns/.ico aren't otherwise reachable at
|
|
486
|
+
* runtime). Dev: read straight from build/, the same source those resources
|
|
487
|
+
* are copied from. Same split as ae-guix's lib/windows.js. */
|
|
488
|
+
function resolveIconsDir(isPackaged, resourcesPath, dirname) {
|
|
489
|
+
return isPackaged ? path.join(resourcesPath, 'icons') : path.join(dirname, 'build');
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/** icon.icns on mac, icon.ico on Windows, icon.png everywhere else (Linux). */
|
|
493
|
+
function iconForPlatform(iconsDir, platform) {
|
|
494
|
+
if (platform === 'darwin') return path.join(iconsDir, 'icon.icns');
|
|
495
|
+
if (platform === 'win32') return path.join(iconsDir, 'icon.ico');
|
|
496
|
+
return path.join(iconsDir, 'icon.png');
|
|
497
|
+
}
|
|
498
|
+
|
|
483
499
|
/** Only http/https may be opened externally — file://, javascript:, etc.
|
|
484
500
|
* would hand the OS shell an arbitrary URI straight from model output. */
|
|
485
501
|
function isSafeExternalUrl(url) {
|
|
@@ -2421,6 +2437,7 @@ function bootstrap() {
|
|
|
2421
2437
|
minimizable: false,
|
|
2422
2438
|
maximizable: false,
|
|
2423
2439
|
backgroundColor: '#0d1117',
|
|
2440
|
+
icon: iconForPlatform(resolveIconsDir(app.isPackaged, process.resourcesPath, __dirname), process.platform),
|
|
2424
2441
|
webPreferences: {
|
|
2425
2442
|
preload: path.join(__dirname, 'preload.js'),
|
|
2426
2443
|
contextIsolation: true,
|
|
@@ -2595,6 +2612,7 @@ function bootstrap() {
|
|
|
2595
2612
|
// connected display — that's a window that "opens" but nobody can see
|
|
2596
2613
|
// or reach.
|
|
2597
2614
|
const bounds = windowState.clampToDisplay(saved, screen.getAllDisplays(), defaultBounds);
|
|
2615
|
+
const iconsDir = resolveIconsDir(app.isPackaged, process.resourcesPath, __dirname);
|
|
2598
2616
|
|
|
2599
2617
|
const win = new BrowserWindow({
|
|
2600
2618
|
...bounds,
|
|
@@ -2602,6 +2620,7 @@ function bootstrap() {
|
|
|
2602
2620
|
minHeight: 480,
|
|
2603
2621
|
backgroundColor: '#0d1117',
|
|
2604
2622
|
title: 'AEGIS Desktop',
|
|
2623
|
+
icon: iconForPlatform(iconsDir, process.platform),
|
|
2605
2624
|
webPreferences: {
|
|
2606
2625
|
preload: path.join(__dirname, 'preload.js'),
|
|
2607
2626
|
contextIsolation: true,
|
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.2",
|
|
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",
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"prepublishOnly": "npm run predist",
|
|
34
34
|
"icon": "node scripts/generate-icon.mjs",
|
|
35
35
|
"shots": "node scripts/capture-marketing-shots.mjs",
|
|
36
|
+
"demo": "node scripts/record-demo-gif.mjs",
|
|
36
37
|
"dist": "npm run predist && electron-builder",
|
|
37
38
|
"dist:dir": "npm run predist && electron-builder --dir",
|
|
38
39
|
"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 bin/aegis.js",
|
|
@@ -52,10 +53,13 @@
|
|
|
52
53
|
"test:renderer-wiring": "node ../test/renderer-wiring.test.mjs"
|
|
53
54
|
},
|
|
54
55
|
"dependencies": {
|
|
55
|
-
"electron": "^33.0.0",
|
|
56
56
|
"electron-updater": "^6.8.9"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
|
-
"electron-builder": "^25.1.8"
|
|
59
|
+
"electron-builder": "^25.1.8",
|
|
60
|
+
"electron": "^33.0.0"
|
|
61
|
+
},
|
|
62
|
+
"optionalDependencies": {
|
|
63
|
+
"electron": "^33.0.0"
|
|
60
64
|
}
|
|
61
65
|
}
|
package/renderer/app.js
CHANGED
|
@@ -160,6 +160,11 @@ const EFFORT_KEY = 'aegis.effort';
|
|
|
160
160
|
const LEGACY_EFFORT_KEY = 'aegis.autonomousEffort';
|
|
161
161
|
const AUTONOMOUS_WORKERS_KEY = 'aegis.autonomousWorkers';
|
|
162
162
|
const EXPLORE_KEY = 'aegis.explore';
|
|
163
|
+
// "Don't show this again" on the welcome panel's connect block. Stored here
|
|
164
|
+
// rather than in the settings store because it is a per-machine UI preference,
|
|
165
|
+
// not a credential, and the settings store is what the main process filters and
|
|
166
|
+
// syncs.
|
|
167
|
+
const WELCOME_DISMISS_KEY = 'aegis.welcomeConnectDismissed';
|
|
163
168
|
// "Work autonomously" (pool_brain worker fan-out, aegis1 services/pool_brain.py)
|
|
164
169
|
// is only billable/routable through the pooled AEGIS Cloud class.
|
|
165
170
|
const AUTONOMOUS_CLASS = 'aegis';
|
|
@@ -595,6 +600,10 @@ function renderStatus(s) {
|
|
|
595
600
|
els.base.textContent = s.apiBase || '–';
|
|
596
601
|
els.key.textContent = s.keyConfigured ? s.keyMask : 'not set';
|
|
597
602
|
setConn(s.keyConfigured, s.keyConfigured ? 'key configured' : 'no API key');
|
|
603
|
+
// A key that arrives while the welcome panel is on screen (pasted, or synced
|
|
604
|
+
// from the CLI) retires the connect block; with no key it stays. No-ops when
|
|
605
|
+
// the panel is not mounted.
|
|
606
|
+
applyWelcomeConnect();
|
|
598
607
|
}
|
|
599
608
|
|
|
600
609
|
// -------------------------------------------------------------- auto-update
|
|
@@ -1858,6 +1867,16 @@ function renderWelcome() {
|
|
|
1858
1867
|
for (const btn of els.messages.querySelectorAll('.chat-quick-pill')) {
|
|
1859
1868
|
btn.addEventListener('click', () => quickAction(btn.dataset.quick || ''));
|
|
1860
1869
|
}
|
|
1870
|
+
// The connect block's three controls. Bound here (not inline, not at boot)
|
|
1871
|
+
// because they only exist in the cloned panel; CSP `script-src 'self'` would
|
|
1872
|
+
// drop an inline handler anyway.
|
|
1873
|
+
const byok = document.getElementById('welcome-byok');
|
|
1874
|
+
if (byok) byok.addEventListener('click', welcomeByok);
|
|
1875
|
+
const cloud = document.getElementById('welcome-cloud');
|
|
1876
|
+
if (cloud) cloud.addEventListener('click', welcomeCloud);
|
|
1877
|
+
const skip = document.getElementById('welcome-dismiss');
|
|
1878
|
+
if (skip) skip.addEventListener('click', dismissWelcomeConnect);
|
|
1879
|
+
applyWelcomeConnect();
|
|
1861
1880
|
}
|
|
1862
1881
|
|
|
1863
1882
|
/** Drop the welcome panel once real transcript content exists. */
|
|
@@ -1866,6 +1885,134 @@ function hideWelcome() {
|
|
|
1866
1885
|
if (w) w.remove();
|
|
1867
1886
|
}
|
|
1868
1887
|
|
|
1888
|
+
// --------------------------------------------- welcome: the connect block
|
|
1889
|
+
//
|
|
1890
|
+
// The welcome panel is the only surface a new install sees before it has a
|
|
1891
|
+
// key, and it is where the app has to say what it is and how it gets paid for.
|
|
1892
|
+
// The block is shown ONLY while nothing is connected (no AEGIS key) and the
|
|
1893
|
+
// user has not dismissed it: the panel is re-rendered on every New chat, so an
|
|
1894
|
+
// always-on version would be an advert in the composition window.
|
|
1895
|
+
//
|
|
1896
|
+
// Both routes are real paths into the existing UI, not links out:
|
|
1897
|
+
//
|
|
1898
|
+
// byok — switches the provider class and lands on Provider settings, which
|
|
1899
|
+
// is where the per-provider key rows are built (loadSettings()).
|
|
1900
|
+
// BYOK still needs an AEGIS key for the handling fee (the engine
|
|
1901
|
+
// refuses a keyless BYOK send outright), so when there is none the
|
|
1902
|
+
// same click also focuses the AEGIS key field and says so, rather
|
|
1903
|
+
// than leaving the user at a row whose Save would fail.
|
|
1904
|
+
// cloud — focuses the AEGIS key field in the Status card, where Save/Verify
|
|
1905
|
+
// and the Upgrade/Top up buttons already live. Getting a key is the
|
|
1906
|
+
// one step that happens off-machine, so the hint carries the link
|
|
1907
|
+
// and openExternal is reached through the anchor, never a bare
|
|
1908
|
+
// window.open (CSP `script-src 'self'` drops nothing here, but the
|
|
1909
|
+
// main process is what validates the URL).
|
|
1910
|
+
|
|
1911
|
+
/** Show the connect block only on an unconnected, un-dismissed install. */
|
|
1912
|
+
function applyWelcomeConnect() {
|
|
1913
|
+
const box = document.getElementById('welcome-connect');
|
|
1914
|
+
if (!box) return;
|
|
1915
|
+
let dismissed = false;
|
|
1916
|
+
try {
|
|
1917
|
+
dismissed = localStorage.getItem(WELCOME_DISMISS_KEY) === 'on';
|
|
1918
|
+
} catch {
|
|
1919
|
+
/* storage disabled — treat as not dismissed */
|
|
1920
|
+
}
|
|
1921
|
+
// keyConfigured is null until status is read, so an unknown state keeps the
|
|
1922
|
+
// block visible: on a cold boot the user has no key far more often than not,
|
|
1923
|
+
// and renderStatus() calls this again the moment the truth arrives.
|
|
1924
|
+
box.hidden = dismissed || keyConfigured === true;
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
/** Scroll a sidebar card into view and mark it, so a welcome click has a
|
|
1928
|
+
* visible landing spot. The sidebar is a plain scroll container, so this is
|
|
1929
|
+
* the whole of the "open settings" affordance. */
|
|
1930
|
+
function revealSidebarCard(node) {
|
|
1931
|
+
if (!node) return;
|
|
1932
|
+
const card = node.closest('section.card') || node;
|
|
1933
|
+
try {
|
|
1934
|
+
card.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
1935
|
+
} catch {
|
|
1936
|
+
card.scrollIntoView();
|
|
1937
|
+
}
|
|
1938
|
+
card.classList.add('flash');
|
|
1939
|
+
setTimeout(() => card.classList.remove('flash'), 1200);
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
/** "Bring your own key": land the user where a provider key can be typed. */
|
|
1943
|
+
async function welcomeByok() {
|
|
1944
|
+
renderWelcomeHint('byok');
|
|
1945
|
+
const hasByok = Array.from(els.classSelect.options).some((o) => o.value === 'byok');
|
|
1946
|
+
if (hasByok && els.classSelect.value !== 'byok') {
|
|
1947
|
+
els.classSelect.value = 'byok';
|
|
1948
|
+
// The change listener persists the class and reloads the catalog, so the
|
|
1949
|
+
// provider rows appear without duplicating any of that here.
|
|
1950
|
+
els.classSelect.dispatchEvent(new Event('change'));
|
|
1951
|
+
}
|
|
1952
|
+
revealSidebarCard(els.settingsList);
|
|
1953
|
+
// A BYOK turn is refused without an AEGIS key (the handling fee is billed
|
|
1954
|
+
// there), so a keyless user is sent to the key field first — the provider row
|
|
1955
|
+
// cannot succeed on its own.
|
|
1956
|
+
if (keyConfigured === false && els.apiKeyHint) {
|
|
1957
|
+
els.apiKeyHint.textContent =
|
|
1958
|
+
'BYOK needs an AEGIS key too — the handling fee is billed to it. ' +
|
|
1959
|
+
'Paste one above, then add your provider key below.';
|
|
1960
|
+
}
|
|
1961
|
+
if (keyConfigured === false && els.apiKeyInput) {
|
|
1962
|
+
els.apiKeyInput.focus();
|
|
1963
|
+
revealSidebarCard(els.apiKeyInput);
|
|
1964
|
+
return;
|
|
1965
|
+
}
|
|
1966
|
+
const row = els.settingsList
|
|
1967
|
+
? els.settingsList.querySelector('.setting-row input[type="password"]')
|
|
1968
|
+
: null;
|
|
1969
|
+
if (row) row.focus();
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
/** "Connect AEGIS Cloud": land the user on the key field that unlocks it. */
|
|
1973
|
+
function welcomeCloud() {
|
|
1974
|
+
renderWelcomeHint('cloud');
|
|
1975
|
+
revealSidebarCard(els.apiKeyInput);
|
|
1976
|
+
if (els.apiKeyInput) els.apiKeyInput.focus();
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
/** The hint under the key field, rewritten to match the route just clicked.
|
|
1980
|
+
* Uses child nodes rather than textContent so the aegiscloud.org link inside
|
|
1981
|
+
* it survives (same shape as the Model card's needsKey hint). */
|
|
1982
|
+
function renderWelcomeHint(kind) {
|
|
1983
|
+
const el = els.apiKeyHint;
|
|
1984
|
+
if (!el) return;
|
|
1985
|
+
el.textContent = '';
|
|
1986
|
+
if (kind === 'byok') {
|
|
1987
|
+
el.appendChild(document.createTextNode(
|
|
1988
|
+
'Bring your own key: paste your AEGIS key here (it bills the handling fee), ' +
|
|
1989
|
+
'then add the provider key in Provider settings. No key yet? ',
|
|
1990
|
+
));
|
|
1991
|
+
} else {
|
|
1992
|
+
el.appendChild(document.createTextNode(
|
|
1993
|
+
'Connect AEGIS Cloud: paste your AEGIS key, then Save. Free key at ',
|
|
1994
|
+
));
|
|
1995
|
+
}
|
|
1996
|
+
const a = document.createElement('a');
|
|
1997
|
+
a.href = GET_AEGIS_KEY_URL;
|
|
1998
|
+
a.target = '_blank';
|
|
1999
|
+
a.rel = 'noreferrer noopener';
|
|
2000
|
+
a.textContent = 'aegiscloud.org';
|
|
2001
|
+
el.appendChild(a);
|
|
2002
|
+
el.appendChild(document.createTextNode('.'));
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
/** Persist "don't show this again" and drop the block for this session. */
|
|
2006
|
+
function dismissWelcomeConnect() {
|
|
2007
|
+
try {
|
|
2008
|
+
localStorage.setItem(WELCOME_DISMISS_KEY, 'on');
|
|
2009
|
+
} catch {
|
|
2010
|
+
/* storage disabled — the block simply comes back next launch */
|
|
2011
|
+
}
|
|
2012
|
+
const box = document.getElementById('welcome-connect');
|
|
2013
|
+
if (box) box.hidden = true;
|
|
2014
|
+
}
|
|
2015
|
+
|
|
1869
2016
|
// Assistant text is rendered as sanitized markdown (headings, lists, links,
|
|
1870
2017
|
// highlighted fenced code with a copy button — see renderer/markdown.js);
|
|
1871
2018
|
// user text always stays plain via textContent, and this is the only place
|
package/renderer/index.html
CHANGED
|
@@ -315,6 +315,7 @@
|
|
|
315
315
|
<div class="glyph">✦</div>
|
|
316
316
|
<div class="title" id="chat-greeting">Hello</div>
|
|
317
317
|
</div>
|
|
318
|
+
<div class="sub" id="welcome-tagline">Your intent, AI automated.</div>
|
|
318
319
|
<div class="sub">
|
|
319
320
|
Ask anything — or start with one of these.
|
|
320
321
|
</div>
|
|
@@ -347,6 +348,79 @@
|
|
|
347
348
|
<span class="qp-ic">⊞</span>Plan a project
|
|
348
349
|
</button>
|
|
349
350
|
</div>
|
|
351
|
+
<!-- Why AEGIS Desktop, and the two ways to power it.
|
|
352
|
+
The app itself is MIT and free; what pays for it is a connected
|
|
353
|
+
account — either an AEGIS Cloud key (pooled models, billed to
|
|
354
|
+
the balance in the Status card) or a provider key of your own
|
|
355
|
+
relayed under BYOK, whose handling fee is the other revenue
|
|
356
|
+
line. A new install therefore has to be told, in the app, both
|
|
357
|
+
why it should stay and how to connect; until this panel existed
|
|
358
|
+
the only route to either was a paragraph in the README.
|
|
359
|
+
|
|
360
|
+
Both buttons are real: #welcome-byok switches the provider
|
|
361
|
+
class to byok and drops the user on the Provider settings card,
|
|
362
|
+
#welcome-cloud focuses the AEGIS key field in the Status card.
|
|
363
|
+
The whole block is hidden once a key is configured or the user
|
|
364
|
+
dismisses it (applyWelcomeConnect() in app.js) — a welcome
|
|
365
|
+
panel that reappears on every New chat is an ad, not a welcome. -->
|
|
366
|
+
<div class="welcome-connect" id="welcome-connect" hidden>
|
|
367
|
+
<div class="wc-why">
|
|
368
|
+
<div class="wc-why-title">Why AEGIS Desktop</div>
|
|
369
|
+
<ul class="wc-why-list">
|
|
370
|
+
<li>
|
|
371
|
+
<b>It runs on your machine.</b> Your repos, sessions and keys
|
|
372
|
+
live on this disk — an agent turn only leaves it for the
|
|
373
|
+
model call you configured, and nothing else is uploaded.
|
|
374
|
+
</li>
|
|
375
|
+
<li>
|
|
376
|
+
<b>Your key, your bill.</b> Paste an OpenAI, Anthropic or
|
|
377
|
+
DeepSeek key and pay your provider directly, point it at a
|
|
378
|
+
local model server for free, or use AEGIS Cloud with no
|
|
379
|
+
provider key at all.
|
|
380
|
+
</li>
|
|
381
|
+
<li>
|
|
382
|
+
<b>It does the work, not just the talking.</b> Exec, write and
|
|
383
|
+
edit run against your actual working tree — with the
|
|
384
|
+
autonomous queue draining multi-step jobs while you are away.
|
|
385
|
+
</li>
|
|
386
|
+
<li>
|
|
387
|
+
<b>Open client, paid service.</b> The desktop app is
|
|
388
|
+
MIT-licensed and needs no account to run a local model; AEGIS
|
|
389
|
+
Cloud is what funds it.
|
|
390
|
+
</li>
|
|
391
|
+
</ul>
|
|
392
|
+
</div>
|
|
393
|
+
<div class="wc-paths">
|
|
394
|
+
<button type="button" class="wc-path" id="welcome-byok">
|
|
395
|
+
<span class="wc-ic" aria-hidden="true">🔑</span>
|
|
396
|
+
<span class="wc-body">
|
|
397
|
+
<span class="wc-name">Bring your own key</span>
|
|
398
|
+
<span class="wc-desc">
|
|
399
|
+
Keep the provider plan you already pay for. The key is
|
|
400
|
+
stored on this machine and authenticates to the provider;
|
|
401
|
+
the relay that carries your turn is what bills the AEGIS
|
|
402
|
+
handling fee.
|
|
403
|
+
</span>
|
|
404
|
+
</span>
|
|
405
|
+
<span class="wc-go">Set up →</span>
|
|
406
|
+
</button>
|
|
407
|
+
<button type="button" class="wc-path" id="welcome-cloud">
|
|
408
|
+
<span class="wc-ic" aria-hidden="true">✦</span>
|
|
409
|
+
<span class="wc-body">
|
|
410
|
+
<span class="wc-name">Connect AEGIS Cloud</span>
|
|
411
|
+
<span class="wc-desc">
|
|
412
|
+
One key, no provider account needed. Pooled models, memory
|
|
413
|
+
sync and the autonomous worker fan-out; free tier to start,
|
|
414
|
+
then upgrade or top up from the Status card.
|
|
415
|
+
</span>
|
|
416
|
+
</span>
|
|
417
|
+
<span class="wc-go">Connect →</span>
|
|
418
|
+
</button>
|
|
419
|
+
<button type="button" class="wc-skip" id="welcome-dismiss">
|
|
420
|
+
Don't show this again
|
|
421
|
+
</button>
|
|
422
|
+
</div>
|
|
423
|
+
</div>
|
|
350
424
|
<div class="hints">
|
|
351
425
|
Enter to send · Shift+Enter for a newline · the discovery lane adds
|
|
352
426
|
two extra paths to every answer
|
package/renderer/style.css
CHANGED
|
@@ -1669,6 +1669,12 @@ label {
|
|
|
1669
1669
|
flex-direction: column;
|
|
1670
1670
|
align-items: center;
|
|
1671
1671
|
justify-content: center;
|
|
1672
|
+
/* The connect block made this panel tall enough to overflow a short window.
|
|
1673
|
+
Without a scroll container the overflow is unreachable, and `center` alone
|
|
1674
|
+
clips the *top* of an overflowing box, so the greeting disappears. `safe`
|
|
1675
|
+
degrades to plain `center` (the previous behaviour) where unsupported. */
|
|
1676
|
+
justify-content: safe center;
|
|
1677
|
+
overflow-y: auto;
|
|
1672
1678
|
flex: 1;
|
|
1673
1679
|
gap: 18px;
|
|
1674
1680
|
text-align: center;
|
|
@@ -1759,6 +1765,216 @@ label {
|
|
|
1759
1765
|
line-height: 1.6;
|
|
1760
1766
|
}
|
|
1761
1767
|
|
|
1768
|
+
/* ------------------------------------------- welcome: the connect block
|
|
1769
|
+
The "why AEGIS Desktop / bring your own key / connect AEGIS Cloud" panel
|
|
1770
|
+
cloned into #messages by renderWelcome(). It is the only surface a new
|
|
1771
|
+
install sees before it has a key, so it carries the two revenue routes:
|
|
1772
|
+
BYOK (provider key relayed, AEGIS handling fee billed) and AEGIS Cloud.
|
|
1773
|
+
|
|
1774
|
+
Deliberately wider and left-aligned against the centred greeting above it:
|
|
1775
|
+
the greeting is a title, this is body copy plus two calls to action, and
|
|
1776
|
+
centred paragraphs at this length are unreadable. */
|
|
1777
|
+
.welcome-connect {
|
|
1778
|
+
display: flex;
|
|
1779
|
+
flex-direction: column;
|
|
1780
|
+
gap: 14px;
|
|
1781
|
+
width: 100%;
|
|
1782
|
+
max-width: 620px;
|
|
1783
|
+
margin-top: 8px;
|
|
1784
|
+
padding: 16px 18px;
|
|
1785
|
+
border: 1px solid var(--border);
|
|
1786
|
+
border-radius: 10px;
|
|
1787
|
+
background: var(--surface);
|
|
1788
|
+
text-align: left;
|
|
1789
|
+
animation: wc-in 0.25s ease-out;
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
/* Required, not cosmetic: `display: flex` above outranks the UA sheet's
|
|
1793
|
+
`[hidden] { display: none }`, so without this the `hidden` attribute the
|
|
1794
|
+
dismiss button and applyWelcomeConnect() rely on would be a silent no-op
|
|
1795
|
+
(same trap already documented for .update-banner and .autonomous-controls). */
|
|
1796
|
+
.welcome-connect[hidden] {
|
|
1797
|
+
display: none;
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
@keyframes wc-in {
|
|
1801
|
+
from {
|
|
1802
|
+
opacity: 0;
|
|
1803
|
+
transform: translateY(6px);
|
|
1804
|
+
}
|
|
1805
|
+
to {
|
|
1806
|
+
opacity: 1;
|
|
1807
|
+
transform: none;
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
.wc-why-title {
|
|
1812
|
+
font-size: 11px;
|
|
1813
|
+
font-weight: 700;
|
|
1814
|
+
text-transform: uppercase;
|
|
1815
|
+
letter-spacing: 0.08em;
|
|
1816
|
+
color: var(--teal);
|
|
1817
|
+
margin-bottom: 8px;
|
|
1818
|
+
font-family: var(--font-display);
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
.wc-why-list {
|
|
1822
|
+
margin: 0;
|
|
1823
|
+
padding: 0;
|
|
1824
|
+
list-style: none;
|
|
1825
|
+
display: flex;
|
|
1826
|
+
flex-direction: column;
|
|
1827
|
+
gap: 7px;
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
.wc-why-list li {
|
|
1831
|
+
position: relative;
|
|
1832
|
+
padding-left: 17px;
|
|
1833
|
+
font-size: 12px;
|
|
1834
|
+
line-height: 1.55;
|
|
1835
|
+
color: var(--text2);
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
.wc-why-list li::before {
|
|
1839
|
+
content: '✦';
|
|
1840
|
+
position: absolute;
|
|
1841
|
+
left: 0;
|
|
1842
|
+
top: 1px;
|
|
1843
|
+
font-size: 10px;
|
|
1844
|
+
color: var(--text3);
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
.wc-why-list b {
|
|
1848
|
+
color: var(--text);
|
|
1849
|
+
font-weight: 600;
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
.wc-paths {
|
|
1853
|
+
display: grid;
|
|
1854
|
+
grid-template-columns: 1fr 1fr;
|
|
1855
|
+
gap: 10px;
|
|
1856
|
+
align-items: stretch;
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
.wc-path {
|
|
1860
|
+
display: flex;
|
|
1861
|
+
flex-direction: column;
|
|
1862
|
+
gap: 8px;
|
|
1863
|
+
text-align: left;
|
|
1864
|
+
padding: 13px 14px;
|
|
1865
|
+
border: 1px solid var(--border2);
|
|
1866
|
+
border-radius: 9px;
|
|
1867
|
+
background: var(--surface2);
|
|
1868
|
+
color: var(--text2);
|
|
1869
|
+
font-family: var(--font-chat);
|
|
1870
|
+
cursor: pointer;
|
|
1871
|
+
transition: all 0.15s;
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
.wc-path:hover,
|
|
1875
|
+
.wc-path:focus-visible {
|
|
1876
|
+
border-color: var(--teal);
|
|
1877
|
+
background: rgba(0, 229, 192, 0.06);
|
|
1878
|
+
outline: none;
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
.wc-ic {
|
|
1882
|
+
font-size: 16px;
|
|
1883
|
+
line-height: 1;
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
.wc-body {
|
|
1887
|
+
display: flex;
|
|
1888
|
+
flex-direction: column;
|
|
1889
|
+
gap: 5px;
|
|
1890
|
+
flex: 1;
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
.wc-name {
|
|
1894
|
+
font-size: 13px;
|
|
1895
|
+
font-weight: 600;
|
|
1896
|
+
color: var(--text);
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
.wc-path:hover .wc-name,
|
|
1900
|
+
.wc-path:focus-visible .wc-name {
|
|
1901
|
+
color: var(--teal);
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
.wc-desc {
|
|
1905
|
+
font-size: 11px;
|
|
1906
|
+
line-height: 1.5;
|
|
1907
|
+
color: var(--text2);
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
.wc-go {
|
|
1911
|
+
font-size: 11px;
|
|
1912
|
+
font-weight: 600;
|
|
1913
|
+
letter-spacing: 0.02em;
|
|
1914
|
+
color: var(--teal);
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
.wc-skip {
|
|
1918
|
+
grid-column: 1 / -1;
|
|
1919
|
+
justify-self: center;
|
|
1920
|
+
border: 0;
|
|
1921
|
+
background: none;
|
|
1922
|
+
padding: 2px 8px;
|
|
1923
|
+
font-family: var(--font-chat);
|
|
1924
|
+
font-size: 11px;
|
|
1925
|
+
color: var(--text3);
|
|
1926
|
+
cursor: pointer;
|
|
1927
|
+
text-decoration: underline;
|
|
1928
|
+
text-underline-offset: 3px;
|
|
1929
|
+
text-decoration-color: var(--border2);
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
.wc-skip:hover {
|
|
1933
|
+
color: var(--text2);
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
/* Sidebar cards are ~280px, so side-by-side path cards stop being readable
|
|
1937
|
+
long before the window is narrow. */
|
|
1938
|
+
@media (max-width: 720px) {
|
|
1939
|
+
.wc-paths {
|
|
1940
|
+
grid-template-columns: 1fr;
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
/* ------------------------------------------------ sidebar "landing" flash
|
|
1945
|
+
revealSidebarCard() adds .flash for 1.2s after a welcome click scrolls a
|
|
1946
|
+
settings card into view. The class was being added with no rule behind it,
|
|
1947
|
+
so the click had no visible landing spot at all — this is that rule. Kept
|
|
1948
|
+
element-agnostic: revealSidebarCard falls back to the node itself when it
|
|
1949
|
+
is not inside a .card. */
|
|
1950
|
+
.flash {
|
|
1951
|
+
animation: card-flash 1.2s ease-out;
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
@keyframes card-flash {
|
|
1955
|
+
0% {
|
|
1956
|
+
border-color: var(--teal);
|
|
1957
|
+
box-shadow: 0 0 0 3px rgba(0, 229, 192, 0.18);
|
|
1958
|
+
}
|
|
1959
|
+
70% {
|
|
1960
|
+
border-color: var(--teal);
|
|
1961
|
+
box-shadow: 0 0 0 3px rgba(0, 229, 192, 0.1);
|
|
1962
|
+
}
|
|
1963
|
+
100% {
|
|
1964
|
+
box-shadow: none;
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1969
|
+
.welcome-connect {
|
|
1970
|
+
animation: none;
|
|
1971
|
+
}
|
|
1972
|
+
.flash {
|
|
1973
|
+
animation: none;
|
|
1974
|
+
border-color: var(--teal);
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1762
1978
|
/* ------------------------------------------------- rendered markdown (D?)
|
|
1763
1979
|
renderer/markdown.js parses assistant text with marked, sanitizes with
|
|
1764
1980
|
DOMPurify, then hands the sanitized HTML to .body/.flow-body via
|
package/vendor/aegis.js
CHANGED
|
@@ -46,6 +46,25 @@ function envVar(name) {
|
|
|
46
46
|
return v && !/^\$\{[A-Z_]+\}$/.test(v) ? v : '';
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Read a positive millisecond count from the environment, or 0 for "unset".
|
|
51
|
+
*
|
|
52
|
+
* Used only for the two stream watchdogs below. Non-positive and unparseable
|
|
53
|
+
* values are IGNORED rather than honoured — deliberately not the rule the
|
|
54
|
+
* engine's host cooldown uses, where `AEGIS_HOST_COOLDOWN_MS=0` meaning "no
|
|
55
|
+
* cooldown" is a coherent thing to ask for. Here `0` would mean "the stream is
|
|
56
|
+
* dead before its first byte", which is a typo rather than a preference, and
|
|
57
|
+
* silently killing every turn is a far worse failure than ignoring a bad
|
|
58
|
+
* number. A test that genuinely wants a sub-second watchdog passes
|
|
59
|
+
* `idleTimeoutMs` per call (test/autonomous-mode.test.mjs does exactly that).
|
|
60
|
+
*/
|
|
61
|
+
function envIdleMs(name) {
|
|
62
|
+
const raw = envVar(name);
|
|
63
|
+
if (!raw) return 0;
|
|
64
|
+
const n = Number(raw);
|
|
65
|
+
return Number.isFinite(n) && n > 0 ? n : 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
49
68
|
/**
|
|
50
69
|
* UUID v4 that works everywhere: Web Crypto first (browsers, Node ≥ 19),
|
|
51
70
|
* then Node's CJS crypto module (Node < 19), then a Math.random fallback for
|
|
@@ -895,9 +914,27 @@ const BRAIN_IDLE_TIMEOUT_MS = 15 * 60_000;
|
|
|
895
914
|
* that runs the fan-out, so a renamed brain id or a caller that forgot its
|
|
896
915
|
* flag cannot desynchronise the two. A response with no header (an older
|
|
897
916
|
* server, or a single-pass call) keeps the caller's budget or the 60s default.
|
|
917
|
+
*
|
|
918
|
+
* ── the two env knobs ───────────────────────────────────────────────────────
|
|
919
|
+
*
|
|
920
|
+
* `AEGIS_SSE_IDLE_TIMEOUT_MS` and `AEGIS_BRAIN_IDLE_TIMEOUT_MS` move those two
|
|
921
|
+
* *defaults* without a code edit: the CLI is a host, not a fork, so before this
|
|
922
|
+
* the only way to give a slow link more room — or to reproduce a stall against
|
|
923
|
+
* a fixed wedged server — was to patch the transport. Same escape hatch the
|
|
924
|
+
* engine gives its host cooldown (`AEGIS_HOST_COOLDOWN_MS`).
|
|
925
|
+
*
|
|
926
|
+
* Both are read at CALL time, not at module load, so a host that sets one after
|
|
927
|
+
* `require()` still gets it, and one that unsets it is back on the shipped
|
|
928
|
+
* default. A per-call `idleTimeoutMs` still outranks both: an explicit number
|
|
929
|
+
* from the caller is a decision about *this* request, while the env var is only
|
|
930
|
+
* a house default. See envIdleMs() for why a bad value is ignored rather than
|
|
931
|
+
* obeyed.
|
|
898
932
|
*/
|
|
899
933
|
function idleBudgetFor(res, requestedMs) {
|
|
900
|
-
|
|
934
|
+
// Caller override -> env knob -> shipped default.
|
|
935
|
+
const stated = Number(requestedMs);
|
|
936
|
+
const envDefault = envIdleMs('AEGIS_SSE_IDLE_TIMEOUT_MS');
|
|
937
|
+
const base = stated > 0 ? stated : envDefault > 0 ? envDefault : SSE_IDLE_TIMEOUT_MS;
|
|
901
938
|
let header = '';
|
|
902
939
|
try {
|
|
903
940
|
const get = res && res.headers && typeof res.headers.get === 'function' ? res.headers.get.bind(res.headers) : null;
|
|
@@ -905,7 +942,13 @@ function idleBudgetFor(res, requestedMs) {
|
|
|
905
942
|
} catch {
|
|
906
943
|
header = ''; // an exotic fetch shim without headers: keep the caller's budget
|
|
907
944
|
}
|
|
908
|
-
|
|
945
|
+
// The fan-out floor is tunable for the same reason the base is — and it has
|
|
946
|
+
// to stay a FLOOR: a server that announced workers=3 is still working, so a
|
|
947
|
+
// smaller base must not cut it off. Raise both if the server's own
|
|
948
|
+
// NEXUS_BRAIN_WORKER_TIMEOUT moves (test/autonomous-mode.test.mjs pins the
|
|
949
|
+
// 600s relationship between them).
|
|
950
|
+
const brainFloor = envIdleMs('AEGIS_BRAIN_IDLE_TIMEOUT_MS') || BRAIN_IDLE_TIMEOUT_MS;
|
|
951
|
+
return header && String(header).trim() ? Math.max(base, brainFloor) : base;
|
|
909
952
|
}
|
|
910
953
|
|
|
911
954
|
const api = { createClient, envVar, randomUUID, DEFAULT_API_BASE, CLIENT_VERSION, idleBudgetFor, SSE_IDLE_TIMEOUT_MS, BRAIN_IDLE_TIMEOUT_MS };
|