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.
Files changed (72) hide show
  1. package/README.md +116 -77
  2. package/dist/brain.d.ts +78 -0
  3. package/dist/brain.js +271 -0
  4. package/dist/brain.js.map +1 -0
  5. package/dist/cli.js +312 -8
  6. package/dist/cli.js.map +1 -1
  7. package/dist/config.d.ts +25 -1
  8. package/dist/config.js +104 -6
  9. package/dist/config.js.map +1 -1
  10. package/dist/crypto.d.ts +34 -4
  11. package/dist/crypto.js +42 -6
  12. package/dist/crypto.js.map +1 -1
  13. package/dist/distill.d.ts +24 -0
  14. package/dist/distill.js +404 -0
  15. package/dist/distill.js.map +1 -0
  16. package/dist/forwarder.d.ts +11 -0
  17. package/dist/forwarder.js +105 -0
  18. package/dist/forwarder.js.map +1 -0
  19. package/dist/local-store.d.ts +7 -0
  20. package/dist/local-store.js +54 -0
  21. package/dist/local-store.js.map +1 -0
  22. package/dist/mqtt-client.d.ts +11 -0
  23. package/dist/mqtt-client.js +369 -27
  24. package/dist/mqtt-client.js.map +1 -1
  25. package/dist/persistence.d.ts +23 -0
  26. package/dist/persistence.js +61 -0
  27. package/dist/persistence.js.map +1 -1
  28. package/dist/server.js +77 -3
  29. package/dist/server.js.map +1 -1
  30. package/dist/store.d.ts +3 -0
  31. package/dist/store.js +16 -2
  32. package/dist/store.js.map +1 -1
  33. package/dist/tools/brain.d.ts +2 -0
  34. package/dist/tools/brain.js +96 -0
  35. package/dist/tools/brain.js.map +1 -0
  36. package/dist/tools/channel.js +6 -6
  37. package/dist/tools/channel.js.map +1 -1
  38. package/dist/tools/get-message.js +1 -1
  39. package/dist/tools/get-message.js.map +1 -1
  40. package/dist/tools/hooks.d.ts +2 -0
  41. package/dist/tools/hooks.js +99 -0
  42. package/dist/tools/hooks.js.map +1 -0
  43. package/dist/tools/index.js +12 -0
  44. package/dist/tools/index.js.map +1 -1
  45. package/dist/tools/info.js +3 -1
  46. package/dist/tools/info.js.map +1 -1
  47. package/dist/tools/kick.d.ts +3 -0
  48. package/dist/tools/kick.js +52 -0
  49. package/dist/tools/kick.js.map +1 -0
  50. package/dist/tools/members.js +3 -3
  51. package/dist/tools/members.js.map +1 -1
  52. package/dist/tools/read.js +9 -6
  53. package/dist/tools/read.js.map +1 -1
  54. package/dist/tools/registry.d.ts +3 -0
  55. package/dist/tools/registry.js +82 -0
  56. package/dist/tools/registry.js.map +1 -0
  57. package/dist/tools/retract.d.ts +3 -0
  58. package/dist/tools/retract.js +27 -0
  59. package/dist/tools/retract.js.map +1 -0
  60. package/dist/tools/update-channel.d.ts +3 -0
  61. package/dist/tools/update-channel.js +50 -0
  62. package/dist/tools/update-channel.js.map +1 -0
  63. package/dist/types.d.ts +43 -1
  64. package/dist/web.d.ts +1 -0
  65. package/dist/web.js +91 -1
  66. package/dist/web.js.map +1 -1
  67. package/package.json +3 -2
  68. package/ui/app.js +715 -86
  69. package/ui/index.html +21 -11
  70. package/ui/marked.min.js +69 -0
  71. package/ui/mqtt.min.js +19 -0
  72. 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
