aegis-desktop 0.3.0 → 0.4.0

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "aegis-desktop",
3
3
  "productName": "AEGIS Desktop",
4
- "version": "0.3.0",
4
+ "version": "0.4.0",
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",
@@ -29,16 +29,20 @@
29
29
  "icon": "node scripts/generate-icon.mjs",
30
30
  "dist": "npm run predist && electron-builder",
31
31
  "dist:dir": "npm run predist && electron-builder --dir",
32
- "check": "node --check main.js && node --check preload.js && node --check renderer/app.js && node --check renderer/max-tokens.js && node --check scripts/predist.mjs && node --check scripts/generate-icon.mjs && node --check lib/local/context.js && node --check lib/local/providers.js && node --check lib/local/ollama.js && node --check lib/local/engine.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/settings.js && node --check lib/sync/sessions.js && node --check lib/sync/memory-queue.js && node --check bin/aegis.js",
32
+ "check": "node --check main.js && node --check preload.js && node --check renderer/app.js && node --check renderer/quick.js && node --check renderer/max-tokens.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/context.js && node --check lib/local/providers.js && node --check lib/local/ollama.js && node --check lib/local/engine.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/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",
33
33
  "test:shell": "node ../test/desktop-shell.mjs",
34
34
  "test:model": "node ../test/model-dispatch.mjs",
35
35
  "test:max-tokens": "node ../test/max-tokens.test.mjs",
36
36
  "test:foreign-memory": "node ../test/foreign-memory.test.mjs",
37
37
  "test:tools": "node ../test/local-tools.test.mjs",
38
- "test:engine": "node ../test/local-engine.test.mjs"
38
+ "test:engine": "node ../test/local-engine.test.mjs",
39
+ "test:highlight": "node ../test/aegis-highlight.test.mjs",
40
+ "test:markdown": "node ../test/markdown.test.mjs",
41
+ "test:deep-link": "node test/deep-link.test.mjs"
39
42
  },
40
43
  "dependencies": {
41
- "electron": "^33.0.0"
44
+ "electron": "^33.0.0",
45
+ "electron-updater": "^6.8.9"
42
46
  },
