@symerian/symi 2.6.32 → 2.6.34

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 (48) hide show
  1. package/dist/build-info.json +3 -3
  2. package/dist/canvas-host/a2ui/.bundle.hash +1 -1
  3. package/dist/control-ui/css/style.css +441 -1
  4. package/dist/control-ui/index.html +1 -0
  5. package/dist/control-ui/js/app.js +4 -0
  6. package/dist/control-ui/js/menu.js +41 -1
  7. package/dist/control-ui/js/settings.js +681 -0
  8. package/extensions/bluebubbles/package.json +1 -1
  9. package/extensions/copilot-proxy/package.json +1 -1
  10. package/extensions/diagnostics-otel/package.json +1 -1
  11. package/extensions/discord/package.json +1 -1
  12. package/extensions/feishu/package.json +1 -1
  13. package/extensions/google-antigravity-auth/package.json +1 -1
  14. package/extensions/google-gemini-cli-auth/package.json +1 -1
  15. package/extensions/googlechat/package.json +1 -1
  16. package/extensions/imessage/package.json +1 -1
  17. package/extensions/irc/package.json +1 -1
  18. package/extensions/learning-loop/package.json +1 -1
  19. package/extensions/line/package.json +1 -1
  20. package/extensions/llm-task/package.json +1 -1
  21. package/extensions/matrix/CHANGELOG.md +12 -0
  22. package/extensions/matrix/package.json +1 -1
  23. package/extensions/mattermost/package.json +1 -1
  24. package/extensions/memory-core/package.json +1 -1
  25. package/extensions/memory-lancedb/package.json +1 -1
  26. package/extensions/minimax-portal-auth/package.json +1 -1
  27. package/extensions/msteams/CHANGELOG.md +12 -0
  28. package/extensions/msteams/package.json +1 -1
  29. package/extensions/nextcloud-talk/package.json +1 -1
  30. package/extensions/nostr/CHANGELOG.md +12 -0
  31. package/extensions/nostr/package.json +1 -1
  32. package/extensions/open-prose/package.json +1 -1
  33. package/extensions/outlook/package.json +1 -1
  34. package/extensions/pipeline/package.json +1 -1
  35. package/extensions/signal/package.json +1 -1
  36. package/extensions/slack/package.json +1 -1
  37. package/extensions/telegram/package.json +1 -1
  38. package/extensions/tlon/package.json +1 -1
  39. package/extensions/twitch/CHANGELOG.md +12 -0
  40. package/extensions/twitch/package.json +1 -1
  41. package/extensions/voice-call/CHANGELOG.md +12 -0
  42. package/extensions/voice-call/package.json +1 -1
  43. package/extensions/whatsapp/package.json +1 -1
  44. package/extensions/zalo/CHANGELOG.md +12 -0
  45. package/extensions/zalo/package.json +1 -1
  46. package/extensions/zalouser/CHANGELOG.md +12 -0
  47. package/extensions/zalouser/package.json +1 -1
  48. package/package.json +1 -1
