@delt/claude-alarm 0.5.5 → 0.6.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.
@@ -506,6 +506,159 @@
506
506
  .message-body li { margin: 2px 0; }
507
507
  .message-body strong { font-weight: 600; }
508
508
 
509
+ /* Permission request - fixed bar above input */
510
+ .permission-bar {
511
+ border-top: 2px solid var(--yellow);
512
+ background: linear-gradient(180deg, rgba(245,158,11,0.08) 0%, transparent 100%);
513
+ padding: 0;
514
+ display: none;
515
+ }
516
+ .permission-bar.active { display: block; }
517
+ .perm-item {
518
+ display: flex;
519
+ align-items: center;
520
+ gap: 14px;
521
+ padding: 12px 20px;
522
+ animation: permSlideIn 0.3s ease-out;
523
+ }
524
+ @keyframes permSlideIn {
525
+ from { opacity: 0; transform: translateY(8px); }
526
+ to { opacity: 1; transform: translateY(0); }
527
+ }
528
+ .perm-item + .perm-item {
529
+ border-top: 1px solid var(--border);
530
+ }
531
+ .perm-icon {
532
+ width: 32px;
533
+ height: 32px;
534
+ border-radius: 8px;
535
+ background: rgba(245,158,11,0.15);
536
+ display: flex;
537
+ align-items: center;
538
+ justify-content: center;
539
+ font-size: 16px;
540
+ flex-shrink: 0;
541
+ animation: permPulse 2s ease-in-out infinite;
542
+ }
543
+ @keyframes permPulse {
544
+ 0%, 100% { box-shadow: 0 0 0 0 rgba(245,158,11,0.2); }
545
+ 50% { box-shadow: 0 0 0 6px rgba(245,158,11,0); }
546
+ }
547
+ .perm-info {
548
+ flex: 1;
549
+ min-width: 0;
550
+ }
551
+ .perm-header {
552
+ display: flex;
553
+ align-items: center;
554
+ gap: 6px;
555
+ font-size: 13px;
556
+ color: var(--text);
557
+ font-weight: 600;
558
+ }
559
+ .perm-tool {
560
+ font-family: monospace;
561
+ background: rgba(245,158,11,0.18);
562
+ color: var(--yellow);
563
+ padding: 2px 8px;
564
+ border-radius: 4px;
565
+ font-size: 12px;
566
+ font-weight: 700;
567
+ }
568
+ .perm-desc {
569
+ font-size: 12px;
570
+ color: var(--text-dim);
571
+ margin-top: 3px;
572
+ overflow: hidden;
573
+ text-overflow: ellipsis;
574
+ white-space: nowrap;
575
+ }
576
+ .perm-preview {
577
+ font-size: 12px;
578
+ color: var(--text);
579
+ font-family: monospace;
580
+ background: var(--bg);
581
+ border: 1px solid var(--border);
582
+ border-radius: 5px;
583
+ padding: 6px 10px;
584
+ margin-top: 5px;
585
+ max-height: 60px;
586
+ overflow-y: auto;
587
+ white-space: pre-wrap;
588
+ word-break: break-all;
589
+ line-height: 1.4;
590
+ }
591
+ .perm-actions {
592
+ display: flex;
593
+ gap: 10px;
594
+ flex-shrink: 0;
595
+ }
596
+ .perm-actions button {
597
+ padding: 10px 28px;
598
+ border-radius: 8px;
599
+ cursor: pointer;
600
+ font-size: 14px;
601
+ font-weight: 700;
602
+ border: none;
603
+ transition: all 0.15s;
604
+ letter-spacing: 0.3px;
605
+ }
606
+ .perm-actions button:active { transform: scale(0.96); }
607
+ .perm-allow {
608
+ background: var(--green);
609
+ color: #fff;
610
+ box-shadow: 0 2px 8px rgba(61,214,140,0.35);
611
+ }
612
+ .perm-allow:hover { background: #2fc77e; box-shadow: 0 3px 12px rgba(61,214,140,0.45); }
613
+ .perm-deny {
614
+ background: var(--red);
615
+ color: #fff;
616
+ box-shadow: 0 2px 8px rgba(239,68,68,0.3);
617
+ border: none !important;
618
+ }
619
+ .perm-deny:hover { background: #dc2626 !important; box-shadow: 0 3px 12px rgba(239,68,68,0.4); }
620
+ .perm-resolved-label {
621
+ font-size: 12px;
622
+ font-weight: 600;
623
+ flex-shrink: 0;
624
+ padding: 4px 12px;
625
+ }
626
+ .perm-resolved-label.allowed { color: var(--green); }
627
+ .perm-resolved-label.denied { color: var(--red); }
628
+ .perm-kbd {
629
+ font-size: 10px;
630
+ color: var(--text-dim);
631
+ margin-left: 2px;
632
+ font-weight: 400;
633
+ }
634
+ /* Permission in notification panel */
635
+ .notif-perm-actions {
636
+ display: flex;
637
+ gap: 6px;
638
+ margin-top: 8px;
639
+ }
640
+ .notif-perm-actions button {
641
+ flex: 1;
642
+ padding: 6px 0;
643
+ border-radius: 6px;
644
+ cursor: pointer;
645
+ font-size: 12px;
646
+ font-weight: 700;
647
+ border: none;
648
+ transition: all 0.15s;
649
+ }
650
+ .notif-perm-actions button:active { transform: scale(0.96); }
651
+ .notif-perm-allow {
652
+ background: var(--green);
653
+ color: #fff;
654
+ }
655
+ .notif-perm-allow:hover { background: #2fc77e; }
656
+ .notif-perm-deny {
657
+ background: var(--red);
658
+ color: #fff;
659
+ }
660
+ .notif-perm-deny:hover { background: #dc2626; }
661
+
509
662
  .empty-state {
510
663
  color: var(--text-dim);
511
664
  font-size: 13px;
@@ -746,6 +899,7 @@
746
899
  </div>
747
900
  <button class="scroll-bottom" id="scrollBottom" title="Scroll to bottom">&#8595;</button>
748
901
  <div class="drag-overlay" id="dragOverlay">Drop image here</div>
902
+ <div class="permission-bar" id="permissionBar"></div>
749
903
  <div class="image-preview" id="imagePreview">
750
904
  <img id="previewImg" src="" alt="preview">
751
905
  <span class="preview-name" id="previewName"></span>
@@ -782,6 +936,7 @@
782
936
  pendingImage: null,
783
937
  unread: {},
784
938
  waitingReply: {},
939
+ permissionRequests: {}, // sessionId -> [{ requestId, toolName, description, inputPreview, timestamp, resolved, behavior }]
785
940
  };
786
941
 
787
942
  const $ = (sel) => document.querySelector(sel);
@@ -891,6 +1046,26 @@
891
1046
  state.notifications.unshift({ sessionId: msg.sessionId, title: msg.title, message: msg.message, level: msg.level || 'info', time: msg.timestamp });
892
1047
  renderNotifications();
893
1048
  break;
1049
+ case 'permission_request':
1050
+ if (!state.permissionRequests[msg.sessionId]) state.permissionRequests[msg.sessionId] = [];
1051
+ state.permissionRequests[msg.sessionId].unshift({
1052
+ requestId: msg.requestId,
1053
+ toolName: msg.toolName,
1054
+ description: msg.description,
1055
+ inputPreview: msg.inputPreview,
1056
+ timestamp: msg.timestamp,
1057
+ resolved: false,
1058
+ behavior: null,
1059
+ });
1060
+ // Auto-select session if none selected
1061
+ if (!state.selectedSession && state.sessions[msg.sessionId]) selectSession(msg.sessionId);
1062
+ if (state.selectedSession === msg.sessionId) { renderMessages(); renderPermissionBar(); }
1063
+ // Also show in notifications with permission info
1064
+ state.notifications.unshift({ sessionId: msg.sessionId, title: 'Permission Request', message: `${msg.toolName}: ${msg.description}`, level: 'warning', time: msg.timestamp, permRequestId: msg.requestId });
1065
+ renderNotifications();
1066
+ // Play notification sound effect via title flash
1067
+ flashTitle('Permission Request');
1068
+ break;
894
1069
  }
895
1070
  }
896
1071
 
@@ -977,6 +1152,7 @@
977
1152
  if (state.waitingReply[state.selectedSession]) {
978
1153
  el.innerHTML += '<div class="typing-indicator active"><div class="typing-dots"><span></span><span></span><span></span></div></div>';
979
1154
  }
1155
+ renderPermissionBar();
980
1156
  setTimeout(() => { el.scrollTop = el.scrollHeight; }, 0);
981
1157
  }
982
1158
 
@@ -993,9 +1169,25 @@
993
1169
  const timeStr = relativeTime(n.time);
994
1170
  const session = state.sessions[n.sessionId];
995
1171
  const sName = session ? session.name : n.sessionId.slice(0, 8);
1172
+ // Check if this is an unresolved permission request
1173
+ let permButtons = '';
1174
+ if (n.permRequestId) {
1175
+ const reqs = state.permissionRequests[n.sessionId] || [];
1176
+ const req = reqs.find(r => r.requestId === n.permRequestId);
1177
+ if (req && !req.resolved) {
1178
+ permButtons = `<div class="notif-perm-actions">
1179
+ <button class="notif-perm-allow" data-req-id="${esc(n.permRequestId)}" data-session-id="${esc(n.sessionId)}">Allow</button>
1180
+ <button class="notif-perm-deny" data-req-id="${esc(n.permRequestId)}" data-session-id="${esc(n.sessionId)}">Deny</button>
1181
+ </div>`;
1182
+ } else if (req && req.resolved) {
1183
+ const label = req.behavior === 'allow' ? 'Allowed' : 'Denied';
1184
+ const cls = req.behavior === 'allow' ? 'allowed' : 'denied';
1185
+ permButtons = `<div class="perm-resolved-label ${cls}">${label}</div>`;
1186
+ }
1187
+ }
996
1188
  return `<div class="notif-item" data-session="${n.sessionId}" data-index="${i}">
997
1189
  <div class="notif-title"><span class="notif-level ${n.level}"></span><span class="notif-title-text">${esc(n.title)}</span><button class="notif-dismiss" data-index="${i}">&times;</button></div>
998
- <div class="notif-message">${esc(n.message)}</div>
1190
+ <div class="notif-message">${esc(n.message)}</div>${permButtons}
999
1191
  <div class="notif-time">${sName} &middot; ${timeStr}</div>
1000
1192
  </div>`;
1001
1193
  }).join('');
