@symerian/symi 3.3.3 → 3.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build-info.json +3 -3
- package/dist/canvas-host/a2ui/.bundle.hash +1 -1
- package/dist/control-ui/css/style.css +0 -100
- package/dist/control-ui/index.html +0 -13
- package/dist/control-ui/js/app.js +7 -104
- package/dist/control-ui/js/render.js +0 -5
- package/package.json +1 -1
- package/dist/control-ui/js/symipulse.js +0 -154
package/dist/build-info.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
e9bde2d55ad1b3cfcd3469d29271f94436c92a5fb3a4c5511b1796090eb6ddeb
|
|
@@ -5484,106 +5484,6 @@ body {
|
|
|
5484
5484
|
padding: 2px 0 0;
|
|
5485
5485
|
}
|
|
5486
5486
|
|
|
5487
|
-
/* ── Symipulse Panel ────────────────────────────────────────────── */
|
|
5488
|
-
.symipulse-panel {
|
|
5489
|
-
cursor: pointer;
|
|
5490
|
-
transition:
|
|
5491
|
-
max-height 0.3s ease,
|
|
5492
|
-
border-color 0.3s;
|
|
5493
|
-
}
|
|
5494
|
-
|
|
5495
|
-
.symipulse-feed {
|
|
5496
|
-
max-height: 3.2em;
|
|
5497
|
-
overflow: hidden;
|
|
5498
|
-
transition: max-height 0.3s ease;
|
|
5499
|
-
font-size: 11px;
|
|
5500
|
-
line-height: 1.5;
|
|
5501
|
-
color: var(--text-dim);
|
|
5502
|
-
font-family: var(--font-mono);
|
|
5503
|
-
}
|
|
5504
|
-
|
|
5505
|
-
.symipulse-panel.expanded .symipulse-feed {
|
|
5506
|
-
max-height: 300px;
|
|
5507
|
-
overflow-y: auto;
|
|
5508
|
-
scrollbar-width: thin;
|
|
5509
|
-
scrollbar-color: rgba(0, 212, 255, 0.2) transparent;
|
|
5510
|
-
}
|
|
5511
|
-
.symipulse-panel.expanded .symipulse-feed::-webkit-scrollbar {
|
|
5512
|
-
width: 3px;
|
|
5513
|
-
}
|
|
5514
|
-
.symipulse-panel.expanded .symipulse-feed::-webkit-scrollbar-track {
|
|
5515
|
-
background: transparent;
|
|
5516
|
-
}
|
|
5517
|
-
.symipulse-panel.expanded .symipulse-feed::-webkit-scrollbar-thumb {
|
|
5518
|
-
background: rgba(0, 212, 255, 0.2);
|
|
5519
|
-
border-radius: 2px;
|
|
5520
|
-
}
|
|
5521
|
-
|
|
5522
|
-
.symipulse-entry {
|
|
5523
|
-
padding: 3px 0;
|
|
5524
|
-
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
|
|
5525
|
-
white-space: nowrap;
|
|
5526
|
-
overflow: hidden;
|
|
5527
|
-
text-overflow: ellipsis;
|
|
5528
|
-
display: flex;
|
|
5529
|
-
align-items: baseline;
|
|
5530
|
-
gap: 6px;
|
|
5531
|
-
}
|
|
5532
|
-
.symipulse-entry:last-child {
|
|
5533
|
-
border-bottom: none;
|
|
5534
|
-
}
|
|
5535
|
-
|
|
5536
|
-
.symipulse-entry-time {
|
|
5537
|
-
color: var(--text-muted);
|
|
5538
|
-
font-size: 10px;
|
|
5539
|
-
flex-shrink: 0;
|
|
5540
|
-
}
|
|
5541
|
-
|
|
5542
|
-
.symipulse-entry-text {
|
|
5543
|
-
overflow: hidden;
|
|
5544
|
-
text-overflow: ellipsis;
|
|
5545
|
-
white-space: nowrap;
|
|
5546
|
-
color: var(--text-dim);
|
|
5547
|
-
}
|
|
5548
|
-
|
|
5549
|
-
.symipulse-panel.expanded .symipulse-entry {
|
|
5550
|
-
white-space: normal;
|
|
5551
|
-
word-break: break-word;
|
|
5552
|
-
}
|
|
5553
|
-
.symipulse-panel.expanded .symipulse-entry-text {
|
|
5554
|
-
white-space: normal;
|
|
5555
|
-
overflow: visible;
|
|
5556
|
-
}
|
|
5557
|
-
|
|
5558
|
-
.symipulse-live-dot {
|
|
5559
|
-
display: inline-block;
|
|
5560
|
-
width: 6px;
|
|
5561
|
-
height: 6px;
|
|
5562
|
-
border-radius: 50%;
|
|
5563
|
-
background: var(--accent-green);
|
|
5564
|
-
margin-left: 6px;
|
|
5565
|
-
vertical-align: middle;
|
|
5566
|
-
opacity: 0;
|
|
5567
|
-
transition: opacity 0.3s ease;
|
|
5568
|
-
}
|
|
5569
|
-
.symipulse-live-dot.active {
|
|
5570
|
-
opacity: 1;
|
|
5571
|
-
animation: pulse 1.2s ease-in-out infinite;
|
|
5572
|
-
}
|
|
5573
|
-
|
|
5574
|
-
.symipulse-empty {
|
|
5575
|
-
display: flex;
|
|
5576
|
-
align-items: center;
|
|
5577
|
-
justify-content: center;
|
|
5578
|
-
padding: 10px 0;
|
|
5579
|
-
color: var(--text-dim);
|
|
5580
|
-
}
|
|
5581
|
-
.symipulse-empty-text {
|
|
5582
|
-
font-size: 11px;
|
|
5583
|
-
font-family: var(--font-mono);
|
|
5584
|
-
opacity: 0.5;
|
|
5585
|
-
}
|
|
5586
|
-
|
|
5587
5487
|
/* ── Lazy-load sentinel ──────────────────────────────────────────── */
|
|
5588
5488
|
.load-more-sentinel {
|
|
5589
5489
|
text-align: center;
|
|
@@ -325,18 +325,6 @@
|
|
|
325
325
|
<div class="debug-entries" id="debug-panel-entries"></div>
|
|
326
326
|
</div>
|
|
327
327
|
|
|
328
|
-
<!-- Symipulse Panel -->
|
|
329
|
-
<div class="glass-panel symipulse-panel" id="symipulse-panel">
|
|
330
|
-
<div class="panel-label">
|
|
331
|
-
SYMIPULSE
|
|
332
|
-
<span class="symipulse-live-dot" id="symipulse-live-dot"></span>
|
|
333
|
-
</div>
|
|
334
|
-
<div class="symipulse-feed" id="symipulse-feed">
|
|
335
|
-
<div class="symipulse-empty" id="symipulse-empty">
|
|
336
|
-
<div class="symipulse-empty-text">No heartbeats yet</div>
|
|
337
|
-
</div>
|
|
338
|
-
</div>
|
|
339
|
-
</div>
|
|
340
328
|
</aside>
|
|
341
329
|
|
|
342
330
|
<!-- Response waterfall area -->
|
|
@@ -1049,7 +1037,6 @@
|
|
|
1049
1037
|
<script src="js/metrics.js"></script>
|
|
1050
1038
|
<script src="js/gateway.js"></script>
|
|
1051
1039
|
<script src="js/render.js"></script>
|
|
1052
|
-
<script src="js/symipulse.js"></script>
|
|
1053
1040
|
<script src="js/app.js"></script>
|
|
1054
1041
|
<script src="js/settings.js"></script>
|
|
1055
1042
|
<script src="js/menu.js"></script>
|
|
@@ -378,13 +378,10 @@ function renderHistory(messages) {
|
|
|
378
378
|
feedCache.save(messages);
|
|
379
379
|
}
|
|
380
380
|
|
|
381
|
-
// Clear reasoning
|
|
381
|
+
// Clear reasoning panel in sync with the feed — prevents double-entries on reconnect
|
|
382
382
|
if (typeof window.clearReasoningPanel === "function") {
|
|
383
383
|
window.clearReasoningPanel();
|
|
384
384
|
}
|
|
385
|
-
if (typeof window.clearSymipulsePanel === "function") {
|
|
386
|
-
window.clearSymipulsePanel();
|
|
387
|
-
}
|
|
388
385
|
responseArea.innerHTML = "";
|
|
389
386
|
responseArea.prepend(loadMoreSentinel);
|
|
390
387
|
for (const m of messages) {
|
|
@@ -414,21 +411,6 @@ function renderHistory(messages) {
|
|
|
414
411
|
continue;
|
|
415
412
|
}
|
|
416
413
|
|
|
417
|
-
// Route symipulse messages to dedicated Symipulse Panel
|
|
418
|
-
if (typeof window.isSymipulseMessage === "function" && window.isSymipulseMessage(m)) {
|
|
419
|
-
const txt = Array.isArray(m.content)
|
|
420
|
-
? m.content
|
|
421
|
-
.filter((b) => b.type === "text")
|
|
422
|
-
.map((b) => b.text ?? "")
|
|
423
|
-
.join("")
|
|
424
|
-
.trim()
|
|
425
|
-
: extractText(m.content).trim();
|
|
426
|
-
if (txt && typeof window.appendToSymipulsePanel === "function") {
|
|
427
|
-
window.appendToSymipulsePanel(txt, m.timestamp);
|
|
428
|
-
}
|
|
429
|
-
continue;
|
|
430
|
-
}
|
|
431
|
-
|
|
432
414
|
// Route plugin context user messages to Reasoning Panel
|
|
433
415
|
if (m.role === "user") {
|
|
434
416
|
const txt = Array.isArray(m.content)
|
|
@@ -550,20 +532,6 @@ async function loadMoreHistory() {
|
|
|
550
532
|
if (m.role === "system") {
|
|
551
533
|
continue;
|
|
552
534
|
}
|
|
553
|
-
// Route symipulse messages to dedicated Symipulse Panel
|
|
554
|
-
if (typeof window.isSymipulseMessage === "function" && window.isSymipulseMessage(m)) {
|
|
555
|
-
const txt = Array.isArray(m.content)
|
|
556
|
-
? m.content
|
|
557
|
-
.filter((b) => b.type === "text")
|
|
558
|
-
.map((b) => b.text ?? "")
|
|
559
|
-
.join("")
|
|
560
|
-
.trim()
|
|
561
|
-
: extractText(m.content).trim();
|
|
562
|
-
if (txt && typeof window.appendToSymipulsePanel === "function") {
|
|
563
|
-
window.appendToSymipulsePanel(txt, m.timestamp);
|
|
564
|
-
}
|
|
565
|
-
continue;
|
|
566
|
-
}
|
|
567
535
|
|
|
568
536
|
// Route plugin context & verification user messages to Reasoning Panel
|
|
569
537
|
if (m.role === "user") {
|
|
@@ -639,19 +607,11 @@ function handleGatewayEvent(event) {
|
|
|
639
607
|
if (event.event === "agent") {
|
|
640
608
|
const a = event.payload;
|
|
641
609
|
if (a) {
|
|
642
|
-
// Skip heartbeat agent events — background maintenance runs should not
|
|
643
|
-
// trigger the working indicator or arm the watchdog. Without this guard,
|
|
644
|
-
// heartbeat tool events (exec, read) arm the watchdog, and when the
|
|
645
|
-
// heartbeat completes its "final" is routed to the symipulse panel
|
|
646
|
-
// without clearing the indicator — leaving the orb stuck in "working".
|
|
647
|
-
if (a.isHeartbeat) {
|
|
648
|
-
return;
|
|
649
|
-
}
|
|
650
610
|
// Cross-session filter: agent events for OTHER sessions (cron-fired
|
|
651
611
|
// channel runs, slack/msteams inbound, scheduled jobs) must not arm the
|
|
652
|
-
// user's watchdog. Without this guard, every background
|
|
653
|
-
//
|
|
654
|
-
//
|
|
612
|
+
// user's watchdog. Without this guard, every background run resets the
|
|
613
|
+
// watchdog and produces phantom "Run timed out" warnings when no
|
|
614
|
+
// follow-up event arrives within WATCHDOG_MS.
|
|
655
615
|
if (a.sessionKey && a.sessionKey !== window.SESSION_KEY) {
|
|
656
616
|
return;
|
|
657
617
|
}
|
|
@@ -703,18 +663,12 @@ function handleGatewayEvent(event) {
|
|
|
703
663
|
return;
|
|
704
664
|
}
|
|
705
665
|
|
|
706
|
-
|
|
707
|
-
// Even though chat:final/error/aborted handlers below have their own
|
|
708
|
-
// heartbeat early-returns, this top-level guard prevents Path A from
|
|
709
|
-
// re-arming the watchdog on heartbeat-tagged deltas/finals.
|
|
710
|
-
if (p.isHeartbeat) {
|
|
711
|
-
// Fall through to the per-state branches below so heartbeat finals can
|
|
712
|
-
// still be routed to the symipulse panel — but skip the watchdog re-arm.
|
|
713
|
-
} else if (p.runId && currentRunId && p.runId !== currentRunId) {
|
|
666
|
+
if (p.runId && currentRunId && p.runId !== currentRunId) {
|
|
714
667
|
// Cross-run chat event (e.g. cron-fired channel run, chat.injectMessage,
|
|
715
668
|
// late event from a finished run). Not relevant to the user's active turn.
|
|
716
669
|
return;
|
|
717
|
-
}
|
|
670
|
+
}
|
|
671
|
+
if (isStreaming) {
|
|
718
672
|
// Any chat event for our session and our run proves the agent is alive —
|
|
719
673
|
// reset watchdog.
|
|
720
674
|
armWatchdog();
|
|
@@ -728,9 +682,6 @@ function handleGatewayEvent(event) {
|
|
|
728
682
|
|
|
729
683
|
// Handle "thinking" state broadcast from gateway (agent started processing)
|
|
730
684
|
if (p.state === "thinking") {
|
|
731
|
-
if (p.isHeartbeat) {
|
|
732
|
-
return; // Heartbeat thinking events never touch user UI
|
|
733
|
-
}
|
|
734
685
|
if (p.runId && currentRunId && p.runId !== currentRunId) {
|
|
735
686
|
return; // Cross-run thinking event
|
|
736
687
|
}
|
|
@@ -744,18 +695,6 @@ function handleGatewayEvent(event) {
|
|
|
744
695
|
}
|
|
745
696
|
|
|
746
697
|
if (p.state === "delta") {
|
|
747
|
-
// Route heartbeat/symipulse deltas to dedicated panel
|
|
748
|
-
if (p.isHeartbeat) {
|
|
749
|
-
const text = extractText(p.message);
|
|
750
|
-
if (text && typeof window.appendToSymipulsePanel === "function") {
|
|
751
|
-
window.appendToSymipulsePanel(text, Date.now());
|
|
752
|
-
}
|
|
753
|
-
if (typeof window.setSymipulseLiveDot === "function") {
|
|
754
|
-
window.setSymipulseLiveDot(true);
|
|
755
|
-
}
|
|
756
|
-
return;
|
|
757
|
-
}
|
|
758
|
-
|
|
759
698
|
// Capture runId on first delta so abort can target the exact run
|
|
760
699
|
if (p.runId && !currentRunId) {
|
|
761
700
|
currentRunId = p.runId;
|
|
@@ -782,34 +721,6 @@ function handleGatewayEvent(event) {
|
|
|
782
721
|
window.updateLiveThinking(thinkingText);
|
|
783
722
|
}
|
|
784
723
|
} else if (p.state === "final") {
|
|
785
|
-
// Route heartbeat/symipulse finals to dedicated panel.
|
|
786
|
-
// Check server flag first, then fall back to regex classifier for
|
|
787
|
-
// edge cases where the flag is missing (e.g. older gateway versions).
|
|
788
|
-
const finalMsg = p.message?.content ? p.message : { role: "assistant", content: p.message };
|
|
789
|
-
if (
|
|
790
|
-
p.isHeartbeat ||
|
|
791
|
-
(typeof window.isSymipulseMessage === "function" && window.isSymipulseMessage(finalMsg))
|
|
792
|
-
) {
|
|
793
|
-
const text = extractText(p.message?.content ?? p.message);
|
|
794
|
-
if (text && typeof window.appendToSymipulsePanel === "function") {
|
|
795
|
-
window.appendToSymipulsePanel(text, Date.now());
|
|
796
|
-
}
|
|
797
|
-
if (typeof window.setSymipulseLiveDot === "function") {
|
|
798
|
-
window.setSymipulseLiveDot(false);
|
|
799
|
-
}
|
|
800
|
-
// Clean up any stream bubble that was opened before detection
|
|
801
|
-
if (streamBubble) {
|
|
802
|
-
streamBubble.closest(".message")?.remove();
|
|
803
|
-
streamBubble = null;
|
|
804
|
-
streamContent = null;
|
|
805
|
-
}
|
|
806
|
-
if (thinkingEl) {
|
|
807
|
-
thinkingEl.remove();
|
|
808
|
-
thinkingEl = null;
|
|
809
|
-
}
|
|
810
|
-
return;
|
|
811
|
-
}
|
|
812
|
-
|
|
813
724
|
stopElapsedTimer();
|
|
814
725
|
clearWatchdog();
|
|
815
726
|
if (typeof window.closeLiveThinking === "function") {
|
|
@@ -864,10 +775,6 @@ function handleGatewayEvent(event) {
|
|
|
864
775
|
enableInput();
|
|
865
776
|
drainQueue();
|
|
866
777
|
} else if (p.state === "aborted") {
|
|
867
|
-
// Silently ignore heartbeat aborts — don't touch user-run state
|
|
868
|
-
if (p.isHeartbeat) {
|
|
869
|
-
return;
|
|
870
|
-
}
|
|
871
778
|
// Clean abort (user-initiated or model decided to stop) — not an error
|
|
872
779
|
stopElapsedTimer();
|
|
873
780
|
clearWatchdog();
|
|
@@ -890,10 +797,6 @@ function handleGatewayEvent(event) {
|
|
|
890
797
|
enableInput();
|
|
891
798
|
drainQueue();
|
|
892
799
|
} else if (p.state === "error") {
|
|
893
|
-
// Silently ignore heartbeat errors — don't inject error bubbles for background runs
|
|
894
|
-
if (p.isHeartbeat) {
|
|
895
|
-
return;
|
|
896
|
-
}
|
|
897
800
|
// Server-reported error — route through unified failure handler.
|
|
898
801
|
// The gateway emits the diagnostic at top-level `errorMessage`
|
|
899
802
|
// (camelCase); fall back to nested `error.message` for forward-compat
|
|
@@ -777,11 +777,6 @@ window.renderMessage = function (message) {
|
|
|
777
777
|
.trim()
|
|
778
778
|
: extractText(content).trim();
|
|
779
779
|
|
|
780
|
-
// Symipulse messages → routed to Symipulse Panel by caller; guard here as safety net
|
|
781
|
-
if (typeof window.isSymipulseMessage === "function" && window.isSymipulseMessage(message)) {
|
|
782
|
-
return null;
|
|
783
|
-
}
|
|
784
|
-
|
|
785
780
|
// NOTE: Monologue suppression moved server-side to OutputNormalizer.
|
|
786
781
|
|
|
787
782
|
// Plugin context messages (e.g. "[Outlook 365] Not connected...")
|
package/package.json
CHANGED
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
// ── Symi UI — Symipulse Panel ─────────────────────────────────────────
|
|
2
|
-
// Dedicated panel for heartbeat/symipulse system health messages.
|
|
3
|
-
// Routes all symipulse traffic out of the main chat feed into a
|
|
4
|
-
// collapsible right-side panel.
|
|
5
|
-
|
|
6
|
-
const SYMIPULSE_MAX_ENTRIES = 50;
|
|
7
|
-
|
|
8
|
-
// ── Centralized classifier ────────────────────────────────────────────
|
|
9
|
-
// Checks server-side tag first (real-time), falls back to regex for
|
|
10
|
-
// history messages that predate the isHeartbeat flag.
|
|
11
|
-
window.isSymipulseMessage = function (message) {
|
|
12
|
-
if (message?.isHeartbeat === true) {
|
|
13
|
-
return true;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const role = message?.role;
|
|
17
|
-
const txt = Array.isArray(message?.content)
|
|
18
|
-
? message.content
|
|
19
|
-
.filter((b) => b.type === "text")
|
|
20
|
-
.map((b) => b.text ?? "")
|
|
21
|
-
.join("")
|
|
22
|
-
.trim()
|
|
23
|
-
: (typeof message?.content === "string"
|
|
24
|
-
? message.content
|
|
25
|
-
: (window.extractText?.(message?.content) ?? "")
|
|
26
|
-
).trim();
|
|
27
|
-
|
|
28
|
-
if (!txt) {
|
|
29
|
-
return false;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
if (role === "assistant") {
|
|
33
|
-
if (
|
|
34
|
-
/SYMIPULSE|heartbeat|System\s*Health|Pattern\s*Detection|systemHealth|count\s*→/i.test(txt)
|
|
35
|
-
) {
|
|
36
|
-
return true;
|
|
37
|
-
}
|
|
38
|
-
if (/^[A-Z_]+_OK$/i.test(txt)) {
|
|
39
|
-
return true;
|
|
40
|
-
}
|
|
41
|
-
if (/^\s*NO_REPLY\s*$/i.test(txt)) {
|
|
42
|
-
return true;
|
|
43
|
-
}
|
|
44
|
-
// Health warnings — broadened to catch ⚠️ with health-check keywords
|
|
45
|
-
if (/^⚠️.*(heartbeat|Agent failed before reply|unreachable|HTTP:\d{3})/i.test(txt)) {
|
|
46
|
-
return true;
|
|
47
|
-
}
|
|
48
|
-
// [quiet window] prefix — heartbeat output during idle periods
|
|
49
|
-
if (/^\[quiet window\]/i.test(txt)) {
|
|
50
|
-
return true;
|
|
51
|
-
}
|
|
52
|
-
// Health log references — "Logged to YYYY-MM-DD.md"
|
|
53
|
-
if (/Logged to \d{4}-\d{2}-\d{2}\.md/i.test(txt)) {
|
|
54
|
-
return true;
|
|
55
|
-
}
|
|
56
|
-
// Heartbeat log acknowledgments — "Logged." followed by status text
|
|
57
|
-
if (/^Logged\./i.test(txt)) {
|
|
58
|
-
return true;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if (role === "user") {
|
|
63
|
-
// Removed ^ anchor — plugin context preamble may precede "Read SYMIPULSE.md"
|
|
64
|
-
if (/Read SYMIPULSE\.md/i.test(txt) || /SYMIPULSE_OK/i.test(txt)) {
|
|
65
|
-
return true;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return false;
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
// ── Panel DOM references ──────────────────────────────────────────────
|
|
73
|
-
const _spPanel = document.getElementById("symipulse-panel");
|
|
74
|
-
const _spFeed = document.getElementById("symipulse-feed");
|
|
75
|
-
const _spEmpty = document.getElementById("symipulse-empty");
|
|
76
|
-
const _spDot = document.getElementById("symipulse-live-dot");
|
|
77
|
-
|
|
78
|
-
// ── Append entry to Symipulse Panel ───────────────────────────────────
|
|
79
|
-
window.appendToSymipulsePanel = function (text, timestamp) {
|
|
80
|
-
if (!_spFeed || !text) {
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Remove empty placeholder
|
|
85
|
-
if (_spEmpty && _spEmpty.parentNode) {
|
|
86
|
-
_spEmpty.remove();
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const entry = document.createElement("div");
|
|
90
|
-
entry.className = "symipulse-entry";
|
|
91
|
-
|
|
92
|
-
const time = timestamp
|
|
93
|
-
? new Date(typeof timestamp === "number" && timestamp < 1e12 ? timestamp * 1000 : timestamp)
|
|
94
|
-
: new Date();
|
|
95
|
-
const hhmm = time.toLocaleTimeString("en-US", {
|
|
96
|
-
hour: "2-digit",
|
|
97
|
-
minute: "2-digit",
|
|
98
|
-
hour12: false,
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
const timeSpan = document.createElement("span");
|
|
102
|
-
timeSpan.className = "symipulse-entry-time";
|
|
103
|
-
timeSpan.textContent = hhmm;
|
|
104
|
-
|
|
105
|
-
const textSpan = document.createElement("span");
|
|
106
|
-
textSpan.className = "symipulse-entry-text";
|
|
107
|
-
textSpan.textContent = text;
|
|
108
|
-
|
|
109
|
-
entry.appendChild(timeSpan);
|
|
110
|
-
entry.appendChild(textSpan);
|
|
111
|
-
_spFeed.appendChild(entry);
|
|
112
|
-
|
|
113
|
-
// Cap entries
|
|
114
|
-
while (_spFeed.querySelectorAll(".symipulse-entry").length > SYMIPULSE_MAX_ENTRIES) {
|
|
115
|
-
const oldest = _spFeed.querySelector(".symipulse-entry");
|
|
116
|
-
if (!oldest) {
|
|
117
|
-
break;
|
|
118
|
-
}
|
|
119
|
-
oldest.remove();
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Auto-scroll when expanded
|
|
123
|
-
if (_spPanel?.classList.contains("expanded")) {
|
|
124
|
-
_spFeed.scrollTop = _spFeed.scrollHeight;
|
|
125
|
-
}
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
// ── Clear panel (on new session) ──────────────────────────────────────
|
|
129
|
-
window.clearSymipulsePanel = function () {
|
|
130
|
-
if (!_spFeed) {
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
_spFeed.innerHTML = `
|
|
134
|
-
<div class="symipulse-empty" id="symipulse-empty">
|
|
135
|
-
<div class="symipulse-empty-text">No heartbeats yet</div>
|
|
136
|
-
</div>`;
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
// ── Live dot control ──────────────────────────────────────────────────
|
|
140
|
-
window.setSymipulseLiveDot = function (active) {
|
|
141
|
-
if (_spDot) {
|
|
142
|
-
_spDot.classList.toggle("active", active);
|
|
143
|
-
}
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
// ── Toggle expand/collapse on click ───────────────────────────────────
|
|
147
|
-
if (_spPanel) {
|
|
148
|
-
_spPanel.addEventListener("click", () => {
|
|
149
|
-
_spPanel.classList.toggle("expanded");
|
|
150
|
-
if (_spPanel.classList.contains("expanded") && _spFeed) {
|
|
151
|
-
_spFeed.scrollTop = _spFeed.scrollHeight;
|
|
152
|
-
}
|
|
153
|
-
});
|
|
154
|
-
}
|