@delt/claude-alarm 1.0.0 → 1.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@delt/claude-alarm",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Monitor and get notifications from multiple Claude Code sessions via MCP Channels",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -731,6 +731,18 @@
731
731
  margin-left: 2px;
732
732
  font-weight: 400;
733
733
  }
734
+ .perm-actions.choices {
735
+ flex-direction: column;
736
+ gap: 6px;
737
+ }
738
+ .perm-actions .perm-choice {
739
+ padding: 7px 16px;
740
+ font-size: 13px;
741
+ background: var(--surface);
742
+ color: var(--text);
743
+ border: 1px solid var(--border);
744
+ }
745
+ .perm-actions .perm-choice:hover { border-color: var(--yellow); }
734
746
  /* Permission in notification panel */
735
747
  .notif-perm-actions {
736
748
  display: flex;
@@ -1405,6 +1417,9 @@
1405
1417
  timestamp: msg.timestamp,
1406
1418
  resolved: false,
1407
1419
  behavior: null,
1420
+ choices: msg.choices || null,
1421
+ sent: null,
1422
+ outcome: null,
1408
1423
  });
1409
1424
  // Auto-select session if none selected
1410
1425
  if (!state.selectedSession && state.sessions[msg.sessionId]) selectSession(msg.sessionId);
@@ -1426,6 +1441,23 @@
1426
1441
  renderNotifications();
1427
1442
  }
1428
1443
  break;
1444
+ case 'permission_pending':
1445
+ for (const m of applyPendingChoices(state.permissionRequests, msg.requests || [])) {
1446
+ handleMessage({ type: 'permission_request', ...m });
1447
+ }
1448
+ renderPermissionBar();
1449
+ renderNotifications();
1450
+ break;
1451
+ case 'permission_resolved': {
1452
+ const req = (state.permissionRequests[msg.sessionId] || []).find(r => r.requestId === msg.requestId);
1453
+ if (req && !req.resolved) {
1454
+ req.resolved = true;
1455
+ req.outcome = msg.state;
1456
+ renderPermissionBar();
1457
+ renderNotifications();
1458
+ }
1459
+ break;
1460
+ }
1429
1461
  }
1430
1462
  }
1431
1463
 