@@ -1014,6 +1206,7 @@
1014
1206
  renderNotifications();
1015
1207
  });
1016
1208
  });
1209
+ bindPermissionButtons(el);
1017
1210
  }
1018
1211
 
1019
1212
  // --- Image handling ---
@@ -1172,6 +1365,127 @@
1172
1365
  return html;
1173
1366
  }
1174
1367
 
1368
+ // --- Permission relay ---
1369
+ function formatPermPreview(toolName, raw) {
1370
+ if (!raw) return '';
1371
+ // Try JSON parse
1372
+ try {
1373
+ const p = JSON.parse(raw);
1374
+ switch (toolName) {
1375
+ case 'Bash':
1376
+ return p.command ? '$ ' + p.command : raw;
1377
+ case 'Write':
1378
+ case 'Edit':
1379
+ if (p.file_path) {
1380
+ let s = p.file_path;
1381
+ if (p.new_string) s += '\n+ ' + p.new_string.slice(0, 200);
1382
+ else if (p.content) s += '\n' + p.content.slice(0, 200);
1383
+ return s;
1384
+ }
1385
+ return raw;
1386
+ case 'Read':
1387
+ return p.file_path || raw;
1388
+ case 'Glob':
1389
+ return p.pattern || raw;
1390
+ case 'Grep':
1391
+ return (p.pattern || '') + (p.path ? ' in ' + p.path : '');
1392
+ default:
1393
+ // Generic: show key=value pairs
1394
+ const entries = Object.entries(p);
1395
+ if (entries.length <= 3) return entries.map(([k, v]) => k + ': ' + String(v).slice(0, 100)).join('\n');
1396
+ return JSON.stringify(p, null, 1).slice(0, 200);
1397
+ }
1398
+ } catch {
1399
+ // Not valid JSON - show raw but clean up
1400
+ return raw.slice(0, 200);
1401
+ }
1402
+ }
1403
+
1404
+ function sendPermissionVerdict(sessionId, requestId, behavior) {
1405
+ if (!state.ws) return;
1406
+ state.ws.send(JSON.stringify({ type: 'permission_response', sessionId, requestId, behavior }));
1407
+ const reqs = state.permissionRequests[sessionId] || [];
1408
+ const req = reqs.find(r => r.requestId === requestId);
1409
+ if (req) {
1410
+ req.resolved = true;
1411
+ req.behavior = behavior;
1412
+ }
1413
+ renderPermissionBar();
1414
+ renderNotifications();
1415
+ }
1416
+
1417
+ function renderPermissionBar() {
1418
+ const bar = $('#permissionBar');
1419
+ const sid = state.selectedSession;
1420
+ if (!sid) { bar.classList.remove('active'); bar.innerHTML = ''; return; }
1421
+ const reqs = (state.permissionRequests[sid] || []).filter(r => !r.resolved);
1422
+ if (!reqs.length) { bar.classList.remove('active'); bar.innerHTML = ''; return; }
1423
+ bar.classList.add('active');
1424
+ bar.innerHTML = reqs.map(r => {
1425
+ const preview = formatPermPreview(r.toolName, r.inputPreview);
1426
+ return `<div class="perm-item">
1427
+ <div class="perm-icon">&#9888;</div>
1428
+ <div class="perm-info">
1429
+ <div class="perm-header">
1430
+ <span class="perm-tool">${esc(r.toolName)}</span>
1431
+ ${esc(r.description)}
1432
+ </div>
1433
+ ${preview ? `<div class="perm-preview">${esc(preview)}</div>` : ''}
1434
+ </div>
1435
+ <div class="perm-actions">
1436
+ <button class="perm-allow" data-req-id="${esc(r.requestId)}" data-session-id="${esc(sid)}">Allow <span class="perm-kbd">(Enter)</span></button>
1437
+ <button class="perm-deny" data-req-id="${esc(r.requestId)}" data-session-id="${esc(sid)}">Deny <span class="perm-kbd">(Esc)</span></button>
1438
+ </div>
1439
+ </div>`;
1440
+ }).join('');
1441
+ bindPermissionButtons(bar);
1442
+ }
1443
+
1444
+ function bindPermissionButtons(container) {
1445
+ (container || document).querySelectorAll('.perm-allow, .notif-perm-allow').forEach(btn => {
1446
+ btn.addEventListener('click', (e) => { e.stopPropagation(); sendPermissionVerdict(btn.dataset.sessionId, btn.dataset.reqId, 'allow'); });
1447
+ });
1448
+ (container || document).querySelectorAll('.perm-deny, .notif-perm-deny').forEach(btn => {
1449
+ btn.addEventListener('click', (e) => { e.stopPropagation(); sendPermissionVerdict(btn.dataset.sessionId, btn.dataset.reqId, 'deny'); });
1450
+ });
1451
+ }
1452
+
1453
+ // Keyboard shortcuts: Enter=allow, Esc=deny — works even in textarea when permission is pending
1454
+ document.addEventListener('keydown', (e) => {
1455
+ if (e.key !== 'Enter' && e.key !== 'Escape') return;
1456
+ const sid = state.selectedSession;
1457
+ if (!sid) return;
1458
+ const reqs = state.permissionRequests[sid] || [];
1459
+ const pending = reqs.find(r => !r.resolved);
1460
+ if (!pending) return;
1461
+ // Permission pending: intercept Enter/Esc everywhere (even in textarea)
1462
+ // Shift+Enter in textarea still inserts newline
1463
+ if (e.key === 'Enter' && e.shiftKey) return;
1464
+ e.preventDefault();
1465
+ e.stopPropagation();
1466
+ sendPermissionVerdict(sid, pending.requestId, e.key === 'Enter' ? 'allow' : 'deny');
1467
+ }, true);
1468
+
1469
+ // Flash title for attention
1470
+ let flashInterval = null;
1471
+ function flashTitle(msg) {
1472
+ const original = document.title;
1473
+ let on = true;
1474
+ clearInterval(flashInterval);
1475
+ flashInterval = setInterval(() => {
1476
+ document.title = on ? `** ${msg} **` : original;
1477
+ on = !on;
1478
+ }, 500);
1479
+ const stopFlash = () => {
1480
+ clearInterval(flashInterval);
1481
+ flashInterval = null;
1482
+ document.title = original;
1483
+ window.removeEventListener('focus', stopFlash);
1484
+ };
1485
+ window.addEventListener('focus', stopFlash);
1486
+ setTimeout(stopFlash, 30000);
1487
+ }
1488
+
1175
1489
  // Clear all notifications
