@oratis/lisa 0.11.1 → 0.14.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.
Files changed (142) hide show
  1. package/dist/agent.d.ts +2 -0
  2. package/dist/agent.d.ts.map +1 -1
  3. package/dist/agent.js +20 -1
  4. package/dist/agent.js.map +1 -1
  5. package/dist/agents/pty.d.ts.map +1 -1
  6. package/dist/agents/pty.js +13 -2
  7. package/dist/agents/pty.js.map +1 -1
  8. package/dist/autonomy/state.d.ts +14 -0
  9. package/dist/autonomy/state.d.ts.map +1 -0
  10. package/dist/autonomy/state.js +54 -0
  11. package/dist/autonomy/state.js.map +1 -0
  12. package/dist/cli/mail.d.ts +2 -0
  13. package/dist/cli/mail.d.ts.map +1 -0
  14. package/dist/cli/mail.js +209 -0
  15. package/dist/cli/mail.js.map +1 -0
  16. package/dist/cli/pair.d.ts +15 -0
  17. package/dist/cli/pair.d.ts.map +1 -0
  18. package/dist/cli/pair.js +152 -0
  19. package/dist/cli/pair.js.map +1 -0
  20. package/dist/cli.js +22 -14
  21. package/dist/cli.js.map +1 -1
  22. package/dist/consent/store.d.ts +2 -2
  23. package/dist/consent/store.d.ts.map +1 -1
  24. package/dist/consent/store.js +3 -2
  25. package/dist/consent/store.js.map +1 -1
  26. package/dist/edition.d.ts +24 -0
  27. package/dist/edition.d.ts.map +1 -0
  28. package/dist/edition.js +19 -0
  29. package/dist/edition.js.map +1 -0
  30. package/dist/heartbeat/runner.d.ts.map +1 -1
  31. package/dist/heartbeat/runner.js +8 -0
  32. package/dist/heartbeat/runner.js.map +1 -1
  33. package/dist/idle/runner.d.ts.map +1 -1
  34. package/dist/idle/runner.js +7 -0
  35. package/dist/idle/runner.js.map +1 -1
  36. package/dist/mail/accounts.d.ts +20 -0
  37. package/dist/mail/accounts.d.ts.map +1 -0
  38. package/dist/mail/accounts.js +113 -0
  39. package/dist/mail/accounts.js.map +1 -0
  40. package/dist/mail/alerts.d.ts +29 -0
  41. package/dist/mail/alerts.d.ts.map +1 -0
  42. package/dist/mail/alerts.js +40 -0
  43. package/dist/mail/alerts.js.map +1 -0
  44. package/dist/mail/classify.d.ts +26 -0
  45. package/dist/mail/classify.d.ts.map +1 -0
  46. package/dist/mail/classify.js +191 -0
  47. package/dist/mail/classify.js.map +1 -0
  48. package/dist/mail/connectors/gmail.d.ts +55 -0
  49. package/dist/mail/connectors/gmail.d.ts.map +1 -0
  50. package/dist/mail/connectors/gmail.js +87 -0
  51. package/dist/mail/connectors/gmail.js.map +1 -0
  52. package/dist/mail/connectors/imap.d.ts +13 -0
  53. package/dist/mail/connectors/imap.d.ts.map +1 -0
  54. package/dist/mail/connectors/imap.js +136 -0
  55. package/dist/mail/connectors/imap.js.map +1 -0
  56. package/dist/mail/digest.d.ts +21 -0
  57. package/dist/mail/digest.d.ts.map +1 -0
  58. package/dist/mail/digest.js +89 -0
  59. package/dist/mail/digest.js.map +1 -0
  60. package/dist/mail/google-oauth.d.ts +50 -0
  61. package/dist/mail/google-oauth.d.ts.map +1 -0
  62. package/dist/mail/google-oauth.js +74 -0
  63. package/dist/mail/google-oauth.js.map +1 -0
  64. package/dist/mail/hosts.d.ts +3 -0
  65. package/dist/mail/hosts.d.ts.map +1 -0
  66. package/dist/mail/hosts.js +19 -0
  67. package/dist/mail/hosts.js.map +1 -0
  68. package/dist/mail/scheduler.d.ts +9 -0
  69. package/dist/mail/scheduler.d.ts.map +1 -0
  70. package/dist/mail/scheduler.js +21 -0
  71. package/dist/mail/scheduler.js.map +1 -0
  72. package/dist/mail/service.d.ts +32 -0
  73. package/dist/mail/service.d.ts.map +1 -0
  74. package/dist/mail/service.js +129 -0
  75. package/dist/mail/service.js.map +1 -0
  76. package/dist/mail/store.d.ts +7 -0
  77. package/dist/mail/store.d.ts.map +1 -0
  78. package/dist/mail/store.js +58 -0
  79. package/dist/mail/store.js.map +1 -0
  80. package/dist/mail/types.d.ts +114 -0
  81. package/dist/mail/types.d.ts.map +1 -0
  82. package/dist/mail/types.js +26 -0
  83. package/dist/mail/types.js.map +1 -0
  84. package/dist/providers/anthropic.d.ts.map +1 -1
  85. package/dist/providers/anthropic.js +43 -21
  86. package/dist/providers/anthropic.js.map +1 -1
  87. package/dist/providers/gemini.d.ts.map +1 -1
  88. package/dist/providers/gemini.js +67 -64
  89. package/dist/providers/gemini.js.map +1 -1
  90. package/dist/providers/openai.d.ts.map +1 -1
  91. package/dist/providers/openai.js +77 -71
  92. package/dist/providers/openai.js.map +1 -1
  93. package/dist/providers/registry.d.ts +10 -0
  94. package/dist/providers/registry.d.ts.map +1 -1
  95. package/dist/providers/registry.js +26 -0
  96. package/dist/providers/registry.js.map +1 -1
  97. package/dist/providers/stream-retry.d.ts +37 -0
  98. package/dist/providers/stream-retry.d.ts.map +1 -0
  99. package/dist/providers/stream-retry.js +92 -0
  100. package/dist/providers/stream-retry.js.map +1 -0
  101. package/dist/providers/types.d.ts +3 -0
  102. package/dist/providers/types.d.ts.map +1 -1
  103. package/dist/subagent.d.ts +2 -0
  104. package/dist/subagent.d.ts.map +1 -1
  105. package/dist/subagent.js +1 -0
  106. package/dist/subagent.js.map +1 -1
  107. package/dist/web/cloudAuth.d.ts +57 -0
  108. package/dist/web/cloudAuth.d.ts.map +1 -0
  109. package/dist/web/cloudAuth.js +139 -0
  110. package/dist/web/cloudAuth.js.map +1 -0
  111. package/dist/web/island.d.ts.map +1 -1
  112. package/dist/web/island.js +1 -0
  113. package/dist/web/island.js.map +1 -1
  114. package/dist/web/lisa-client.d.ts +1 -1
  115. package/dist/web/lisa-client.d.ts.map +1 -1
  116. package/dist/web/lisa-client.js +703 -40
  117. package/dist/web/lisa-client.js.map +1 -1
  118. package/dist/web/lisa-css.d.ts +1 -1
  119. package/dist/web/lisa-css.d.ts.map +1 -1
  120. package/dist/web/lisa-css.js +568 -43
  121. package/dist/web/lisa-css.js.map +1 -1
  122. package/dist/web/lisa-html.d.ts +1 -1
  123. package/dist/web/lisa-html.d.ts.map +1 -1
  124. package/dist/web/lisa-html.js +71 -21
  125. package/dist/web/lisa-html.js.map +1 -1
  126. package/dist/web/pairing.d.ts +37 -0
  127. package/dist/web/pairing.d.ts.map +1 -0
  128. package/dist/web/pairing.js +79 -0
  129. package/dist/web/pairing.js.map +1 -0
  130. package/dist/web/push.d.ts +91 -0
  131. package/dist/web/push.d.ts.map +1 -1
  132. package/dist/web/push.js +250 -9
  133. package/dist/web/push.js.map +1 -1
  134. package/dist/web/qr-svg.d.ts +11 -0
  135. package/dist/web/qr-svg.d.ts.map +1 -0
  136. package/dist/web/qr-svg.js +41 -0
  137. package/dist/web/qr-svg.js.map +1 -0
  138. package/dist/web/server.d.ts +1 -1
  139. package/dist/web/server.d.ts.map +1 -1
  140. package/dist/web/server.js +378 -14
  141. package/dist/web/server.js.map +1 -1
  142. package/package.json +4 -1
