agentchannel 0.8.2 → 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 +152 -50
- package/dist/brain.d.ts +78 -0
- package/dist/brain.js +271 -0
- package/dist/brain.js.map +1 -0
- package/dist/cli.js +226 -8
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +15 -0
- package/dist/config.js +66 -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/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 +9 -0
- package/dist/mqtt-client.js +311 -21
- package/dist/mqtt-client.js.map +1 -1
- package/dist/server.js +45 -0
- 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.js +4 -4
- package/dist/tools/hooks.js.map +1 -1
- package/dist/tools/index.js +8 -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 +5 -4
- package/dist/tools/read.js.map +1 -1
- 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 +23 -1
- package/dist/web.d.ts +1 -0
- package/dist/web.js +85 -0
- package/dist/web.js.map +1 -1
- package/package.json +3 -2
- package/ui/app.js +517 -88
- package/ui/index.html +5 -6
- package/ui/style.css +39 -12
- package/LICENSE +0 -21
package/ui/app.js
CHANGED
|
@@ -181,6 +181,7 @@ async function hashSubWeb(channelKey, subName) {
|
|
|
181
181
|
return Array.from(topicBytes).map(function(b) { return b.toString(16).padStart(2, "0"); }).join("");
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
+
|
|
184
185
|
async function deriveDmKeyWeb(fpA, fpB) {
|
|
185
186
|
var sorted = [fpA, fpB].sort();
|
|
186
187
|
var ikm = sorted[0] + sorted[1];
|
|
@@ -228,11 +229,11 @@ function chId(ch) {
|
|
|
228
229
|
}
|
|
229
230
|
|
|
230
231
|
function chLabel(ch) {
|
|
231
|
-
return ch.subchannel ? '
|
|
232
|
+
return ch.subchannel ? '#' + ch.channel + '/' + ch.subchannel : '#' + ch.channel;
|
|
232
233
|
}
|
|
233
234
|
|
|
234
235
|
function chFullLabel(ch) {
|
|
235
|
-
return ch.subchannel ? '#' + ch.channel + '
|
|
236
|
+
return ch.subchannel ? '#' + ch.channel + '/' + ch.subchannel : '#' + ch.channel;
|
|
236
237
|
}
|
|
237
238
|
|
|
238
239
|
var INLINE_TAG_COLORS = {
|
|
@@ -243,7 +244,7 @@ var INLINE_TAG_COLORS = {
|
|
|
243
244
|
};
|
|
244
245
|
|
|
245
246
|
// ---------------------------------------------------------------------------
|
|
246
|
-
// Rich text rendering (markdown + @mentions + #channels +
|
|
247
|
+
// Rich text rendering (markdown + @mentions + #channels + /subchannels)
|
|
247
248
|
// ---------------------------------------------------------------------------
|
|
248
249
|
function richText(t) {
|
|
249
250
|
// Let marked parse markdown (preserves code blocks with <pre><code>)
|
|
@@ -253,10 +254,10 @@ function richText(t) {
|
|
|
253
254
|
var knownChannels = CONFIG.channels.filter(function(c) { return !c.subchannel; }).map(function(c) { return c.channel; });
|
|
254
255
|
var knownSubs = CONFIG.channels.filter(function(c) { return c.subchannel; }).map(function(c) { return c.subchannel; });
|
|
255
256
|
|
|
256
|
-
// Replace
|
|
257
|
+
// Replace /subchannel references in message text
|
|
257
258
|
for (var ki = 0; ki < knownSubs.length; ki++) {
|
|
258
|
-
s = s.split('
|
|
259
|
-
'<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>'
|
|
260
261
|
);
|
|
261
262
|
}
|
|
262
263
|
// Replace #channel references
|
|
@@ -352,7 +353,7 @@ function render() {
|
|
|
352
353
|
var msgFp = msg.senderKey ? '(' + msg.senderKey.slice(0, 4) + ')' : '';
|
|
353
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>';
|
|
354
355
|
if (activeChannel === "@me") {
|
|
355
|
-
var mlabel = msg.subchannel ? '#' + esc(msg.channel) + '
|
|
356
|
+
var mlabel = msg.subchannel ? '#' + esc(msg.channel) + '/' + esc(msg.subchannel) : '#' + esc(msg.channel);
|
|
356
357
|
html += '<span class="conversation__channel">' + mlabel + '</span>';
|
|
357
358
|
}
|
|
358
359
|
html += '<span class="conversation__time">' + time + '</span>';
|
|
@@ -363,7 +364,12 @@ function render() {
|
|
|
363
364
|
if (msg.tags && msg.tags.length) {
|
|
364
365
|
html += '<div class="conversation__tags">' + msg.tags.map(function(t) { return '<span class="tag">[' + esc(t) + ']</span>'; }).join(' ') + '</div>';
|
|
365
366
|
}
|
|
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>';
|
|
367
373
|
|
|
368
374
|
lastSender = msg.sender;
|
|
369
375
|
lastChannel = msg.channel;
|
|
@@ -373,6 +379,16 @@ function render() {
|
|
|
373
379
|
|
|
374
380
|
msgsEl.innerHTML = html;
|
|
375
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
|
+
}
|
|
376
392
|
}
|
|
377
393
|
|
|
378
394
|
// ---------------------------------------------------------------------------
|
|
@@ -383,6 +399,8 @@ function renderSidebar() {
|
|
|
383
399
|
el.innerHTML = "";
|
|
384
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>';
|
|
385
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>';
|
|
386
404
|
|
|
387
405
|
// Sort channels alphabetically, group subchannels under parent
|
|
388
406
|
var sorted = CONFIG.channels.slice().sort(function(a, b) { return chId(a).localeCompare(chId(b)); });
|
|
@@ -466,23 +484,38 @@ function renderSidebar() {
|
|
|
466
484
|
var cid = chId(ch);
|
|
467
485
|
div.className = "sidebar__channel" + (activeChannel === cid ? " active" : "");
|
|
468
486
|
var count = unreadCounts[cid] || 0;
|
|
469
|
-
var
|
|
470
|
-
|
|
471
|
-
var
|
|
472
|
-
|
|
473
|
-
|
|
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>';
|
|
474
501
|
if (hasChildren) {
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
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) {
|
|
478
512
|
(function(chName, wasCollapsed) {
|
|
479
|
-
|
|
513
|
+
arrowEl.onclick = function(e) {
|
|
480
514
|
e.stopPropagation();
|
|
481
515
|
collapsedGroups[chName] = !wasCollapsed;
|
|
482
516
|
renderSidebar();
|
|
483
517
|
};
|
|
484
518
|
})(ch.channel, collapsed);
|
|
485
|
-
div.appendChild(arrowBtn);
|
|
486
519
|
}
|
|
487
520
|
|
|
488
521
|
(function(chObj, channelId) {
|
|
@@ -509,14 +542,14 @@ function renderSidebar() {
|
|
|
509
542
|
var subDiv = document.createElement("div");
|
|
510
543
|
subDiv.className = "sidebar__channel sub" + (activeChannel === subCid ? " active" : "");
|
|
511
544
|
var subCount = unreadCounts[subCid] || 0;
|
|
512
|
-
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>' : "");
|
|
513
546
|
(function(subObj, parentChannel, subChannelId) {
|
|
514
547
|
subDiv.onclick = function() {
|
|
515
548
|
activeChannel = subChannelId;
|
|
516
549
|
unreadCounts[subChannelId] = 0;
|
|
517
|
-
headerName.textContent = "
|
|
550
|
+
headerName.textContent = "#" + ch.channel + "/" + subObj.subchannel;
|
|
518
551
|
var subDesc = (channelMetas[parentChannel] && channelMetas[parentChannel].descriptions && channelMetas[parentChannel].descriptions[subObj.subchannel]) || "";
|
|
519
|
-
headerDesc.textContent =
|
|
552
|
+
headerDesc.textContent = subDesc;
|
|
520
553
|
document.title = "AgentChannel";
|
|
521
554
|
history.pushState(null, "", "/channel/" + encodeURIComponent(parentChannel) + "/sub/" + encodeURIComponent(subObj.subchannel));
|
|
522
555
|
renderSidebar();
|
|
@@ -528,8 +561,112 @@ function renderSidebar() {
|
|
|
528
561
|
}
|
|
529
562
|
}
|
|
530
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);
|
|
531
571
|
}
|
|
532
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
|
+
|
|
533
670
|
// ---------------------------------------------------------------------------
|
|
534
671
|
// Channel actions
|
|
535
672
|
// ---------------------------------------------------------------------------
|
|
@@ -561,15 +698,20 @@ window.shareChannel = function() {
|
|
|
561
698
|
|
|
562
699
|
window.leaveChannel = function() {
|
|
563
700
|
if (!confirm("Leave #" + activeChannel + "?")) return;
|
|
564
|
-
|
|
565
|
-
CONFIG.channels = CONFIG.channels.filter(function(c) { return c.channel !==
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
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 = "";
|
|
569
712
|
renderSidebar();
|
|
570
713
|
render();
|
|
571
714
|
if (window.renderMembers) window.renderMembers();
|
|
572
|
-
alert('Left channel. Run "agentchannel leave --channel <name>" in CLI to persist.');
|
|
573
715
|
};
|
|
574
716
|
|
|
575
717
|
window.copyCode = function(btn) {
|
|
@@ -598,6 +740,7 @@ async function init() {
|
|
|
598
740
|
if (ident) {
|
|
599
741
|
CONFIG.fingerprint = ident.fingerprint;
|
|
600
742
|
}
|
|
743
|
+
try { CONFIG.version = await API.invoke('get_version'); } catch(e) {}
|
|
601
744
|
}
|
|
602
745
|
|
|
603
746
|
renderSidebar();
|
|
@@ -612,6 +755,7 @@ async function init() {
|
|
|
612
755
|
channels[id] = {
|
|
613
756
|
key: await deriveSubKeyWeb(ch.key, ch.subchannel),
|
|
614
757
|
hash: await hashSubWeb(ch.key, ch.subchannel),
|
|
758
|
+
channelHash: ch.channelHash || await hashSubWeb(ch.key, ch.subchannel),
|
|
615
759
|
name: ch.channel,
|
|
616
760
|
sub: ch.subchannel
|
|
617
761
|
};
|
|
@@ -619,6 +763,7 @@ async function init() {
|
|
|
619
763
|
channels[id] = {
|
|
620
764
|
key: await deriveKey(ch.key),
|
|
621
765
|
hash: await hashRoom(ch.key),
|
|
766
|
+
channelHash: ch.channelHash || await hashRoom(ch.key),
|
|
622
767
|
name: ch.channel
|
|
623
768
|
};
|
|
624
769
|
}
|
|
@@ -628,8 +773,8 @@ async function init() {
|
|
|
628
773
|
var pendingSubs = [];
|
|
629
774
|
var fetchPromises = Object.keys(channels).map(function(chKey) {
|
|
630
775
|
var ch = channels[chKey];
|
|
631
|
-
return fetch("https://api.agentchannel.workers.dev/messages?channel_hash=" + ch.
|
|
632
|
-
.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 []; })
|
|
633
778
|
.then(async function(rows) {
|
|
634
779
|
for (var ri = 0; ri < rows.length; ri++) {
|
|
635
780
|
try {
|
|
@@ -667,11 +812,12 @@ async function init() {
|
|
|
667
812
|
if (channels[subId]) continue;
|
|
668
813
|
var subKey = await deriveSubKeyWeb(ps.key, ps.sub);
|
|
669
814
|
var subHash = await hashSubWeb(ps.key, ps.sub);
|
|
670
|
-
|
|
671
|
-
|
|
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});
|
|
672
818
|
// Load subchannel history
|
|
673
819
|
try {
|
|
674
|
-
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");
|
|
675
821
|
var srows = await sres.json();
|
|
676
822
|
for (var sri = 0; sri < srows.length; sri++) {
|
|
677
823
|
try {
|
|
@@ -727,46 +873,33 @@ async function init() {
|
|
|
727
873
|
}
|
|
728
874
|
var total = Object.values(unreadCounts).reduce(function(a, b) { return a + b; }, 0);
|
|
729
875
|
if (total > 0) document.title = "(" + total + ") AgentChannel";
|
|
730
|
-
var nlabel = msg.subchannel ? "#" + msg.channel + "
|
|
731
|
-
|
|
732
|
-
|
|
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
|
+
}
|
|
733
881
|
}
|
|
734
882
|
render();
|
|
735
883
|
renderMembers();
|
|
736
884
|
});
|
|
737
|
-
}
|
|
738
885
|
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
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;
|
|
744
891
|
banner.innerHTML = '<div style="text-align:center;padding:16px 24px">' +
|
|
745
|
-
'<div style="font-size:0.9rem;font-weight:600;color:var(--text);margin-bottom:
|
|
746
|
-
'<button id="
|
|
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>' +
|
|
747
894
|
'</div>';
|
|
748
895
|
banner.style.display = "block";
|
|
749
|
-
document.getElementById("
|
|
750
|
-
this.textContent = "
|
|
896
|
+
document.getElementById("relaunch-btn").onclick = function() {
|
|
897
|
+
this.textContent = "Updating...";
|
|
751
898
|
this.disabled = true;
|
|
752
|
-
|
|
753
|
-
banner.innerHTML = '<div style="text-align:center;padding:12px;font-size:0.
|
|
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>';
|
|
754
901
|
});
|
|
755
902
|
};
|
|
756
|
-
} else {
|
|
757
|
-
banner.innerHTML = '<div style="text-align:center;padding:16px 24px">' +
|
|
758
|
-
'<div style="font-size:0.9rem;font-weight:600;color:var(--text);margin-bottom:4px">v' + version + ' available</div>' +
|
|
759
|
-
'<div style="font-size:0.75rem;color:var(--text-muted);margin-bottom:12px">Run to update</div>' +
|
|
760
|
-
'<button onclick="navigator.clipboard.writeText(\'npm install -g agentchannel\');this.textContent=\'Copied!\'" 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">npm install -g agentchannel</button>' +
|
|
761
|
-
'</div>';
|
|
762
|
-
banner.style.display = "block";
|
|
763
|
-
}
|
|
764
|
-
};
|
|
765
|
-
|
|
766
|
-
// Tauri: listen for auto-updater event from Rust backend
|
|
767
|
-
if (isTauri) {
|
|
768
|
-
window.__TAURI__.event.listen("update_available", function(event) {
|
|
769
|
-
window.showUpdateBanner(event.payload, true);
|
|
770
903
|
});
|
|
771
904
|
}
|
|
772
905
|
|
|
@@ -781,13 +914,21 @@ async function init() {
|
|
|
781
914
|
client.subscribe("ac/1/" + ch.hash);
|
|
782
915
|
client.subscribe("ac/1/" + ch.hash + "/p");
|
|
783
916
|
}
|
|
784
|
-
// Check for updates —
|
|
917
|
+
// Check for updates — show banner (skip in Tauri mode, it has its own updater)
|
|
785
918
|
if (!isTauri) {
|
|
786
919
|
fetch("https://registry.npmjs.org/agentchannel/latest").then(function(r) {
|
|
787
920
|
return r.json();
|
|
788
921
|
}).then(function(d) {
|
|
789
922
|
if (d.version && d.version !== CONFIG.version) {
|
|
790
|
-
|
|
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
|
+
}
|
|
791
932
|
}
|
|
792
933
|
}).catch(function() {});
|
|
793
934
|
}
|
|
@@ -810,7 +951,7 @@ async function init() {
|
|
|
810
951
|
for (var chKey in channels) {
|
|
811
952
|
var ch = channels[chKey];
|
|
812
953
|
try {
|
|
813
|
-
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);
|
|
814
955
|
var rows = await res.json();
|
|
815
956
|
var cid = ch.sub ? ch.name + '/' + ch.sub : ch.name;
|
|
816
957
|
cloudMembers[cid] = rows;
|
|
@@ -1012,13 +1153,14 @@ async function init() {
|
|
|
1012
1153
|
if (parentCfg) {
|
|
1013
1154
|
var subKey = await deriveSubKeyWeb(parentCfg.key, subName);
|
|
1014
1155
|
var subHash = await hashSubWeb(parentCfg.key, subName);
|
|
1015
|
-
|
|
1016
|
-
|
|
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});
|
|
1017
1159
|
client.subscribe("ac/1/" + subHash);
|
|
1018
1160
|
client.subscribe("ac/1/" + subHash + "/p");
|
|
1019
1161
|
// Load history for new subchannel
|
|
1020
1162
|
try {
|
|
1021
|
-
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");
|
|
1022
1164
|
var hrows = await hres.json();
|
|
1023
1165
|
for (var hi = 0; hi < hrows.length; hi++) {
|
|
1024
1166
|
try {
|
|
@@ -1054,7 +1196,7 @@ async function init() {
|
|
|
1054
1196
|
}
|
|
1055
1197
|
var total = Object.values(unreadCounts).reduce(function(a, b) { return a + b; }, 0);
|
|
1056
1198
|
if (total > 0) document.title = "(" + total + ") AgentChannel";
|
|
1057
|
-
var nlabel = ch.isDm ? "DM" : (ch.sub ? "#" + ch.name + "
|
|
1199
|
+
var nlabel = ch.isDm ? "DM" : (ch.sub ? "#" + ch.name + "/" + ch.sub : "#" + ch.name);
|
|
1058
1200
|
if (!isTauri && Notification.permission === "granted" && (document.hidden || activeChannel !== chKeyName)) {
|
|
1059
1201
|
var n = new Notification(nlabel + " @" + msg.sender, {body: msg.content});
|
|
1060
1202
|
n.onclick = function() {
|
|
@@ -1096,9 +1238,9 @@ async function init() {
|
|
|
1096
1238
|
var cid = parent.channel + "/" + subName;
|
|
1097
1239
|
activeChannel = cid;
|
|
1098
1240
|
unreadCounts[cid] = 0;
|
|
1099
|
-
headerName.textContent = "
|
|
1241
|
+
headerName.textContent = "#" + activeChannel.split("/")[0] + "/" + subName;
|
|
1100
1242
|
var subDesc2 = (channelMetas[parent.channel] && channelMetas[parent.channel].descriptions && channelMetas[parent.channel].descriptions[subName]) || "";
|
|
1101
|
-
headerDesc.textContent =
|
|
1243
|
+
headerDesc.textContent = subDesc2;
|
|
1102
1244
|
document.title = "AgentChannel";
|
|
1103
1245
|
history.pushState(null, "", "/channel/" + encodeURIComponent(parent.channel) + "/sub/" + encodeURIComponent(subName));
|
|
1104
1246
|
renderSidebar();
|
|
@@ -1211,33 +1353,161 @@ function toggleTheme() {
|
|
|
1211
1353
|
}
|
|
1212
1354
|
window.toggleTheme = toggleTheme;
|
|
1213
1355
|
|
|
1214
|
-
// Settings modal
|
|
1356
|
+
// Settings modal — tabbed layout
|
|
1215
1357
|
function openSettings() {
|
|
1216
1358
|
var fp = CONFIG.fingerprint || '';
|
|
1217
1359
|
var overlay = document.createElement('div');
|
|
1218
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';
|
|
1219
1361
|
overlay.onclick = function(e) { if (e.target === overlay) document.body.removeChild(overlay); };
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
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>' +
|
|
1228
1470
|
'</div>' +
|
|
1229
|
-
|
|
1230
|
-
'<div style="
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
'
|
|
1234
|
-
'
|
|
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>' +
|
|
1235
1485
|
'</div>' +
|
|
1236
|
-
'<div style="display:flex;gap:8px;justify-content:flex-end">' +
|
|
1237
|
-
'<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>' +
|
|
1238
|
-
'<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>' +
|
|
1239
1486
|
'</div></div>';
|
|
1487
|
+
|
|
1240
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();
|
|
1241
1511
|
}
|
|
1242
1512
|
window.openSettings = openSettings;
|
|
1243
1513
|
|
|
@@ -1267,11 +1537,170 @@ function saveName() {
|
|
|
1267
1537
|
}
|
|
1268
1538
|
window.saveName = saveName;
|
|
1269
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
|
+
|
|
1270
1693
|
// ── Input: send message + @autocomplete ──────────────────
|
|
1271
1694
|
|
|
1272
1695
|
function sendMsg() {
|
|
1273
1696
|
var input = document.getElementById('msg-input');
|
|
1274
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
|
+
}
|
|
1275
1704
|
// DM mode: use sendDmMessage
|
|
1276
1705
|
if (activeChannel && activeChannel.indexOf('dm:') === 0) {
|
|
1277
1706
|
var theirFp = null;
|