aegis-desktop 0.4.3 → 0.4.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.
@@ -473,6 +473,17 @@ function createLocalEngine({ aegis, settings, ollama, providers, tools, promptBu
473
473
 
474
474
  async function listModels(cls) {
475
475
  if (cls === 'aegis') {
476
+ // The catalog is behind the account's key: GET /api/v1/models answers
477
+ // `401 {"error":{"message":"No API key"}}` without one (verified against
478
+ // aegiscloud.org). Aegis Cloud is the *default* class, so firing the call
479
+ // anyway painted a raw "listModels failed: … 401" over the default model
480
+ // picker for every user who had just installed the app and not yet
481
+ // pasted a key — the one state where the UI must say what unblocks it and
482
+ // not what went wrong. Report the missing key as a state (`needsKey`) and
483
+ // let the renderer invite the user to connect; nothing else about the
484
+ // class changes, and the model dropdown keeps its "server default (auto)"
485
+ // entry so the class is usable the moment a key lands.
486
+ if (!aegis.apiKey) return { class: cls, models: [], needsKey: true };
476
487
  const data = await aegis.listModels();
477
488
  return { class: cls, models: filterAegisCatalog(normalizeCatalog(data && data.models)) };
478
489
  }
@@ -535,7 +546,7 @@ function createLocalEngine({ aegis, settings, ollama, providers, tools, promptBu
535
546
  model: opts.model,
536
547
  mode: opts.mode,
537
548
  maxTokens: opts.maxTokens,
538
- stream: true,
549
+ stream: opts.stream !== false,
539
550
  // The pooled (Nexus) brain is streamed, and an OpenAI-compatible SSE
540
551
  // stream reports no token usage unless asked. Without this the Aegis
541
552
  // Cloud class — the desktop's default — was the one class that answered
@@ -699,6 +710,11 @@ function createLocalEngine({ aegis, settings, ollama, providers, tools, promptBu
699
710
  workers: payload && payload.workers,
700
711
  onReasoning,
701
712
  idleTimeoutMs: autonomous ? AUTONOMOUS_IDLE_TIMEOUT_MS : undefined,
713
+ // A caller with no live streaming surface (a `--no-stream` CLI flag, a
714
+ // one-shot script) can ask for the buffered non-stream wire form
715
+ // instead. Undefined/anything but `false` keeps every existing caller
716
+ // (the desktop renderer never sets this) on the streamed path.
717
+ stream: payload && payload.stream === false ? false : true,
702
718
  };
703
719
 
704
720
  // No round cap: a model that keeps calling tools keeps going for as
@@ -782,6 +798,15 @@ function createLocalEngine({ aegis, settings, ollama, providers, tools, promptBu
782
798
  }
783
799
  addUsage(res);
784
800
 
801
+ // A cancelled turn is over. Both recoveries below exist for "the model
802
+ // said nothing", and an aborted request resolves with exactly that —
803
+ // no text — so without this guard a user pressing Esc to stop a slow
804
+ // turn immediately issued ANOTHER billed provider call (and, because
805
+ // every transport concatenates the deltas it forwards, streamed the
806
+ // partial answer a second time onto the same bubble). Measured before
807
+ // the guard: one interrupted turn == two dispatches.
808
+ if (signal.aborted) return withTurnUsage(res);
809
+
785
810
  // Budget exhausted before the answer was written. Doubling it costs
786
811
  // one request and converts a dead turn into a real one; a second
787
812
  // 'length' result is accepted as-is so a hard-capped model can't
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "aegis-desktop",
3
3
  "productName": "AEGIS Desktop",
4
- "version": "0.4.3",
4
+ "version": "0.4.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
@@ -171,6 +171,11 @@ const CUSTOM_MODEL_PRESETS = {
171
171
  // already filters out of settings.list(); never render it as a provider row
172
172
  // even if a stale store still surfaces it (defect #1).
173
173
  const RESERVED_PROVIDERS = new Set(['__aegis', 'aegis']);
174
+ // Where a user without a key gets one. The class picker defaults to Aegis Cloud
175
+ // and the catalog is key-gated, so this is the first thing a new install needs;
176
+ // it lives here rather than inline so the Model hint and any future "connect"
177
+ // affordance cannot drift to two different pages.
178
+ const GET_AEGIS_KEY_URL = 'https://aegiscloud.org';
174
179
 
175
180
  let pendingEl = null;
176
181
  let pendingSessionId = null;
@@ -1858,6 +1863,11 @@ async function loadModels(cls) {
1858
1863
  try {
1859
1864
  const data = await models.listModels(cls);
1860
1865
  const list = Array.isArray(data && data.models) ? data.models : [];
1866
+ // No key on the default class: the engine reports the missing credential as
1867
+ // a state instead of letting the catalog call 401 (see engine.listModels).
1868
+ // The hint is where a user finds out they can connect at all — a raw
1869
+ // "listModels failed" told them only that something was broken.
1870
+ const needsKey = Boolean(data && data.needsKey);
1861
1871
  for (const m of list) {
1862
1872
  modelMeta.set(m.id, m);
1863
1873
  const opt = document.createElement('option');
@@ -1866,12 +1876,28 @@ async function loadModels(cls) {
1866
1876
  els.modelSelect.appendChild(opt);
1867
1877
  }
1868
1878
  let hint;
1869
- if (!list.length) {
1879
+ if (needsKey) {
1880
+ hint = null; // carries a link, built below
1881
+ } else if (!list.length) {
1870
1882
  hint = cls === 'ollama' ? 'Ollama not running or no models pulled.' : 'No models listed.';
1871
1883
  } else {
1872
1884
  hint = `${list.length} model${list.length === 1 ? '' : 's'} available.`;
1873
1885
  }
1874
1886
  const ceiling = applyMaxTokensClamp(els.modelSelect.value);
1887
+ if (needsKey) {
1888
+ // The hint elements are bare <p>s, so the link has to be a real child
1889
+ // node — a text assignment would wipe it (same shape as capNotice).
1890
+ els.modelHint.textContent =
1891
+ 'Connect Aegis Cloud to load its models: paste your AEGIS API key in the Status card above (';
1892
+ const a = document.createElement('a');
1893
+ a.href = GET_AEGIS_KEY_URL;
1894
+ a.target = '_blank';
1895
+ a.rel = 'noreferrer noopener';
1896
+ a.textContent = 'free key at aegiscloud.org';
1897
+ els.modelHint.appendChild(a);
1898
+ els.modelHint.appendChild(document.createTextNode('), then Save.'));
1899
+ return;
1900
+ }
1875
1901
  els.modelHint.textContent =
1876
1902
  ceiling < FLAT_CEILING ? `${hint} · max output: ${ceiling.toLocaleString()}` : hint;
1877
1903
  } catch (err) {