@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.
- package/dist/channel/server.js +39 -1
- package/dist/channel/server.js.map +1 -1
- package/dist/cli.js +60 -2
- package/dist/cli.js.map +1 -1
- package/dist/dashboard/index.html +315 -1
- package/dist/hub/server.js +20 -0
- package/dist/hub/server.js.map +1 -1
- package/dist/index.d.ts +13 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/dashboard/index.html +315 -1
package/src/dashboard/index.html
CHANGED
|
@@ -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">↓</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}">×</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} · ${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">⚠</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 = [];
|