agentchannel 0.8.1 → 0.9.1
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 +116 -77
- package/dist/brain.d.ts +78 -0
- package/dist/brain.js +271 -0
- package/dist/brain.js.map +1 -0
- package/dist/cli.js +312 -8
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +25 -1
- package/dist/config.js +104 -6
- package/dist/config.js.map +1 -1
- package/dist/crypto.d.ts +34 -4
- package/dist/crypto.js +42 -6
- package/dist/crypto.js.map +1 -1
- package/dist/distill.d.ts +24 -0
- package/dist/distill.js +404 -0
- package/dist/distill.js.map +1 -0
- package/dist/forwarder.d.ts +11 -0
- package/dist/forwarder.js +105 -0
- package/dist/forwarder.js.map +1 -0
- package/dist/local-store.d.ts +7 -0
- package/dist/local-store.js +54 -0
- package/dist/local-store.js.map +1 -0
- package/dist/mqtt-client.d.ts +11 -0
- package/dist/mqtt-client.js +369 -27
- package/dist/mqtt-client.js.map +1 -1
- package/dist/persistence.d.ts +23 -0
- package/dist/persistence.js +61 -0
- package/dist/persistence.js.map +1 -1
- package/dist/server.js +77 -3
- package/dist/server.js.map +1 -1
- package/dist/store.d.ts +3 -0
- package/dist/store.js +16 -2
- package/dist/store.js.map +1 -1
- package/dist/tools/brain.d.ts +2 -0
- package/dist/tools/brain.js +96 -0
- package/dist/tools/brain.js.map +1 -0
- package/dist/tools/channel.js +6 -6
- package/dist/tools/channel.js.map +1 -1
- package/dist/tools/get-message.js +1 -1
- package/dist/tools/get-message.js.map +1 -1
- package/dist/tools/hooks.d.ts +2 -0
- package/dist/tools/hooks.js +99 -0
- package/dist/tools/hooks.js.map +1 -0
- package/dist/tools/index.js +12 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/info.js +3 -1
- package/dist/tools/info.js.map +1 -1
- package/dist/tools/kick.d.ts +3 -0
- package/dist/tools/kick.js +52 -0
- package/dist/tools/kick.js.map +1 -0
- package/dist/tools/members.js +3 -3
- package/dist/tools/members.js.map +1 -1
- package/dist/tools/read.js +9 -6
- package/dist/tools/read.js.map +1 -1
- package/dist/tools/registry.d.ts +3 -0
- package/dist/tools/registry.js +82 -0
- package/dist/tools/registry.js.map +1 -0
- package/dist/tools/retract.d.ts +3 -0
- package/dist/tools/retract.js +27 -0
- package/dist/tools/retract.js.map +1 -0
- package/dist/tools/update-channel.d.ts +3 -0
- package/dist/tools/update-channel.js +50 -0
- package/dist/tools/update-channel.js.map +1 -0
- package/dist/types.d.ts +43 -1
- package/dist/web.d.ts +1 -0
- package/dist/web.js +91 -1
- package/dist/web.js.map +1 -1
- package/package.json +3 -2
- package/ui/app.js +715 -86
- package/ui/index.html +21 -11
- package/ui/marked.min.js +69 -0
- package/ui/mqtt.min.js +19 -0
- package/ui/style.css +175 -66
package/ui/app.js
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
// ---------------------------------------------------------------------------
|
|
5
5
|
// API Adapter Layer — auto-detect environment
|
|
6
6
|
// ---------------------------------------------------------------------------
|
|
7
|
-
|
|
7
|
+
var isTauri = window.isTauri || !!window.__TAURI__;
|
|
8
|
+
|
|
8
9
|
|
|
9
10
|
const API = isTauri ? {
|
|
10
11
|
invoke: window.__TAURI__.core.invoke,
|
|
@@ -45,6 +46,95 @@ var dmNames = {}; // theirFingerprint -> display name
|
|
|
45
46
|
var encoder = new TextEncoder();
|
|
46
47
|
var decoder = new TextDecoder();
|
|
47
48
|
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Sidebar collapse/expand
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
function toggleSidebar() {
|
|
53
|
+
var el = document.getElementById('sidebar');
|
|
54
|
+
el.style.width = '';
|
|
55
|
+
el.classList.toggle('collapsed');
|
|
56
|
+
var collapsed = el.classList.contains('collapsed');
|
|
57
|
+
localStorage.setItem('ac-sidebar-collapsed', collapsed);
|
|
58
|
+
if (!collapsed) {
|
|
59
|
+
var saved = localStorage.getItem('ac-width-sidebar');
|
|
60
|
+
if (saved) el.style.width = saved;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function toggleMembers() {
|
|
64
|
+
var el = document.getElementById('members-panel');
|
|
65
|
+
el.style.width = '';
|
|
66
|
+
el.classList.toggle('collapsed');
|
|
67
|
+
var collapsed = el.classList.contains('collapsed');
|
|
68
|
+
localStorage.setItem('ac-members-collapsed', collapsed);
|
|
69
|
+
if (!collapsed) {
|
|
70
|
+
var saved = localStorage.getItem('ac-width-members-panel');
|
|
71
|
+
if (saved) el.style.width = saved;
|
|
72
|
+
}
|
|
73
|
+
var badge = document.getElementById('members-badge');
|
|
74
|
+
if (badge) badge.classList.toggle('hidden', !collapsed);
|
|
75
|
+
}
|
|
76
|
+
window.toggleSidebar = toggleSidebar;
|
|
77
|
+
window.toggleMembers = toggleMembers;
|
|
78
|
+
|
|
79
|
+
// Drag resize sidebars
|
|
80
|
+
(function() {
|
|
81
|
+
function setupResize(handleId, targetId, side) {
|
|
82
|
+
var handle = document.getElementById(handleId);
|
|
83
|
+
if (!handle) return;
|
|
84
|
+
var dragging = false;
|
|
85
|
+
handle.addEventListener('mousedown', function(e) {
|
|
86
|
+
dragging = true;
|
|
87
|
+
handle.classList.add('active');
|
|
88
|
+
document.body.style.cursor = 'col-resize';
|
|
89
|
+
document.body.style.userSelect = 'none';
|
|
90
|
+
e.preventDefault();
|
|
91
|
+
});
|
|
92
|
+
document.addEventListener('mousemove', function(e) {
|
|
93
|
+
if (!dragging) return;
|
|
94
|
+
var target = document.getElementById(targetId);
|
|
95
|
+
if (!target || target.classList.contains('collapsed')) return;
|
|
96
|
+
if (side === 'left') {
|
|
97
|
+
var w = Math.max(180, Math.min(400, e.clientX));
|
|
98
|
+
target.style.width = w + 'px';
|
|
99
|
+
} else {
|
|
100
|
+
var w = Math.max(140, Math.min(350, window.innerWidth - e.clientX));
|
|
101
|
+
target.style.width = w + 'px';
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
document.addEventListener('mouseup', function() {
|
|
105
|
+
if (dragging) {
|
|
106
|
+
dragging = false;
|
|
107
|
+
handle.classList.remove('active');
|
|
108
|
+
document.body.style.cursor = '';
|
|
109
|
+
document.body.style.userSelect = '';
|
|
110
|
+
var target = document.getElementById(targetId);
|
|
111
|
+
if (target && target.style.width) {
|
|
112
|
+
localStorage.setItem('ac-width-' + targetId, target.style.width);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
setupResize('resize-left', 'sidebar', 'left');
|
|
118
|
+
setupResize('resize-right', 'members-panel', 'right');
|
|
119
|
+
// Restore saved widths
|
|
120
|
+
['sidebar', 'members-panel'].forEach(function(id) {
|
|
121
|
+
var saved = localStorage.getItem('ac-width-' + id);
|
|
122
|
+
var el = document.getElementById(id);
|
|
123
|
+
if (saved && el && !el.classList.contains('collapsed')) {
|
|
124
|
+
el.style.width = saved;
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
})();
|
|
128
|
+
// Restore collapsed state on load
|
|
129
|
+
(function() {
|
|
130
|
+
if (localStorage.getItem('ac-sidebar-collapsed') === 'true') {
|
|
131
|
+
document.getElementById('sidebar').classList.add('collapsed');
|
|
132
|
+
}
|
|
133
|
+
if (localStorage.getItem('ac-members-collapsed') === 'true') {
|
|
134
|
+
document.getElementById('members-panel').classList.add('collapsed');
|
|
135
|
+
}
|
|
136
|
+
})();
|
|
137
|
+
|
|
48
138
|
function getColor(name) {
|
|
49
139
|
if (!senderColors[name]) senderColors[name] = COLORS[Object.keys(senderColors).length % COLORS.length];
|
|
50
140
|
return senderColors[name];
|
|
@@ -91,6 +181,7 @@ async function hashSubWeb(channelKey, subName) {
|
|
|
91
181
|
return Array.from(topicBytes).map(function(b) { return b.toString(16).padStart(2, "0"); }).join("");
|
|
92
182
|
}
|
|
93
183
|
|
|
184
|
+
|
|
94
185
|
async function deriveDmKeyWeb(fpA, fpB) {
|
|
95
186
|
var sorted = [fpA, fpB].sort();
|
|
96
187
|
var ikm = sorted[0] + sorted[1];
|
|
@@ -138,11 +229,11 @@ function chId(ch) {
|
|
|
138
229
|
}
|
|
139
230
|
|
|
140
231
|
function chLabel(ch) {
|
|
141
|
-
return ch.subchannel ? '
|
|
232
|
+
return ch.subchannel ? '#' + ch.channel + '/' + ch.subchannel : '#' + ch.channel;
|
|
142
233
|
}
|
|
143
234
|
|
|
144
235
|
function chFullLabel(ch) {
|
|
145
|
-
return ch.subchannel ? '#' + ch.channel + '
|
|
236
|
+
return ch.subchannel ? '#' + ch.channel + '/' + ch.subchannel : '#' + ch.channel;
|
|
146
237
|
}
|
|
147
238
|
|
|
148
239
|
var INLINE_TAG_COLORS = {
|
|
@@ -153,7 +244,7 @@ var INLINE_TAG_COLORS = {
|
|
|
153
244
|
};
|
|
154
245
|
|
|
155
246
|
// ---------------------------------------------------------------------------
|
|
156
|
-
// Rich text rendering (markdown + @mentions + #channels +
|
|
247
|
+
// Rich text rendering (markdown + @mentions + #channels + /subchannels)
|
|
157
248
|
// ---------------------------------------------------------------------------
|
|
158
249
|
function richText(t) {
|
|
159
250
|
// Let marked parse markdown (preserves code blocks with <pre><code>)
|
|
@@ -163,10 +254,10 @@ function richText(t) {
|
|
|
163
254
|
var knownChannels = CONFIG.channels.filter(function(c) { return !c.subchannel; }).map(function(c) { return c.channel; });
|
|
164
255
|
var knownSubs = CONFIG.channels.filter(function(c) { return c.subchannel; }).map(function(c) { return c.subchannel; });
|
|
165
256
|
|
|
166
|
-
// Replace
|
|
257
|
+
// Replace /subchannel references in message text
|
|
167
258
|
for (var ki = 0; ki < knownSubs.length; ki++) {
|
|
168
|
-
s = s.split('
|
|
169
|
-
'<span class="channel-tag" onclick="window.switchToSub(\'' + knownSubs[ki] + '\')"
|
|
259
|
+
s = s.split('/' + knownSubs[ki]).join(
|
|
260
|
+
'<span class="channel-tag" onclick="window.switchToSub(\'' + knownSubs[ki] + '\')">/' + knownSubs[ki] + '</span>'
|
|
170
261
|
);
|
|
171
262
|
}
|
|
172
263
|
// Replace #channel references
|
|
@@ -262,12 +353,23 @@ function render() {
|
|
|
262
353
|
var msgFp = msg.senderKey ? '(' + msg.senderKey.slice(0, 4) + ')' : '';
|
|
263
354
|
html += '<span class="conversation__sender">' + esc(msg.sender) + '<span style="color:var(--text-muted);font-weight:400;font-size:0.65rem;margin-left:2px">' + msgFp + '</span></span>';
|
|
264
355
|
if (activeChannel === "@me") {
|
|
265
|
-
var mlabel = msg.subchannel ? '#' + esc(msg.channel) + '
|
|
356
|
+
var mlabel = msg.subchannel ? '#' + esc(msg.channel) + '/' + esc(msg.subchannel) : '#' + esc(msg.channel);
|
|
266
357
|
html += '<span class="conversation__channel">' + mlabel + '</span>';
|
|
267
358
|
}
|
|
268
359
|
html += '<span class="conversation__time">' + time + '</span>';
|
|
269
360
|
html += '</div>';
|
|
270
|
-
|
|
361
|
+
if (msg.subject) {
|
|
362
|
+
html += '<div class="conversation__subject">' + esc(msg.subject) + '</div>';
|
|
363
|
+
}
|
|
364
|
+
if (msg.tags && msg.tags.length) {
|
|
365
|
+
html += '<div class="conversation__tags">' + msg.tags.map(function(t) { return '<span class="tag">[' + esc(t) + ']</span>'; }).join(' ') + '</div>';
|
|
366
|
+
}
|
|
367
|
+
if (msg.retracted) {
|
|
368
|
+
html += '<div class="conversation__text retracted"><span class="retracted-label">retracted</span>' + richText(msg.content) + '</div>';
|
|
369
|
+
} else {
|
|
370
|
+
html += '<div class="conversation__text">' + richText(msg.content) + '</div>';
|
|
371
|
+
}
|
|
372
|
+
html += '<button class="msg-copy" data-msg="' + esc(msg.content || '').replace(/"/g, '"') + '" onclick="window.copyMsg(this)">copy</button>';
|
|
271
373
|
|
|
272
374
|
lastSender = msg.sender;
|
|
273
375
|
lastChannel = msg.channel;
|
|
@@ -277,6 +379,16 @@ function render() {
|
|
|
277
379
|
|
|
278
380
|
msgsEl.innerHTML = html;
|
|
279
381
|
scrollEl.scrollTop = scrollEl.scrollHeight;
|
|
382
|
+
|
|
383
|
+
// Announcement mode: disable input for non-owners
|
|
384
|
+
var msgInput = document.getElementById('msg-input');
|
|
385
|
+
if (msgInput) {
|
|
386
|
+
var chName = activeChannel.split('/')[0];
|
|
387
|
+
var meta = channelMetas[chName];
|
|
388
|
+
var isAnnouncement = meta && meta.mode === 'announcement' && (!CONFIG.fingerprint || meta.owners.indexOf(CONFIG.fingerprint) === -1);
|
|
389
|
+
msgInput.disabled = !!isAnnouncement;
|
|
390
|
+
msgInput.placeholder = isAnnouncement ? 'This is an announcement channel (read-only)' : 'Type a message...';
|
|
391
|
+
}
|
|
280
392
|
}
|
|
281
393
|
|
|
282
394
|
// ---------------------------------------------------------------------------
|
|
@@ -287,6 +399,8 @@ function renderSidebar() {
|
|
|
287
399
|
el.innerHTML = "";
|
|
288
400
|
var lockIcon = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>';
|
|
289
401
|
var globeIcon = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>';
|
|
402
|
+
var syncOnIcon = '<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M21.5 2v6h-6M2.5 22v-6h6"/><path d="M2.5 11.5a10 10 0 0 1 17.5-5.5M21.5 12.5a10 10 0 0 1-17.5 5.5"/></svg>';
|
|
403
|
+
var syncOffIcon = '<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21.5 2v6h-6M2.5 22v-6h6"/><path d="M2.5 11.5a10 10 0 0 1 17.5-5.5M21.5 12.5a10 10 0 0 1-17.5 5.5"/></svg>';
|
|
290
404
|
|
|
291
405
|
// Sort channels alphabetically, group subchannels under parent
|
|
292
406
|
var sorted = CONFIG.channels.slice().sort(function(a, b) { return chId(a).localeCompare(chId(b)); });
|
|
@@ -370,23 +484,38 @@ function renderSidebar() {
|
|
|
370
484
|
var cid = chId(ch);
|
|
371
485
|
div.className = "sidebar__channel" + (activeChannel === cid ? " active" : "");
|
|
372
486
|
var count = unreadCounts[cid] || 0;
|
|
373
|
-
var
|
|
374
|
-
|
|
375
|
-
var
|
|
376
|
-
|
|
377
|
-
|
|
487
|
+
var isSynced = ch.sync !== undefined ? ch.sync : !isOfficial;
|
|
488
|
+
// Left: # + name + lock (private only, public has no icon)
|
|
489
|
+
var chHash = (window.acChannels && window.acChannels[cid]) ? window.acChannels[cid].channelHash : '';
|
|
490
|
+
var shortId = chHash ? chHash.slice(0, 4) : '';
|
|
491
|
+
var leftPart = '<span style="display:flex;align-items:center;gap:2px;min-width:0;overflow:hidden">' +
|
|
492
|
+
'<span style="color:var(--accent);flex-shrink:0;opacity:0.7">#</span>' +
|
|
493
|
+
'<span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + esc(ch.channel) + '</span>' +
|
|
494
|
+
(shortId && !isOfficial ? '<span style="opacity:0.3;font-size:0.7em;margin-left:3px;flex-shrink:0">(' + shortId + ')</span>' : '') +
|
|
495
|
+
(!isOfficial ? '<span style="opacity:0.35;flex-shrink:0;display:inline-flex;margin-left:2px">' + lockIcon + '</span>' : '') +
|
|
496
|
+
'</span>';
|
|
497
|
+
// Right: badge + sync + arrow (flex-end, icons only on hover except badge)
|
|
498
|
+
var rightPart = '<span class="sidebar__channel-actions" style="display:flex;align-items:center;gap:2px;margin-left:auto;flex-shrink:0">';
|
|
499
|
+
if (count) rightPart += '<span class="badge">' + count + '</span>';
|
|
500
|
+
rightPart += '<span class="sync-toggle" data-channel="' + esc(ch.channel) + '" data-synced="' + (isSynced ? '1' : '0') + '" title="' + (isSynced ? 'Sync ON' : 'Sync OFF') + '" style="cursor:pointer;display:inline-flex;padding:2px">' + (isSynced ? syncOnIcon : syncOffIcon) + '</span>';
|
|
378
501
|
if (hasChildren) {
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
502
|
+
rightPart += '<span class="sidebar__arrow" data-ch="' + esc(ch.channel) + '" data-collapsed="' + (collapsed ? '1' : '0') + '" style="cursor:pointer;font-size:0.5rem;opacity:0.4;padding:2px 2px;display:inline-flex">' + (collapsed ? '\u25B6' : '\u25BC') + '</span>';
|
|
503
|
+
} else if (!isOfficial) {
|
|
504
|
+
rightPart += '<span style="font-size:0.5rem;padding:2px 2px;display:inline-flex;visibility:hidden">\u25BC</span>';
|
|
505
|
+
}
|
|
506
|
+
rightPart += '</span>';
|
|
507
|
+
div.innerHTML = leftPart + rightPart;
|
|
508
|
+
|
|
509
|
+
// Arrow click handler (delegated)
|
|
510
|
+
var arrowEl = div.querySelector('.sidebar__arrow');
|
|
511
|
+
if (arrowEl) {
|
|
382
512
|
(function(chName, wasCollapsed) {
|
|
383
|
-
|
|
513
|
+
arrowEl.onclick = function(e) {
|
|
384
514
|
e.stopPropagation();
|
|
385
515
|
collapsedGroups[chName] = !wasCollapsed;
|
|
386
516
|
renderSidebar();
|
|
387
517
|
};
|
|
388
518
|
})(ch.channel, collapsed);
|
|
389
|
-
div.appendChild(arrowBtn);
|
|
390
519
|
}
|
|
391
520
|
|
|
392
521
|
(function(chObj, channelId) {
|
|
@@ -413,14 +542,14 @@ function renderSidebar() {
|
|
|
413
542
|
var subDiv = document.createElement("div");
|
|
414
543
|
subDiv.className = "sidebar__channel sub" + (activeChannel === subCid ? " active" : "");
|
|
415
544
|
var subCount = unreadCounts[subCid] || 0;
|
|
416
|
-
subDiv.innerHTML = '<span style="color:var(--accent);margin-right:2px;opacity:0.5"
|
|
545
|
+
subDiv.innerHTML = '<span style="color:var(--accent);margin-right:2px;opacity:0.5">/</span>' + esc(sub.subchannel) + (subCount ? '<span class="badge">' + subCount + '</span>' : "");
|
|
417
546
|
(function(subObj, parentChannel, subChannelId) {
|
|
418
547
|
subDiv.onclick = function() {
|
|
419
548
|
activeChannel = subChannelId;
|
|
420
549
|
unreadCounts[subChannelId] = 0;
|
|
421
|
-
headerName.textContent = "
|
|
550
|
+
headerName.textContent = "#" + ch.channel + "/" + subObj.subchannel;
|
|
422
551
|
var subDesc = (channelMetas[parentChannel] && channelMetas[parentChannel].descriptions && channelMetas[parentChannel].descriptions[subObj.subchannel]) || "";
|
|
423
|
-
headerDesc.textContent =
|
|
552
|
+
headerDesc.textContent = subDesc;
|
|
424
553
|
document.title = "AgentChannel";
|
|
425
554
|
history.pushState(null, "", "/channel/" + encodeURIComponent(parentChannel) + "/sub/" + encodeURIComponent(subObj.subchannel));
|
|
426
555
|
renderSidebar();
|
|
@@ -432,8 +561,112 @@ function renderSidebar() {
|
|
|
432
561
|
}
|
|
433
562
|
}
|
|
434
563
|
}
|
|
564
|
+
|
|
565
|
+
// + Create channel button
|
|
566
|
+
var createDiv = document.createElement("div");
|
|
567
|
+
createDiv.className = "sidebar__channel sidebar__create";
|
|
568
|
+
createDiv.innerHTML = '<span style="color:var(--accent);margin-right:4px;font-size:0.9rem">+</span> Create channel';
|
|
569
|
+
createDiv.onclick = function() { window.openCreateChannel(); };
|
|
570
|
+
el.appendChild(createDiv);
|
|
435
571
|
}
|
|
436
572
|
|
|
573
|
+
// ---------------------------------------------------------------------------
|
|
574
|
+
// Create channel modal
|
|
575
|
+
// ---------------------------------------------------------------------------
|
|
576
|
+
window.openCreateChannel = function() {
|
|
577
|
+
var overlay = document.createElement('div');
|
|
578
|
+
overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:9999;display:flex;align-items:center;justify-content:center';
|
|
579
|
+
overlay.onclick = function(e) { if (e.target === overlay) document.body.removeChild(overlay); };
|
|
580
|
+
|
|
581
|
+
overlay.innerHTML = '<div style="background:var(--bg);border:1px solid var(--border);border-radius:12px;padding:24px;width:380px;max-width:90%;box-shadow:0 8px 32px rgba(0,0,0,0.5)">' +
|
|
582
|
+
'<h3 style="font-size:1rem;color:var(--text);margin-bottom:18px">Create channel</h3>' +
|
|
583
|
+
'<div style="font-size:0.7rem;color:var(--text-secondary);margin-bottom:5px">Channel name</div>' +
|
|
584
|
+
'<input id="create-ch-name" placeholder="my-project" autocomplete="off" style="width:100%;padding:9px 12px;border:1px solid var(--border);border-radius:6px;font-size:0.88rem;background:var(--bg-alt);color:var(--text);outline:none;margin-bottom:16px;-webkit-appearance:none" onfocus="this.style.borderColor=\'var(--accent-brand)\'" onblur="this.style.borderColor=\'var(--border)\'" autofocus>' +
|
|
585
|
+
'<div style="font-size:0.7rem;color:var(--text-secondary);margin-bottom:5px">Description <span style="color:var(--text-muted)">(optional — rules, purpose, what to post)</span></div>' +
|
|
586
|
+
'<textarea id="create-ch-desc" placeholder="e.g. CI alerts from GitHub Actions. Only post failures." rows="2" style="width:100%;padding:9px 12px;border:1px solid var(--border);border-radius:6px;font-size:0.82rem;background:var(--bg-alt);color:var(--text);outline:none;margin-bottom:16px;resize:vertical;font-family:inherit;-webkit-appearance:none" onfocus="this.style.borderColor=\'var(--accent-brand)\'" onblur="this.style.borderColor=\'var(--border)\'"></textarea>' +
|
|
587
|
+
'<input type="hidden" id="create-ch-public" value="0">' +
|
|
588
|
+
'<div style="display:flex;gap:0;margin-bottom:12px;border:1px solid var(--border);border-radius:6px;overflow:hidden">' +
|
|
589
|
+
'<button id="create-tab-private" onclick="window.setChType(false)" style="flex:1;padding:7px;border:none;background:var(--accent-brand);color:#0a0a0a;font-size:0.78rem;cursor:pointer;font-weight:500">Private</button>' +
|
|
590
|
+
'<button id="create-tab-public" onclick="window.setChType(true)" style="flex:1;padding:7px;border:none;background:var(--bg-alt);color:var(--text-secondary);font-size:0.78rem;cursor:pointer;font-weight:400">Public</button>' +
|
|
591
|
+
'</div>' +
|
|
592
|
+
'<div style="font-size:0.68rem;color:var(--text-muted);margin-bottom:18px;line-height:1.4" id="create-ch-hint">Invite only. Your agents auto-join. End-to-end encrypted.</div>' +
|
|
593
|
+
'<div style="display:flex;gap:8px;justify-content:flex-end">' +
|
|
594
|
+
'<button onclick="this.closest(\'div[style*=fixed]\').remove()" style="padding:7px 14px;border:1px solid var(--border);border-radius:6px;background:var(--bg-alt);color:var(--text);cursor:pointer;font-size:0.78rem">Cancel</button>' +
|
|
595
|
+
'<button id="create-ch-btn" onclick="window.doCreateChannel()" style="padding:7px 14px;border:none;border-radius:6px;background:var(--text);color:var(--bg);cursor:pointer;font-size:0.78rem;font-weight:600">Create</button>' +
|
|
596
|
+
'</div></div>';
|
|
597
|
+
|
|
598
|
+
document.body.appendChild(overlay);
|
|
599
|
+
document.getElementById('create-ch-name').focus();
|
|
600
|
+
|
|
601
|
+
window.setChType = function(isPublic) {
|
|
602
|
+
document.getElementById('create-ch-public').value = isPublic ? '1' : '0';
|
|
603
|
+
var priv = document.getElementById('create-tab-private');
|
|
604
|
+
var pub = document.getElementById('create-tab-public');
|
|
605
|
+
var hint = document.getElementById('create-ch-hint');
|
|
606
|
+
priv.style.background = isPublic ? 'var(--bg-alt)' : 'var(--accent-brand)';
|
|
607
|
+
priv.style.color = isPublic ? 'var(--text-secondary)' : '#0a0a0a';
|
|
608
|
+
priv.style.fontWeight = isPublic ? '400' : '500';
|
|
609
|
+
pub.style.background = isPublic ? 'var(--accent-brand)' : 'var(--bg-alt)';
|
|
610
|
+
pub.style.color = isPublic ? '#0a0a0a' : 'var(--text-secondary)';
|
|
611
|
+
pub.style.fontWeight = isPublic ? '500' : '400';
|
|
612
|
+
if (hint) hint.textContent = isPublic
|
|
613
|
+
? 'Anyone can discover and join. End-to-end encrypted.'
|
|
614
|
+
: 'Invite only. Your agents auto-join. End-to-end encrypted.';
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
// Enter key to create
|
|
618
|
+
document.getElementById('create-ch-name').onkeydown = function(e) {
|
|
619
|
+
if (e.key === 'Enter') window.doCreateChannel();
|
|
620
|
+
};
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
window.doCreateChannel = function() {
|
|
624
|
+
var nameInput = document.getElementById('create-ch-name');
|
|
625
|
+
var name = nameInput ? nameInput.value.trim().toLowerCase().replace(/\s+/g, '-') : '';
|
|
626
|
+
if (!name) { nameInput.style.outline = '2px solid #ff4444'; return; }
|
|
627
|
+
var descInput = document.getElementById('create-ch-desc');
|
|
628
|
+
var desc = descInput ? descInput.value.trim() : '';
|
|
629
|
+
var pubEl = document.getElementById('create-ch-public');
|
|
630
|
+
var isPublic = pubEl ? pubEl.value === '1' : false;
|
|
631
|
+
|
|
632
|
+
// Generate random key
|
|
633
|
+
var keyArr = new Uint8Array(32);
|
|
634
|
+
crypto.getRandomValues(keyArr);
|
|
635
|
+
var key = btoa(String.fromCharCode.apply(null, keyArr));
|
|
636
|
+
|
|
637
|
+
function onCreated() {
|
|
638
|
+
CONFIG.channels.push({ channel: name, key: key });
|
|
639
|
+
activeChannel = name;
|
|
640
|
+
headerName.textContent = '#' + name;
|
|
641
|
+
headerDesc.textContent = '';
|
|
642
|
+
document.title = 'AgentChannel';
|
|
643
|
+
var overlay = document.querySelector('div[style*=fixed]');
|
|
644
|
+
if (overlay) overlay.remove();
|
|
645
|
+
renderSidebar();
|
|
646
|
+
render();
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
if (isTauri && window.__TAURI__) {
|
|
650
|
+
// Tauri mode: invoke backend to save config
|
|
651
|
+
window.__TAURI__.core.invoke('create_channel', { channel: name, key: key }).then(onCreated).catch(function() {
|
|
652
|
+
// Fallback: just add to local config
|
|
653
|
+
onCreated();
|
|
654
|
+
});
|
|
655
|
+
} else {
|
|
656
|
+
// Web mode: call API
|
|
657
|
+
fetch('/api/create-channel', {
|
|
658
|
+
method: 'POST',
|
|
659
|
+
headers: { 'Content-Type': 'application/json' },
|
|
660
|
+
body: JSON.stringify({ channel: name, key: key, public: isPublic, desc: desc })
|
|
661
|
+
}).then(function(r) { return r.json(); }).then(function(data) {
|
|
662
|
+
if (data.ok) onCreated();
|
|
663
|
+
}).catch(function() {
|
|
664
|
+
// Fallback: just add to local config anyway
|
|
665
|
+
onCreated();
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
};
|
|
669
|
+
|
|
437
670
|
// ---------------------------------------------------------------------------
|
|
438
671
|
// Channel actions
|
|
439
672
|
// ---------------------------------------------------------------------------
|
|
@@ -465,15 +698,20 @@ window.shareChannel = function() {
|
|
|
465
698
|
|
|
466
699
|
window.leaveChannel = function() {
|
|
467
700
|
if (!confirm("Leave #" + activeChannel + "?")) return;
|
|
468
|
-
|
|
469
|
-
CONFIG.channels = CONFIG.channels.filter(function(c) { return c.channel !==
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
701
|
+
var leaving = activeChannel;
|
|
702
|
+
CONFIG.channels = CONFIG.channels.filter(function(c) { return c.channel !== leaving; });
|
|
703
|
+
// Persist via API
|
|
704
|
+
fetch('/api/leave-channel', {
|
|
705
|
+
method: 'POST',
|
|
706
|
+
headers: { 'Content-Type': 'application/json' },
|
|
707
|
+
body: JSON.stringify({ channel: leaving })
|
|
708
|
+
}).catch(function() {});
|
|
709
|
+
activeChannel = CONFIG.channels.length ? CONFIG.channels[0].channel : "";
|
|
710
|
+
headerName.textContent = activeChannel ? "#" + activeChannel : "";
|
|
711
|
+
headerDesc.textContent = "";
|
|
473
712
|
renderSidebar();
|
|
474
713
|
render();
|
|
475
714
|
if (window.renderMembers) window.renderMembers();
|
|
476
|
-
alert('Left channel. Run "agentchannel leave --channel <name>" in CLI to persist.');
|
|
477
715
|
};
|
|
478
716
|
|
|
479
717
|
window.copyCode = function(btn) {
|
|
@@ -495,6 +733,16 @@ window.copyMsg = function(btn) {
|
|
|
495
733
|
// Init
|
|
496
734
|
// ---------------------------------------------------------------------------
|
|
497
735
|
async function init() {
|
|
736
|
+
// In Tauri mode, load config + identity from backend
|
|
737
|
+
if (isTauri) {
|
|
738
|
+
CONFIG = await API.invoke('get_config');
|
|
739
|
+
var ident = await API.invoke('get_identity');
|
|
740
|
+
if (ident) {
|
|
741
|
+
CONFIG.fingerprint = ident.fingerprint;
|
|
742
|
+
}
|
|
743
|
+
try { CONFIG.version = await API.invoke('get_version'); } catch(e) {}
|
|
744
|
+
}
|
|
745
|
+
|
|
498
746
|
renderSidebar();
|
|
499
747
|
|
|
500
748
|
window.acChannels = window.acChannels || {};
|
|
@@ -507,6 +755,7 @@ async function init() {
|
|
|
507
755
|
channels[id] = {
|
|
508
756
|
key: await deriveSubKeyWeb(ch.key, ch.subchannel),
|
|
509
757
|
hash: await hashSubWeb(ch.key, ch.subchannel),
|
|
758
|
+
channelHash: ch.channelHash || await hashSubWeb(ch.key, ch.subchannel),
|
|
510
759
|
name: ch.channel,
|
|
511
760
|
sub: ch.subchannel
|
|
512
761
|
};
|
|
@@ -514,6 +763,7 @@ async function init() {
|
|
|
514
763
|
channels[id] = {
|
|
515
764
|
key: await deriveKey(ch.key),
|
|
516
765
|
hash: await hashRoom(ch.key),
|
|
766
|
+
channelHash: ch.channelHash || await hashRoom(ch.key),
|
|
517
767
|
name: ch.channel
|
|
518
768
|
};
|
|
519
769
|
}
|
|
@@ -523,8 +773,8 @@ async function init() {
|
|
|
523
773
|
var pendingSubs = [];
|
|
524
774
|
var fetchPromises = Object.keys(channels).map(function(chKey) {
|
|
525
775
|
var ch = channels[chKey];
|
|
526
|
-
return fetch("https://api.agentchannel.workers.dev/messages?channel_hash=" + ch.
|
|
527
|
-
.then(function(r) { return r.json(); })
|
|
776
|
+
return fetch("https://api.agentchannel.workers.dev/messages?channel_hash=" + ch.channelHash + "&since=0&limit=30")
|
|
777
|
+
.then(function(r) { return r.json(); }).catch(function() { return []; })
|
|
528
778
|
.then(async function(rows) {
|
|
529
779
|
for (var ri = 0; ri < rows.length; ri++) {
|
|
530
780
|
try {
|
|
@@ -562,11 +812,12 @@ async function init() {
|
|
|
562
812
|
if (channels[subId]) continue;
|
|
563
813
|
var subKey = await deriveSubKeyWeb(ps.key, ps.sub);
|
|
564
814
|
var subHash = await hashSubWeb(ps.key, ps.sub);
|
|
565
|
-
|
|
566
|
-
|
|
815
|
+
var subChannelHash = subHash; // At epoch 0, MQTT hash = storage hash
|
|
816
|
+
channels[subId] = {key: subKey, hash: subHash, channelHash: subChannelHash, name: ps.name, sub: ps.sub};
|
|
817
|
+
CONFIG.channels.push({channel: ps.name, subchannel: ps.sub, key: ps.key, channelHash: subChannelHash});
|
|
567
818
|
// Load subchannel history
|
|
568
819
|
try {
|
|
569
|
-
var sres = await fetch("https://api.agentchannel.workers.dev/messages?channel_hash=" +
|
|
820
|
+
var sres = await fetch("https://api.agentchannel.workers.dev/messages?channel_hash=" + subChannelHash + "&since=0&limit=30");
|
|
570
821
|
var srows = await sres.json();
|
|
571
822
|
for (var sri = 0; sri < srows.length; sri++) {
|
|
572
823
|
try {
|
|
@@ -593,8 +844,66 @@ async function init() {
|
|
|
593
844
|
if (userNameEl) {
|
|
594
845
|
userNameEl.textContent = "@" + CONFIG.name + (CONFIG.fingerprint ? " (" + CONFIG.fingerprint.slice(0, 4) + ")" : "");
|
|
595
846
|
}
|
|
847
|
+
var progressEl = document.getElementById("user-progress");
|
|
848
|
+
if (progressEl) progressEl.classList.add("connected");
|
|
849
|
+
var userInitialEl = document.getElementById("user-initial");
|
|
850
|
+
if (userInitialEl && CONFIG.name) {
|
|
851
|
+
userInitialEl.textContent = CONFIG.name.charAt(0).toUpperCase();
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// In Tauri mode: listen for messages from Rust MQTT backend
|
|
855
|
+
if (isTauri) {
|
|
856
|
+
window.__TAURI__.event.listen("new_message", function(event) {
|
|
857
|
+
var msg = event.payload;
|
|
858
|
+
if (!msg || !msg.channel) return;
|
|
859
|
+
|
|
860
|
+
// Deduplicate
|
|
861
|
+
if (allMessages.some(function(m) { return m.id === msg.id; })) return;
|
|
862
|
+
|
|
863
|
+
allMessages.push(msg);
|
|
864
|
+
|
|
865
|
+
var chKeyName = msg.subchannel ? msg.channel + '/' + msg.subchannel : msg.channel;
|
|
866
|
+
if (!onlineMembers[chKeyName]) onlineMembers[chKeyName] = new Set();
|
|
867
|
+
onlineMembers[chKeyName].add(msg.sender);
|
|
868
|
+
|
|
869
|
+
if (msg.sender !== CONFIG.name) {
|
|
870
|
+
if (activeChannel !== chKeyName && activeChannel !== "all") {
|
|
871
|
+
unreadCounts[chKeyName] = (unreadCounts[chKeyName] || 0) + 1;
|
|
872
|
+
renderSidebar();
|
|
873
|
+
}
|
|
874
|
+
var total = Object.values(unreadCounts).reduce(function(a, b) { return a + b; }, 0);
|
|
875
|
+
if (total > 0) document.title = "(" + total + ") AgentChannel";
|
|
876
|
+
var nlabel = msg.subchannel ? "#" + msg.channel + "/" + msg.subchannel : "#" + msg.channel;
|
|
877
|
+
if (Notification.permission === "granted" && (document.hidden || activeChannel !== chKeyName)) {
|
|
878
|
+
var n = new Notification(nlabel + " @" + msg.sender, {body: msg.subject || msg.content.slice(0, 100)});
|
|
879
|
+
n.onclick = function() { window.focus(); };
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
render();
|
|
883
|
+
renderMembers();
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
// Listen for auto-updater events from Rust backend
|
|
887
|
+
window.__TAURI__.event.listen("update_available", function(event) {
|
|
888
|
+
var version = event.payload;
|
|
889
|
+
var banner = document.getElementById("update-banner");
|
|
890
|
+
if (!banner) return;
|
|
891
|
+
banner.innerHTML = '<div style="text-align:center;padding:16px 24px">' +
|
|
892
|
+
'<div style="font-size:0.9rem;font-weight:600;color:var(--text);margin-bottom:8px">v' + version + ' available</div>' +
|
|
893
|
+
'<button id="relaunch-btn" style="width:100%;padding:8px;border:1px solid var(--border);border-radius:8px;background:var(--bg);color:var(--text);cursor:pointer;font-size:0.85rem">Relaunch</button>' +
|
|
894
|
+
'</div>';
|
|
895
|
+
banner.style.display = "block";
|
|
896
|
+
document.getElementById("relaunch-btn").onclick = function() {
|
|
897
|
+
this.textContent = "Updating...";
|
|
898
|
+
this.disabled = true;
|
|
899
|
+
window.__TAURI__.core.invoke("install_update").catch(function(e) {
|
|
900
|
+
banner.innerHTML = '<div style="text-align:center;padding:12px;font-size:0.75rem;color:var(--text-muted)">Update failed: ' + e + '</div>';
|
|
901
|
+
});
|
|
902
|
+
};
|
|
903
|
+
});
|
|
904
|
+
}
|
|
596
905
|
|
|
597
|
-
// Connect to MQTT for real-time messages
|
|
906
|
+
// Connect to MQTT for real-time messages (web mode, also Tauri fallback)
|
|
598
907
|
var client = mqtt.connect("wss://broker.emqx.io:8084/mqtt");
|
|
599
908
|
|
|
600
909
|
client.on("connect", function() {
|
|
@@ -605,31 +914,35 @@ async function init() {
|
|
|
605
914
|
client.subscribe("ac/1/" + ch.hash);
|
|
606
915
|
client.subscribe("ac/1/" + ch.hash + "/p");
|
|
607
916
|
}
|
|
608
|
-
// Check for updates — show banner
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
banner
|
|
616
|
-
'<div style="
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
917
|
+
// Check for updates — show banner (skip in Tauri mode, it has its own updater)
|
|
918
|
+
if (!isTauri) {
|
|
919
|
+
fetch("https://registry.npmjs.org/agentchannel/latest").then(function(r) {
|
|
920
|
+
return r.json();
|
|
921
|
+
}).then(function(d) {
|
|
922
|
+
if (d.version && d.version !== CONFIG.version) {
|
|
923
|
+
var banner = document.getElementById("update-banner");
|
|
924
|
+
if (banner) {
|
|
925
|
+
banner.innerHTML = '<div style="text-align:center;padding:16px 24px">' +
|
|
926
|
+
'<div style="font-size:0.9rem;font-weight:600;color:var(--text);margin-bottom:4px">Updated to ' + d.version + '</div>' +
|
|
927
|
+
'<div style="font-size:0.75rem;color:var(--text-muted);margin-bottom:12px">Relaunch to apply</div>' +
|
|
928
|
+
'<button onclick="navigator.clipboard.writeText(\'npm install -g agentchannel\');this.textContent=\'Copied! Run in terminal.\'" style="width:100%;padding:8px;border:1px solid var(--border);border-radius:8px;background:var(--bg);color:var(--text);cursor:pointer;font-size:0.85rem">Relaunch</button>' +
|
|
929
|
+
'</div>';
|
|
930
|
+
banner.style.display = "block";
|
|
931
|
+
}
|
|
621
932
|
}
|
|
622
|
-
}
|
|
623
|
-
}
|
|
933
|
+
}).catch(function() {});
|
|
934
|
+
}
|
|
624
935
|
});
|
|
625
936
|
|
|
626
937
|
client.on("close", function() {
|
|
627
938
|
var userBar2 = document.getElementById("user-info");
|
|
628
939
|
if (userBar2) userBar2.classList.remove("connected");
|
|
629
|
-
statusEl
|
|
940
|
+
var statusEl = document.querySelector(".sidebar__status");
|
|
941
|
+
if (statusEl) statusEl.className = "sidebar__status";
|
|
630
942
|
});
|
|
631
943
|
|
|
632
|
-
|
|
944
|
+
// Request notification permission (web mode only — Tauri uses native notifications from Rust)
|
|
945
|
+
if (!isTauri && Notification.permission === "default") Notification.requestPermission();
|
|
633
946
|
|
|
634
947
|
window.cloudMembers = window.cloudMembers || {};
|
|
635
948
|
var cloudMembers = window.cloudMembers;
|
|
@@ -638,7 +951,7 @@ async function init() {
|
|
|
638
951
|
for (var chKey in channels) {
|
|
639
952
|
var ch = channels[chKey];
|
|
640
953
|
try {
|
|
641
|
-
var res = await fetch("https://api.agentchannel.workers.dev/members?channel_hash=" + ch.
|
|
954
|
+
var res = await fetch("https://api.agentchannel.workers.dev/members?channel_hash=" + ch.channelHash);
|
|
642
955
|
var rows = await res.json();
|
|
643
956
|
var cid = ch.sub ? ch.name + '/' + ch.sub : ch.name;
|
|
644
957
|
cloudMembers[cid] = rows;
|
|
@@ -672,13 +985,17 @@ async function init() {
|
|
|
672
985
|
// Hide members for @me and public channels (AgentChannel)
|
|
673
986
|
var isPublic = channelMetas[activeChannel] && channelMetas[activeChannel].public;
|
|
674
987
|
var isOfficialPublic = activeChannel.toLowerCase() === "agentchannel";
|
|
675
|
-
|
|
988
|
+
var isDm = activeChannel && activeChannel.indexOf("dm:") === 0;
|
|
989
|
+
var membersBtn = document.getElementById("toggle-members-btn");
|
|
990
|
+
if (activeChannel === "all" || activeChannel === "@me" || isDm) {
|
|
676
991
|
if (header) header.textContent = "MEMBERS";
|
|
677
992
|
list.innerHTML = "";
|
|
678
993
|
panel.style.display = "none";
|
|
994
|
+
if (membersBtn) membersBtn.style.display = "none";
|
|
679
995
|
return;
|
|
680
996
|
}
|
|
681
997
|
panel.style.display = "";
|
|
998
|
+
if (membersBtn) membersBtn.style.display = "";
|
|
682
999
|
|
|
683
1000
|
var memberMap = {};
|
|
684
1001
|
var online = new Set();
|
|
@@ -769,19 +1086,28 @@ async function init() {
|
|
|
769
1086
|
? '<span style="color:var(--text-muted);font-size:0.6rem;margin-left:2px">(' + fp.slice(0, 4) + ')</span>'
|
|
770
1087
|
: '';
|
|
771
1088
|
var dmClick = (!isYou && fp) ? ' onclick="window.openDm(\x27' + fp + '\x27,\x27' + esc(name).replace(/'/g, '') + '\x27)" style="cursor:pointer" title="Open DM"' : '';
|
|
772
|
-
return '<div class="members__item"' + dmClick + '><span class="members__dot" style="background:' + (isOnline ? "#
|
|
1089
|
+
return '<div class="members__item"' + dmClick + '><span class="members__dot" style="background:' + (isOnline ? "#00c858" : "#666") + '"></span><span class="members__name">' + esc(name) + fpStr + '</span>' + (isYou ? '<span class="members__role">you</span>' : '') + '</div>';
|
|
773
1090
|
}).join("");
|
|
774
1091
|
|
|
775
1092
|
list.innerHTML = html;
|
|
776
1093
|
|
|
777
|
-
// Update
|
|
778
|
-
var
|
|
779
|
-
if (
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
1094
|
+
// Update members badge count (hidden when panel is open)
|
|
1095
|
+
var badge = document.getElementById("members-badge");
|
|
1096
|
+
if (badge) {
|
|
1097
|
+
var count = memberCount;
|
|
1098
|
+
badge.textContent = count > 99 ? "99+" : count > 0 ? count : "";
|
|
1099
|
+
var panelCollapsed = document.getElementById("members-panel").classList.contains("collapsed") || document.getElementById("members-panel").style.display === "none";
|
|
1100
|
+
badge.classList.toggle("hidden", !panelCollapsed);
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
// Update header actions (share/leave in title bar)
|
|
1104
|
+
var headerActions = document.getElementById("header-actions");
|
|
1105
|
+
if (headerActions) {
|
|
1106
|
+
if (activeChannel !== "all" && activeChannel !== "@me" && !isDm) {
|
|
1107
|
+
headerActions.innerHTML = '<button class="collapse-btn" onclick="window.shareChannel()" title="Share channel"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/></svg></button>'
|
|
1108
|
+
+ '<button class="collapse-btn" onclick="window.leaveChannel()" title="Leave channel"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg></button>';
|
|
783
1109
|
} else {
|
|
784
|
-
|
|
1110
|
+
headerActions.innerHTML = "";
|
|
785
1111
|
}
|
|
786
1112
|
}
|
|
787
1113
|
}
|
|
@@ -827,13 +1153,14 @@ async function init() {
|
|
|
827
1153
|
if (parentCfg) {
|
|
828
1154
|
var subKey = await deriveSubKeyWeb(parentCfg.key, subName);
|
|
829
1155
|
var subHash = await hashSubWeb(parentCfg.key, subName);
|
|
830
|
-
|
|
831
|
-
|
|
1156
|
+
var subChHash = subHash; // epoch 0: MQTT hash = storage hash
|
|
1157
|
+
channels[subId] = {key: subKey, hash: subHash, channelHash: subChHash, name: ch.name, sub: subName};
|
|
1158
|
+
CONFIG.channels.push({channel: ch.name, subchannel: subName, key: parentCfg.key, channelHash: subChHash});
|
|
832
1159
|
client.subscribe("ac/1/" + subHash);
|
|
833
1160
|
client.subscribe("ac/1/" + subHash + "/p");
|
|
834
1161
|
// Load history for new subchannel
|
|
835
1162
|
try {
|
|
836
|
-
var hres = await fetch("https://api.agentchannel.workers.dev/messages?channel_hash=" +
|
|
1163
|
+
var hres = await fetch("https://api.agentchannel.workers.dev/messages?channel_hash=" + subChHash + "&since=0&limit=30");
|
|
837
1164
|
var hrows = await hres.json();
|
|
838
1165
|
for (var hi = 0; hi < hrows.length; hi++) {
|
|
839
1166
|
try {
|
|
@@ -869,8 +1196,8 @@ async function init() {
|
|
|
869
1196
|
}
|
|
870
1197
|
var total = Object.values(unreadCounts).reduce(function(a, b) { return a + b; }, 0);
|
|
871
1198
|
if (total > 0) document.title = "(" + total + ") AgentChannel";
|
|
872
|
-
var nlabel = ch.isDm ? "DM" : (ch.sub ? "#" + ch.name + "
|
|
873
|
-
if (Notification.permission === "granted" && (document.hidden || activeChannel !== chKeyName)) {
|
|
1199
|
+
var nlabel = ch.isDm ? "DM" : (ch.sub ? "#" + ch.name + "/" + ch.sub : "#" + ch.name);
|
|
1200
|
+
if (!isTauri && Notification.permission === "granted" && (document.hidden || activeChannel !== chKeyName)) {
|
|
874
1201
|
var n = new Notification(nlabel + " @" + msg.sender, {body: msg.content});
|
|
875
1202
|
n.onclick = function() {
|
|
876
1203
|
window.focus();
|
|
@@ -911,9 +1238,9 @@ async function init() {
|
|
|
911
1238
|
var cid = parent.channel + "/" + subName;
|
|
912
1239
|
activeChannel = cid;
|
|
913
1240
|
unreadCounts[cid] = 0;
|
|
914
|
-
headerName.textContent = "
|
|
1241
|
+
headerName.textContent = "#" + activeChannel.split("/")[0] + "/" + subName;
|
|
915
1242
|
var subDesc2 = (channelMetas[parent.channel] && channelMetas[parent.channel].descriptions && channelMetas[parent.channel].descriptions[subName]) || "";
|
|
916
|
-
headerDesc.textContent =
|
|
1243
|
+
headerDesc.textContent = subDesc2;
|
|
917
1244
|
document.title = "AgentChannel";
|
|
918
1245
|
history.pushState(null, "", "/channel/" + encodeURIComponent(parent.channel) + "/sub/" + encodeURIComponent(subName));
|
|
919
1246
|
renderSidebar();
|
|
@@ -1026,33 +1353,161 @@ function toggleTheme() {
|
|
|
1026
1353
|
}
|
|
1027
1354
|
window.toggleTheme = toggleTheme;
|
|
1028
1355
|
|
|
1029
|
-
// Settings modal
|
|
1356
|
+
// Settings modal — tabbed layout
|
|
1030
1357
|
function openSettings() {
|
|
1031
1358
|
var fp = CONFIG.fingerprint || '';
|
|
1032
1359
|
var overlay = document.createElement('div');
|
|
1033
1360
|
overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:9999;display:flex;align-items:center;justify-content:center';
|
|
1034
1361
|
overlay.onclick = function(e) { if (e.target === overlay) document.body.removeChild(overlay); };
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1362
|
+
|
|
1363
|
+
var sub = 'font-size:0.7rem;color:var(--text-secondary);margin-top:6px;line-height:1.5';
|
|
1364
|
+
var row = 'display:flex;align-items:center;justify-content:space-between;padding:10px 0;border-bottom:1px solid var(--border)';
|
|
1365
|
+
var rowLast = 'display:flex;align-items:center;justify-content:space-between;padding:10px 0';
|
|
1366
|
+
var rl = 'font-size:0.82rem;color:var(--text)';
|
|
1367
|
+
var pathStyle = 'flex:1;font-size:0.78rem;color:var(--text-body);padding:8px 12px;background:var(--bg-alt);border-radius:6px;border:1px solid var(--border);overflow:hidden;text-overflow:ellipsis;white-space:nowrap';
|
|
1368
|
+
var folderIcon = '<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display:block"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>';
|
|
1369
|
+
var folderBtn = 'display:flex;align-items:center;justify-content:center;width:34px;height:34px;border:1px solid var(--border);border-radius:6px;background:var(--bg-alt);color:var(--text-secondary);cursor:pointer;flex-shrink:0';
|
|
1370
|
+
// Switch toggle registry — handlers bound after tab render
|
|
1371
|
+
var switchDefs = {};
|
|
1372
|
+
function sw(id, checked, handler, onLabel, offLabel) {
|
|
1373
|
+
var on = onLabel || 'On';
|
|
1374
|
+
var off = offLabel || 'Off';
|
|
1375
|
+
switchDefs[id] = { handler: handler, on: on, off: off };
|
|
1376
|
+
var stateText = '<span id="' + id + '-label" style="font-size:0.7rem;color:var(--text-secondary);margin-right:6px">' + (checked ? on : off) + '</span>';
|
|
1377
|
+
return '<div style="display:flex;align-items:center">' + stateText +
|
|
1378
|
+
'<label class="switch"><input type="checkbox" id="' + id + '"' + (checked ? ' checked' : '') +
|
|
1379
|
+
'><span class="slider"></span></label></div>';
|
|
1380
|
+
}
|
|
1381
|
+
function bindSwitches() {
|
|
1382
|
+
for (var sid in switchDefs) {
|
|
1383
|
+
var el = document.getElementById(sid);
|
|
1384
|
+
if (!el || el._bound) continue;
|
|
1385
|
+
el._bound = true;
|
|
1386
|
+
(function(def, input) {
|
|
1387
|
+
input.addEventListener('change', function() {
|
|
1388
|
+
def.handler(input.checked);
|
|
1389
|
+
var lbl = document.getElementById(input.id + '-label');
|
|
1390
|
+
if (lbl) lbl.textContent = input.checked ? def.on : def.off;
|
|
1391
|
+
});
|
|
1392
|
+
})(switchDefs[sid], el);
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
// Tab content definitions
|
|
1397
|
+
var tabs = {
|
|
1398
|
+
Profile:
|
|
1399
|
+
'<div style="margin-bottom:18px">' +
|
|
1400
|
+
'<div style="font-size:0.7rem;color:var(--text-secondary);margin-bottom:5px">Display Name</div>' +
|
|
1401
|
+
'<input id="settings-name" value="' + (CONFIG.name || '') + '" style="width:100%;padding:9px 12px;border:1px solid var(--border);border-radius:6px;font-size:0.88rem;background:var(--bg-alt);color:var(--text);outline:none">' +
|
|
1402
|
+
'</div>' +
|
|
1403
|
+
'<div>' +
|
|
1404
|
+
'<div style="font-size:0.7rem;color:var(--text-secondary);margin-bottom:5px">Fingerprint</div>' +
|
|
1405
|
+
'<div style="display:flex;gap:8px;align-items:center">' +
|
|
1406
|
+
'<code style="flex:1;padding:8px 10px;background:var(--bg-alt);border:1px solid var(--border);border-radius:6px;font-size:0.82rem;color:var(--text);overflow:hidden;text-overflow:ellipsis">' + fp + '</code>' +
|
|
1407
|
+
'<button onclick="navigator.clipboard.writeText(\'' + fp + '\');this.textContent=\'Copied!\';setTimeout(function(){this.textContent=\'Copy\'},1000)" style="padding:6px 14px;border:1px solid var(--border);border-radius:6px;background:var(--bg-alt);color:var(--text);cursor:pointer;font-size:0.75rem;white-space:nowrap">Copy</button>' +
|
|
1408
|
+
'</div>' +
|
|
1409
|
+
'<div style="' + sub + '">Share this so others can reach you directly</div>' +
|
|
1410
|
+
'</div>',
|
|
1411
|
+
|
|
1412
|
+
Sync:
|
|
1413
|
+
'<div style="' + row + '">' +
|
|
1414
|
+
'<span style="' + rl + '">Private channels</span>' +
|
|
1415
|
+
sw('settings-sync-private', CONFIG.syncPrivate !== false, window.toggleSyncPrivate, 'Sync on', 'Sync off') +
|
|
1416
|
+
'</div>' +
|
|
1417
|
+
'<div style="' + rowLast + '">' +
|
|
1418
|
+
'<span style="' + rl + '">Public channels</span>' +
|
|
1419
|
+
sw('settings-sync-public', !!CONFIG.syncPublic, window.toggleSyncPublic, 'Sync on', 'Sync off') +
|
|
1420
|
+
'</div>' +
|
|
1421
|
+
'<div style="font-size:0.65rem;color:var(--text-secondary);text-transform:uppercase;letter-spacing:0.05em;margin-top:14px;margin-bottom:5px">Sync folder</div>' +
|
|
1422
|
+
'<div style="display:flex;gap:6px;align-items:center">' +
|
|
1423
|
+
'<div style="' + pathStyle + '">~/agentchannel/messages/</div>' +
|
|
1424
|
+
'<button onclick="window.openSyncFolder(event)" style="' + folderBtn + '" title="Open folder">' + folderIcon + '</button>' +
|
|
1425
|
+
'</div>' +
|
|
1426
|
+
'<div style="' + sub + '">Toggle per-channel in the sidebar.</div>',
|
|
1427
|
+
|
|
1428
|
+
Brain:
|
|
1429
|
+
'<div style="' + rowLast + '">' +
|
|
1430
|
+
'<span style="' + rl + '">Brain</span>' +
|
|
1431
|
+
sw('settings-distill', CONFIG.distill !== false, window.toggleDistill, 'Learning', 'Paused') +
|
|
1432
|
+
'</div>' +
|
|
1433
|
+
'<div style="font-size:0.65rem;color:var(--text-secondary);text-transform:uppercase;letter-spacing:0.05em;margin-top:14px;margin-bottom:5px">Brain folder</div>' +
|
|
1434
|
+
'<div style="display:flex;gap:6px;align-items:center">' +
|
|
1435
|
+
'<div id="brain-path" style="' + pathStyle + '">~/agentchannel/brain/</div>' +
|
|
1436
|
+
'<button onclick="window.openBrainFolder(event)" style="' + folderBtn + '" title="Open folder">' + folderIcon + '</button>' +
|
|
1437
|
+
'</div>' +
|
|
1438
|
+
'<div id="brain-activity" style="margin-top:12px;padding:12px;background:var(--bg-alt);border-radius:6px;border:1px solid var(--border);font-size:0.75rem;color:var(--text-body);line-height:1.6">' +
|
|
1439
|
+
'<div style="color:var(--text-secondary);font-size:0.65rem;margin-bottom:8px">ACTIVITY</div>' +
|
|
1440
|
+
'<div id="brain-graph" style="display:flex;gap:2px;flex-wrap:wrap;margin-bottom:8px"></div>' +
|
|
1441
|
+
'<div id="brain-stats">Preparing...</div>' +
|
|
1442
|
+
'</div>',
|
|
1443
|
+
|
|
1444
|
+
Security:
|
|
1445
|
+
'<div style="' + row + '">' +
|
|
1446
|
+
'<span style="' + rl + '">End-to-end encryption</span>' +
|
|
1447
|
+
'<span style="font-size:0.72rem;color:var(--accent-brand)">Active</span>' +
|
|
1448
|
+
'</div>' +
|
|
1449
|
+
'<div style="' + row + '">' +
|
|
1450
|
+
'<span style="' + rl + '">Message signing</span>' +
|
|
1451
|
+
'<span style="font-size:0.72rem;color:var(--accent-brand)">Active</span>' +
|
|
1452
|
+
'</div>' +
|
|
1453
|
+
'<div style="' + rowLast + '">' +
|
|
1454
|
+
'<span style="' + rl + '">Private key</span>' +
|
|
1455
|
+
'<span style="font-size:0.72rem;color:var(--text-secondary)">Local only</span>' +
|
|
1456
|
+
'</div>' +
|
|
1457
|
+
'<div style="' + sub + '">No one — not even the server — can read your messages. Every message is signed so you know who sent it.</div>'
|
|
1458
|
+
};
|
|
1459
|
+
|
|
1460
|
+
var tabNames = ['Profile', 'Sync', 'Brain', 'Security'];
|
|
1461
|
+
var tabBtnStyle = 'padding:6px 14px 8px;border:none;border-bottom:2px solid transparent;border-radius:0;cursor:pointer;font-size:0.78rem;transition:all 0.1s;background:transparent';
|
|
1462
|
+
var tabBtnActive = 'color:var(--text);border-bottom-color:var(--accent-brand)';
|
|
1463
|
+
var tabBtnInactive = 'color:var(--text-secondary);border-bottom-color:transparent';
|
|
1464
|
+
|
|
1465
|
+
overlay.innerHTML = '<div style="background:var(--bg);border:1px solid var(--border);border-radius:12px;width:440px;max-width:92%;box-shadow:0 8px 32px rgba(0,0,0,0.5);overflow:hidden">' +
|
|
1466
|
+
// Header
|
|
1467
|
+
'<div style="display:flex;align-items:center;justify-content:space-between;padding:16px 20px 0">' +
|
|
1468
|
+
'<h3 style="font-size:1rem;color:var(--text);margin:0">Settings</h3>' +
|
|
1469
|
+
'<button onclick="this.closest(\'div[style*=fixed]\').remove()" style="background:none;border:none;color:var(--text-muted);cursor:pointer;font-size:1.1rem;padding:4px;line-height:1">×</button>' +
|
|
1043
1470
|
'</div>' +
|
|
1044
|
-
|
|
1045
|
-
'<div style="
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
'
|
|
1049
|
-
'
|
|
1471
|
+
// Tabs
|
|
1472
|
+
'<div id="settings-tabs" style="display:flex;gap:4px;padding:12px 20px 0">' +
|
|
1473
|
+
tabNames.map(function(name, i) {
|
|
1474
|
+
return '<button class="settings-tab" data-tab="' + name + '" style="' + tabBtnStyle + ';' + (i === 0 ? tabBtnActive : tabBtnInactive) + '">' + name + '</button>';
|
|
1475
|
+
}).join('') +
|
|
1476
|
+
'</div>' +
|
|
1477
|
+
// Content
|
|
1478
|
+
'<div id="settings-content" style="padding:16px 20px 20px;height:220px;overflow-y:auto">' + tabs.Profile + '</div>' +
|
|
1479
|
+
// Footer
|
|
1480
|
+
'<div style="border-top:1px solid var(--border);padding:12px 20px;display:flex;align-items:center;justify-content:space-between">' +
|
|
1481
|
+
'<span style="font-size:0.72rem;color:var(--text-secondary)">v' + (CONFIG.version || '?') + '</span>' +
|
|
1482
|
+
'<div style="display:flex;gap:8px">' +
|
|
1483
|
+
'<button onclick="this.closest(\'div[style*=fixed]\').remove()" style="padding:7px 14px;border:1px solid var(--border);border-radius:6px;background:var(--bg-alt);color:var(--text);cursor:pointer;font-size:0.78rem">Cancel</button>' +
|
|
1484
|
+
'<button onclick="saveName()" style="padding:7px 14px;border:none;border-radius:6px;background:var(--text);color:var(--bg);cursor:pointer;font-size:0.78rem;font-weight:600">Save</button>' +
|
|
1050
1485
|
'</div>' +
|
|
1051
|
-
'<div style="display:flex;gap:8px;justify-content:flex-end">' +
|
|
1052
|
-
'<button onclick="this.closest(\'div[style*=fixed]\').remove()" style="padding:8px 16px;border:1px solid var(--border);border-radius:6px;background:var(--bg-alt);color:var(--text);cursor:pointer;font-size:0.8rem">Cancel</button>' +
|
|
1053
|
-
'<button onclick="saveName()" style="padding:8px 16px;border:none;border-radius:6px;background:#1a1a1a;color:#fff;cursor:pointer;font-size:0.8rem;font-weight:600">Save</button>' +
|
|
1054
1486
|
'</div></div>';
|
|
1487
|
+
|
|
1055
1488
|
document.body.appendChild(overlay);
|
|
1489
|
+
bindSwitches();
|
|
1490
|
+
|
|
1491
|
+
// Tab switching
|
|
1492
|
+
var tabButtons = overlay.querySelectorAll('.settings-tab');
|
|
1493
|
+
var contentEl = overlay.querySelector('#settings-content');
|
|
1494
|
+
for (var ti = 0; ti < tabButtons.length; ti++) {
|
|
1495
|
+
tabButtons[ti].onclick = function(e) {
|
|
1496
|
+
e.stopPropagation();
|
|
1497
|
+
var name = this.getAttribute('data-tab');
|
|
1498
|
+
contentEl.innerHTML = tabs[name];
|
|
1499
|
+
bindSwitches();
|
|
1500
|
+
for (var j = 0; j < tabButtons.length; j++) {
|
|
1501
|
+
tabButtons[j].style.color = 'var(--text-secondary)';
|
|
1502
|
+
tabButtons[j].style.borderBottomColor = 'transparent';
|
|
1503
|
+
}
|
|
1504
|
+
this.style.color = 'var(--text)';
|
|
1505
|
+
this.style.borderBottomColor = 'var(--accent-brand)';
|
|
1506
|
+
if (name === 'Brain' && window.loadDistillStatus) window.loadDistillStatus();
|
|
1507
|
+
};
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
if (window.loadDistillStatus) window.loadDistillStatus();
|
|
1056
1511
|
}
|
|
1057
1512
|
window.openSettings = openSettings;
|
|
1058
1513
|
|
|
@@ -1073,6 +1528,8 @@ function saveName() {
|
|
|
1073
1528
|
CONFIG.name = newName;
|
|
1074
1529
|
var userNameEl = document.getElementById('user-name');
|
|
1075
1530
|
if (userNameEl) userNameEl.textContent = '@' + newName + (CONFIG.fingerprint ? ' (' + CONFIG.fingerprint.slice(0, 4) + ')' : '');
|
|
1531
|
+
var initEl = document.getElementById('user-initial');
|
|
1532
|
+
if (initEl) initEl.textContent = newName.charAt(0).toUpperCase();
|
|
1076
1533
|
document.querySelector('div[style*=fixed]').remove();
|
|
1077
1534
|
}).catch(function() {
|
|
1078
1535
|
alert('Failed to save name');
|
|
@@ -1080,11 +1537,182 @@ function saveName() {
|
|
|
1080
1537
|
}
|
|
1081
1538
|
window.saveName = saveName;
|
|
1082
1539
|
|
|
1540
|
+
// ── Distill toggle ───────────────────────────────────────
|
|
1541
|
+
|
|
1542
|
+
// ── Sync toggle (click handler on sidebar icons) ─────────
|
|
1543
|
+
|
|
1544
|
+
document.addEventListener('click', function(e) {
|
|
1545
|
+
var toggle = e.target.closest('.sync-toggle');
|
|
1546
|
+
if (!toggle) return;
|
|
1547
|
+
e.stopPropagation();
|
|
1548
|
+
var channel = toggle.getAttribute('data-channel');
|
|
1549
|
+
var wasSynced = toggle.getAttribute('data-synced') === '1';
|
|
1550
|
+
var nowSynced = !wasSynced;
|
|
1551
|
+
fetch('/api/sync', {
|
|
1552
|
+
method: 'POST',
|
|
1553
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1554
|
+
body: JSON.stringify({ channel: channel, enabled: nowSynced })
|
|
1555
|
+
}).then(function() {
|
|
1556
|
+
// Update local config
|
|
1557
|
+
for (var i = 0; i < CONFIG.channels.length; i++) {
|
|
1558
|
+
if (CONFIG.channels[i].channel === channel && !CONFIG.channels[i].subchannel) {
|
|
1559
|
+
CONFIG.channels[i].sync = nowSynced;
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
renderSidebar();
|
|
1563
|
+
});
|
|
1564
|
+
});
|
|
1565
|
+
|
|
1566
|
+
window.toggleDistill = function(enabled) {
|
|
1567
|
+
fetch('/api/distill', {
|
|
1568
|
+
method: 'POST',
|
|
1569
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1570
|
+
body: JSON.stringify({ enabled: enabled })
|
|
1571
|
+
});
|
|
1572
|
+
};
|
|
1573
|
+
|
|
1574
|
+
window.toggleSyncPrivate = function(enabled) {
|
|
1575
|
+
CONFIG.syncPrivate = enabled;
|
|
1576
|
+
for (var i = 0; i < CONFIG.channels.length; i++) {
|
|
1577
|
+
var ch = CONFIG.channels[i];
|
|
1578
|
+
if (ch.channel.toLowerCase() !== 'agentchannel' && !ch.subchannel) {
|
|
1579
|
+
fetch('/api/sync', {
|
|
1580
|
+
method: 'POST',
|
|
1581
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1582
|
+
body: JSON.stringify({ channel: ch.channel, enabled: enabled })
|
|
1583
|
+
});
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
renderSidebar();
|
|
1587
|
+
};
|
|
1588
|
+
|
|
1589
|
+
window.toggleSyncPublic = function(enabled) {
|
|
1590
|
+
// Toggle sync default for all public channels
|
|
1591
|
+
CONFIG.syncPublic = enabled;
|
|
1592
|
+
var official = 'agentchannel';
|
|
1593
|
+
for (var i = 0; i < CONFIG.channels.length; i++) {
|
|
1594
|
+
var ch = CONFIG.channels[i];
|
|
1595
|
+
if (ch.channel.toLowerCase() === official && !ch.subchannel) {
|
|
1596
|
+
fetch('/api/sync', {
|
|
1597
|
+
method: 'POST',
|
|
1598
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1599
|
+
body: JSON.stringify({ channel: ch.channel, enabled: enabled })
|
|
1600
|
+
});
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
renderSidebar();
|
|
1604
|
+
};
|
|
1605
|
+
|
|
1606
|
+
function openFolder(path, btnEvent) {
|
|
1607
|
+
var btn = btnEvent && btnEvent.target ? btnEvent.target.closest('button') : null;
|
|
1608
|
+
if (window.__TAURI__) {
|
|
1609
|
+
window.__TAURI__.shell.open(path);
|
|
1610
|
+
} else {
|
|
1611
|
+
navigator.clipboard.writeText(path);
|
|
1612
|
+
if (btn) {
|
|
1613
|
+
var orig = btn.innerHTML;
|
|
1614
|
+
btn.innerHTML = '<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="var(--accent-brand)" stroke-width="2.5" style="display:block"><polyline points="20 6 9 17 4 12"/></svg>';
|
|
1615
|
+
setTimeout(function() { btn.innerHTML = orig; }, 1200);
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
window.openSyncFolder = function(e) {
|
|
1621
|
+
openFolder(CONFIG.syncPath || (window.HOME || '') + '/agentchannel/messages', e || window.event);
|
|
1622
|
+
};
|
|
1623
|
+
|
|
1624
|
+
window.openBrainFolder = function(e) {
|
|
1625
|
+
openFolder(CONFIG.brainPath || (window.HOME || '') + '/agentchannel/brain', e || window.event);
|
|
1626
|
+
};
|
|
1627
|
+
|
|
1628
|
+
// Load distill status into settings
|
|
1629
|
+
window.loadDistillStatus = function() {
|
|
1630
|
+
fetch('/api/distill-status').then(function(r) { return r.json(); }).then(function(status) {
|
|
1631
|
+
var el = document.getElementById('brain-path');
|
|
1632
|
+
if (el) {
|
|
1633
|
+
el.textContent = status.brainDir || '~/agentchannel/brain/';
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
// Render contribution graph
|
|
1637
|
+
var graphEl = document.getElementById('brain-graph');
|
|
1638
|
+
if (graphEl) {
|
|
1639
|
+
var days = 30;
|
|
1640
|
+
var html = '';
|
|
1641
|
+
var tc = status.topicCount || 0;
|
|
1642
|
+
// Generate mock activity data based on topic count
|
|
1643
|
+
// In production this would come from distill/log.jsonl
|
|
1644
|
+
for (var di = 0; di < days; di++) {
|
|
1645
|
+
var age = days - 1 - di;
|
|
1646
|
+
var level = 0;
|
|
1647
|
+
if (tc > 0) {
|
|
1648
|
+
// Simulate: recent days more active, older days less
|
|
1649
|
+
var rand = Math.sin(di * 7.3 + tc) * 0.5 + 0.5;
|
|
1650
|
+
if (age < 3) level = rand > 0.2 ? (rand > 0.6 ? 3 : 2) : 1;
|
|
1651
|
+
else if (age < 10) level = rand > 0.4 ? (rand > 0.7 ? 2 : 1) : 0;
|
|
1652
|
+
else level = rand > 0.6 ? 1 : 0;
|
|
1653
|
+
}
|
|
1654
|
+
var colors = [
|
|
1655
|
+
'var(--border)',
|
|
1656
|
+
'rgba(0,200,88,0.2)',
|
|
1657
|
+
'rgba(0,200,88,0.45)',
|
|
1658
|
+
'rgba(0,200,88,0.75)'
|
|
1659
|
+
];
|
|
1660
|
+
html += '<div style="width:10px;height:10px;border-radius:2px;background:' + colors[level] + '" title="' + age + 'd ago"></div>';
|
|
1661
|
+
}
|
|
1662
|
+
graphEl.innerHTML = html;
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
// Render stats text
|
|
1666
|
+
var statsEl = document.getElementById('brain-stats');
|
|
1667
|
+
if (statsEl) {
|
|
1668
|
+
var tc = status.topicCount || 0;
|
|
1669
|
+
var chList = status.channelsProcessed || [];
|
|
1670
|
+
var lastRun = status.lastRun ? timeAgo(status.lastRun) : null;
|
|
1671
|
+
|
|
1672
|
+
if (tc === 0 && !lastRun) {
|
|
1673
|
+
statsEl.innerHTML = '<span style="color:var(--text-secondary)">Waiting for first messages...</span>';
|
|
1674
|
+
} else {
|
|
1675
|
+
var topicLabel = tc === 1 ? '1 topic' : tc + ' topics';
|
|
1676
|
+
var chLabel = chList.length === 1 ? '1 channel' : chList.length + ' channels';
|
|
1677
|
+
var parts = '<span style="color:var(--text);font-weight:600">' + topicLabel + '</span> from ' + chLabel;
|
|
1678
|
+
if (lastRun) parts += ' · ' + lastRun;
|
|
1679
|
+
statsEl.innerHTML = parts;
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
}).catch(function() {});
|
|
1683
|
+
};
|
|
1684
|
+
|
|
1685
|
+
function timeAgo(ts) {
|
|
1686
|
+
var diff = Math.floor((Date.now() - ts) / 1000);
|
|
1687
|
+
if (diff < 60) return 'just now';
|
|
1688
|
+
if (diff < 3600) return Math.floor(diff / 60) + 'min ago';
|
|
1689
|
+
if (diff < 86400) return Math.floor(diff / 3600) + 'h ago';
|
|
1690
|
+
return Math.floor(diff / 86400) + 'd ago';
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1083
1693
|
// ── Input: send message + @autocomplete ──────────────────
|
|
1084
1694
|
|
|
1085
1695
|
function sendMsg() {
|
|
1086
1696
|
var input = document.getElementById('msg-input');
|
|
1087
1697
|
if (!input || !input.value.trim() || activeChannel === '@me') return;
|
|
1698
|
+
// Announcement channels: only owners can send
|
|
1699
|
+
var chName = activeChannel.split('/')[0];
|
|
1700
|
+
var meta = channelMetas[chName];
|
|
1701
|
+
if (meta && meta.mode === 'announcement' && (!CONFIG.fingerprint || meta.owners.indexOf(CONFIG.fingerprint) === -1)) {
|
|
1702
|
+
return;
|
|
1703
|
+
}
|
|
1704
|
+
// DM mode: use sendDmMessage
|
|
1705
|
+
if (activeChannel && activeChannel.indexOf('dm:') === 0) {
|
|
1706
|
+
var theirFp = null;
|
|
1707
|
+
for (var fp in dmChannels) {
|
|
1708
|
+
if (dmChannels[fp].channelId === activeChannel) { theirFp = fp; break; }
|
|
1709
|
+
}
|
|
1710
|
+
if (theirFp && window.sendDmMessage) {
|
|
1711
|
+
window.sendDmMessage(theirFp, input.value.trim());
|
|
1712
|
+
input.value = '';
|
|
1713
|
+
}
|
|
1714
|
+
return;
|
|
1715
|
+
}
|
|
1088
1716
|
fetch('/api/send', {
|
|
1089
1717
|
method: 'POST',
|
|
1090
1718
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -1143,3 +1771,4 @@ var themeBtn = document.getElementById('theme-toggle');
|
|
|
1143
1771
|
if (themeBtn) themeBtn.innerHTML = savedTheme === 'dark' ? sunIcon : moonIcon;
|
|
1144
1772
|
|
|
1145
1773
|
init();
|
|
1774
|
+
|