aegis-desktop 0.8.12 → 0.8.13

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/renderer/app.js CHANGED
@@ -42,6 +42,12 @@ const quickLauncher = window.quickLauncher;
42
42
  // has no `queue` key, and every function below no-ops on it rather than
43
43
  // crashing boot the way a chat flow without window.aegis would.
44
44
  const queueApi = window.queue;
45
+ // The two opt-in automations (main.js registerAutoIpc -> lib/auto.js). Guarded
46
+ // everywhere it is used, for the same reason queueApi is: a preload predating
47
+ // this surface has no `window.auto` at all, and the toggles must degrade to
48
+ // inert markup rather than throwing at boot and taking every later binding with
49
+ // them.
50
+ const autoApi = window.auto;
45
51
 
46
52
  if (!aegis || !models) {
47
53
  document.body.textContent =
@@ -126,6 +132,13 @@ const ELEMENT_IDS = {
126
132
  memoryCount: 'memory-count',
127
133
  memoryOverlayEntry: 'memory-overlay-entry',
128
134
  memoryOverlaySave: 'memory-overlay-save',
135
+ // φ(α). The panel's own nodes are read by desktop/renderer/fingerprint.js,
136
+ // which declares them itself; app.js only needs the sidebar state line it
137
+ // writes when mount() throws. Listed here all the same so the "every id
138
+ // app.js asks for exists in index.html" check covers them.
139
+ fpCardStatus: 'fp-card-status',
140
+ fpOpen: 'fp-open',
141
+ fpOverlay: 'fp-overlay',
129
142
  messages: 'messages',
130
143
  composer: 'composer',
131
144
  prompt: 'prompt',
@@ -142,6 +155,10 @@ const ELEMENT_IDS = {
142
155
  queueHint: 'queue-hint',
143
156
  queueFanout: 'queue-fanout',
144
157
  queueFanoutLabel: 'queue-fanout-label',
158
+ queueAuto: 'queue-auto',
159
+ queueAutoLabel: 'queue-auto-label',
160
+ queueAutoInterval: 'queue-auto-interval',
161
+ queueAutoHint: 'queue-auto-hint',
145
162
  queueBoundary: 'queue-boundary',
146
163
  queueUpgrade: 'queue-upgrade',
147
164
  };
@@ -1543,6 +1560,119 @@ async function queueTaskAction(name, id) {
1543
1560
  }
1544
1561
  }
1545
1562
 
1563
+ /**
1564
+ * The auto-drain control on the queue card (main.js registerAutoIpc ->
1565
+ * desktop/lib/auto.js -> settings.js `__autoDrain`).
1566
+ *
1567
+ * This is the ONE thing on this card that can start a drain with no click, and
1568
+ * it exists because the user asked for it explicitly. Everything the queue
1569
+ * section above promises is still true of a default install: the switch is
1570
+ * off, main.js's timer is not even created, and nothing drains at boot or on
1571
+ * idle. What changed is that "off" is now a persisted default rather than an
1572
+ * absolute — so the honesty this file owes is a truthful paint of that switch,
1573
+ * including when the timer is armed but skipping (another host holds the lock,
1574
+ * the queue is empty), which is the state that otherwise reads as "the feature
1575
+ * is broken".
1576
+ */
1577
+ function renderAutoDrain(state) {
1578
+ if (!els.queueAuto || !state || typeof state !== 'object') return;
1579
+ const enabled = state.enabled === true;
1580
+ els.queueAuto.checked = enabled;
1581
+ if (els.queueAutoInterval) {
1582
+ // A pick is only meaningful while the switch is on; disabling it keeps the
1583
+ // select from reading as an armed cadence next to an off switch.
1584
+ els.queueAutoInterval.disabled = !enabled;
1585
+ const ms = Number(state.intervalMs);
1586
+ if (Number.isFinite(ms)) els.queueAutoInterval.value = String(ms);
1587
+ }
1588
+ if (els.queueAutoLabel) {
1589
+ els.queueAutoLabel.textContent = enabled
1590
+ ? `drain automatically · ${formatAutoInterval(state.intervalMs)}`
1591
+ : 'drain automatically';
1592
+ }
1593
+ if (!els.queueAutoHint) return;
1594
+ if (!enabled) {
1595
+ els.queueAutoHint.textContent = 'off — Run one / Drain all are the only triggers';
1596
+ return;
1597
+ }
1598
+ const bits = [];
1599
+ if (state.busy) bits.push('a drain is running');
1600
+ else if (state.armed) bits.push('armed');
1601
+ if (state.ticks) bits.push(`${state.ticks} tick${state.ticks === 1 ? '' : 's'}`);
1602
+ if (state.drains) bits.push(`${state.drains} drained`);
1603
+ const skip = state.lastSkip && state.lastSkip.reason;
1604
+ if (skip) bits.push(`last check skipped: ${autoSkipText(skip, state.lastSkip)}`);
1605
+ else if (!state.ticks) bits.push('first check one interval after arming');
1606
+ els.queueAutoHint.textContent = bits.join(' · ');
1607
+ }
1608
+
1609
+ /** The skip reasons lib/auto.js records, in the card's own words. An
1610
+ * unrecognised reason is shown verbatim rather than dropped: a new skip the
1611
+ * card does not know about is exactly the one worth reading. */
1612
+ function autoSkipText(reason, detail = {}) {
1613
+ if (reason === 'empty') return 'queue is empty';
1614
+ if (reason === 'busy') return 'the previous drain is still running';
1615
+ if (reason === 'locked') {
1616
+ const pid = detail.holder && detail.holder.pid;
1617
+ return pid ? `another drain holds the lock (pid ${pid})` : 'another drain holds the lock';
1618
+ }
1619
+ if (reason === 'disabled') return 'switched off';
1620
+ if (reason === 'no queue access') return 'this window has no queue access';
1621
+ return String(reason);
1622
+ }
1623
+
1624
+ function formatAutoInterval(ms) {
1625
+ const n = Number(ms);
1626
+ if (!Number.isFinite(n) || n <= 0) return 'every interval';
1627
+ if (n < 60_000) return `every ${Math.round(n / 1000)}s`;
1628
+ const min = n / 60_000;
1629
+ return Number.isInteger(min) ? `every ${min} min` : `every ${min.toFixed(1)} min`;
1630
+ }
1631
+
1632
+ /** Paint both automation states from main's answer (`auto.status()`). */
1633
+ function renderAutoStatus(payload) {
1634
+ if (!payload || typeof payload !== 'object') return;
1635
+ if (payload.drain) renderAutoDrain(payload.drain);
1636
+ if (typeof renderAutoMint === 'function' && payload.mint) renderAutoMint(payload.mint);
1637
+ }
1638
+
1639
+ /**
1640
+ * Load the two switches and paint them. A read, never an arm: main.js is what
1641
+ * armed the timer at boot, from the same persisted settings this paints.
1642
+ */
1643
+ async function loadAutoState() {
1644
+ if (!autoApi || typeof autoApi.status !== 'function') return null;
1645
+ try {
1646
+ const state = await autoApi.status();
1647
+ renderAutoStatus(state);
1648
+ return state;
1649
+ } catch (err) {
1650
+ if (els.queueAutoHint) {
1651
+ els.queueAutoHint.textContent = `automation status unavailable: ${err && err.message ? err.message : err}`;
1652
+ }
1653
+ return null;
1654
+ }
1655
+ }
1656
+
1657
+ /** The drain toggle. Persists through main, then paints what main says it
1658
+ * persisted — never the optimistic checkbox state. */
1659
+ async function setAutoDrain(enabled) {
1660
+ if (!autoApi || typeof autoApi.setDrain !== 'function') return;
1661
+ const patch = { enabled: Boolean(enabled) };
1662
+ if (els.queueAutoInterval && els.queueAutoInterval.value) {
1663
+ patch.intervalMs = Number(els.queueAutoInterval.value);
1664
+ }
1665
+ try {
1666
+ const res = await autoApi.setDrain(patch);
1667
+ if (res && res.ok === false) {
1668
+ if (els.queueAutoHint) els.queueAutoHint.textContent = res.error || 'could not save that';
1669
+ }
1670
+ renderAutoStatus(res);
1671
+ } catch (err) {
1672
+ if (els.queueAutoHint) els.queueAutoHint.textContent = `could not save that: ${err && err.message ? err.message : err}`;
1673
+ }
1674
+ }
1675
+
1546
1676
  /** Live drain progress -> one status line. `delta`/`reasoning` frames are
1547
1677
  * deliberately dropped: they are a worker's own output, and the place for a
1548
1678
  * task's result is its row (from queue.list()), not the open transcript. */
@@ -1628,10 +1758,35 @@ const memoryView = {
1628
1758
  loading: false,
1629
1759
  };
1630
1760
 
1761
+ /** Is any sheet up?
1762
+ *
1763
+ * Both overlays count, because the one thing this predicate is used for is
1764
+ * Escape precedence: while a sheet is open, Escape must not reach past it to
1765
+ * cancel a turn the user may not be looking at (see the bindEscapeInterrupt
1766
+ * call). The memory inspector and the φ(α) panel are each opened over the same
1767
+ * sidebar, so "an overlay is open" has to mean either one — the wiring guard in
1768
+ * test/renderer-wiring.test.mjs pins this name against exactly that job.
1769
+ *
1770
+ * Callers that mean the MEMORY inspector specifically — which one is up decides
1771
+ * where a save reads from and what gets repainted — ask memoryOverlayOpen()
1772
+ * instead. Confusing the two is how a save would read a hidden textarea. */
1631
1773
  function overlayOpen() {
1774
+ if (memoryOverlayOpen()) return true;
1775
+ return !!(window.aegisFingerprint && window.aegisFingerprint.isOpen());
1776
+ }
1777
+
1778
+ /** The memory inspector alone. Split out of overlayOpen() when the authorship
1779
+ * panel gained the same Escape treatment: the two questions were the same
1780
+ * question right up until there was a second sheet. */
1781
+ function memoryOverlayOpen() {
1632
1782
  return !!els.memoryOverlay && !els.memoryOverlay.hidden;
1633
1783
  }
1634
1784
 
1785
+ /** The φ(α) panel handle, set by init() when desktop/renderer/fingerprint.js is
1786
+ * loaded. Kept here rather than inside fingerprint.js so the Escape policy
1787
+ * above can ask it whether it is open without reaching into its internals. */
1788
+ let fpPanel = null;
1789
+
1635
1790
  /** "3m ago" / "2d ago" from epoch ms. Empty string when unknown. */
1636
1791
  function relTime(ms) {
1637
1792
  if (!Number.isFinite(ms)) return '';
@@ -1927,7 +2082,7 @@ function renderMemoryChips() {
1927
2082
 
1928
2083
  /** Repaint the inspector from `memoryView`. Pure — makes no network calls. */
1929
2084
  function renderMemoryOverlay() {
1930
- if (!overlayOpen()) return;
2085
+ if (!memoryOverlayOpen()) return;
1931
2086
  const total = memoryView.entries.length;
1932
2087
  els.memoryList.innerHTML = '';
1933
2088
  renderMemoryChips();
@@ -2040,7 +2195,7 @@ function closeMemoryOverlay() {
2040
2195
  /** Textarea the save button should read from — the inspector's when it is up,
2041
2196
  * otherwise the sidebar's. Keeps both save paths on one code path. */
2042
2197
  function activeMemoryEntry() {
2043
- return overlayOpen() && els.memoryOverlayEntry ? els.memoryOverlayEntry : els.memoryEntry;
2198
+ return memoryOverlayOpen() && els.memoryOverlayEntry ? els.memoryOverlayEntry : els.memoryEntry;
2044
2199
  }
2045
2200
 
2046
2201
  async function importMemory() {
@@ -2123,7 +2278,7 @@ async function saveMemory() {
2123
2278
  async function refreshMemoryViews() {
2124
2279
  const q = els.memoryQuery ? els.memoryQuery.value.trim() : '';
2125
2280
  await searchMemory(q);
2126
- if (overlayOpen()) await loadMemoryOverlay(memoryView.query);
2281
+ if (memoryOverlayOpen()) await loadMemoryOverlay(memoryView.query);
2127
2282
  }
2128
2283
 
2129
2284
  // ------------------------------------------------------------ chat welcome
@@ -4288,6 +4443,27 @@ async function init() {
4288
4443
  await loadQueueState();
4289
4444
  }
4290
4445
 
4446
+ // ── the two automation switches ──────────────────────────────────────────
4447
+ // Guarded like the queue block above (a preload predating registerAutoIpc has
4448
+ // no window.auto). The `change` handlers are the ONLY things that arm a
4449
+ // timer: loadAutoState() below reads and paints, and main.js armed whatever
4450
+ // was already persisted before this window existed. Nothing here fires one on
4451
+ // its own.
4452
+ if (autoApi) {
4453
+ if (els.queueAuto) {
4454
+ els.queueAuto.addEventListener('change', () => setAutoDrain(els.queueAuto.checked));
4455
+ }
4456
+ if (els.queueAutoInterval) {
4457
+ // A cadence change while armed re-arms the timer in main.js (arm() sees a
4458
+ // new intervalMs); while off it only persists, which is what the user
4459
+ // picked the value for.
4460
+ els.queueAutoInterval.addEventListener('change', () => {
4461
+ if (els.queueAuto && els.queueAuto.checked) setAutoDrain(true);
4462
+ });
4463
+ }
4464
+ await loadAutoState();
4465
+ }
4466
+
4291
4467
  // The transcript's scroll/paint/Escape policy. Both listeners are registered
4292
4468
  // inside transcript-view.js so the behaviours they enforce are the ones
4293
4469
  // test/renderer-dom.test.mjs drives: the passive `scroll` listener is the
@@ -4315,8 +4491,20 @@ async function init() {
4315
4491
  doc: document,
4316
4492
  // The memory overlay wins: while it is open, Escape closes it rather than
4317
4493
  // reaching past it to cancel a turn the user may not be looking at.
4494
+ //
4495
+ // φ(α) is composed into overlayOpen() rather than given its own listener:
4496
+ // two keydown handlers racing to consume one Escape is how a panel ends up
4497
+ // closing and a turn getting cancelled by the same keypress. The escaping
4498
+ // order is the φ(α) panel first only because it is the outermost sheet when
4499
+ // both are somehow up.
4318
4500
  isOverlayOpen: overlayOpen,
4319
- onOverlayEscape: closeMemoryOverlay,
4501
+ onOverlayEscape: () => {
4502
+ if (window.aegisFingerprint && window.aegisFingerprint.isOpen()) {
4503
+ window.aegisFingerprint.close();
4504
+ return;
4505
+ }
4506
+ closeMemoryOverlay();
4507
+ },
4320
4508
  hasPendingTurn: () => !!pendingSessionId,
4321
4509
  stopTurn: stopPendingTurn,
4322
4510
  });
@@ -4373,6 +4561,22 @@ async function init() {
4373
4561
  await refreshSyncStatus();
4374
4562
  loadAccountInfo();
4375
4563
  searchMemory('');
4564
+
4565
+ // φ(α). `?`-guarded like the update banner above: desktop/renderer/
4566
+ // fingerprint.js owns its own markup and its own listeners, so the sidebar
4567
+ // card and the panel are painted from one mount call and a missing bridge
4568
+ // (a host that predates the IPC handlers) leaves the card on "checking…"
4569
+ // instead of crashing boot. Not awaited: the panel's first status read is a
4570
+ // filesystem stat, and the welcome panel must not wait on it.
4571
+ if (window.aegisFingerprint) {
4572
+ try {
4573
+ fpPanel = window.aegisFingerprint.mount({ api: aegis });
4574
+ fpPanel.refresh().catch(() => {});
4575
+ } catch (e) {
4576
+ const card = els.fpCardStatus;
4577
+ if (card) card.textContent = `authorship panel unavailable: ${(e && e.message) || e}`;
4578
+ }
4579
+ }
4376
4580
  }
4377
4581
 
4378
4582
  // Boot only after the DOM is parsed so every control this script queries
@@ -0,0 +1,143 @@
1
+ /* ---------------------------------------------------------- φ(α) panel
2
+ The panel reuses the memory inspector's sheet (.memory-overlay /
3
+ .memory-panel, style.css:483+) so the two overlays are the same object to the
4
+ eye — one translucent layer, one sheet, Escape closes both. Only the rows the
5
+ fingerprint panel adds are styled here, in their own file, because
6
+ style.css is not where this feature lives.
7
+
8
+ Loaded by index.html after style.css; `style-src 'self'` allows it. */
9
+
10
+ .fp-rows {
11
+ display: flex;
12
+ flex-wrap: wrap;
13
+ gap: 2px 18px;
14
+ padding: 12px 16px 4px;
15
+ font-family: var(--font-mono);
16
+ font-size: 11px;
17
+ color: var(--text2);
18
+ }
19
+
20
+ .fp-row {
21
+ display: flex;
22
+ gap: 6px;
23
+ min-width: 0;
24
+ }
25
+
26
+ .fp-k {
27
+ color: var(--text3, var(--text2));
28
+ opacity: 0.75;
29
+ }
30
+
31
+ .fp-v {
32
+ color: var(--text);
33
+ overflow-wrap: anywhere;
34
+ }
35
+
36
+ /* The 0644 refusal and any other "the fix is a command" message. Warning
37
+ colour, not error colour: the key works, it is just readable by others. */
38
+ .fp-warn {
39
+ margin: 6px 16px 0;
40
+ font-size: 11px;
41
+ font-family: var(--font-mono);
42
+ color: var(--warn, #e0a33a);
43
+ overflow-wrap: anywhere;
44
+ }
45
+
46
+ .fp-warn[hidden] {
47
+ display: none;
48
+ }
49
+
50
+ .fp-controls {
51
+ display: flex;
52
+ flex-direction: column;
53
+ gap: 6px;
54
+ padding: 10px 16px 0;
55
+ }
56
+
57
+ .fp-line {
58
+ display: flex;
59
+ gap: 6px;
60
+ align-items: center;
61
+ }
62
+
63
+ /* The backup row sits outside .fp-controls, so it carries its own inset.
64
+ A class rather than a style attribute — the CSP is `style-src 'self'`. */
65
+ .fp-line-inset {
66
+ padding: 8px 16px 0;
67
+ }
68
+
69
+ .fp-line input[type='text'] {
70
+ flex: 1;
71
+ min-width: 0;
72
+ background: var(--surface);
73
+ color: var(--text);
74
+ border: 1px solid var(--border);
75
+ border-radius: 6px;
76
+ padding: 6px 8px;
77
+ font: inherit;
78
+ font-size: 12px;
79
+ }
80
+
81
+ .fp-actions {
82
+ display: flex;
83
+ flex-wrap: wrap;
84
+ gap: 6px;
85
+ padding: 8px 16px 0;
86
+ }
87
+
88
+ .fp-vault-list {
89
+ list-style: none;
90
+ margin: 6px 0 0;
91
+ padding: 0 16px;
92
+ max-height: 150px;
93
+ overflow: auto;
94
+ }
95
+
96
+ .fp-vault-row {
97
+ display: flex;
98
+ gap: 8px;
99
+ align-items: baseline;
100
+ padding: 4px 0;
101
+ border-bottom: 1px solid var(--border);
102
+ font-size: 11px;
103
+ font-family: var(--font-mono);
104
+ }
105
+
106
+ .fp-vault-name {
107
+ color: var(--text);
108
+ overflow-wrap: anywhere;
109
+ }
110
+
111
+ .fp-vault-detail {
112
+ color: var(--text2);
113
+ overflow-wrap: anywhere;
114
+ }
115
+
116
+ /* The result line. `fp-bad` is a refusal, which is always a sentence naming
117
+ the reason — never a blank pane that reads like success. */
118
+ .fp-out {
119
+ margin: 10px 16px 14px;
120
+ padding: 8px 10px;
121
+ border: 1px solid var(--border);
122
+ border-radius: 6px;
123
+ font-size: 11px;
124
+ font-family: var(--font-mono);
125
+ color: var(--text2);
126
+ overflow-wrap: anywhere;
127
+ min-height: 30px;
128
+ }
129
+
130
+ .fp-out.fp-bad {
131
+ color: var(--danger, #e06c75);
132
+ border-color: color-mix(in srgb, var(--danger, #e06c75) 45%, var(--border));
133
+ }
134
+
135
+ /* Sidebar card: the state line under the heading, and the button that opens
136
+ the sheet. */
137
+ #fp-card-status {
138
+ font-family: var(--font-mono);
139
+ font-size: 11px;
140
+ color: var(--text2);
141
+ overflow-wrap: anywhere;
142
+ margin: 0 0 6px;
143
+ }