@@ -114,7 +114,7 @@ let capturing = false;
114
114
  window.lisaCaptureAndAttach = async function (mode) {
115
115
  if (capturing) return false;
116
116
  capturing = true;
117
- const btn = document.getElementById('captureBtn');
117
+ const btn = document.getElementById('plusBtn');
118
118
  if (btn) btn.classList.add('flash');
119
119
  try {
120
120
  const res = await fetch('/api/vision/capture', {
@@ -170,10 +170,26 @@ fileInput.addEventListener('change', async () => {
170
170
  // listener that forwards the click synchronously (preserving the
171
171
  // user-gesture context) and logs to console so we can verify in the
172
172
  // inspector if it ever silently no-ops again.
173
- // 📷 capture button interactive crosshair screenshot into the composer.
174
- const captureBtnEl = document.getElementById('captureBtn');
175
- if (captureBtnEl) {
176
- captureBtnEl.addEventListener('click', () => { void window.lisaCaptureAndAttach('interactive'); });
173
+ // ── composer menu: merges 📎 attach + 📷 screenshot into one button ──
174
+ const plusBtnEl = document.getElementById('plusBtn');
175
+ const plusMenuEl = document.getElementById('plusMenu');
176
+ function closePlusMenu() { if (plusMenuEl) plusMenuEl.classList.remove('open'); }
177
+ if (plusBtnEl && plusMenuEl) {
178
+ plusBtnEl.addEventListener('click', (e) => { e.stopPropagation(); plusMenuEl.classList.toggle('open'); });
179
+ document.addEventListener('click', (e) => {
180
+ if (plusMenuEl.classList.contains('open') && !plusMenuEl.contains(e.target) && e.target !== plusBtnEl) closePlusMenu();
181
+ });
182
+ }
183
+ const pmAttachEl = document.getElementById('pmAttach');
184
+ if (pmAttachEl) {
185
+ pmAttachEl.addEventListener('click', () => {
186
+ closePlusMenu();
187
+ try { fileInput.click(); } catch (err) { console.error('[attach] fileInput.click failed:', err); }
188
+ });
189
+ }
190
+ const pmShotEl = document.getElementById('pmShot');
191
+ if (pmShotEl) {
192
+ pmShotEl.addEventListener('click', () => { closePlusMenu(); void window.lisaCaptureAndAttach('interactive'); });
177
193
  }
178
194
 
179
195
  // ── Audio recording → transcribe → Lisa summarizes ─────────────────
@@ -308,21 +324,7 @@ if (recordBtnEl) {
308
324
  });
309
325
  }
310
326
 
311
- const attachBtnEl = document.getElementById('attachBtn');
312
- if (attachBtnEl) {
313
- attachBtnEl.addEventListener('click', (ev) => {
314
- // Don't preventDefault — that cancels the implicit label-forward
315
- // and removes the redundant gesture path. Letting both fire is
316
- // fine because <input type=file>.click() fires the picker only
317
- // once per user gesture.
318
- console.log('[attach] click forwarded to fileInput');
319
- try {
320
- fileInput.click();
321
- } catch (err) {
322
- console.error('[attach] fileInput.click failed:', err);
323
- }
324
- });
325
- }
327
+ // (📎 attach is now an item in the + composer menu — see pmAttach above.)
326
328
 
327
329
  // Paste-to-attach: when the user has copied an image (screenshot,
328
330
  // image from a webpage, etc.) and presses ⌘V, pull the image off the
@@ -452,6 +454,8 @@ function connectEvents() {
452
454
  // "sidebar live wiring" block). Generalized from claude_session_update
453
455
  // so codex / opencode / git / … sessions update the sidebar too.
454
456
  if (typeof refreshClaudeSessions === 'function') refreshClaudeSessions();
457
+ } else if (ev.type === 'mail_digest_update' || ev.type === 'mail_accounts_update') {
458
+ if (typeof window.refreshMail === 'function') window.refreshMail();
455
459
  }
456
460
  });
457
461
  es.onerror = () => {
@@ -825,6 +829,65 @@ async function selectPlan(plan) {
825
829
  }
826
830
  }
827
831
 
832
+ // ── Pair phone: mint a device token + show copyable pairing details ──────────
833
+ // Mirrors "lisa pair" / the Mac app Pair iPhone window for browser users. The mint
834
+ // endpoint is loopback-only, so this works from a localhost browser on the Mac
835
+ // (a LAN browser gets 403, handled below). The server detects the Mac's LAN host
836
+ // and returns the lisa-pair:// link + host/port/token so the phone can paste the
837
+ // link OR type the fields into Lisa Pocket → Settings → Pair.
838
+ function pairRow(label, value) {
839
+ return '<div class="pair-row"><span class="pair-label">' + escapeHtml(label) + '</span>'
840
+ + '<code class="pair-val">' + escapeHtml(value) + '</code>'
841
+ + '<button class="pair-copy" type="button">Copy</button></div>';
842
+ }
843
+ async function showPair() {
844
+ openModal('PAIR PHONE', '<div class="empty">minting a pairing code…</div>');
845
+ let res;
846
+ try {
847
+ res = await fetch('/api/pair/start', {
848
+ method: 'POST',
849
+ headers: { 'content-type': 'application/json' },
850
+ body: JSON.stringify({ name: 'phone', platform: 'ios' }),
851
+ });
852
+ } catch (e) {
853
+ modalBody.innerHTML = '<div class="empty">Couldn\\'t reach the Lisa backend.</div>';
854
+ return;
855
+ }
856
+ if (res.status === 403) {
857
+ modalBody.innerHTML = '<div class="empty">Pairing can only be started on the Mac itself. Open this page at <code>http://localhost:' + escapeHtml(location.port || '5757') + '</code> on your Mac, then try again.</div>';
858
+ return;
859
+ }
860
+ if (!res.ok) {
861
+ modalBody.innerHTML = '<div class="empty">Pairing failed (HTTP ' + res.status + ').</div>';
862
+ return;
863
+ }
864
+ const data = await res.json().catch(function () { return {}; });
865
+ if (!data.token) { modalBody.innerHTML = '<div class="empty">The server returned no token.</div>'; return; }
866
+ const port = data.port || 5757;
867
+ const host = data.host || '';
868
+ const link = data.url || ('lisa-pair://v1?host=' + encodeURIComponent(host) + '&port=' + port + '&token=' + encodeURIComponent(data.token) + '&name=phone');
869
+ let html = '';
870
+ if (data.qrSvg) html += '<div class="pair-qr">' + data.qrSvg + '</div>';
871
+ html += '<div class="empty">Scan the code in Lisa Pocket → Settings → Scan QR — or paste the link / type the three fields below. Keep the phone on the same Wi-Fi (or tailnet) as this Mac.</div>';
872
+ html += pairRow('Link', link);
873
+ html += pairRow('Host', host || '(your Mac\\'s Wi-Fi IP)');
874
+ html += pairRow('Port', String(port));
875
+ html += pairRow('Token', data.token);
876
+ if (!host) html += '<div class="empty">Couldn\\'t detect your Mac\\'s LAN address — enter its Wi-Fi IP or tailnet name on the phone.</div>';
877
+ modalBody.innerHTML = html;
878
+ const codes = modalBody.querySelectorAll('.pair-row');
879
+ for (let i = 0; i < codes.length; i++) {
880
+ const row = codes[i];
881
+ const btn = row.querySelector('.pair-copy');
882
+ const val = row.querySelector('.pair-val');
883
+ btn.addEventListener('click', function () {
884
+ navigator.clipboard.writeText(val.textContent).then(function () {
885
+ const prev = btn.textContent; btn.textContent = 'Copied'; setTimeout(function () { btn.textContent = prev; }, 1200);
886
+ }).catch(function () {});
887
+ });
888
+ }
889
+ }
890
+
828
891
  async function showSoul() {
829
892
  openModal('★ SOUL', '<div class="empty">loading…</div>');
830
893
  const data = await fetch('/api/soul').then(r => r.json());
@@ -867,7 +930,8 @@ async function showSoul() {
867
930
  modalBody.innerHTML = html;
868
931
  }
869
932
 
870
- document.querySelectorAll('.badge').forEach(b => {
933
+ // Panel openers — the top function bar (.fbtn) + any legacy .badge share this.
934
+ document.querySelectorAll('[data-panel]').forEach(b => {
871
935
  b.addEventListener('click', () => {
872
936
  const which = b.dataset.panel;
873
937
  if (which === 'soul') showSoul();
@@ -875,9 +939,54 @@ document.querySelectorAll('.badge').forEach(b => {
875
939
  else if (which === 'memory') showMemory();
876
940
  else if (which === 'tools') showTools();
877
941
  else if (which === 'plans') showPlans();
942
+ else if (which === 'pair') showPair();
878
943
  });
879
944
  });
880
945
 
946
+ // Find in conversation — toggle a filter box that hides non-matching log rows.
947
+ const fnSearchBtn = document.getElementById('fnSearchBtn');
948
+ const fnFind = document.getElementById('fnFind');
949
+ function filterLog(q) {
950
+ const logEl = document.getElementById('log');
951
+ if (!logEl) return;
952
+ for (const child of logEl.children) {
953
+ child.style.display = !q || (child.textContent || '').toLowerCase().includes(q) ? '' : 'none';
954
+ }
955
+ }
956
+ if (fnSearchBtn && fnFind) {
957
+ fnSearchBtn.addEventListener('click', () => {
958
+ const show = fnFind.style.display === 'none';
959
+ fnFind.style.display = show ? '' : 'none';
960
+ if (show) { fnFind.focus(); } else { fnFind.value = ''; filterLog(''); }
961
+ });
962
+ fnFind.addEventListener('input', () => filterLog(fnFind.value.trim().toLowerCase()));
963
+ fnFind.addEventListener('keydown', (e) => {
964
+ if (e.key === 'Escape') { fnFind.value = ''; filterLog(''); fnFind.style.display = 'none'; }
965
+ });
966
+ }
967
+
968
+ // Compact / sidebar mode — force the narrow stacked layout at any width, persisted.
969
+ const compactToggleEl = document.getElementById('compactToggle');
970
+ if (compactToggleEl) {
971
+ let compactOn = false;
972
+ try { compactOn = localStorage.getItem('lisaCompact') === '1'; } catch (e) {}
973
+ const applyCompact = () => {
974
+ document.body.classList.toggle('force-compact', compactOn);
975
+ compactToggleEl.classList.toggle('on', compactOn);
976
+ compactToggleEl.setAttribute('aria-checked', compactOn ? 'true' : 'false');
977
+ };
978
+ applyCompact();
979
+ const toggleCompact = () => {
980
+ compactOn = !compactOn;
981
+ try { localStorage.setItem('lisaCompact', compactOn ? '1' : '0'); } catch (e) {}
982
+ applyCompact();
983
+ };
984
+ compactToggleEl.addEventListener('click', toggleCompact);
985
+ compactToggleEl.addEventListener('keydown', (e) => {
986
+ if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggleCompact(); }
987
+ });
988
+ }
989
+
881
990
  let currentLisaSpan = null;
882
991
  let pendingTools = new Map();
883
992
  let thinkingEl = null;
@@ -918,7 +1027,6 @@ function previewInput(name, input) {
918
1027
  async function send(message) {
919
1028
  input.value = '';
920
1029
  input.style.height = 'auto';
921
- sendBtn.disabled = true;
922
1030
  el('div', 'role you', 'YOU');
923
1031
  el('span', 'msg', message || '(attachment)');
924
1032
  if (pendingFiles.length) {
@@ -928,9 +1036,48 @@ async function send(message) {
928
1036
  const filesToSend = [...pendingFiles];
929
1037
  pendingFiles = [];
930
1038
  renderAttachPreview();
1039
+ await runChat(message, filesToSend);
1040
+ }
1041
+
1042
+ // On failure, show the error detail with a retry button that re-runs the same
1043
+ // turn. Kept separate from send() so retry never re-appends the user's bubble
1044
+ // or re-reads the (already-cleared) attachment tray.
1045
+ function showError(detail, message, filesToSend) {
1046
+ if (thinkingEl) { thinkingEl.remove(); thinkingEl = null; }
1047
+ const block = el('div', 'err-block', null);
1048
+ const head = document.createElement('div');
1049
+ head.className = 'err-head';
1050
+ head.textContent = '⚠ 请求出错';
1051
+ block.appendChild(head);
1052
+ const body = document.createElement('div');
1053
+ body.className = 'err-detail';
1054
+ body.textContent = detail || 'unknown error';
1055
+ block.appendChild(body);
1056
+ const retry = document.createElement('button');
1057
+ retry.type = 'button';
1058
+ retry.className = 'err-retry';
1059
+ retry.textContent = '↻ 重试';
1060
+ retry.addEventListener('click', () => {
1061
+ block.remove();
1062
+ runChat(message, filesToSend);
1063
+ });
1064
+ block.appendChild(retry);
1065
+ log.scrollTop = log.scrollHeight;
1066
+ }
1067
+
1068
+ async function runChat(message, filesToSend) {
1069
+ sendBtn.disabled = true;
931
1070
  currentLisaSpan = null;
932
1071
  pendingTools.clear();
933
1072
  thinkingEl = el('div', 'thinking', '⋯ thinking');
1073
+ // The agent emits an error event AND the server re-sends it from its turn
1074
+ // catch — dedupe so one failure renders exactly one error block.
1075
+ let errored = false;
1076
+ const fail = (detail) => {
1077
+ if (errored) return;
1078
+ errored = true;
1079
+ showError(detail, message, filesToSend);
1080
+ };
934
1081
  try {
935
1082
  const res = await fetch('/chat', {
936
1083
  method: 'POST',
@@ -987,7 +1134,7 @@ async function send(message) {
987
1134
  } else if (ev.type === 'mood') {
988
1135
  setMood(ev.slug);
989
1136
  } else if (ev.type === 'error') {
990
- el('div', 'err', '[error] ' + ev.message);
1137
+ fail(ev.message);
991
1138
  } else if (ev.type === 'done') {
992
1139
  if (thinkingEl) { thinkingEl.remove(); thinkingEl = null; }
993
1140
  }
@@ -995,8 +1142,7 @@ async function send(message) {
995
1142
  }
996
1143
  if (thinkingEl) { thinkingEl.remove(); thinkingEl = null; }
997
1144
  } catch (err) {
998
- if (thinkingEl) { thinkingEl.remove(); thinkingEl = null; }
999
- el('div', 'err', '[error] ' + err.message);
1145
+ fail(err.message);
1000
1146
  } finally {
1001
1147
  sendBtn.disabled = false;
1002
1148
  input.focus();
@@ -1291,31 +1437,66 @@ if ('serviceWorker' in navigator) {
1291
1437
  } catch {}
1292
1438
  };
1293
1439
 
1294
- // "Delegate a task" → start an agent. managed = LISA-run (controllable);
1295
- // claude/codex = a real CLI under a PTY (Stage C spike, needs LISA_PTY_AGENTS=1
1296
- // a 503 surfaces its hint in the modal).
1297
- const sbDelegate = document.getElementById('sbDelegate');
1298
- if (sbDelegate) {
1299
- sbDelegate.addEventListener('submit', function (e) {
1300
- e.preventDefault();
1301
- const inp = document.getElementById('sbDelegateTask');
1302
- const kindEl = document.getElementById('sbDelegateKind');
1303
- const task = inp && inp.value.trim();
1304
- if (!task) return;
1440
+ // "Delegate a task" → open a modal to pick the agent kind + write the task.
1441
+ // (A roomy dialog beats the cramped 280px sidebar.) managed = LISA-run
1442
+ // (controllable); claude/codex = a real CLI under a PTY (needs LISA_PTY_AGENTS=1
1443
+ // a 503/error surfaces inline in the dialog).
1444
+ function openDelegateModal() {
1445
+ openModal(
1446
+ 'Delegate a task',
1447
+ '<div class="delegate-modal">' +
1448
+ '<label class="dm-label">Agent</label>' +
1449
+ '<select id="dmKind" class="dm-kind">' +
1450
+ '<option value="managed">managed — LISA runs it (approve each step)</option>' +
1451
+ '<option value="claude">claude — real CLI under a PTY</option>' +
1452
+ '<option value="codex">codex — real CLI under a PTY</option>' +
1453
+ '</select>' +
1454
+ '<label class="dm-label">Task</label>' +
1455
+ '<textarea id="dmTask" class="dm-task" rows="5" placeholder="Describe the task… (⌘/Ctrl+Enter to start)"></textarea>' +
1456
+ '<div class="dm-actions"><button id="dmStart" class="dm-start" type="button">Start agent →</button></div>' +
1457
+ '<div id="dmErr" class="dm-err"></div>' +
1458
+ '</div>'
1459
+ );
1460
+ const kindEl = document.getElementById('dmKind');
1461
+ const taskEl = document.getElementById('dmTask');
1462
+ const startEl = document.getElementById('dmStart');
1463
+ const errEl = document.getElementById('dmErr');
1464
+ if (taskEl) taskEl.focus();
1465
+ function submitDelegate() {
1466
+ const task = taskEl && taskEl.value.trim();
1467
+ if (!task) { if (taskEl) taskEl.focus(); return; }
1305
1468
  const kind = kindEl ? kindEl.value : 'managed';
1306
1469
  const url = kind === 'managed' ? '/api/agents/managed/start' : '/api/agents/pty/start';
1307
1470
  const body = kind === 'managed' ? { task: task } : { agent: kind, task: task };
1471
+ if (errEl) errEl.textContent = '';
1472
+ if (startEl) { startEl.disabled = true; startEl.textContent = 'Starting…'; }
1308
1473
  fetch(url, {
1309
1474
  method: 'POST',
1310
1475
  headers: { 'content-type': 'application/json' },
1311
1476
  body: JSON.stringify(body),
1312
1477
  }).then(function (r) {
1313
- if (!r.ok) { return r.text().then(function (t) { openModal('agent', '<pre>' + escapeHtml(t) + '</pre>'); }); }
1314
- if (inp) inp.value = '';
1478
+ if (!r.ok) {
1479
+ return r.text().then(function (t) {
1480
+ if (errEl) errEl.textContent = t || ('failed (' + r.status + ')');
1481
+ if (startEl) { startEl.disabled = false; startEl.textContent = 'Start agent →'; }
1482
+ });
1483
+ }
1484
+ closeModal();
1315
1485
  if (typeof refreshClaudeSessions === 'function') refreshClaudeSessions();
1316
- }).catch(function () {});
1486
+ }).catch(function () {
1487
+ if (errEl) errEl.textContent = 'network error';
1488
+ if (startEl) { startEl.disabled = false; startEl.textContent = 'Start agent →'; }
1489
+ });
1490
+ }
1491
+ if (startEl) startEl.addEventListener('click', submitDelegate);
1492
+ if (taskEl) taskEl.addEventListener('keydown', function (e) {
1493
+ if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') { e.preventDefault(); submitDelegate(); }
1317
1494
  });
1318
1495
  }
1496
+ const sbDelegateBtn = document.getElementById('sbDelegateBtn');
1497
+ if (sbDelegateBtn) sbDelegateBtn.addEventListener('click', openDelegateModal);
1498
+ // Exposed so the console Dashboard / Control views reuse the same dialog.
1499
+ window.lisaOpenDelegate = openDelegateModal;
1319
1500
 
1320
1501
  async function refreshIdentity() {
1321
1502
  try {
@@ -1345,14 +1526,496 @@ if ('serviceWorker' in navigator) {
1345
1526
  }
1346
1527
  }
1347
1528
 
1529
+ // ── Mail card: connect a mailbox + show the daily classified digest ──
1530
+ function renderMail(accounts, digest) {
1531
+ const body = document.getElementById('sbMailBody');
1532
+ const count = document.getElementById('sbMailCount');
1533
+ const connectBtn = document.getElementById('sbMailConnectBtn');
1534
+ if (!body) return;
1535
+ while (body.firstChild) body.removeChild(body.firstChild);
1536
+ const hasAccounts = accounts && accounts.length > 0;
1537
+ if (connectBtn) connectBtn.textContent = hasAccounts ? '+ add mailbox' : '+ connect mailbox';
1538
+ if (!hasAccounts) {
1539
+ const empty = document.createElement('div');
1540
+ empty.className = 'session-empty';
1541
+ empty.textContent = '(not connected)';
1542
+ body.appendChild(empty);
1543
+ if (count) count.textContent = '';
1544
+ return;
1545
+ }
1546
+ const sum = document.createElement('div');
1547
+ sum.className = 'mail-summary';
1548
+ sum.textContent = digest && digest.summary ? digest.summary : 'No digest yet — sweep to build one.';
1549
+ body.appendChild(sum);
1550
+ const needs = digest && digest.needsYou ? digest.needsYou : [];
1551
+ if (count) count.textContent = needs.length ? ('✦ ' + needs.length) : '';
1552
+ for (const i of needs.slice(0, 5)) {
1553
+ const row = document.createElement('div');
1554
+ row.className = 'mail-row';
1555
+ const bang = document.createElement('span');
1556
+ bang.className = 'mail-bang' + (i.importance >= 3 ? ' urgent' : '');
1557
+ bang.textContent = i.importance >= 3 ? '‼' : '!';
1558
+ const subj = document.createElement('span');
1559
+ subj.className = 'mail-subj';
1560
+ subj.textContent = i.subject || '(no subject)';
1561
+ subj.title = (i.from || '') + ' — ' + (i.reason || '');
1562
+ row.appendChild(bang);
1563
+ row.appendChild(subj);
1564
+ body.appendChild(row);
1565
+ }
1566
+ const sweep = document.createElement('button');
1567
+ sweep.className = 'mail-sweep';
1568
+ sweep.type = 'button';
1569
+ sweep.textContent = 'sweep now';
1570
+ sweep.addEventListener('click', function () {
1571
+ sweep.disabled = true; sweep.textContent = 'sweeping…';
1572
+ fetch('/api/mail/sweep', { method: 'POST' }).then(function () {
1573
+ if (window.refreshMail) window.refreshMail();
1574
+ }).catch(function () {}).then(function () { sweep.disabled = false; sweep.textContent = 'sweep now'; });
1575
+ });
1576
+ body.appendChild(sweep);
1577
+ }
1578
+
1579
+ window.refreshMail = async function () {
1580
+ try {
1581
+ const a = await fetch('/api/mail/accounts').then(function (r) { return r.ok ? r.json() : null; });
1582
+ const d = await fetch('/api/mail/digest').then(function (r) { return r.ok ? r.json() : null; });
1583
+ renderMail(a ? a.accounts : [], d ? d.digest : null);
1584
+ } catch (e) {}
1585
+ };
1586
+
1587
+ function openMailModal() {
1588
+ openModal('Connect a mailbox',
1589
+ '<div class="delegate-modal">' +
1590
+ '<label class="dm-label">Email</label>' +
1591
+ '<input id="mmEmail" class="dm-kind" type="email" placeholder="you@qq.com" autocomplete="off" />' +
1592
+ '<label class="dm-label">App-password / authorization code</label>' +
1593
+ '<input id="mmPass" class="dm-kind" type="password" placeholder="not your login password" autocomplete="off" />' +
1594
+ '<label class="dm-label">IMAP host (optional — auto-detected)</label>' +
1595
+ '<input id="mmHost" class="dm-kind" type="text" placeholder="imap.qq.com" autocomplete="off" />' +
1596
+ '<div class="dm-actions"><button id="mmStart" class="dm-start" type="button">Connect →</button></div>' +
1597
+ '<div id="mmErr" class="dm-err"></div>' +
1598
+ '<div class="dm-note">Read-only. Stored locally (0600). Most providers need an app-password, not your login password.</div>' +
1599
+ '</div>');
1600
+ const emailEl = document.getElementById('mmEmail');
1601
+ const passEl = document.getElementById('mmPass');
1602
+ const hostEl = document.getElementById('mmHost');
1603
+ const startEl = document.getElementById('mmStart');
1604
+ const errEl = document.getElementById('mmErr');
1605
+ if (emailEl) emailEl.focus();
1606
+ function submitMail() {
1607
+ const email = emailEl && emailEl.value.trim();
1608
+ const pass = passEl && passEl.value;
1609
+ if (!email || !pass) { if (errEl) errEl.textContent = 'email + app-password required'; return; }
1610
+ const body = { email: email, password: pass };
1611
+ if (hostEl && hostEl.value.trim()) body.host = hostEl.value.trim();
1612
+ if (errEl) errEl.textContent = '';
1613
+ if (startEl) { startEl.disabled = true; startEl.textContent = 'Connecting…'; }
1614
+ fetch('/api/mail/connect', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) })
1615
+ .then(function (r) {
1616
+ if (!r.ok) { return r.text().then(function (t) { if (errEl) errEl.textContent = t || ('failed (' + r.status + ')'); if (startEl) { startEl.disabled = false; startEl.textContent = 'Connect →'; } }); }
1617
+ closeModal();
1618
+ if (window.refreshMail) window.refreshMail();
1619
+ })
1620
+ .catch(function () { if (errEl) errEl.textContent = 'network error'; if (startEl) { startEl.disabled = false; startEl.textContent = 'Connect →'; } });
1621
+ }
1622
+ if (startEl) startEl.addEventListener('click', submitMail);
1623
+ if (passEl) passEl.addEventListener('keydown', function (e) { if (e.key === 'Enter') { e.preventDefault(); submitMail(); } });
1624
+ }
1625
+ var sbMailConnectBtn = document.getElementById('sbMailConnectBtn');
1626
+ if (sbMailConnectBtn) sbMailConnectBtn.addEventListener('click', openMailModal);
1627
+
1348
1628
  // Bootstrap + periodic resync. SSE handles the fast-path updates;
1349
1629
  // these timers are belt-and-braces in case the stream silently dies.
1350
1630
  refreshPing();
1351
1631
  window.refreshClaudeSessions();
1632
+ window.refreshMail();
1352
1633
  refreshIdentity();
1353
1634
  refreshSessionsBadge();
1354
1635
  setInterval(refreshPing, 30_000);
1355
- setInterval(window.refreshClaudeSessions, 60_000);
1636
+ // Resolve refreshClaudeSessions at call time (arrow), not now: setupConsole
1637
+ // later wraps window.refreshClaudeSessions to re-render the active console
1638
+ // view + nav count, and the wrapper must win on this 60s tick too.
1639
+ setInterval(() => window.refreshClaudeSessions(), 60_000);
1640
+ setInterval(window.refreshMail, 5 * 60_000);
1356
1641
  setInterval(refreshSessionsBadge, 5 * 60_000);
1642
+ })();
1643
+
1644
+ // ════════════════════════════════════════════════════════════════════
1645
+ // Console view-switcher — adds Dashboard / Control / Reve / Sense /
1646
+ // Memory views beside the default Chat. Purely additive: the chat
1647
+ // pipeline and every existing id stay untouched. Each non-chat view is
1648
+ // built lazily from the real endpoints on first activation, and the live
1649
+ // agent stream (refreshClaudeSessions) is wrapped to re-render the active
1650
+ // console view. No backticks / no template placeholders in this block.
1651
+ // ════════════════════════════════════════════════════════════════════
1652
+ (function setupConsole() {
1653
+ var navList = document.getElementById('navList');
1654
+ if (!navList) return;
1655
+ var views = {
1656
+ chat: document.getElementById('viewChat'),
1657
+ dashboard: document.getElementById('viewDashboard'),
1658
+ control: document.getElementById('viewControl'),
1659
+ reve: document.getElementById('viewReve'),
1660
+ sense: document.getElementById('viewSense'),
1661
+ memory: document.getElementById('viewMemory'),
1662
+ };
1663
+ var loaded = {};
1664
+ var active = 'chat';
1665
+ var proactiveOn = true;
1666
+
1667
+ function esc(s) { return (typeof escapeHtml === 'function') ? escapeHtml(s) : String(s == null ? '' : s); }
1668
+ function getJSON(u) { return fetch(u).then(function (r) { return r.ok ? r.json() : null; }).catch(function () { return null; }); }
1669
+
1670
+ function statusClass(state) {
1671
+ if (state === 'working') return 'working';
1672
+ if (state === 'waiting') return 'waiting';
1673
+ if (state === 'error') return 'error';
1674
+ if (state === 'done' || state === 'idle') return 'done';
1675
+ return '';
1676
+ }
1677
+ function statusLabel(state) {
1678
+ if (state === 'working') return 'Working';
1679
+ if (state === 'waiting') return 'Waiting';
1680
+ if (state === 'error') return 'Error';
1681
+ if (state === 'done') return 'Done';
1682
+ if (state === 'idle') return 'Idle';
1683
+ return 'Unknown';
1684
+ }
1685
+ // Compact activity line (local copy of the sidebar helper — kept decoupled).
1686
+ function actLine(s) {
1687
+ var a = s && s.activity;
1688
+ if (!a || typeof a !== 'object') return '';
1689
+ if (a.pendingPermission) return 'wants to run ' + a.pendingPermission;
1690
+ var bits = [];
1691
+ if (a.lastError) bits.push(a.lastError);
1692
+ if (typeof a.turnCount === 'number' && a.turnCount > 0) bits.push('turn ' + a.turnCount);
1693
+ if (a.tokens && (a.tokens.input || a.tokens.output)) {
1694
+ var tot = (a.tokens.input || 0) + (a.tokens.output || 0);
1695
+ bits.push(tot >= 1000 ? Math.round(tot / 1000) + 'k tok' : tot + ' tok');
1696
+ }
1697
+ var tool = a.lastTools && a.lastTools.length ? a.lastTools[a.lastTools.length - 1] : '';
1698
+ var file = a.filesTouched && a.filesTouched.length ? (String(a.filesTouched[a.filesTouched.length - 1]).split('/').pop() || '') : '';
1699
+ if (tool && file) bits.push(tool + ' ' + file);
1700
+ else if (tool) bits.push(tool);
1701
+ else if (file) bits.push(file);
1702
+ return bits.join(' · ');
1703
+ }
1704
+ function updateAgentCount(n) {
1705
+ var c = document.getElementById('navAgentCount');
1706
+ if (c) c.textContent = String(n);
1707
+ }
1708
+
1709
+ // ── view switching ──────────────────────────────────────────────
1710
+ function loadView(name) {
1711
+ if (loaded[name]) return;
1712
+ loaded[name] = true;
1713
+ if (name === 'dashboard') loadDashboard();
1714
+ else if (name === 'control') loadControl();
1715
+ else if (name === 'reve') loadReve();
1716
+ else if (name === 'sense') loadSense();
1717
+ else if (name === 'memory') loadMemory();
1718
+ }
1719
+ function showView(name) {
1720
+ if (!views[name]) name = 'chat';
1721
+ active = name;
1722
+ for (var k in views) { if (views[k]) views[k].classList.toggle('active', k === name); }
1723
+ var items = navList.querySelectorAll('.nav-item');
1724
+ for (var i = 0; i < items.length; i++) {
1725
+ items[i].classList.toggle('active', items[i].getAttribute('data-view') === name);
1726
+ }
1727
+ if (name !== 'chat') loadView(name);
1728
+ else { try { document.getElementById('input').focus(); } catch (e) {} }
1729
+ try { history.replaceState(null, '', name === 'chat' ? location.pathname + location.search : '#' + name); } catch (e) {}
1730
+ }
1731
+ navList.addEventListener('click', function (e) {
1732
+ var btn = e.target && e.target.closest ? e.target.closest('.nav-item') : null;
1733
+ if (btn && btn.getAttribute('data-view')) showView(btn.getAttribute('data-view'));
1734
+ });
1735
+ window.lisaShowView = showView;
1736
+
1737
+ // ── proactive toggle ────────────────────────────────────────────
1738
+ var ptEl = document.getElementById('proactiveToggle');
1739
+ function setProactiveUI(on) {
1740
+ proactiveOn = !!on;
1741
+ if (ptEl) { ptEl.classList.toggle('on', proactiveOn); ptEl.setAttribute('aria-checked', proactiveOn ? 'true' : 'false'); }
1742
+ var pp = document.getElementById('ppPanel');
1743
+ if (pp) {
1744
+ pp.classList.toggle('off', !proactiveOn);
1745
+ var st = document.getElementById('ppState');
1746
+ if (st) st.textContent = proactiveOn ? 'On · watching' : 'Paused';
1747
+ var sd = document.getElementById('ppDesc');
1748
+ if (sd) sd.textContent = proactiveOn ? 'Lisa watches your agents, tasks and signals for blockers and next steps.' : 'Resting — she will only act when you talk to her.';
1749
+ }
1750
+ }
1751
+ function syncProactive() {
1752
+ getJSON('/api/autonomy/state').then(function (s) { if (s && typeof s.enabled === 'boolean') setProactiveUI(s.enabled); });
1753
+ }
1754
+ function toggleProactive() {
1755
+ var on = !proactiveOn;
1756
+ setProactiveUI(on);
1757
+ fetch('/api/autonomy/state', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ enabled: on }) })
1758
+ .then(function (r) { return r && r.ok ? r.json() : null; })
1759
+ .then(function (j) { if (j && typeof j.enabled === 'boolean') setProactiveUI(j.enabled); else setProactiveUI(!on); })
1760
+ .catch(function () { setProactiveUI(!on); });
1761
+ }
1762
+ if (ptEl) {
1763
+ ptEl.addEventListener('click', toggleProactive);
1764
+ ptEl.addEventListener('keydown', function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggleProactive(); } });
1765
+ }
1766
+ syncProactive();
1767
+
1768
+ // ── Dashboard ───────────────────────────────────────────────────
1769
+ function agentCardHTML(s) {
1770
+ var cls = statusClass(s.state);
1771
+ var act = actLine(s);
1772
+ return '<div class="ac">' +
1773
+ '<div class="ac-top"><span>' + esc(s.agent || 'agent') + '</span><span class="ac-status ' + cls + '">' + esc(statusLabel(s.state)) + '</span></div>' +
1774
+ '<div class="ac-title">' + esc(s.project || s.agent || 'agent') + '</div>' +
1775
+ '<div class="ac-desc">' + (act ? esc(act) : 'no recent activity') + '</div>' +
1776
+ '</div>';
1777
+ }
1778
+ function taskCardHTML(d) {
1779
+ return '<div class="ac">' +
1780
+ '<div class="ac-top"><span>' + esc(d.agent || 'dispatch') + '</span><span class="ac-status ' + (d.alive ? 'working' : 'done') + '">' + (d.alive ? 'Running' : 'Done') + '</span></div>' +
1781
+ '<div class="ac-title">' + esc(String(d.task || 'task').slice(0, 80)) + '</div>' +
1782
+ '<div class="ac-meta">' + esc(d.cwd ? String(d.cwd).split('/').pop() : 'dispatch') + '</div>' +
1783
+ '</div>';
1784
+ }
1785
+ function loadDashboard() {
1786
+ views.dashboard.innerHTML =
1787
+ '<div class="view-head"><div><h2>Dashboard</h2><div class="vh-sub">Operations at a glance</div></div>' +
1788
+ '<button class="view-act" id="dashDelegate">+ Delegate a task</button></div>' +
1789
+ '<div class="view-scroll" id="dashScroll"><div class="view-empty">loading…</div></div>';
1790
+ var del = document.getElementById('dashDelegate');
1791
+ if (del) del.addEventListener('click', function () { if (typeof window.lisaOpenDelegate === 'function') window.lisaOpenDelegate(); });
1792
+ renderDashboard();
1793
+ }
1794
+ function renderDashboard() {
1795
+ var scroll = document.getElementById('dashScroll');
1796
+ if (!scroll) return;
1797
+ Promise.all([
1798
+ getJSON('/api/agents/sessions'), getJSON('/api/dispatch/list'),
1799
+ getJSON('/api/sense/recent'), getJSON('/api/island/ping'),
1800
+ ]).then(function (res) {
1801
+ var sessions = (res[0] && res[0].sessions) || [];
1802
+ var dispatches = (res[1] && res[1].dispatches) || [];
1803
+ var events = (res[2] && res[2].events) || [];
1804
+ var ping = res[3] || {};
1805
+ updateAgentCount(sessions.length);
1806
+ var aliveTasks = dispatches.filter(function (d) { return d.alive; }).length;
1807
+ var waiting = sessions.filter(function (s) { return s.state === 'waiting'; }).length;
1808
+ var errored = sessions.filter(function (s) { return s.state === 'error'; }).length;
1809
+ var desire = ping.current_desire || '';
1810
+ var html = '';
1811
+ html += '<div class="stat-bar">' +
1812
+ '<div class="stat"><div class="n">' + sessions.length + '</div><div class="k">Agents</div></div>' +
1813
+ '<div class="stat"><div class="n">' + aliveTasks + '</div><div class="k">Tasks</div></div>' +
1814
+ '<div class="stat"><div class="n">' + dispatches.length + '</div><div class="k">Dispatched</div></div>' +
1815
+ '<div class="stat"><div class="n">' + events.length + '</div><div class="k">Signals</div></div></div>';
1816
+ html += '<div class="proactive-panel' + (proactiveOn ? '' : ' off') + '" id="ppPanel"><div class="pp-head"><span class="pp-dot"></span>' +
1817
+ '<div><div class="pp-title">Proactive mode <b id="ppState">' + (proactiveOn ? 'On · watching' : 'Paused') + '</b></div>' +
1818
+ '<div class="pp-desc" id="ppDesc">' + (proactiveOn ? 'Lisa watches your agents, tasks and signals for blockers and next steps.' : 'Resting — she will only act when you talk to her.') + '</div></div></div>' +
1819
+ '<div class="pp-tags"><span class="pp-tag">' + sessions.length + ' agents</span><span class="pp-tag">' + aliveTasks + ' tasks</span>' +
1820
+ (waiting ? '<span class="pp-tag warn">' + waiting + ' waiting</span>' : '') +
1821
+ (errored ? '<span class="pp-tag warn">' + errored + ' errored</span>' : '') +
1822
+ '<span class="pp-tag">owner-routed</span><span class="pp-tag">quiet on waits</span></div></div>';
1823
+ html += '<div class="view-sec-label">Focus · currently pursuing</div>';
1824
+ if (desire) {
1825
+ html += '<div class="focus-card"><div class="fc-top"><div>' +
1826
+ '<div class="fc-title">' + esc(desire) + '</div>' +
1827
+ '<div class="fc-desc">A self-driven desire Lisa is pursuing on her own time.</div></div>' +
1828
+ '<span class="fc-pill">Active</span></div>' +
1829
+ '<div class="fc-meta"><span>self-driven</span><span>' + sessions.length + ' agents live</span></div></div>';
1830
+ } else {
1831
+ html += '<div class="focus-card"><div class="fc-desc">Nothing actively pursued right now.</div></div>';
1832
+ }
1833
+ html += '<div class="view-sec-label">Agents &amp; tasks</div>';
1834
+ var cards = sessions.map(agentCardHTML).concat(dispatches.slice(0, 6).map(taskCardHTML));
1835
+ html += cards.length ? '<div class="card-scroll">' + cards.join('') + '</div>'
1836
+ : '<div class="view-empty">No active agents or tasks. Delegate one to get started.</div>';
1837
+ scroll.innerHTML = html;
1838
+ });
1839
+ }
1840
+
1841
+ // ── Control ─────────────────────────────────────────────────────
1842
+ function controlRowHTML(s) {
1843
+ var act = actLine(s);
1844
+ var fam = s.controllable === 'pty' ? 'pty' : (s.controllable === 'managed' ? 'managed' : '');
1845
+ var pend = s.activity && s.activity.pendingPermission;
1846
+ var id = esc(s.sessionId);
1847
+ var ctrl = '';
1848
+ if (fam && pend) {
1849
+ ctrl += '<button class="mc approve" data-fam="' + fam + '" data-id="' + id + '" data-act="approve">approve</button>';
1850
+ ctrl += '<button class="mc deny" data-fam="' + fam + '" data-id="' + id + '" data-act="deny">deny</button>';
1851
+ }
1852
+ if (fam) ctrl += '<button class="mc cancel" data-fam="' + fam + '" data-id="' + id + '" data-act="cancel">cancel</button>';
1853
+ if (fam === 'pty') ctrl += '<button class="mc" data-id="' + id + '" data-act="output">output</button>';
1854
+ var badge = (s.agent && s.agent !== 'claude-code') ? '<span class="agent-badge">' + esc(s.agent) + '</span>' : '';
1855
+ return '<div class="session-row">' +
1856
+ '<div class="pip ' + (s.state || 'unknown') + '"></div>' +
1857
+ '<div class="name">' + badge + esc(s.project || s.agent || 'agent') + '</div>' +
1858
+ '<div class="when">' + (s.controllable ? esc(s.controllable) : esc(statusLabel(s.state))) + '</div>' +
1859
+ (act ? '<div class="session-act">' + esc(act) + '</div>' : '') +
1860
+ (ctrl ? '<div class="session-ctrl">' + ctrl + '</div>' : '') +
1861
+ '</div>';
1862
+ }
1863
+ function controlAction(fam, id, action) {
1864
+ return fetch('/api/agents/' + fam + '/' + encodeURIComponent(id) + '/' + action, { method: 'POST' })
1865
+ .then(function (r) { if (!r.ok) return r.text().then(function (t) { throw new Error(String(r.status) + (t ? ' ' + t : '')); }); });
1866
+ }
1867
+ function ctrlPtyOutput(id) {
1868
+ getJSON('/api/agents/pty/' + encodeURIComponent(id) + '/output').then(function (d) {
1869
+ if (typeof openModal === 'function') openModal('agent output', '<pre>' + esc(d && d.output ? d.output : '(no output yet)') + '</pre>');
1870
+ });
1871
+ }
1872
+ function loadControl() {
1873
+ views.control.innerHTML =
1874
+ '<div class="view-head"><div><h2>Control</h2><div class="vh-sub">Live agents &amp; control plane</div></div>' +
1875
+ '<button class="view-act" id="ctrlDelegate">+ Delegate a task</button></div>' +
1876
+ '<div class="view-scroll" id="ctrlScroll"><div class="view-empty">loading…</div></div>';
1877
+ var del = document.getElementById('ctrlDelegate');
1878
+ if (del) del.addEventListener('click', function () { if (typeof window.lisaOpenDelegate === 'function') window.lisaOpenDelegate(); });
1879
+ renderControl();
1880
+ }
1881
+ function renderControl() {
1882
+ var scroll = document.getElementById('ctrlScroll');
1883
+ if (!scroll) return;
1884
+ Promise.all([getJSON('/api/agents/sessions'), getJSON('/api/control/policy')]).then(function (res) {
1885
+ var sessions = (res[0] && res[0].sessions) || [];
1886
+ var policy = res[1] || {};
1887
+ updateAgentCount(sessions.length);
1888
+ var html = '<div class="pp-tags" style="margin-bottom:14px">' +
1889
+ '<span class="pp-tag">remote control: ' + (policy.remoteControl ? 'on' : 'off') + '</span>' +
1890
+ '<span class="pp-tag">adopt external: ' + (policy.remoteAdoptExternal ? 'on' : 'off') + '</span></div>';
1891
+ if (!sessions.length) { scroll.innerHTML = html + '<div class="view-empty">No agents running. Delegate a task to start one.</div>'; return; }
1892
+ sessions.forEach(function (s) { html += controlRowHTML(s); });
1893
+ scroll.innerHTML = html;
1894
+ var btns = scroll.querySelectorAll('[data-act]');
1895
+ for (var i = 0; i < btns.length; i++) {
1896
+ btns[i].addEventListener('click', function () {
1897
+ var act = this.getAttribute('data-act');
1898
+ var fam = this.getAttribute('data-fam');
1899
+ var id = this.getAttribute('data-id');
1900
+ if (act === 'output') { ctrlPtyOutput(id); return; }
1901
+ controlAction(fam, id, act).then(function () { renderControl(); }).catch(function (err) {
1902
+ scroll.insertAdjacentHTML('afterbegin', '<div class="view-empty" style="color:var(--err-color)">' + esc(err.message) + '</div>');
1903
+ });
1904
+ });
1905
+ }
1906
+ });
1907
+ }
1908
+
1909
+ // ── Reve ────────────────────────────────────────────────────────
1910
+ function loadReve() {
1911
+ views.reve.innerHTML =
1912
+ '<div class="view-head"><div><h2>Rêve</h2><div class="vh-sub">Reflections from her own time</div></div>' +
1913
+ '<select class="v-sel" id="reveWindow"><option value="120">last 2h</option><option value="480">last 8h</option><option value="1440">last 24h</option></select></div>' +
1914
+ '<div class="view-scroll" id="reveScroll"><div class="view-empty">loading…</div></div>';
1915
+ var sel = document.getElementById('reveWindow');
1916
+ if (sel) sel.addEventListener('change', function () { renderReve(sel.value); });
1917
+ renderReve('120');
1918
+ }
1919
+ function renderReve(mins) {
1920
+ var scroll = document.getElementById('reveScroll');
1921
+ if (!scroll) return;
1922
+ Promise.all([getJSON('/api/island/ping'), getJSON('/api/agents/recap?sinceMinutes=' + encodeURIComponent(mins))]).then(function (res) {
1923
+ var ping = res[0] || {}; var recap = res[1] || {};
1924
+ var html = '';
1925
+ if (ping.last_idle_message_text) html += '<div class="v-card"><h3>While you were away</h3><div style="font-size:13px;color:var(--fg);line-height:1.55">' + esc(ping.last_idle_message_text) + '</div></div>';
1926
+ if (ping.current_desire) html += '<div class="v-card"><h3>Currently pursuing</h3><div style="font-size:13px;color:var(--fg)">' + esc(ping.current_desire) + '</div></div>';
1927
+ html += '<div class="v-card"><h3>Recap</h3><pre class="v-pre">' + esc(recap.text || 'No activity in this window.') + '</pre></div>';
1928
+ scroll.innerHTML = html;
1929
+ });
1930
+ }
1931
+
1932
+ // ── Sense ───────────────────────────────────────────────────────
1933
+ function loadSense() {
1934
+ views.sense.innerHTML =
1935
+ '<div class="view-head"><div><h2>Sense</h2><div class="vh-sub">Ambient signals Lisa may see</div></div></div>' +
1936
+ '<div class="view-scroll" id="senseScroll"><div class="view-empty">loading…</div></div>';
1937
+ renderSense();
1938
+ }
1939
+ function renderSense() {
1940
+ var scroll = document.getElementById('senseScroll');
1941
+ if (!scroll) return;
1942
+ Promise.all([getJSON('/api/consent'), getJSON('/api/sense/recent')]).then(function (res) {
1943
+ var grants = (res[0] && res[0].grants) || [];
1944
+ var events = (res[1] && res[1].events) || [];
1945
+ var html = '<div class="view-sec-label">Consent</div><div class="v-card">';
1946
+ if (!grants.length) html += '<div class="view-empty" style="padding:6px 0">No signals configured.</div>';
1947
+ grants.forEach(function (g) {
1948
+ html += '<div class="v-row"><div class="v-main"><div class="v-name">' + esc(g.signal) + '</div>' +
1949
+ (g.description ? '<div class="v-sub">' + esc(g.description) + '</div>' : '') + '</div>' +
1950
+ '<button class="v-toggle' + (g.granted ? ' on' : '') + '" data-signal="' + esc(g.signal) + '" data-on="' + (g.granted ? '1' : '0') + '">' + (g.granted ? 'on' : 'off') + '</button></div>';
1951
+ });
1952
+ html += '</div><div class="view-sec-label">Recently sensed</div><div class="v-card">';
1953
+ if (!events.length) html += '<div class="view-empty" style="padding:6px 0">Nothing captured.</div>';
1954
+ events.slice(0, 30).forEach(function (e) {
1955
+ html += '<div class="v-row"><div class="v-main"><div class="v-name" style="font-weight:400">' + esc(e.summary || '') + '</div>' +
1956
+ '<div class="v-sub">' + esc([e.signal, e.kind, e.app].filter(Boolean).join(' · ')) + '</div></div></div>';
1957
+ });
1958
+ html += '</div>';
1959
+ scroll.innerHTML = html;
1960
+ var tgs = scroll.querySelectorAll('.v-toggle');
1961
+ for (var i = 0; i < tgs.length; i++) {
1962
+ tgs[i].addEventListener('click', function () {
1963
+ var sig = this.getAttribute('data-signal');
1964
+ var on = this.getAttribute('data-on') === '1';
1965
+ fetch(on ? '/api/consent/revoke' : '/api/consent/grant', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ signal: sig }) })
1966
+ .then(function () { renderSense(); }).catch(function () {});
1967
+ });
1968
+ }
1969
+ });
1970
+ }
1971
+
1972
+ // ── Memory (launchers for the existing modal panels) ────────────
1973
+ function memBtn(key, ico, title, sub) {
1974
+ return '<button class="mem-btn" data-mem="' + key + '"><span class="mem-ico">' + ico + '</span>' + title + '<span class="mem-sub">' + sub + '</span></button>';
1975
+ }
1976
+ function loadMemory() {
1977
+ views.memory.innerHTML =
1978
+ '<div class="view-head"><div><h2>Memory</h2><div class="vh-sub">Soul · skills · memory · tools</div></div></div>' +
1979
+ '<div class="view-scroll">' +
1980
+ memBtn('soul', '★', 'Soul', 'identity · values · emotions') +
1981
+ memBtn('skills', '✦', 'Skills', 'saved workflows') +
1982
+ memBtn('memory', '▤', 'Memory', 'USER.md · MEMORY.md') +
1983
+ memBtn('tools', '⚒', 'Tools', 'capabilities') +
1984
+ memBtn('plans', '◆', 'Coding plans', 'subscription CLIs') +
1985
+ '</div>';
1986
+ var mbs = views.memory.querySelectorAll('.mem-btn');
1987
+ for (var i = 0; i < mbs.length; i++) {
1988
+ mbs[i].addEventListener('click', function () {
1989
+ var w = this.getAttribute('data-mem');
1990
+ if (w === 'soul' && typeof showSoul === 'function') showSoul();
1991
+ else if (w === 'skills' && typeof showSkills === 'function') showSkills();
1992
+ else if (w === 'memory' && typeof showMemory === 'function') showMemory();
1993
+ else if (w === 'tools' && typeof showTools === 'function') showTools();
1994
+ else if (w === 'plans' && typeof showPlans === 'function') showPlans();
1995
+ });
1996
+ }
1997
+ }
1998
+
1999
+ // ── live refresh: wrap the agent-session refresh so the active console
2000
+ // view + the Control nav count update on SSE agent_session_update.
2001
+ var origRefresh = window.refreshClaudeSessions;
2002
+ window.refreshClaudeSessions = function () {
2003
+ var r = origRefresh ? origRefresh.apply(this, arguments) : undefined;
2004
+ Promise.resolve(r).then(function () {
2005
+ if (active === 'dashboard') renderDashboard();
2006
+ else if (active === 'control') renderControl();
2007
+ else getJSON('/api/agents/sessions').then(function (d) { updateAgentCount((d && d.sessions) ? d.sessions.length : 0); });
2008
+ });
2009
+ return r;
2010
+ };
2011
+ getJSON('/api/agents/sessions').then(function (d) { updateAgentCount((d && d.sessions) ? d.sessions.length : 0); });
2012
+
2013
+ // ── init: honor a deep-link hash, else default to chat ──────────
2014
+ var initial = (location.hash || '').replace('#', '');
2015
+ showView(views[initial] ? initial : 'chat');
2016
+ window.addEventListener('hashchange', function () {
2017
+ var h = (location.hash || '').replace('#', '');
2018
+ if (views[h] && h !== active) showView(h);
2019
+ });
1357
2020
  })();`;
1358
2021
  //# sourceMappingURL=lisa-client.js.map