@@ -0,0 +1,681 @@
1
+ // ── Native Settings Panels ────────────────────────────────────────────
2
+ // Renders Config, Debug, and Logs natively inside the page overlay.
3
+ // No iframe, no SPA proxy — uses gateway RPC over WebSocket.
4
+
5
+ // ── Shared helpers ────────────────────────────────────────────────────
6
+
7
+ function escapeSettingsHtml(s) {
8
+ if (!s) {
9
+ return "";
10
+ }
11
+ return String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
12
+ }
13
+
14
+ /** Create or get the native content container inside the page overlay. */
15
+ function getSettingsContainer() {
16
+ let el = document.getElementById("native-settings-container");
17
+ if (!el) {
18
+ const overlay = document.querySelector("body > .page-overlay");
19
+ if (!overlay) {
20
+ return null;
21
+ }
22
+ el = document.createElement("div");
23
+ el.id = "native-settings-container";
24
+ overlay.appendChild(el);
25
+ }
26
+ return el;
27
+ }
28
+
29
+ /** Show native container, hide iframe. */
30
+ function showSettingsContainer() {
31
+ const container = getSettingsContainer();
32
+ const frame = document.getElementById("page-overlay-frame");
33
+ if (frame) {
34
+ frame.style.display = "none";
35
+ }
36
+ if (container) {
37
+ container.style.display = "flex";
38
+ }
39
+ return container;
40
+ }
41
+
42
+ /** Hide native container, restore iframe. */
43
+ function hideSettingsContainer() {
44
+ const container = document.getElementById("native-settings-container");
45
+ if (container) {
46
+ container.style.display = "none";
47
+ container.innerHTML = "";
48
+ }
49
+ const frame = document.getElementById("page-overlay-frame");
50
+ if (frame) {
51
+ frame.style.display = "";
52
+ }
53
+ }
54
+
55
+ // ══════════════════════════════════════════════════════════════════════
56
+ // CONFIG PANEL
57
+ // ══════════════════════════════════════════════════════════════════════
58
+
59
+ let configData = null;
60
+ let configBaseHash = null;
61
+ let configEditMode = false;
62
+
63
+ async function fetchConfig() {
64
+ if (!window.gateway?.connected) {
65
+ return null;
66
+ }
67
+ try {
68
+ const result = await window.gateway.rpc("config.get", {});
69
+ return result;
70
+ } catch (err) {
71
+ console.error("[settings:config] fetch error:", err);
72
+ return null;
73
+ }
74
+ }
75
+
76
+ function renderConfigValue(key, value, depth) {
77
+ const indent = depth * 1;
78
+ if (value === null || value === undefined) {
79
+ return `<span class="cfg-null">null</span>`;
80
+ }
81
+ if (typeof value === "boolean") {
82
+ return `<span class="cfg-bool">${value}</span>`;
83
+ }
84
+ if (typeof value === "number") {
85
+ return `<span class="cfg-num">${value}</span>`;
86
+ }
87
+ if (typeof value === "string") {
88
+ if (value.length > 120) {
89
+ return `<span class="cfg-str">"${escapeSettingsHtml(value.slice(0, 120))}..."</span>`;
90
+ }
91
+ return `<span class="cfg-str">"${escapeSettingsHtml(value)}"</span>`;
92
+ }
93
+ if (Array.isArray(value)) {
94
+ if (value.length === 0) {
95
+ return `<span class="cfg-bracket">[]</span>`;
96
+ }
97
+ const items = value
98
+ .map(
99
+ (v, i) =>
100
+ `<div class="cfg-row" style="padding-left:${indent + 1}em">${renderConfigValue(i, v, depth + 1)}</div>`,
101
+ )
102
+ .join("");
103
+ return `<span class="cfg-bracket">[</span>${items}<div style="padding-left:${indent}em"><span class="cfg-bracket">]</span></div>`;
104
+ }
105
+ if (typeof value === "object") {
106
+ const entries = Object.entries(value);
107
+ if (entries.length === 0) {
108
+ return `<span class="cfg-bracket">{}</span>`;
109
+ }
110
+ const rows = entries
111
+ .map(
112
+ ([k, v]) =>
113
+ `<div class="cfg-row" style="padding-left:${indent + 1}em"><span class="cfg-key">${escapeSettingsHtml(k)}</span><span class="cfg-colon">: </span>${renderConfigValue(k, v, depth + 1)}</div>`,
114
+ )
115
+ .join("");
116
+ return `<span class="cfg-bracket">{</span>${rows}<div style="padding-left:${indent}em"><span class="cfg-bracket">}</span></div>`;
117
+ }
118
+ return escapeSettingsHtml(String(value));
119
+ }
120
+
121
+ function renderConfigSection(key, value, isRedacted) {
122
+ const isExpanded = typeof value === "object" && value !== null;
123
+ const icon = isExpanded ? "&#9662;" : "&#9656;";
124
+ const redactedBadge = isRedacted ? '<span class="cfg-redacted">REDACTED</span>' : "";
125
+
126
+ return `
127
+ <div class="settings-section" data-key="${escapeSettingsHtml(key)}">
128
+ <div class="settings-section-header" onclick="this.parentElement.classList.toggle('collapsed')">
129
+ <span class="settings-section-arrow">${icon}</span>
130
+ <span class="settings-section-key">${escapeSettingsHtml(key)}</span>
131
+ ${redactedBadge}
132
+ </div>
133
+ <div class="settings-section-body">
134
+ ${renderConfigValue(key, value, 0)}
135
+ </div>
136
+ </div>
137
+ `;
138
+ }
139
+
140
+ async function openConfigPanel() {
141
+ const container = showSettingsContainer();
142
+ if (!container) {
143
+ return;
144
+ }
145
+
146
+ container.innerHTML = `
147
+ <div class="settings-panel">
148
+ <div class="settings-toolbar">
149
+ <div class="settings-toolbar-left">
150
+ <span class="settings-toolbar-label">SYSTEM CONFIGURATION</span>
151
+ </div>
152
+ <div class="settings-toolbar-right">
153
+ <button class="settings-btn" id="config-refresh-btn" title="Refresh">Refresh</button>
154
+ <button class="settings-btn settings-btn--primary" id="config-edit-btn" title="Edit raw JSON">Edit</button>
155
+ </div>
156
+ </div>
157
+ <div class="settings-content" id="config-content">
158
+ <div class="settings-loading">Loading configuration...</div>
159
+ </div>
160
+ </div>
161
+ `;
162
+
163
+ // Wire refresh
164
+ document
165
+ .getElementById("config-refresh-btn")
166
+ .addEventListener("click", () => void loadConfigView());
167
+ document.getElementById("config-edit-btn").addEventListener("click", () => toggleConfigEditor());
168
+
169
+ await loadConfigView();
170
+ }
171
+
172
+ async function loadConfigView() {
173
+ const content = document.getElementById("config-content");
174
+ if (!content) {
175
+ return;
176
+ }
177
+
178
+ content.innerHTML = '<div class="settings-loading">Loading...</div>';
179
+ const result = await fetchConfig();
180
+
181
+ if (!result || !result.config) {
182
+ content.innerHTML =
183
+ '<div class="settings-empty">Unable to load configuration. Gateway may not be connected.</div>';
184
+ return;
185
+ }
186
+
187
+ configData = result.config;
188
+ configBaseHash = result.hash;
189
+ configEditMode = false;
190
+
191
+ // Render tree view
192
+ const redacted = new Set(result.redactedPaths || []);
193
+ const sections = Object.entries(configData);
194
+
195
+ if (sections.length === 0) {
196
+ content.innerHTML = '<div class="settings-empty">Configuration is empty.</div>';
197
+ return;
198
+ }
199
+
200
+ content.innerHTML = sections
201
+ .map(([key, value]) => {
202
+ const isRedacted = redacted.has(key);
203
+ return renderConfigSection(key, value, isRedacted);
204
+ })
205
+ .join("");
206
+ }
207
+
208
+ function toggleConfigEditor() {
209
+ const content = document.getElementById("config-content");
210
+ const editBtn = document.getElementById("config-edit-btn");
211
+ if (!content) {
212
+ return;
213
+ }
214
+
215
+ if (configEditMode) {
216
+ // Switch back to tree view
217
+ configEditMode = false;
218
+ editBtn.textContent = "Edit";
219
+ editBtn.classList.remove("settings-btn--active");
220
+ void loadConfigView();
221
+ return;
222
+ }
223
+
224
+ // Switch to editor
225
+ configEditMode = true;
226
+ editBtn.textContent = "View";
227
+ editBtn.classList.add("settings-btn--active");
228
+
229
+ const json = JSON.stringify(configData, null, 2);
230
+ content.innerHTML = `
231
+ <div class="config-editor-wrap">
232
+ <textarea class="config-editor" id="config-editor-textarea" spellcheck="false">${escapeSettingsHtml(json)}</textarea>
233
+ <div class="config-editor-actions">
234
+ <span class="config-editor-hint">Edit the JSON above, then save to apply changes.</span>
235
+ <button class="settings-btn settings-btn--danger" id="config-save-btn">Save & Apply</button>
236
+ </div>
237
+ </div>
238
+ `;
239
+
240
+ document.getElementById("config-save-btn").addEventListener("click", saveConfig);
241
+ }
242
+
243
+ async function saveConfig() {
244
+ const textarea = document.getElementById("config-editor-textarea");
245
+ const saveBtn = document.getElementById("config-save-btn");
246
+ if (!textarea || !saveBtn) {
247
+ return;
248
+ }
249
+
250
+ let parsed;
251
+ try {
252
+ parsed = JSON.parse(textarea.value);
253
+ } catch (err) {
254
+ alert("Invalid JSON: " + err.message);
255
+ return;
256
+ }
257
+
258
+ saveBtn.textContent = "Saving...";
259
+ saveBtn.disabled = true;
260
+
261
+ try {
262
+ await window.gateway.rpc("config.apply", {
263
+ raw: parsed,
264
+ baseHash: configBaseHash,
265
+ });
266
+ saveBtn.textContent = "Saved!";
267
+ setTimeout(() => {
268
+ configEditMode = false;
269
+ const editBtn = document.getElementById("config-edit-btn");
270
+ if (editBtn) {
271
+ editBtn.textContent = "Edit";
272
+ editBtn.classList.remove("settings-btn--active");
273
+ }
274
+ void loadConfigView();
275
+ }, 800);
276
+ } catch (err) {
277
+ saveBtn.textContent = "Save & Apply";
278
+ saveBtn.disabled = false;
279
+ alert("Save failed: " + err.message);
280
+ }
281
+ }
282
+
283
+ // ══════════════════════════════════════════════════════════════════════
284
+ // DEBUG PANEL
285
+ // ══════════════════════════════════════════════════════════════════════
286
+
287
+ let debugEntries = [];
288
+ let debugPaused = false;
289
+ const DEBUG_MAX = 500;
290
+
291
+ function openDebugPanel() {
292
+ const container = showSettingsContainer();
293
+ if (!container) {
294
+ return;
295
+ }
296
+
297
+ container.innerHTML = `
298
+ <div class="settings-panel">
299
+ <div class="settings-toolbar">
300
+ <div class="settings-toolbar-left">
301
+ <span class="settings-toolbar-label">DEBUG CONSOLE</span>
302
+ <span class="settings-toolbar-count" id="debug-count">0 events</span>
303
+ </div>
304
+ <div class="settings-toolbar-right">
305
+ <button class="settings-btn" id="debug-pause-btn">Pause</button>
306
+ <button class="settings-btn" id="debug-clear-btn">Clear</button>
307
+ <button class="settings-btn" id="debug-export-btn">Export</button>
308
+ </div>
309
+ </div>
310
+ <div class="settings-content debug-scroll" id="debug-content">
311
+ <div class="settings-empty">Listening for events...</div>
312
+ </div>
313
+ </div>
314
+ `;
315
+
316
+ debugEntries = [];
317
+ debugPaused = false;
318
+
319
+ document.getElementById("debug-pause-btn").addEventListener("click", () => {
320
+ debugPaused = !debugPaused;
321
+ document.getElementById("debug-pause-btn").textContent = debugPaused ? "Resume" : "Pause";
322
+ });
323
+
324
+ document.getElementById("debug-clear-btn").addEventListener("click", () => {
325
+ debugEntries = [];
326
+ renderDebugEntries();
327
+ });
328
+
329
+ document.getElementById("debug-export-btn").addEventListener("click", () => {
330
+ const text = debugEntries.map((e) => `${e.time} [${e.type}] ${e.summary}`).join("\n");
331
+ const blob = new Blob([text], { type: "text/plain" });
332
+ const a = document.createElement("a");
333
+ a.href = URL.createObjectURL(blob);
334
+ a.download = `symi-debug-${new Date().toISOString().slice(0, 19)}.txt`;
335
+ a.click();
336
+ });
337
+
338
+ // Start capturing events
339
+ window.__debugPanelCapture = function (type, payload) {
340
+ if (debugPaused) {
341
+ return;
342
+ }
343
+ const time = new Date().toLocaleTimeString("en-US", { hour12: false });
344
+ let summary = "";
345
+ try {
346
+ summary =
347
+ typeof payload === "object"
348
+ ? JSON.stringify(payload).slice(0, 200)
349
+ : String(payload).slice(0, 200);
350
+ } catch {
351
+ summary = "[unserializable]";
352
+ }
353
+ debugEntries.unshift({ time, type, summary, payload });
354
+ if (debugEntries.length > DEBUG_MAX) {
355
+ debugEntries.length = DEBUG_MAX;
356
+ }
357
+ renderDebugEntries();
358
+ };
359
+ }
360
+
361
+ function renderDebugEntries() {
362
+ const content = document.getElementById("debug-content");
363
+ const count = document.getElementById("debug-count");
364
+ if (!content) {
365
+ return;
366
+ }
367
+ if (count) {
368
+ count.textContent = `${debugEntries.length} events`;
369
+ }
370
+
371
+ if (debugEntries.length === 0) {
372
+ content.innerHTML = '<div class="settings-empty">Listening for events...</div>';
373
+ return;
374
+ }
375
+
376
+ const html = debugEntries
377
+ .map((e) => {
378
+ const typeClass =
379
+ e.type === "error"
380
+ ? "debug-type--error"
381
+ : e.type === "chat"
382
+ ? "debug-type--chat"
383
+ : e.type === "agent"
384
+ ? "debug-type--agent"
385
+ : "debug-type--other";
386
+ return `<div class="debug-entry-row">
387
+ <span class="debug-time">${e.time}</span>
388
+ <span class="debug-type ${typeClass}">${escapeSettingsHtml(e.type)}</span>
389
+ <span class="debug-summary">${escapeSettingsHtml(e.summary)}</span>
390
+ </div>`;
391
+ })
392
+ .join("");
393
+
394
+ content.innerHTML = html;
395
+ }
396
+
397
+ function closeDebugPanel() {
398
+ window.__debugPanelCapture = null;
399
+ }
400
+
401
+ // ══════════════════════════════════════════════════════════════════════
402
+ // LOGS PANEL (replaces the dead-code logs.js)
403
+ // ══════════════════════════════════════════════════════════════════════
404
+
405
+ const LOGS_POLL_MS = 3000;
406
+ const LOGS_LIMIT = 500;
407
+ const LOGS_MAX_BYTES = 250000;
408
+
409
+ const LEVEL_COLORS = {
410
+ TRACE: "#6b7280",
411
+ DEBUG: "#8b5cf6",
412
+ INFO: "#60a5fa",
413
+ WARN: "#f59e0b",
414
+ ERROR: "#ef4444",
415
+ FATAL: "#dc2626",
416
+ };
417
+
418
+ const LEVEL_ORDER = ["TRACE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL"];
419
+
420
+ let logsEntries = [];
421
+ let logsCursor = null;
422
+ let logsTimer = null;
423
+ let logsAutoFollow = true;
424
+ let logsLevelFilters = new Set(LEVEL_ORDER);
425
+ let logsFilterText = "";
426
+
427
+ function parseLogLine(line) {
428
+ try {
429
+ const obj = JSON.parse(line);
430
+ const level = obj._meta?.logLevelName || "INFO";
431
+ const time = obj.time || obj._meta?.date || "";
432
+ const msg = obj["1"] || obj.msg || obj.message || "";
433
+ const sub = obj["0"] || "";
434
+ return { level, time, msg, sub, raw: line, parsed: obj };
435
+ } catch {
436
+ return { level: "INFO", time: "", msg: line, sub: "", raw: line, parsed: null };
437
+ }
438
+ }
439
+
440
+ function getVisibleLogEntries() {
441
+ return logsEntries.filter((e) => {
442
+ if (!logsLevelFilters.has(e.level)) {
443
+ return false;
444
+ }
445
+ if (logsFilterText && !e.raw.toLowerCase().includes(logsFilterText)) {
446
+ return false;
447
+ }
448
+ return true;
449
+ });
450
+ }
451
+
452
+ function renderLogEntries() {
453
+ const el = document.getElementById("logs-entries");
454
+ if (!el) {
455
+ return;
456
+ }
457
+
458
+ const visible = getVisibleLogEntries();
459
+ const html = visible
460
+ .map((e) => {
461
+ const color = LEVEL_COLORS[e.level] || "#999";
462
+ const timeStr = e.time ? new Date(e.time).toLocaleTimeString("en-US", { hour12: false }) : "";
463
+ const escapedMsg = escapeSettingsHtml(e.msg).substring(0, 500);
464
+ const escapedSub = escapeSettingsHtml(e.sub);
465
+ return `<div class="log-row">
466
+ <span class="log-time">${timeStr}</span>
467
+ <span class="log-level" style="color:${color}">${e.level.padEnd(5)}</span>
468
+ ${escapedSub ? `<span class="log-sub">${escapedSub}</span>` : ""}
469
+ <span class="log-msg">${escapedMsg}</span>
470
+ </div>`;
471
+ })
472
+ .join("");
473
+
474
+ el.innerHTML = html || '<div class="settings-empty">No log entries.</div>';
475
+
476
+ if (logsAutoFollow) {
477
+ const scroll = document.getElementById("logs-scroll-area");
478
+ if (scroll) {
479
+ scroll.scrollTop = scroll.scrollHeight;
480
+ }
481
+ }
482
+ }
483
+
484
+ async function fetchLogs() {
485
+ if (!window.gateway?.connected) {
486
+ return;
487
+ }
488
+ try {
489
+ const params = { limit: LOGS_LIMIT, maxBytes: LOGS_MAX_BYTES };
490
+ if (logsCursor != null) {
491
+ params.cursor = logsCursor;
492
+ }
493
+ const result = await window.gateway.rpc("logs.tail", params);
494
+ if (!result) {
495
+ return;
496
+ }
497
+
498
+ if (result.reset) {
499
+ logsEntries = [];
500
+ logsCursor = null;
501
+ }
502
+
503
+ if (result.lines && result.lines.length > 0) {
504
+ const newEntries = result.lines.map(parseLogLine);
505
+ logsEntries.push(...newEntries);
506
+ if (logsEntries.length > 2000) {
507
+ logsEntries = logsEntries.slice(logsEntries.length - 2000);
508
+ }
509
+ renderLogEntries();
510
+ }
511
+
512
+ if (typeof result.cursor === "number") {
513
+ logsCursor = result.cursor;
514
+ }
515
+ } catch (err) {
516
+ console.error("[settings:logs] fetch error:", err);
517
+ }
518
+ }
519
+
520
+ function startLogsPolling() {
521
+ logsCursor = null;
522
+ logsEntries = [];
523
+
524
+ if (!window.gateway?.connected) {
525
+ const waitIv = setInterval(() => {
526
+ if (window.gateway?.connected) {
527
+ clearInterval(waitIv);
528
+ void fetchLogs();
529
+ if (logsTimer) {
530
+ clearInterval(logsTimer);
531
+ }
532
+ logsTimer = setInterval(fetchLogs, LOGS_POLL_MS);
533
+ }
534
+ }, 500);
535
+ setTimeout(() => clearInterval(waitIv), 30000);
536
+ return;
537
+ }
538
+
539
+ void fetchLogs();
540
+ if (logsTimer) {
541
+ clearInterval(logsTimer);
542
+ }
543
+ logsTimer = setInterval(fetchLogs, LOGS_POLL_MS);
544
+ }
545
+
546
+ function stopLogsPolling() {
547
+ if (logsTimer) {
548
+ clearInterval(logsTimer);
549
+ logsTimer = null;
550
+ }
551
+ }
552
+
553
+ function openLogsPanel() {
554
+ const container = showSettingsContainer();
555
+ if (!container) {
556
+ return;
557
+ }
558
+
559
+ container.innerHTML = `
560
+ <div class="settings-panel">
561
+ <div class="settings-toolbar">
562
+ <div class="settings-toolbar-left">
563
+ <span class="settings-toolbar-label">SYSTEM LOGS</span>
564
+ <span class="settings-toolbar-count" id="logs-count">0 entries</span>
565
+ </div>
566
+ <div class="settings-toolbar-right">
567
+ <div class="logs-level-filters">
568
+ ${LEVEL_ORDER.map(
569
+ (l) => `
570
+ <label class="logs-level-chip" data-level="${l}">
571
+ <input type="checkbox" checked data-level="${l}">
572
+ <span style="color:${LEVEL_COLORS[l]}">${l}</span>
573
+ </label>
574
+ `,
575
+ ).join("")}
576
+ </div>
577
+ <input type="text" class="settings-search" id="logs-filter-input" placeholder="Filter..." />
578
+ <label class="settings-checkbox-label">
579
+ <input type="checkbox" id="logs-follow-cb" checked> Follow
580
+ </label>
581
+ <button class="settings-btn" id="logs-export-btn">Export</button>
582
+ </div>
583
+ </div>
584
+ <div class="settings-content logs-scroll" id="logs-scroll-area">
585
+ <div id="logs-entries">
586
+ <div class="settings-loading">Connecting to log stream...</div>
587
+ </div>
588
+ </div>
589
+ </div>
590
+ `;
591
+
592
+ // Wire level filters
593
+ container.querySelectorAll(".logs-level-chip input").forEach((cb) => {
594
+ cb.addEventListener("change", () => {
595
+ const level = cb.dataset.level;
596
+ if (cb.checked) {
597
+ logsLevelFilters.add(level);
598
+ } else {
599
+ logsLevelFilters.delete(level);
600
+ }
601
+ renderLogEntries();
602
+ });
603
+ });
604
+
605
+ // Wire filter text
606
+ document.getElementById("logs-filter-input").addEventListener("input", (e) => {
607
+ logsFilterText = e.target.value.toLowerCase();
608
+ renderLogEntries();
609
+ });
610
+
611
+ // Wire auto-follow
612
+ document.getElementById("logs-follow-cb").addEventListener("change", (e) => {
613
+ logsAutoFollow = e.target.checked;
614
+ if (logsAutoFollow) {
615
+ const scroll = document.getElementById("logs-scroll-area");
616
+ if (scroll) {
617
+ scroll.scrollTop = scroll.scrollHeight;
618
+ }
619
+ }
620
+ });
621
+
622
+ // Wire export
623
+ document.getElementById("logs-export-btn").addEventListener("click", () => {
624
+ const visible = getVisibleLogEntries();
625
+ const text = visible.map((e) => e.raw).join("\n");
626
+ const blob = new Blob([text], { type: "text/plain" });
627
+ const a = document.createElement("a");
628
+ a.href = URL.createObjectURL(blob);
629
+ a.download = `symi-logs-${new Date().toISOString().slice(0, 19)}.txt`;
630
+ a.click();
631
+ });
632
+
633
+ startLogsPolling();
634
+ }
635
+
636
+ function closeLogsPanel() {
637
+ stopLogsPolling();
638
+ }
639
+
640
+ // ══════════════════════════════════════════════════════════════════════
641
+ // PUBLIC API — called by menu.js
642
+ // ══════════════════════════════════════════════════════════════════════
643
+
644
+ let activeSettingsPanel = null;
645
+
646
+ window.openNativeSettings = function (page) {
647
+ // Close previous panel cleanup
648
+ closeActiveSettingsPanel();
649
+
650
+ activeSettingsPanel = page;
651
+
652
+ switch (page) {
653
+ case "config":
654
+ void openConfigPanel();
655
+ break;
656
+ case "debug":
657
+ openDebugPanel();
658
+ break;
659
+ case "logs":
660
+ openLogsPanel();
661
+ break;
662
+ default:
663
+ console.warn("[settings] unknown page:", page);
664
+ return;
665
+ }
666
+ };
667
+
668
+ window.closeNativeSettings = function () {
669
+ closeActiveSettingsPanel();
670
+ hideSettingsContainer();
671
+ activeSettingsPanel = null;
672
+ };
673
+
674
+ function closeActiveSettingsPanel() {
675
+ if (activeSettingsPanel === "debug") {
676
+ closeDebugPanel();
677
+ }
678
+ if (activeSettingsPanel === "logs") {
679
+ closeLogsPanel();
680
+ }
681
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@symi/bluebubbles",
3
- "version": "2.0.0",
3
+ "version": "2.6.34",
4
4
  "description": "Symi BlueBubbles channel plugin",
5
5
  "type": "module",
6
6
  "devDependencies": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@symi/copilot-proxy",
3
- "version": "2.0.0",
3
+ "version": "2.6.34",
4
4
  "private": true,
5
5
  "description": "Symi Copilot Proxy provider plugin",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@symi/diagnostics-otel",
3
- "version": "2.0.0",
3
+ "version": "2.6.34",
4
4
  "description": "Symi diagnostics OpenTelemetry exporter",
5
5
  "type": "module",
6
6
  "dependencies": {