@delt/claude-alarm 0.8.2 → 0.9.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/README.md +14 -0
- package/dist/channel/server.js +69 -5
- package/dist/channel/server.js.map +1 -1
- package/dist/cli.js +125 -33
- package/dist/cli.js.map +1 -1
- package/dist/dashboard/index.html +213 -9
- package/dist/hub/server.js +24 -2
- package/dist/hub/server.js.map +1 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.js +28 -4
- package/dist/index.js.map +1 -1
- package/package.json +4 -2
- package/src/dashboard/index.html +213 -9
package/src/dashboard/index.html
CHANGED
|
@@ -187,6 +187,7 @@
|
|
|
187
187
|
outline: none;
|
|
188
188
|
font-family: inherit;
|
|
189
189
|
}
|
|
190
|
+
.name-edit-input.invalid { border-color: var(--red); }
|
|
190
191
|
.session-name {
|
|
191
192
|
font-size: 14px;
|
|
192
193
|
font-weight: 500;
|
|
@@ -400,6 +401,39 @@
|
|
|
400
401
|
}
|
|
401
402
|
.drag-overlay.active { display: flex; }
|
|
402
403
|
|
|
404
|
+
.mention-error {
|
|
405
|
+
display: none;
|
|
406
|
+
padding: 6px 20px 0;
|
|
407
|
+
color: var(--red);
|
|
408
|
+
font-size: 12px;
|
|
409
|
+
}
|
|
410
|
+
.mention-error.show { display: block; }
|
|
411
|
+
.message-input-area { position: relative; }
|
|
412
|
+
.mention-popup {
|
|
413
|
+
display: none;
|
|
414
|
+
position: absolute;
|
|
415
|
+
bottom: calc(100% + 4px);
|
|
416
|
+
left: 20px;
|
|
417
|
+
min-width: 220px;
|
|
418
|
+
max-height: 200px;
|
|
419
|
+
overflow-y: auto;
|
|
420
|
+
background: var(--surface);
|
|
421
|
+
border: 1px solid var(--border);
|
|
422
|
+
border-radius: 6px;
|
|
423
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
|
|
424
|
+
z-index: 20;
|
|
425
|
+
}
|
|
426
|
+
.mention-popup.open { display: block; }
|
|
427
|
+
.mention-item {
|
|
428
|
+
display: flex;
|
|
429
|
+
justify-content: space-between;
|
|
430
|
+
gap: 12px;
|
|
431
|
+
padding: 6px 10px;
|
|
432
|
+
font-size: 13px;
|
|
433
|
+
cursor: pointer;
|
|
434
|
+
}
|
|
435
|
+
.mention-item.active, .mention-item:hover { background: var(--border); }
|
|
436
|
+
.mention-folder { color: var(--text-dim); font-size: 11px; }
|
|
403
437
|
.message-input-area {
|
|
404
438
|
border-top: 1px solid var(--border);
|
|
405
439
|
padding: 12px 20px;
|
|
@@ -963,7 +997,9 @@
|
|
|
963
997
|
<span class="preview-name" id="previewName"></span>
|
|
964
998
|
<button class="preview-cancel" id="previewCancel">×</button>
|
|
965
999
|
</div>
|
|
1000
|
+
<div class="mention-error" id="mentionError"></div>
|
|
966
1001
|
<div class="message-input-area">
|
|
1002
|
+
<div class="mention-popup" id="mentionPopup"></div>
|
|
967
1003
|
<button class="attach-btn" id="attachBtn" disabled title="Attach image">📎</button>
|
|
968
1004
|
<input type="file" id="fileInput" accept="image/*" style="display:none">
|
|
969
1005
|
<textarea id="msgInput" placeholder="Send a message to session... (Shift+Enter for new line)" disabled rows="1"></textarea>
|
|
@@ -1095,6 +1131,146 @@
|
|
|
1095
1131
|
return state.sessionMeta.names[s.id] || s.displayName || s.name;
|
|
1096
1132
|
}
|
|
1097
1133
|
|
|
1134
|
+
function normName(s) {
|
|
1135
|
+
return String(s).normalize('NFC').trim().toLowerCase();
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
function folderName(s) {
|
|
1139
|
+
return s.displayName || s.name;
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
function renameError(val, id) {
|
|
1143
|
+
if (/[\[\]@]/.test(val)) return 'Name cannot contain [, ] or @';
|
|
1144
|
+
const key = normName(val);
|
|
1145
|
+
const taken = Object.values(state.sessions).some(o => o.id !== id &&
|
|
1146
|
+
(normName(sessionDisplayName(o)) === key || normName(folderName(o)) === key));
|
|
1147
|
+
return taken ? 'Name already used by another session' : null;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
function sessionNames(s) {
|
|
1151
|
+
return [state.sessionMeta.names[s.id], folderName(s), s.peerName].filter(Boolean);
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
// Built-in SendMessage only reaches sessions on the hub's own machine.
|
|
1155
|
+
function mentionTargets() {
|
|
1156
|
+
const selected = state.sessions[state.selectedSession];
|
|
1157
|
+
if (!selected || !selected.isLocal) return [];
|
|
1158
|
+
return Object.values(state.sessions).filter(s => s.id !== selected.id && s.isLocal && s.peerName);
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
function resolveMention(name) {
|
|
1162
|
+
const key = normName(name);
|
|
1163
|
+
const matches = mentionTargets().filter(s => sessionNames(s).some(n => normName(n) === key));
|
|
1164
|
+
return matches.length === 1 ? matches[0] : null;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
function mentionLabel(s) {
|
|
1168
|
+
return [sessionDisplayName(s), folderName(s), s.peerName].find(n => resolveMention(n) === s) || s.peerName;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
// A token only counts as a mention when it names (or starts with) some
|
|
1172
|
+
// session, so pasted code like @Override or npm @types/x is left alone.
|
|
1173
|
+
function looksLikeSessionMention(name) {
|
|
1174
|
+
const key = normName(name);
|
|
1175
|
+
return Object.values(state.sessions).some(s => sessionNames(s).some(n => key.startsWith(normName(n))));
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
function extractMentions(text) {
|
|
1179
|
+
const prose = text.replace(/```[\s\S]*?(```|$)/g, ' ').replace(/`[^`\n]*`/g, ' ');
|
|
1180
|
+
const re = /(^|\s)@(?:\[([^\]\n]+)\]|([^\s\[\]]+))/g;
|
|
1181
|
+
const found = [];
|
|
1182
|
+
let m;
|
|
1183
|
+
while ((m = re.exec(prose))) {
|
|
1184
|
+
const bracketed = m[2] !== undefined;
|
|
1185
|
+
const name = bracketed ? m[2] : m[3].replace(/[.,!?;:)]+$/, '');
|
|
1186
|
+
found.push({ raw: m[0].trim(), name, bracketed });
|
|
1187
|
+
}
|
|
1188
|
+
return found;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
// The routing line is the contract with the channel instructions in
|
|
1192
|
+
// src/channel/server.ts (ROUTING) — change both together.
|
|
1193
|
+
function buildRouting(text) {
|
|
1194
|
+
const mentions = extractMentions(text);
|
|
1195
|
+
if (!mentions.length) return { ok: true, content: text };
|
|
1196
|
+
const unknown = [];
|
|
1197
|
+
const lines = [];
|
|
1198
|
+
const seen = new Set();
|
|
1199
|
+
for (const m of mentions) {
|
|
1200
|
+
const s = resolveMention(m.name);
|
|
1201
|
+
if (!s) {
|
|
1202
|
+
if (m.bracketed || looksLikeSessionMention(m.name)) unknown.push(m.raw);
|
|
1203
|
+
continue;
|
|
1204
|
+
}
|
|
1205
|
+
if (seen.has(s.id)) continue;
|
|
1206
|
+
seen.add(s.id);
|
|
1207
|
+
lines.push(`[claude-alarm] @${m.name} = SendMessage to "${s.peerName}"`);
|
|
1208
|
+
}
|
|
1209
|
+
if (unknown.length) return { ok: false, unknown };
|
|
1210
|
+
if (!lines.length) return { ok: true, content: text };
|
|
1211
|
+
return { ok: true, content: `${text}\n\n${lines.join('\n')}` };
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
function showMentionError(msg) {
|
|
1215
|
+
const el = $('#mentionError');
|
|
1216
|
+
el.textContent = msg || '';
|
|
1217
|
+
el.classList.toggle('show', !!msg);
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
let mentionState = { open: false, items: [], index: 0, start: -1 };
|
|
1221
|
+
|
|
1222
|
+
function currentMentionQuery(input) {
|
|
1223
|
+
const upto = input.value.slice(0, input.selectionStart);
|
|
1224
|
+
const m = upto.match(/(^|\s)@(\[[^\]\n]*|[^\s\[\]]*)$/);
|
|
1225
|
+
if (!m) return null;
|
|
1226
|
+
return { start: upto.length - m[2].length - 1, query: m[2].replace(/^\[/, '') };
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
function closeMentionPopup() {
|
|
1230
|
+
mentionState = { open: false, items: [], index: 0, start: -1 };
|
|
1231
|
+
$('#mentionPopup').classList.remove('open');
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
function renderMentionPopup() {
|
|
1235
|
+
$('#mentionPopup').innerHTML = mentionState.items.map((s, i) =>
|
|
1236
|
+
`<div class="mention-item${i === mentionState.index ? ' active' : ''}" data-idx="${i}">` +
|
|
1237
|
+
`<span>${esc(sessionDisplayName(s))}</span><span class="mention-folder">${esc(folderName(s))}</span></div>`
|
|
1238
|
+
).join('');
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
function updateMentionPopup() {
|
|
1242
|
+
const q = currentMentionQuery($('#msgInput'));
|
|
1243
|
+
if (!q) return closeMentionPopup();
|
|
1244
|
+
const key = normName(q.query);
|
|
1245
|
+
const items = mentionTargets().filter(s =>
|
|
1246
|
+
normName(sessionDisplayName(s)).includes(key) || normName(folderName(s)).includes(key));
|
|
1247
|
+
if (!items.length) return closeMentionPopup();
|
|
1248
|
+
const index = mentionState.open ? Math.min(mentionState.index, items.length - 1) : 0;
|
|
1249
|
+
mentionState = { open: true, items, index, start: q.start };
|
|
1250
|
+
renderMentionPopup();
|
|
1251
|
+
$('#mentionPopup').classList.add('open');
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
function applyMention(s) {
|
|
1255
|
+
const input = $('#msgInput');
|
|
1256
|
+
const label = mentionLabel(s);
|
|
1257
|
+
const token = /\s/.test(label) ? `@[${label}] ` : `@${label} `;
|
|
1258
|
+
const before = input.value.slice(0, mentionState.start);
|
|
1259
|
+
const after = input.value.slice(input.selectionStart);
|
|
1260
|
+
input.value = before + token + after;
|
|
1261
|
+
const pos = before.length + token.length;
|
|
1262
|
+
input.setSelectionRange(pos, pos);
|
|
1263
|
+
closeMentionPopup();
|
|
1264
|
+
input.focus();
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
$('#mentionPopup').addEventListener('mousedown', (e) => {
|
|
1268
|
+
const item = e.target.closest('.mention-item');
|
|
1269
|
+
if (!item) return;
|
|
1270
|
+
e.preventDefault();
|
|
1271
|
+
applyMention(mentionState.items[Number(item.dataset.idx)]);
|
|
1272
|
+
});
|
|
1273
|
+
|
|
1098
1274
|
// --- Theme ---
|
|
1099
1275
|
function initTheme() {
|
|
1100
1276
|
const saved = localStorage.getItem('claude-alarm-theme') || 'dark';
|
|
@@ -1307,12 +1483,18 @@
|
|
|
1307
1483
|
input.select();
|
|
1308
1484
|
|
|
1309
1485
|
let done = false;
|
|
1310
|
-
const commit = (save) => {
|
|
1486
|
+
const commit = (save, fromBlur) => {
|
|
1311
1487
|
if (done) return;
|
|
1488
|
+
const val = input.value.trim();
|
|
1489
|
+
const baseName = s.displayName || s.name;
|
|
1490
|
+
const err = save && val && val !== baseName ? renameError(val, id) : null;
|
|
1491
|
+
if (err && !fromBlur) {
|
|
1492
|
+
input.classList.add('invalid');
|
|
1493
|
+
input.title = err;
|
|
1494
|
+
return;
|
|
1495
|
+
}
|
|
1312
1496
|
done = true;
|
|
1313
|
-
if (save) {
|
|
1314
|
-
const val = input.value.trim();
|
|
1315
|
-
const baseName = s.displayName || s.name;
|
|
1497
|
+
if (save && !err) {
|
|
1316
1498
|
if (!val || val === baseName) delete state.sessionMeta.names[id];
|
|
1317
1499
|
else state.sessionMeta.names[id] = val;
|
|
1318
1500
|
saveSessionMeta();
|
|
@@ -1321,10 +1503,11 @@
|
|
|
1321
1503
|
};
|
|
1322
1504
|
input.addEventListener('keydown', (e) => {
|
|
1323
1505
|
e.stopPropagation();
|
|
1324
|
-
if (e.key === 'Enter') { e.preventDefault(); commit(true); }
|
|
1325
|
-
else if (e.key === 'Escape') { e.preventDefault(); commit(false); }
|
|
1506
|
+
if (e.key === 'Enter') { e.preventDefault(); commit(true, false); }
|
|
1507
|
+
else if (e.key === 'Escape') { e.preventDefault(); commit(false, false); }
|
|
1326
1508
|
});
|
|
1327
|
-
input.addEventListener('
|
|
1509
|
+
input.addEventListener('input', () => { input.classList.remove('invalid'); input.title = ''; });
|
|
1510
|
+
input.addEventListener('blur', () => commit(true, true));
|
|
1328
1511
|
input.addEventListener('click', (e) => e.stopPropagation());
|
|
1329
1512
|
}
|
|
1330
1513
|
|
|
@@ -1577,6 +1760,13 @@
|
|
|
1577
1760
|
if (!content && !hasImage) return;
|
|
1578
1761
|
if (!state.selectedSession || !state.ws) return;
|
|
1579
1762
|
|
|
1763
|
+
const routing = buildRouting(content);
|
|
1764
|
+
if (!routing.ok) {
|
|
1765
|
+
showMentionError(`Unknown session: ${routing.unknown.join(', ')} — pick from the @ list, or put a space right after the name.`);
|
|
1766
|
+
return;
|
|
1767
|
+
}
|
|
1768
|
+
showMentionError(null);
|
|
1769
|
+
|
|
1580
1770
|
if (hasImage) {
|
|
1581
1771
|
// Send image + text together
|
|
1582
1772
|
state.ws.send(JSON.stringify({
|
|
@@ -1585,11 +1775,11 @@
|
|
|
1585
1775
|
imageData: state.pendingImage.data,
|
|
1586
1776
|
mimeType: state.pendingImage.mimeType,
|
|
1587
1777
|
originalName: state.pendingImage.name,
|
|
1588
|
-
content: content || '',
|
|
1778
|
+
content: routing.content || '',
|
|
1589
1779
|
}));
|
|
1590
1780
|
} else if (content) {
|
|
1591
1781
|
// Text only
|
|
1592
|
-
state.ws.send(JSON.stringify({ type: 'message_to_session', sessionId: state.selectedSession, content }));
|
|
1782
|
+
state.ws.send(JSON.stringify({ type: 'message_to_session', sessionId: state.selectedSession, content: routing.content }));
|
|
1593
1783
|
}
|
|
1594
1784
|
|
|
1595
1785
|
// Add to local messages
|
|
@@ -1604,6 +1794,7 @@
|
|
|
1604
1794
|
saveMessages();
|
|
1605
1795
|
|
|
1606
1796
|
input.value = '';
|
|
1797
|
+
closeMentionPopup();
|
|
1607
1798
|
input.style.height = 'auto';
|
|
1608
1799
|
clearPendingImage();
|
|
1609
1800
|
state.waitingReply[state.selectedSession] = true;
|
|
@@ -1612,11 +1803,24 @@
|
|
|
1612
1803
|
|
|
1613
1804
|
$('#sendBtn').addEventListener('click', sendMessage);
|
|
1614
1805
|
$('#msgInput').addEventListener('keydown', (e) => {
|
|
1806
|
+
if (mentionState.open) {
|
|
1807
|
+
// Enter while an IME (e.g. Korean) is composing only commits the syllable.
|
|
1808
|
+
if (e.isComposing) return;
|
|
1809
|
+
const n = mentionState.items.length;
|
|
1810
|
+
if (e.key === 'ArrowDown') { e.preventDefault(); mentionState.index = (mentionState.index + 1) % n; renderMentionPopup(); return; }
|
|
1811
|
+
if (e.key === 'ArrowUp') { e.preventDefault(); mentionState.index = (mentionState.index - 1 + n) % n; renderMentionPopup(); return; }
|
|
1812
|
+
if (e.key === 'Enter' || e.key === 'Tab') { e.preventDefault(); applyMention(mentionState.items[mentionState.index]); return; }
|
|
1813
|
+
if (e.key === 'Escape') { e.preventDefault(); closeMentionPopup(); return; }
|
|
1814
|
+
}
|
|
1615
1815
|
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); }
|
|
1616
1816
|
});
|
|
1817
|
+
$('#msgInput').addEventListener('blur', closeMentionPopup);
|
|
1818
|
+
$('#msgInput').addEventListener('click', updateMentionPopup);
|
|
1617
1819
|
$('#msgInput').addEventListener('input', function() {
|
|
1820
|
+
showMentionError(null);
|
|
1618
1821
|
this.style.height = 'auto';
|
|
1619
1822
|
this.style.height = Math.min(this.scrollHeight, 120) + 'px';
|
|
1823
|
+
updateMentionPopup();
|
|
1620
1824
|
});
|
|
1621
1825
|
|
|
1622
1826
|
function relativeTime(ts) {
|