agentchannel 0.7.25 → 0.7.26
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 +1 -1
- package/ui/app.js +196 -6
- package/ui/index.html +7 -0
package/package.json
CHANGED
package/ui/app.js
CHANGED
|
@@ -38,6 +38,9 @@ var collapsedGroups = { "AgentChannel": true };
|
|
|
38
38
|
var onlineMembers = {}; // channel -> Set of names
|
|
39
39
|
var channelMetas = {}; // channel name -> meta object
|
|
40
40
|
|
|
41
|
+
var dmChannels = {}; // theirFingerprint -> {key, hash, name, theirFp}
|
|
42
|
+
var dmNames = {}; // theirFingerprint -> display name
|
|
43
|
+
|
|
41
44
|
var encoder = new TextEncoder();
|
|
42
45
|
var decoder = new TextDecoder();
|
|
43
46
|
|
|
@@ -87,6 +90,22 @@ async function hashSubWeb(channelKey, subName) {
|
|
|
87
90
|
return Array.from(topicBytes).map(function(b) { return b.toString(16).padStart(2, "0"); }).join("");
|
|
88
91
|
}
|
|
89
92
|
|
|
93
|
+
async function deriveDmKeyWeb(fpA, fpB) {
|
|
94
|
+
var sorted = [fpA, fpB].sort();
|
|
95
|
+
var ikm = sorted[0] + sorted[1];
|
|
96
|
+
var prk = await hkdfExtract(ikm);
|
|
97
|
+
var keyBytes = await hkdfExpand(prk, "acp1:dm", 32);
|
|
98
|
+
return crypto.subtle.importKey("raw", keyBytes, {name:"AES-GCM",length:256}, false, ["encrypt","decrypt"]);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function hashDmWeb(fpA, fpB) {
|
|
102
|
+
var sorted = [fpA, fpB].sort();
|
|
103
|
+
var ikm = sorted[0] + sorted[1];
|
|
104
|
+
var prk = await hkdfExtract(ikm);
|
|
105
|
+
var topicBytes = await hkdfExpand(prk, "acp1:topic:dm", 16);
|
|
106
|
+
return Array.from(topicBytes).map(function(b) { return b.toString(16).padStart(2, "0"); }).join("");
|
|
107
|
+
}
|
|
108
|
+
|
|
90
109
|
async function decryptPayload(payload, key) {
|
|
91
110
|
var p = JSON.parse(payload);
|
|
92
111
|
var iv = Uint8Array.from(atob(p.iv), function(c) { return c.charCodeAt(0); });
|
|
@@ -172,7 +191,7 @@ function richText(t) {
|
|
|
172
191
|
function render() {
|
|
173
192
|
var filtered;
|
|
174
193
|
if (activeChannel === "all") {
|
|
175
|
-
filtered = allMessages.
|
|
194
|
+
filtered = allMessages.filter(function(m) { return !m.channel || !m.channel.startsWith("dm:"); });
|
|
176
195
|
} else if (activeChannel === "@me") {
|
|
177
196
|
filtered = allMessages.filter(function(m) {
|
|
178
197
|
return m.content && CONFIG.name && m.content.indexOf("@" + CONFIG.name) !== -1;
|
|
@@ -303,6 +322,39 @@ function renderSidebar() {
|
|
|
303
322
|
el.appendChild(meDiv);
|
|
304
323
|
}
|
|
305
324
|
|
|
325
|
+
// Direct Messages section
|
|
326
|
+
var dmKeys = Object.keys(dmChannels);
|
|
327
|
+
if (dmKeys.length > 0) {
|
|
328
|
+
var dmHeader = document.createElement("div");
|
|
329
|
+
dmHeader.style.cssText = "font-size:0.6rem;color:var(--text-muted);padding:12px 12px 4px;text-transform:uppercase;letter-spacing:0.05em;font-weight:600";
|
|
330
|
+
dmHeader.textContent = "Direct Messages";
|
|
331
|
+
el.appendChild(dmHeader);
|
|
332
|
+
|
|
333
|
+
for (var di = 0; di < dmKeys.length; di++) {
|
|
334
|
+
var dmFp = dmKeys[di];
|
|
335
|
+
var dmInfo = dmChannels[dmFp];
|
|
336
|
+
var dmCid = dmInfo.channelId;
|
|
337
|
+
var dmDisplayName = dmNames[dmFp] || dmFp.slice(0, 8);
|
|
338
|
+
var dmDiv = document.createElement("div");
|
|
339
|
+
dmDiv.className = "sidebar__channel" + (activeChannel === dmCid ? " active" : "");
|
|
340
|
+
var dmCount = unreadCounts[dmCid] || 0;
|
|
341
|
+
dmDiv.innerHTML = '<span style="color:var(--accent);margin-right:2px;opacity:0.7">@</span>' + esc(dmDisplayName) + '<span style="color:var(--text-muted);font-size:0.6rem;margin-left:3px;opacity:0.8">(' + dmFp.slice(0, 4) + ')</span>' + (dmCount ? '<span class="badge">' + dmCount + '</span>' : "");
|
|
342
|
+
(function(fp, channelId, displayName) {
|
|
343
|
+
dmDiv.onclick = function() {
|
|
344
|
+
activeChannel = channelId;
|
|
345
|
+
unreadCounts[channelId] = 0;
|
|
346
|
+
headerName.textContent = "@" + displayName;
|
|
347
|
+
headerDesc.textContent = "DM with " + fp.slice(0, 8);
|
|
348
|
+
document.title = "AgentChannel — DM";
|
|
349
|
+
renderSidebar();
|
|
350
|
+
render();
|
|
351
|
+
if (window.renderMembers) window.renderMembers();
|
|
352
|
+
};
|
|
353
|
+
})(dmFp, dmCid, dmDisplayName);
|
|
354
|
+
el.appendChild(dmDiv);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
306
358
|
// Render each parent + children
|
|
307
359
|
for (var pi = 0; pi < parents.length; pi++) {
|
|
308
360
|
var ch = parents[pi];
|
|
@@ -709,10 +761,12 @@ async function init() {
|
|
|
709
761
|
var isOnline = memberMap[name].online;
|
|
710
762
|
var isYou = name === CONFIG.name;
|
|
711
763
|
var memberInfo = Object.values(window.cloudMembers || {}).flat().find(function(m) { return m.name === name; });
|
|
712
|
-
var
|
|
713
|
-
|
|
764
|
+
var fp = memberInfo && memberInfo.fingerprint ? memberInfo.fingerprint : '';
|
|
765
|
+
var fpStr = fp
|
|
766
|
+
? '<span style="color:var(--text-muted);font-size:0.6rem;margin-left:2px">(' + fp.slice(0, 4) + ')</span>'
|
|
714
767
|
: '';
|
|
715
|
-
|
|
768
|
+
var dmClick = (!isYou && fp) ? ' onclick="window.openDm(\x27' + fp + '\x27,\x27' + esc(name).replace(/'/g, '') + '\x27)" style="cursor:pointer" title="Open DM"' : '';
|
|
769
|
+
return '<div class="members__item"' + dmClick + '><span class="members__dot" style="background:' + (isOnline ? "#22c55e" : "#666") + '"></span><span class="members__name">' + esc(name) + fpStr + '</span>' + (isYou ? '<span class="members__role">you</span>' : '') + '</div>';
|
|
716
770
|
}).join("");
|
|
717
771
|
|
|
718
772
|
list.innerHTML = html;
|
|
@@ -812,12 +866,17 @@ async function init() {
|
|
|
812
866
|
}
|
|
813
867
|
var total = Object.values(unreadCounts).reduce(function(a, b) { return a + b; }, 0);
|
|
814
868
|
if (total > 0) document.title = "(" + total + ") AgentChannel";
|
|
815
|
-
var nlabel = ch.sub ? "#" + ch.name + " ##" + ch.sub : "#" + ch.name;
|
|
869
|
+
var nlabel = ch.isDm ? "DM" : (ch.sub ? "#" + ch.name + " ##" + ch.sub : "#" + ch.name);
|
|
816
870
|
if (Notification.permission === "granted" && (document.hidden || activeChannel !== chKeyName)) {
|
|
817
871
|
var n = new Notification(nlabel + " @" + msg.sender, {body: msg.content});
|
|
818
872
|
n.onclick = function() {
|
|
819
873
|
window.focus();
|
|
820
|
-
if (ch.
|
|
874
|
+
if (ch.isDm) {
|
|
875
|
+
activeChannel = ch.name;
|
|
876
|
+
unreadCounts[ch.name] = 0;
|
|
877
|
+
renderSidebar();
|
|
878
|
+
render();
|
|
879
|
+
} else if (ch.sub) { window.switchToSub(ch.sub); }
|
|
821
880
|
else { window.switchToChannel(ch.name); }
|
|
822
881
|
};
|
|
823
882
|
}
|
|
@@ -859,6 +918,81 @@ async function init() {
|
|
|
859
918
|
renderMembers();
|
|
860
919
|
};
|
|
861
920
|
|
|
921
|
+
window.openDm = async function(theirFp, theirName) {
|
|
922
|
+
if (!CONFIG.fingerprint || theirFp === CONFIG.fingerprint) return;
|
|
923
|
+
// Derive DM key and hash
|
|
924
|
+
if (!dmChannels[theirFp]) {
|
|
925
|
+
var dmKey = await deriveDmKeyWeb(CONFIG.fingerprint, theirFp);
|
|
926
|
+
var dmHash = await hashDmWeb(CONFIG.fingerprint, theirFp);
|
|
927
|
+
var sorted = [CONFIG.fingerprint, theirFp].sort();
|
|
928
|
+
var dmCid = "dm:" + sorted[0] + ":" + sorted[1];
|
|
929
|
+
dmChannels[theirFp] = {key: dmKey, hash: dmHash, channelId: dmCid, theirFp: theirFp};
|
|
930
|
+
// Register in channels map for MQTT handling
|
|
931
|
+
channels[dmCid] = {key: dmKey, hash: dmHash, name: dmCid, isDm: true, theirFp: theirFp};
|
|
932
|
+
// Subscribe to DM topic
|
|
933
|
+
client.subscribe("ac/1/" + dmHash);
|
|
934
|
+
// Load DM history from cloud
|
|
935
|
+
try {
|
|
936
|
+
var dres = await fetch("https://api.agentchannel.workers.dev/messages?channel_hash=" + dmHash + "&since=0&limit=30");
|
|
937
|
+
var drows = await dres.json();
|
|
938
|
+
for (var dri = 0; dri < drows.length; dri++) {
|
|
939
|
+
try {
|
|
940
|
+
var dmsg = JSON.parse(await decryptPayload(drows[dri].ciphertext, dmKey));
|
|
941
|
+
dmsg.channel = dmCid;
|
|
942
|
+
if (dmsg.type !== "channel_meta") {
|
|
943
|
+
// Avoid duplicates
|
|
944
|
+
var isDup = allMessages.some(function(m) { return m.id === dmsg.id; });
|
|
945
|
+
if (!isDup) allMessages.push(dmsg);
|
|
946
|
+
}
|
|
947
|
+
} catch(e) {}
|
|
948
|
+
}
|
|
949
|
+
allMessages.sort(function(a, b) { return a.timestamp - b.timestamp; });
|
|
950
|
+
} catch(e) {}
|
|
951
|
+
}
|
|
952
|
+
if (theirName) dmNames[theirFp] = theirName;
|
|
953
|
+
var dmCid = dmChannels[theirFp].channelId;
|
|
954
|
+
activeChannel = dmCid;
|
|
955
|
+
unreadCounts[dmCid] = 0;
|
|
956
|
+
headerName.textContent = "@" + (theirName || theirFp.slice(0, 8));
|
|
957
|
+
headerDesc.textContent = "DM with " + theirFp.slice(0, 8);
|
|
958
|
+
document.title = "AgentChannel — DM";
|
|
959
|
+
renderSidebar();
|
|
960
|
+
render();
|
|
961
|
+
renderMembers();
|
|
962
|
+
};
|
|
963
|
+
|
|
964
|
+
// DM send via web UI — encrypt and publish to DM topic
|
|
965
|
+
window.sendDmMessage = async function(theirFp, content) {
|
|
966
|
+
if (!dmChannels[theirFp]) return;
|
|
967
|
+
var dm = dmChannels[theirFp];
|
|
968
|
+
var iv = crypto.getRandomValues(new Uint8Array(12));
|
|
969
|
+
var plaintext = JSON.stringify({
|
|
970
|
+
id: Array.from(crypto.getRandomValues(new Uint8Array(8))).map(function(b){return b.toString(16).padStart(2,"0")}).join(""),
|
|
971
|
+
channel: dm.channelId,
|
|
972
|
+
sender: CONFIG.name,
|
|
973
|
+
content: content,
|
|
974
|
+
timestamp: Date.now(),
|
|
975
|
+
type: "chat",
|
|
976
|
+
senderKey: CONFIG.fingerprint
|
|
977
|
+
});
|
|
978
|
+
var encoded = encoder.encode(plaintext);
|
|
979
|
+
var encrypted = await crypto.subtle.encrypt({name:"AES-GCM", iv:iv}, dm.key, encoded);
|
|
980
|
+
var cipherData = new Uint8Array(encrypted.slice(0, encrypted.byteLength - 16));
|
|
981
|
+
var tag = new Uint8Array(encrypted.slice(encrypted.byteLength - 16));
|
|
982
|
+
var payload = JSON.stringify({
|
|
983
|
+
iv: btoa(String.fromCharCode.apply(null, iv)),
|
|
984
|
+
data: btoa(String.fromCharCode.apply(null, cipherData)),
|
|
985
|
+
tag: btoa(String.fromCharCode.apply(null, tag))
|
|
986
|
+
});
|
|
987
|
+
client.publish("ac/1/" + dm.hash, payload, {qos: 1});
|
|
988
|
+
// Also store to cloud
|
|
989
|
+
fetch("https://api.agentchannel.workers.dev/messages", {
|
|
990
|
+
method: "POST",
|
|
991
|
+
headers: {"Content-Type": "application/json"},
|
|
992
|
+
body: JSON.stringify({channel_hash: dm.hash, id: JSON.parse(plaintext).id, ciphertext: payload, timestamp: Date.now()})
|
|
993
|
+
}).catch(function(){});
|
|
994
|
+
};
|
|
995
|
+
|
|
862
996
|
renderMembers();
|
|
863
997
|
loadCloudMembers();
|
|
864
998
|
}
|
|
@@ -943,6 +1077,62 @@ function saveName() {
|
|
|
943
1077
|
}
|
|
944
1078
|
window.saveName = saveName;
|
|
945
1079
|
|
|
1080
|
+
// ── Input: send message + @autocomplete ──────────────────
|
|
1081
|
+
|
|
1082
|
+
function sendMsg() {
|
|
1083
|
+
var input = document.getElementById('msg-input');
|
|
1084
|
+
if (!input || !input.value.trim() || activeChannel === '@me') return;
|
|
1085
|
+
fetch('/api/send', {
|
|
1086
|
+
method: 'POST',
|
|
1087
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1088
|
+
body: JSON.stringify({ channel: activeChannel, message: input.value.trim() })
|
|
1089
|
+
}).then(function() { input.value = ''; }).catch(function(e) { console.error('Send failed:', e); });
|
|
1090
|
+
}
|
|
1091
|
+
window.sendMsg = sendMsg;
|
|
1092
|
+
|
|
1093
|
+
var acSelected = 0;
|
|
1094
|
+
function onInputChange(input) {
|
|
1095
|
+
var val = input.value;
|
|
1096
|
+
var atIdx = val.lastIndexOf('@');
|
|
1097
|
+
var ac = document.getElementById('autocomplete');
|
|
1098
|
+
if (atIdx === -1 || atIdx < val.length - 20) { ac.style.display = 'none'; return; }
|
|
1099
|
+
var query = val.slice(atIdx + 1).toLowerCase();
|
|
1100
|
+
if (query.indexOf(' ') !== -1) { ac.style.display = 'none'; return; }
|
|
1101
|
+
var members = [];
|
|
1102
|
+
var cm = window.cloudMembers || {};
|
|
1103
|
+
for (var k in cm) { for (var i = 0; i < cm[k].length; i++) { var m = cm[k][i]; if (members.indexOf(m.name) === -1) members.push(m.name); } }
|
|
1104
|
+
var filtered = members.filter(function(n) { return n.toLowerCase().indexOf(query) === 0 && n !== CONFIG.name; });
|
|
1105
|
+
if (!filtered.length) { ac.style.display = 'none'; return; }
|
|
1106
|
+
acSelected = 0;
|
|
1107
|
+
ac.style.display = 'block';
|
|
1108
|
+
ac.innerHTML = filtered.slice(0, 6).map(function(n, i) {
|
|
1109
|
+
return '<div style="padding:6px 10px;cursor:pointer;border-radius:4px;font-size:0.8rem;color:var(--text)' + (i === 0 ? ';background:var(--bg-alt)' : '') + '" onmousedown="insertMention(\'' + n + '\')">' + n + '</div>';
|
|
1110
|
+
}).join('');
|
|
1111
|
+
}
|
|
1112
|
+
window.onInputChange = onInputChange;
|
|
1113
|
+
|
|
1114
|
+
function insertMention(name) {
|
|
1115
|
+
var input = document.getElementById('msg-input');
|
|
1116
|
+
var val = input.value;
|
|
1117
|
+
input.value = val.slice(0, val.lastIndexOf('@')) + '@' + name + ' ';
|
|
1118
|
+
input.focus();
|
|
1119
|
+
document.getElementById('autocomplete').style.display = 'none';
|
|
1120
|
+
}
|
|
1121
|
+
window.insertMention = insertMention;
|
|
1122
|
+
|
|
1123
|
+
function onInputKey(e) {
|
|
1124
|
+
if (e.key === 'Enter' && document.getElementById('autocomplete').style.display === 'none') { sendMsg(); return; }
|
|
1125
|
+
var ac = document.getElementById('autocomplete');
|
|
1126
|
+
if (ac.style.display === 'none') return;
|
|
1127
|
+
var items = ac.children;
|
|
1128
|
+
if (e.key === 'ArrowDown') { acSelected = Math.min(acSelected + 1, items.length - 1); e.preventDefault(); }
|
|
1129
|
+
if (e.key === 'ArrowUp') { acSelected = Math.max(acSelected - 1, 0); e.preventDefault(); }
|
|
1130
|
+
if (e.key === 'Tab' || e.key === 'Enter') { if (items[acSelected]) { insertMention(items[acSelected].textContent); e.preventDefault(); } }
|
|
1131
|
+
if (e.key === 'Escape') { ac.style.display = 'none'; }
|
|
1132
|
+
for (var i = 0; i < items.length; i++) items[i].style.background = i === acSelected ? 'var(--bg-alt)' : '';
|
|
1133
|
+
}
|
|
1134
|
+
window.onInputKey = onInputKey;
|
|
1135
|
+
|
|
946
1136
|
// Restore saved theme (default: dark)
|
|
947
1137
|
var savedTheme = localStorage.getItem('ac-theme') || 'dark';
|
|
948
1138
|
document.documentElement.classList.add(savedTheme);
|
package/ui/index.html
CHANGED
|
@@ -32,6 +32,13 @@
|
|
|
32
32
|
<div class="empty">Waiting for messages...</div>
|
|
33
33
|
</div>
|
|
34
34
|
</div>
|
|
35
|
+
<div style="position:relative">
|
|
36
|
+
<div id="autocomplete" style="display:none;position:absolute;bottom:100%;left:24px;background:var(--bg);border:1px solid var(--border);border-radius:6px;padding:4px;max-height:150px;overflow-y:auto;box-shadow:0 -4px 12px rgba(0,0,0,0.2);z-index:100"></div>
|
|
37
|
+
<div style="padding:8px 24px;border-top:1px solid var(--border);display:flex;gap:8px">
|
|
38
|
+
<input type="text" id="msg-input" placeholder="Type a message... (@name to mention)" style="flex:1;padding:8px 12px;border:1px solid var(--border);border-radius:8px;font-size:0.85rem;background:var(--bg);color:var(--text);outline:none" oninput="onInputChange(this)" onkeydown="onInputKey(event)">
|
|
39
|
+
<button onclick="sendMsg()" style="padding:8px 16px;border:none;border-radius:8px;background:#1a1a1a;color:#fff;font-size:0.85rem;cursor:pointer;font-weight:600">Send</button>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
35
42
|
</div>
|
|
36
43
|
<div class="members" id="members-panel">
|
|
37
44
|
<div class="members__header">Members</div>
|