- const isTauri = !!window.__TAURI__;
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 ? '##' + ch.subchannel : '#' + ch.channel;
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 + ' ##' + 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 + ##subchannels)
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 ##subchannel references (do subs first to avoid # matching partial ##)
257
+ // Replace /subchannel references in message text
167
258
  for (var ki = 0; ki < knownSubs.length; ki++) {
168
- s = s.split('##' + knownSubs[ki]).join(
169
- '<span class="channel-tag" onclick="window.switchToSub(\'' + knownSubs[ki] + '\')">##' + knownSubs[ki] + '</span>'
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) + ' ##' + esc(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
- html += '<div class="conversation__text">' + richText(msg.content) + '</div>';
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, '&quot;') + '" 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 chInfo = (window.acChannels || {})[cid];
374
- var chHash = chInfo ? chInfo.hash : '';
375
- var chTail = chHash ? '<span style="color:var(--text-muted);font-size:0.6rem;margin-left:3px;opacity:0.8">(' + chHash.slice(0, 4) + ')</span>' : '';
376
- div.innerHTML = '<span style="color:var(--accent);margin-right:2px;opacity:0.7">#</span>' + esc(ch.channel) + chTail + '<span style="opacity:0.5;margin-left:4px;display:inline-flex">' + statusIcon + '</span>' + (count ? '<span class="badge">' + count + '</span>' : "");
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
- var arrowBtn = document.createElement("span");
380
- arrowBtn.style.cssText = "font-size:0.55rem;margin-left:auto;opacity:0.4;padding:2px 4px;cursor:pointer";
381
- arrowBtn.textContent = collapsed ? "\u25B6" : "\u25BC";
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
- arrowBtn.onclick = function(e) {
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">##</span>' + esc(sub.subchannel) + (subCount ? '<span class="badge">' + subCount + '</span>' : "");
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 = "##" + subObj.subchannel;
550
+ headerName.textContent = "#" + ch.channel + "/" + subObj.subchannel;
422
551
  var subDesc = (channelMetas[parentChannel] && channelMetas[parentChannel].descriptions && channelMetas[parentChannel].descriptions[subObj.subchannel]) || "";
423
- headerDesc.textContent = "#" + parentChannel + (subDesc ? " \u00B7 " + subDesc : "");
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
- // Remove from config display (actual config change needs CLI)
469
- CONFIG.channels = CONFIG.channels.filter(function(c) { return c.channel !== activeChannel; });
470
- activeChannel = "all";
471
- headerName.textContent = "# All channels";
472
- headerDesc.textContent = "All channels";
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.hash + "&since=0&limit=30")
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
- channels[subId] = {key: subKey, hash: subHash, name: ps.name, sub: ps.sub};
566
- CONFIG.channels.push({channel: ps.name, subchannel: ps.sub, key: ps.key});
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=" + subHash + "&since=0&limit=30");
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
- fetch("https://registry.npmjs.org/agentchannel/latest").then(function(r) {
610
- return r.json();
611
- }).then(function(d) {
612
- if (d.version && d.version !== CONFIG.version) {
613
- var banner = document.getElementById("update-banner");
614
- if (banner) {
615
- banner.innerHTML = '<div style="text-align:center;padding:16px 24px">' +
616
- '<div style="font-size:0.9rem;font-weight:600;color:var(--text);margin-bottom:4px">Updated to ' + d.version + '</div>' +
617
- '<div style="font-size:0.75rem;color:var(--text-muted);margin-bottom:12px">Relaunch to apply</div>' +
618
- '<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>' +
619
- '</div>';
620
- banner.style.display = "block";
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
- }).catch(function() {});
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.className = "sidebar__status";
940
+ var statusEl = document.querySelector(".sidebar__status");
941
+ if (statusEl) statusEl.className = "sidebar__status";
630
942
  });
631
943
 
632
- if (Notification.permission === "default") Notification.requestPermission();
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.hash);
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
- if (activeChannel === "all" || activeChannel === "@me" || isPublic || isOfficialPublic) {
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 ? "#22c55e" : "#666") + '"></span><span class="members__name">' + esc(name) + fpStr + '</span>' + (isYou ? '<span class="members__role">you</span>' : '') + '</div>';
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 actions (fixed at bottom)
778
- var actions = document.getElementById("members-actions");
779
- if (actions) {
780
- if (activeChannel !== "all" && activeChannel.toLowerCase() !== "agentchannel") {
781
- actions.innerHTML = '<button class="members__btn" onclick="window.shareChannel()"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"/><polyline points="16 6 12 2 8 6"/><line x1="12" y1="2" x2="12" y2="15"/></svg> Share</button>'
782
- + '<button class="members__btn members__btn--leave" onclick="window.leaveChannel()"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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> Leave</button>';
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
- actions.innerHTML = "";
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
- channels[subId] = {key: subKey, hash: subHash, name: ch.name, sub: subName};
831
- CONFIG.channels.push({channel: ch.name, subchannel: subName, key: parentCfg.key});
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=" + subHash + "&since=0&limit=30");
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 + " ##" + 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 = "##" + subName;
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 = "#" + parent.channel + (subDesc2 ? " \u00B7 " + subDesc2 : "");
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
- overlay.innerHTML = '<div style="background:var(--bg);border:1px solid var(--border);border-radius:12px;padding:24px;width:360px;max-width:90%;box-shadow:0 8px 32px rgba(0,0,0,0.5)">' +
1036
- '<h3 style="margin-bottom:16px;font-size:1rem;color:var(--text)">Settings</h3>' +
1037
- '<label style="font-size:0.75rem;color:var(--text-muted);display:block;margin-bottom:4px">Display Name</label>' +
1038
- '<input id="settings-name" value="' + (CONFIG.name || '') + '" style="width:100%;padding:8px 12px;border:1px solid var(--border);border-radius:6px;font-size:0.85rem;background:var(--bg-alt);color:var(--text);margin-bottom:16px;outline:none">' +
1039
- '<label style="font-size:0.75rem;color:var(--text-muted);display:block;margin-bottom:4px">Fingerprint</label>' +
1040
- '<div style="display:flex;gap:8px;align-items:center;margin-bottom:16px">' +
1041
- '<code style="flex:1;padding:8px 12px;background:var(--bg-alt);border:1px solid var(--border);border-radius:6px;font-size:0.8rem;color:var(--text-muted);overflow:hidden;text-overflow:ellipsis">' + fp + '</code>' +
1042
- '<button onclick="navigator.clipboard.writeText(\'' + fp + '\');this.textContent=\'Copied!\';setTimeout(function(){this.textContent=\'Copy\'},1000)" style="padding:6px 12px;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>' +
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">&times;</button>' +
1043
1470
  '</div>' +
1044
- '<label style="font-size:0.75rem;color:var(--text-muted);display:block;margin-bottom:4px">Version</label>' +
1045
- '<div style="font-size:0.8rem;color:var(--text-muted);margin-bottom:20px">v' + (CONFIG.version || '?') + '</div>' +
1046
- '<div style="border-top:1px solid var(--border);padding-top:16px;margin-bottom:16px;font-size:0.7rem;color:var(--text-muted);line-height:1.6">' +
1047
- '<div style="margin-bottom:4px">&#128274; Messages are end-to-end encrypted (AES-256-GCM)</div>' +
1048
- '<div style="margin-bottom:4px">&#128273; Your private key never leaves this device</div>' +
1049
- '<div>&#128206; Fingerprint = your public identity (safe to share)</div>' +
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
+