43
47
  "devDependencies": {
44
48
  "electron-builder": "^25.1.8"
package/preload.js CHANGED
@@ -16,7 +16,15 @@ const { contextBridge, ipcRenderer } = require('electron');
16
16
  const IPC_PREFIX = 'aegis:';
17
17
  const MODEL_PREFIX = 'model:';
18
18
  const SYNC_PREFIX = 'sync:';
19
+ const QUICK_PREFIX = 'quick:';
19
20
  const CHAT_DELTA_CHANNEL = `${IPC_PREFIX}chatDelta`;
21
+ const UPDATE_STATUS_CHANNEL = `${IPC_PREFIX}updateStatus`;
22
+ const MENU_NEW_CHAT_CHANNEL = `${IPC_PREFIX}menuNewChat`;
23
+ const MENU_SEARCH_CHANNEL = `${IPC_PREFIX}menuSearch`;
24
+ const MENU_EXPORT_MARKDOWN_CHANNEL = `${IPC_PREFIX}menuExportMarkdown`;
25
+ const MENU_EXPORT_JSON_CHANNEL = `${IPC_PREFIX}menuExportJson`;
26
+ const DEEP_LINK_CHANNEL = `${IPC_PREFIX}deepLink`;
27
+ const QUICK_LAUNCHER_PUSH_CHANNEL = `${IPC_PREFIX}quickLauncherPush`;
20
28
 
21
29
  function invoke(name, payload) {
22
30
  return ipcRenderer.invoke(
@@ -39,6 +47,13 @@ function invokeSync(name, payload) {
39
47
  );
40
48
  }
41
49
 
50
+ function invokeQuick(name, payload) {
51
+ return ipcRenderer.invoke(
52
+ QUICK_PREFIX + name,
53
+ payload === undefined ? undefined : payload
54
+ );
55
+ }
56
+
42
57
  /**
43
58
  * Build the chatDelta listener for one streaming request.
44
59
  *
@@ -107,6 +122,83 @@ const api = {
107
122
  memorySaveBatch: (entries) => invoke('memorySaveBatch', { entries }),
108
123
  memoryImport: (payload) => invoke('memoryImport', payload || {}),
109
124
  importConversation: (payload) => invoke('importConversation', payload || {}),
125
+ // Rendered markdown's links (and the discovery lane's) open in the OS
126
+ // default browser, never the app's own BrowserWindow — see main.js
127
+ // isSafeExternalUrl for the http/https-only allowlist.
128
+ openExternal: (url) => invoke('openExternal', { url }),
129
+
130
+ // Tool-call approval toggle (Settings → "Confirm before running tools"):
131
+ // main.js createConfirmModeDispatch persists it in the settings store's
132
+ // reserved `__confirmMode` namespace, and the engine's gate
133
+ // (lib/local/engine.js gatedExecuteTool) reads it on every mutating tool
134
+ // call. Both resolve `{ enabled }`.
135
+ getConfirmMode: () => invoke('getConfirmMode'),
136
+ setConfirmMode: (enabled) => invoke('setConfirmMode', { enabled: Boolean(enabled) }),
137
+
138
+ // Auto-update (electron-updater over GitHub Releases — see main.js
139
+ // createUpdateManager). check/download resolve the same status shape the
140
+ // push channel delivers; both are no-ops that resolve `{ status:
141
+ // 'disabled' }` in an unpackaged dev build. quitAndInstallUpdate must only
142
+ // ever be called from an explicit user action (the banner's "Restart to
143
+ // install" button) — main.js never restarts on its own.
144
+ checkForUpdates: () => invoke('checkForUpdates'),
145
+ downloadUpdate: () => invoke('downloadUpdate'),
146
+ quitAndInstallUpdate: () => invoke('quitAndInstallUpdate'),
147
+ updateStatus: () => invoke('updateStatus'),
148
+ // Live push as the state machine advances (checking -> available ->
149
+ // downloading -> downloaded, or -> error at any point). Returns an
150
+ // unsubscribe function, same shape as the chat delta listeners above.
151
+ onUpdateStatus: (onStatus) => {
152
+ const listener = (_event, state) => onStatus(state);
153
+ ipcRenderer.on(UPDATE_STATUS_CHANNEL, listener);
154
+ return () => ipcRenderer.removeListener(UPDATE_STATUS_CHANNEL, listener);
155
+ },
156
+ // Native menu accelerators with no built-in Electron role (Cmd/Ctrl+N,
157
+ // Cmd/Ctrl+K — see main.js buildAppMenu): main.js just pings the channel,
158
+ // the renderer owns what "new chat" / "search" actually do.
159
+ onMenuNewChat: (onTrigger) => {
160
+ const listener = () => onTrigger();
161
+ ipcRenderer.on(MENU_NEW_CHAT_CHANNEL, listener);
162
+ return () => ipcRenderer.removeListener(MENU_NEW_CHAT_CHANNEL, listener);
163
+ },
164
+ onMenuSearch: (onTrigger) => {
165
+ const listener = () => onTrigger();
166
+ ipcRenderer.on(MENU_SEARCH_CHANNEL, listener);
167
+ return () => ipcRenderer.removeListener(MENU_SEARCH_CHANNEL, listener);
168
+ },
169
+ // File > Save as… / Export Session… (see main.js buildAppMenu): the menu
170
+ // has no renderer state of its own, so it just pings which format was
171
+ // asked for; the renderer's handler knows the open session id and calls
172
+ // exportSession() below — the same path the sidebar's Export button uses.
173
+ onMenuExportMarkdown: (onTrigger) => {
174
+ const listener = () => onTrigger();
175
+ ipcRenderer.on(MENU_EXPORT_MARKDOWN_CHANNEL, listener);
176
+ return () => ipcRenderer.removeListener(MENU_EXPORT_MARKDOWN_CHANNEL, listener);
177
+ },
178
+ onMenuExportJson: (onTrigger) => {
179
+ const listener = () => onTrigger();
180
+ ipcRenderer.on(MENU_EXPORT_JSON_CHANNEL, listener);
181
+ return () => ipcRenderer.removeListener(MENU_EXPORT_JSON_CHANNEL, listener);
182
+ },
183
+ // Writes the given session to a user-picked file (dialog.showSaveDialog
184
+ // runs in main — see main.js createExportDispatch); resolves
185
+ // { ok, filePath } or { ok: false, canceled | reason }.
186
+ exportSession: (sessionId, format) => invoke('exportSession', { sessionId, format }),
187
+ // aegis:// deep link (main.js sendDeepLinkToWindow): fires with
188
+ // { action: 'open', sessionId } or { action: 'new', prompt }.
189
+ onDeepLink: (onLink) => {
190
+ const listener = (_event, parsed) => onLink(parsed);
191
+ ipcRenderer.on(DEEP_LINK_CHANNEL, listener);
192
+ return () => ipcRenderer.removeListener(DEEP_LINK_CHANNEL, listener);
193
+ },
194
+ // Quick launcher "add to chat" (main.js pushQuickLauncherResult): fires
195
+ // with { prompt, response, model } once the user pushes a launcher answer
196
+ // into the main window — see renderer/app.js's handler for what it builds.
197
+ onQuickLauncherPush: (onPush) => {
198
+ const listener = (_event, payload) => onPush(payload);
199
+ ipcRenderer.on(QUICK_LAUNCHER_PUSH_CHANNEL, listener);
200
+ return () => ipcRenderer.removeListener(QUICK_LAUNCHER_PUSH_CHANNEL, listener);
201
+ },
110
202
  };
111
203
 
112
204
  // Model-class surface (plan P1 §5.3): backed by the `model:` channels in
@@ -148,6 +240,14 @@ const models = {
148
240
  remove: (provider) => invokeModel('settings.remove', { provider }),
149
241
  },
150
242
  cancel: (sessionId) => invokeModel('cancel', { sessionId }),
243
+ // Tool-call approval gate: the renderer's approval card calls this to
244
+ // answer a pending exec/writeFile/editFile request (the approval itself
245
+ // arrives as a `{ approval }` chunk on the same chat() delta stream — see
246
+ // deltaListener above). clearApprovals wipes a conversation's "allow for
247
+ // this session" grants; newChat() calls it so a fresh thread starts clean.
248
+ respondApproval: (approvalId, decision) =>
249
+ invokeModel('respondApproval', { approvalId, decision }),
250
+ clearApprovals: (sessionId) => invokeModel('clearApprovals', { sessionId }),
151
251
  };
152
252
 
153
253
  // Session sync surface (plan P1 §5.3 / P3 §7): local persistence now, cloud
@@ -163,6 +263,27 @@ const sync = {
163
263
  status: () => invokeSync('status'),
164
264
  };
165
265
 
266
+ // Quick launcher surface: loaded by BOTH renderer/index.html (the settings
267
+ // card that configures the global shortcut) and renderer/quick.html (the
268
+ // launcher popup itself, which calls pushToMain when the user keeps an
269
+ // answer) — one bridge, two consumers, same whitelist-only shape as
270
+ // aegis/models/sync above.
271
+ const quickLauncher = {
272
+ // { enabled, shortcut, packaged, active, reason } — see main.js
273
+ // createQuickLauncherDispatch. `active` reflects whether the shortcut is
274
+ // actually registered right now; `reason` explains a failed registration.
275
+ status: () => invokeQuick('status'),
276
+ setConfig: (cfg) =>
277
+ invokeQuick('setConfig', {
278
+ enabled: cfg && cfg.enabled,
279
+ shortcut: cfg && cfg.shortcut,
280
+ }),
281
+ // Called by renderer/quick.js once an answer exists; resolves
282
+ // { ok, reason? }.
283
+ pushToMain: (payload) => invokeQuick('pushToMain', payload || {}),
284
+ };
285
+
166
286
  contextBridge.exposeInMainWorld('aegis', Object.freeze(api));
167
287
  contextBridge.exposeInMainWorld('models', Object.freeze(models));
168
288
  contextBridge.exposeInMainWorld('sync', Object.freeze(sync));
289
+ contextBridge.exposeInMainWorld('quickLauncher', Object.freeze(quickLauncher));