@@ -1664,7 +1696,10 @@
1664
1696
  if (n.permRequestId) {
1665
1697
  const reqs = state.permissionRequests[n.sessionId] || [];
1666
1698
  const req = reqs.find(r => r.requestId === n.permRequestId);
1667
- if (req && !req.resolved) {
1699
+ if (req && req.choices) {
1700
+ if (req.resolved) permButtons = `<div class="perm-resolved-label">${req.outcome === 'expired' ? 'Expired' : 'Resolved'}</div>`;
1701
+ else if (req.sent != null) permButtons = '<div class="perm-resolved-label">Sent</div>';
1702
+ } else if (req && !req.resolved) {
1668
1703
  permButtons = `<div class="notif-perm-actions">
1669
1704
  <button class="notif-perm-allow" data-req-id="${esc(n.permRequestId)}" data-session-id="${esc(n.sessionId)}">Allow</button>
1670
1705
  <button class="notif-perm-deny" data-req-id="${esc(n.permRequestId)}" data-session-id="${esc(n.sessionId)}">Deny</button>
@@ -1951,6 +1986,7 @@
1951
1986
  let kind = 'code';
1952
1987
  let lang = '';
1953
1988
  switch (toolName) {
1989
+ case 'Command':
1954
1990
  case 'Bash':
1955
1991
  text = p.command ? '$ ' + p.command : raw; lang = 'bash'; break;
1956
1992
  case 'Write':
@@ -2001,6 +2037,47 @@
2001
2037
  renderNotifications();
2002
2038
  }
2003
2039
 
2040
+ // After a reconnect the hub lists the choice requests still waiting; any other unresolved one died with the old connection.
2041
+ function applyPendingChoices(permissionRequests, pending) {
2042
+ const missing = pending.filter(p => !(permissionRequests[p.sessionId] || []).some(r => r.requestId === p.requestId));
2043
+ const live = new Set(pending.map(p => p.sessionId + '\n' + p.requestId));
2044
+ for (const [sid, reqs] of Object.entries(permissionRequests)) {
2045
+ for (const r of reqs) {
2046
+ if (!r.choices || r.resolved) continue;
2047
+ if (!live.has(sid + '\n' + r.requestId)) { r.resolved = true; r.outcome = 'expired'; }
2048
+ else r.sent = null;
2049
+ }
2050
+ }
2051
+ return missing;
2052
+ }
2053
+
2054
+ function sendPermissionChoice(sessionId, requestId, choiceId) {
2055
+ if (!state.ws || state.ws.readyState !== WebSocket.OPEN) return;
2056
+ state.ws.send(JSON.stringify({ type: 'permission_response', sessionId, requestId, choiceId }));
2057
+ const req = (state.permissionRequests[sessionId] || []).find(r => r.requestId === requestId);
2058
+ if (req) req.sent = choiceId;
2059
+ renderPermissionBar();
2060
+ renderNotifications();
2061
+ }
2062
+
2063
+ function permissionActions(r, sid) {
2064
+ const ids = `data-req-id="${esc(r.requestId)}" data-session-id="${esc(sid)}"`;
2065
+ if (!r.choices) {
2066
+ return `<button class="perm-allow" ${ids}>Allow <span class="perm-kbd">(Enter)</span></button>
2067
+ <button class="perm-deny" ${ids}>Deny <span class="perm-kbd">(Esc)</span></button>`;
2068
+ }
2069
+ if (r.sent != null) {
2070
+ const chosen = r.choices.find(c => c.id === r.sent);
2071
+ return `<div class="perm-resolved-label">Sent: ${esc(chosen ? chosen.label : r.sent)}</div>`;
2072
+ }
2073
+ return r.choices.map(c => `<button class="perm-choice" ${ids} data-choice-id="${esc(c.id)}">${esc(c.label)}</button>`).join('');
2074
+ }
2075
+
2076
+ // Codex choices have no safe default, so Enter/Esc stay with the message box for them.
2077
+ function shortcutRequest(reqs) {
2078
+ return reqs.find(r => !r.resolved && !r.choices);
2079
+ }
2080
+
2004
2081
  function renderPermissionBar() {
2005
2082
  const bar = $('#permissionBar');
2006
2083
  const sid = state.selectedSession;
@@ -2017,12 +2094,10 @@
2017
2094
  <div class="perm-header">
2018
2095
  <span class="perm-tool">${esc(displayName)}</span>
2019
2096
  </div>
2097
+ ${r.choices && r.description ? `<div class="perm-desc">${esc(r.description)}</div>` : ''}
2020
2098
  ${previewText ? renderPermPreview(previewText, previewKind, previewLang) : ''}
2021
2099
  </div>
2022
- <div class="perm-actions">
2023
- <button class="perm-allow" data-req-id="${esc(r.requestId)}" data-session-id="${esc(sid)}">Allow <span class="perm-kbd">(Enter)</span></button>
2024
- <button class="perm-deny" data-req-id="${esc(r.requestId)}" data-session-id="${esc(sid)}">Deny <span class="perm-kbd">(Esc)</span></button>
2025
- </div>
2100
+ <div class="perm-actions${r.choices ? ' choices' : ''}">${permissionActions(r, sid)}</div>
2026
2101
  </div>`;
2027
2102
  }).join('');
2028
2103
  bindPermissionButtons(bar);
@@ -2035,6 +2110,9 @@
2035
2110
  (container || document).querySelectorAll('.perm-deny, .notif-perm-deny').forEach(btn => {
2036
2111
  btn.addEventListener('click', (e) => { e.stopPropagation(); sendPermissionVerdict(btn.dataset.sessionId, btn.dataset.reqId, 'deny'); });
2037
2112
  });
2113
+ (container || document).querySelectorAll('.perm-choice').forEach(btn => {
2114
+ btn.addEventListener('click', (e) => { e.stopPropagation(); sendPermissionChoice(btn.dataset.sessionId, btn.dataset.reqId, btn.dataset.choiceId); });
2115
+ });
2038
2116
  }
2039
2117
 
2040
2118
  // Keyboard shortcuts: Enter=allow, Esc=deny — works even in textarea when permission is pending
@@ -2044,7 +2122,7 @@
2044
2122
  const sid = state.selectedSession;
2045
2123
  if (!sid) return;
2046
2124
  const reqs = state.permissionRequests[sid] || [];
2047
- const pending = reqs.find(r => !r.resolved);
2125
+ const pending = shortcutRequest(reqs);
2048
2126
  if (!pending) return;
2049
2127
  // Permission pending: intercept Enter/Esc everywhere (even in textarea)
2050
2128
  // Shift+Enter in textarea still inserts newline