1176
1490
  $('#notifClearAll').addEventListener('click', () => {
1177
1491
  state.notifications = [];
@@ -762,6 +762,20 @@ var HubServer = class {
762
762
  });
763
763
  break;
764
764
  }
765
+ case "permission_request": {
766
+ this.sessions.updateActivity(msg.sessionId);
767
+ logger.info(`Permission request [${msg.requestId}] from ${msg.sessionId}: ${msg.toolName}`);
768
+ this.broadcastToDashboards({
769
+ type: "permission_request",
770
+ sessionId: msg.sessionId,
771
+ requestId: msg.requestId,
772
+ toolName: msg.toolName,
773
+ description: msg.description,
774
+ inputPreview: msg.inputPreview,
775
+ timestamp: msg.timestamp
776
+ });
777
+ break;
778
+ }
765
779
  }
766
780
  }
767
781
  // --- Dashboard WebSocket ---
@@ -783,6 +797,12 @@ var HubServer = class {
783
797
  }
784
798
  } else if (msg.type === "image_upload") {
785
799
  this.handleImageUpload(msg);
800
+ } else if (msg.type === "permission_response") {
801
+ const channelWs = this.channelSockets.get(msg.sessionId);
802
+ if (channelWs?.readyState === WebSocket.OPEN) {
803
+ channelWs.send(JSON.stringify(msg));
804
+ logger.info(`Permission verdict [${msg.requestId}]: ${msg.behavior} -> session ${msg.sessionId}`);
805
+ }
786
806
  }
787
807
  } catch {
788
808
  logger.warn("Invalid message from dashboard");