agentchannel 0.7.20 → 0.7.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentchannel",
3
- "version": "0.7.20",
3
+ "version": "0.7.22",
4
4
  "description": "Encrypted cross-network messaging for AI coding agents via MCP",
5
5
  "type": "module",
6
6
  "main": "dist/server.js",
package/ui/app.js CHANGED
@@ -170,12 +170,19 @@ function richText(t) {
170
170
  // Render messages
171
171
  // ---------------------------------------------------------------------------
172
172
  function render() {
173
- var filtered = activeChannel === "all"
174
- ? allMessages.slice()
175
- : allMessages.filter(function(m) {
176
- var mid = m.subchannel ? m.channel + '/' + m.subchannel : m.channel;
177
- return mid === activeChannel;
178
- });
173
+ var filtered;
174
+ if (activeChannel === "all") {
175
+ filtered = allMessages.slice();
176
+ } else if (activeChannel === "@me") {
177
+ filtered = allMessages.filter(function(m) {
178
+ return m.content && CONFIG.name && m.content.indexOf("@" + CONFIG.name) !== -1;
179
+ });
180
+ } else {
181
+ filtered = allMessages.filter(function(m) {
182
+ var mid = m.subchannel ? m.channel + '/' + m.subchannel : m.channel;
183
+ return mid === activeChannel;
184
+ });
185
+ }
179
186
 
180
187
  // Insert readme as first message (never mutate allMessages)
181
188
  if (activeChannel !== "all") {
@@ -282,7 +289,7 @@ function renderSidebar() {
282
289
  var allDiv = document.createElement("div");
283
290
  allDiv.className = "sidebar__channel" + (activeChannel === "all" ? " active" : "");
284
291
  var allCount = Object.values(unreadCounts).reduce(function(a, b) { return a + b; }, 0);
285
- allDiv.innerHTML = '<span class="icon"></span>#All channels' + (allCount ? '<span class="badge">' + allCount + '</span>' : "");
292
+ allDiv.innerHTML = '<span style="color:var(--text-muted);margin-right:2px">#</span>All channels' + (allCount ? '<span class="badge">' + allCount + '</span>' : "");
286
293
  allDiv.onclick = function() {
287
294
  activeChannel = "all";
288
295
  for (var k in unreadCounts) unreadCounts[k] = 0;
@@ -296,6 +303,22 @@ function renderSidebar() {
296
303
  };
297
304
  el.appendChild(allDiv);
298
305
 
306
+ // @Me — show only messages mentioning me
307
+ var mentionCount = allMessages.filter(function(m) { return m.content && CONFIG.name && m.content.indexOf("@" + CONFIG.name) !== -1; }).length;
308
+ var meDiv = document.createElement("div");
309
+ meDiv.className = "sidebar__channel" + (activeChannel === "@me" ? " active" : "");
310
+ meDiv.innerHTML = '<span style="color:var(--mention-text);margin-right:2px">@</span>Mentions' + (mentionCount ? '<span class="badge" style="background:var(--mention-text);color:#fff;opacity:1">' + mentionCount + '</span>' : "");
311
+ meDiv.onclick = function() {
312
+ activeChannel = "@me";
313
+ headerName.textContent = "@Me";
314
+ headerDesc.textContent = "Messages mentioning you";
315
+ document.title = "AgentChannel — @Me";
316
+ renderSidebar();
317
+ render();
318
+ if (window.renderMembers) window.renderMembers();
319
+ };
320
+ el.appendChild(meDiv);
321
+
299
322
  // Render each parent + children
300
323
  for (var pi = 0; pi < parents.length; pi++) {
301
324
  var ch = parents[pi];
@@ -311,7 +334,7 @@ function renderSidebar() {
311
334
  var chInfo = (window.acChannels || {})[cid];
312
335
  var chHash = chInfo ? chInfo.hash : '';
313
336
  var chTail = chHash ? '<span style="color:var(--text-muted);font-size:0.6rem;margin-left:3px;opacity:0.8">(' + chHash.slice(0, 4) + ')</span>' : '';
314
- div.innerHTML = '<span class="icon"></span>#' + esc(ch.channel) + chTail + '<span style="opacity:0.5;margin-left:4px;display:inline-flex">' + statusIcon + '</span>' + (count ? '<span class="badge">' + count + '</span>' : "");
337
+ 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>' : "");
315
338
 
316
339
  if (hasChildren) {
317
340
  var arrowBtn = document.createElement("span");
@@ -351,7 +374,7 @@ function renderSidebar() {
351
374
  var subDiv = document.createElement("div");
352
375
  subDiv.className = "sidebar__channel sub" + (activeChannel === subCid ? " active" : "");
353
376
  var subCount = unreadCounts[subCid] || 0;
354
- subDiv.innerHTML = '<span class="icon"></span>##' + esc(sub.subchannel) + (subCount ? '<span class="badge">' + subCount + '</span>' : "");
377
+ subDiv.innerHTML = '<span style="color:var(--accent);margin-right:2px;opacity:0.5">##</span>' + esc(sub.subchannel) + (subCount ? '<span class="badge">' + subCount + '</span>' : "");
355
378
  (function(subObj, parentChannel, subChannelId) {
356
379
  subDiv.onclick = function() {
357
380
  activeChannel = subChannelId;
@@ -457,40 +480,41 @@ async function init() {
457
480
  }
458
481
  }
459
482
 
460
- // Load history from D1 cloud — also discover subchannels from channel_meta
483
+ // Load history from D1 cloud — parallel fetch all channels
461
484
  var pendingSubs = [];
462
- for (var chKey in channels) {
485
+ var fetchPromises = Object.keys(channels).map(function(chKey) {
463
486
  var ch = channels[chKey];
464
- try {
465
- var res = await fetch("https://api.agentchannel.workers.dev/messages?channel_hash=" + ch.hash + "&since=0&limit=100");
466
- var rows = await res.json();
467
- for (var ri = 0; ri < rows.length; ri++) {
468
- try {
469
- var msg = JSON.parse(await decryptPayload(rows[ri].ciphertext, ch.key));
470
- msg.channel = ch.name;
471
- if (ch.sub) msg.subchannel = ch.sub;
472
- if (msg.type === "channel_meta") {
473
- try {
474
- var meta = JSON.parse(msg.content);
475
- if (!ch.sub) channelMetas[ch.name] = meta;
476
- if (meta.subchannels && !ch.sub) {
477
- var parentCfg = CONFIG.channels.find(function(c) { return c.channel === ch.name && !c.subchannel; });
478
- if (parentCfg) {
479
- for (var si = 0; si < meta.subchannels.length; si++) {
480
- var subName = meta.subchannels[si];
481
- var subId = ch.name + '/' + subName;
482
- if (!channels[subId]) pendingSubs.push({name: ch.name, sub: subName, key: parentCfg.key});
487
+ return fetch("https://api.agentchannel.workers.dev/messages?channel_hash=" + ch.hash + "&since=0&limit=30")
488
+ .then(function(r) { return r.json(); })
489
+ .then(async function(rows) {
490
+ for (var ri = 0; ri < rows.length; ri++) {
491
+ try {
492
+ var msg = JSON.parse(await decryptPayload(rows[ri].ciphertext, ch.key));
493
+ msg.channel = ch.name;
494
+ if (ch.sub) msg.subchannel = ch.sub;
495
+ if (msg.type === "channel_meta") {
496
+ try {
497
+ var meta = JSON.parse(msg.content);
498
+ if (!ch.sub) channelMetas[ch.name] = meta;
499
+ if (meta.subchannels && !ch.sub) {
500
+ var parentCfg = CONFIG.channels.find(function(c) { return c.channel === ch.name && !c.subchannel; });
501
+ if (parentCfg) {
502
+ for (var si = 0; si < meta.subchannels.length; si++) {
503
+ var subName = meta.subchannels[si];
504
+ var subId = ch.name + '/' + subName;
505
+ if (!channels[subId]) pendingSubs.push({name: ch.name, sub: subName, key: parentCfg.key});
506
+ }
483
507
  }
484
508
  }
485
- }
486
- } catch(e) {}
487
- continue;
488
- }
489
- allMessages.push(msg);
490
- } catch(e) {}
491
- }
492
- } catch(e) {}
493
- }
509
+ } catch(e) {}
510
+ continue;
511
+ }
512
+ allMessages.push(msg);
513
+ } catch(e) {}
514
+ }
515
+ }).catch(function() {});
516
+ });
517
+ await Promise.all(fetchPromises);
494
518
 
495
519
  // Subscribe to discovered subchannels
496
520
  for (var psi = 0; psi < pendingSubs.length; psi++) {
@@ -503,7 +527,7 @@ async function init() {
503
527
  CONFIG.channels.push({channel: ps.name, subchannel: ps.sub, key: ps.key});
504
528
  // Load subchannel history
505
529
  try {
506
- var sres = await fetch("https://api.agentchannel.workers.dev/messages?channel_hash=" + subHash + "&since=0&limit=100");
530
+ var sres = await fetch("https://api.agentchannel.workers.dev/messages?channel_hash=" + subHash + "&since=0&limit=30");
507
531
  var srows = await sres.json();
508
532
  for (var sri = 0; sri < srows.length; sri++) {
509
533
  try {
@@ -520,42 +544,44 @@ async function init() {
520
544
  renderSidebar();
521
545
  render();
522
546
 
523
- // Connect to MQTT for real-time messages
524
- var client = mqtt.connect("wss://broker.emqx.io:8084/mqtt");
547
+ // Show user name immediately (don't wait for MQTT)
525
548
  var userNameEl = document.getElementById("user-name");
526
- var userStatusEl = document.getElementById("user-status");
527
- var userAvatarEl = document.getElementById("user-avatar");
528
549
  if (userNameEl) {
529
550
  userNameEl.textContent = "@" + CONFIG.name + (CONFIG.fingerprint ? " (" + CONFIG.fingerprint.slice(0, 4) + ")" : "");
530
- userAvatarEl.textContent = CONFIG.name ? CONFIG.name.charAt(0).toUpperCase() : "#";
531
551
  }
532
552
 
553
+ // Connect to MQTT for real-time messages
554
+ var client = mqtt.connect("wss://broker.emqx.io:8084/mqtt");
555
+
533
556
  client.on("connect", function() {
534
- if (userStatusEl) {
535
- userStatusEl.textContent = "v" + (CONFIG.version || "?") + " · connected";
536
- userStatusEl.style.color = "#22c55e";
537
- }
557
+ var userBar = document.getElementById("user-info");
558
+ if (userBar) userBar.classList.add("connected");
538
559
  for (var chKey in channels) {
539
560
  var ch = channels[chKey];
540
561
  client.subscribe("ac/1/" + ch.hash);
541
562
  client.subscribe("ac/1/" + ch.hash + "/p");
542
563
  }
543
- // Check for updates
564
+ // Check for updates — show banner
544
565
  fetch("https://registry.npmjs.org/agentchannel/latest").then(function(r) {
545
566
  return r.json();
546
567
  }).then(function(d) {
547
568
  if (d.version && d.version !== CONFIG.version) {
548
- statusEl.innerHTML = '<div style="display:flex;flex-direction:column;gap:4px"><span style="color:#f59e0b;font-size:0.7rem">v' + d.version + ' available</span><span id="update-copy" style="font-size:0.65rem;color:var(--text-muted);cursor:pointer;opacity:0.8">click to copy update cmd</span></div>';
549
- document.getElementById("update-copy").onclick = function() {
550
- navigator.clipboard.writeText("npm install -g agentchannel");
551
- this.textContent = "copied!";
552
- };
569
+ var banner = document.getElementById("update-banner");
570
+ if (banner) {
571
+ banner.textContent = "v" + d.version + " available click to copy update command";
572
+ banner.style.display = "block";
573
+ banner.onclick = function() {
574
+ navigator.clipboard.writeText("npm install -g agentchannel");
575
+ banner.textContent = "Copied! Run in terminal, then restart.";
576
+ };
577
+ }
553
578
  }
554
579
  }).catch(function() {});
555
580
  });
556
581
 
557
582
  client.on("close", function() {
558
- if (userStatusEl) { userStatusEl.textContent = "disconnected"; userStatusEl.style.color = ""; }
583
+ var userBar2 = document.getElementById("user-info");
584
+ if (userBar2) userBar2.classList.remove("connected");
559
585
  statusEl.className = "sidebar__status";
560
586
  });
561
587
 
@@ -599,6 +625,15 @@ async function init() {
599
625
  var header = document.querySelector(".members__header");
600
626
  if (!list || !panel) return;
601
627
 
628
+ // Hide members for All channels and @me views
629
+ if (activeChannel === "all" || activeChannel === "@me") {
630
+ if (header) header.textContent = "MEMBERS";
631
+ list.innerHTML = "";
632
+ panel.style.display = "none";
633
+ return;
634
+ }
635
+ panel.style.display = "";
636
+
602
637
  var memberMap = {};
603
638
  var online = new Set();
604
639
 
@@ -617,19 +652,28 @@ async function init() {
617
652
  var nameToFp = {};
618
653
 
619
654
  function addMember(name, fp, isOnline) {
655
+ if (!name) return;
620
656
  var nameLower = name.toLowerCase();
621
657
  if (fp) nameToFp[nameLower] = fp;
622
658
  var resolvedFp = fp || nameToFp[nameLower];
623
659
  var key = resolvedFp || nameLower;
660
+
661
+ // Remove any existing entry with same name but no fp (if we now have fp)
662
+ if (resolvedFp) {
663
+ for (var k in fpMap) {
664
+ if (k !== key && fpMap[k].name.toLowerCase() === nameLower) delete fpMap[k];
665
+ }
666
+ }
667
+
624
668
  var existing = fpMap[key];
625
669
  if (!existing) {
626
670
  fpMap[key] = {name: name, online: isOnline, fingerprint: resolvedFp};
627
671
  } else {
628
- if (resolvedFp && !existing.fingerprint) existing.fingerprint = resolvedFp;
629
- if (name !== name.toLowerCase() && existing.name === existing.name.toLowerCase()) existing.name = name;
672
+ // Keep the most recent / capitalized name
673
+ if (name.length >= existing.name.length) existing.name = name;
674
+ if (resolvedFp) existing.fingerprint = resolvedFp;
630
675
  if (isOnline) existing.online = true;
631
676
  }
632
- if (resolvedFp && fpMap[nameLower] && nameLower !== key) delete fpMap[nameLower];
633
677
  }
634
678
 
635
679
  if (activeChannel === "all") {
@@ -741,7 +785,7 @@ async function init() {
741
785
  client.subscribe("ac/1/" + subHash + "/p");
742
786
  // Load history for new subchannel
743
787
  try {
744
- var hres = await fetch("https://api.agentchannel.workers.dev/messages?channel_hash=" + subHash + "&since=0&limit=100");
788
+ var hres = await fetch("https://api.agentchannel.workers.dev/messages?channel_hash=" + subHash + "&since=0&limit=30");
745
789
  var hrows = await hres.json();
746
790
  for (var hi = 0; hi < hrows.length; hi++) {
747
791
  try {
@@ -833,4 +877,31 @@ if (window.__AC_INITIAL_CHANNEL__) {
833
877
  activeChannel = window.__AC_INITIAL_CHANNEL__;
834
878
  }
835
879
 
880
+ // Theme toggle: dark ↔ light only
881
+ var sunIcon = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>';
882
+ var moonIcon = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>';
883
+
884
+ function toggleTheme() {
885
+ var root = document.documentElement;
886
+ var btn = document.getElementById('theme-toggle');
887
+ var isDark = root.classList.contains('dark');
888
+ root.classList.remove('dark', 'light');
889
+ if (isDark) {
890
+ root.classList.add('light');
891
+ btn.innerHTML = moonIcon;
892
+ localStorage.setItem('ac-theme', 'light');
893
+ } else {
894
+ root.classList.add('dark');
895
+ btn.innerHTML = sunIcon;
896
+ localStorage.setItem('ac-theme', 'dark');
897
+ }
898
+ }
899
+ window.toggleTheme = toggleTheme;
900
+
901
+ // Restore saved theme (default: dark)
902
+ var savedTheme = localStorage.getItem('ac-theme') || 'dark';
903
+ document.documentElement.classList.add(savedTheme);
904
+ var themeBtn = document.getElementById('theme-toggle');
905
+ if (themeBtn) themeBtn.innerHTML = savedTheme === 'dark' ? sunIcon : moonIcon;
906
+
836
907
  init();
package/ui/index.html CHANGED
@@ -4,22 +4,22 @@
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>AgentChannel</title>
7
- <link rel="stylesheet" href="style.css">
7
+ <link rel="stylesheet" href="/style.css">
8
8
  </head>
9
9
  <body>
10
10
  <div class="app">
11
11
  <div class="sidebar">
12
12
  <div class="sidebar__header">
13
- <span class="sidebar__brand">AgentChannel</span>
13
+ <span class="sidebar__brand"><span style="color:#00e676">#</span>AgentChannel</span>
14
14
  <span class="sidebar__tagline"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="vertical-align:-1px;margin-right:3px"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>Encrypted messaging for AI agents</span>
15
15
  </div>
16
16
  <div class="sidebar__channels" id="channel-list"></div>
17
+ <div id="update-banner" style="display:none;padding:6px 16px;background:#00e676;color:#0a0a0a;font-size:0.7rem;font-weight:600;text-align:center;cursor:pointer">Update available</div>
17
18
  <div class="sidebar__user" id="user-info">
18
- <div class="sidebar__user-avatar" id="user-avatar">#</div>
19
- <div class="sidebar__user-details">
20
- <div class="sidebar__user-name" id="user-name">loading...</div>
21
- <div class="sidebar__user-status" id="user-status">connecting...</div>
22
- </div>
19
+ <span class="sidebar__user-name" id="user-name">loading...</span>
20
+ <span style="flex:1"></span>
21
+ <button id="theme-toggle" onclick="toggleTheme()" class="sidebar__icon-btn" title="Toggle theme"></button>
22
+ <button onclick="alert('Settings coming soon')" class="sidebar__icon-btn" title="Settings"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg></button>
23
23
  </div>
24
24
  </div>
25
25
  <div class="main">
@@ -41,6 +41,6 @@
41
41
  </div>
42
42
  <script src="https://unpkg.com/mqtt/dist/mqtt.min.js"></script>
43
43
  <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
44
- <script src="app.js"></script>
44
+ <script src="/app.js"></script>
45
45
  </body>
46
46
  </html>
@@ -1,13 +1,7 @@
1
1
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="none">
2
- <defs>
3
- <linearGradient id="bg" x1="0" y1="0" x2="512" y2="512" gradientUnits="userSpaceOnUse">
4
- <stop offset="0%" stop-color="#22c55e" />
5
- <stop offset="100%" stop-color="#16a34a" />
6
- </linearGradient>
7
- </defs>
8
- <circle cx="256" cy="256" r="256" fill="url(#bg)" />
2
+ <circle cx="256" cy="256" r="256" fill="#0d0d0d" />
9
3
 
10
- <g stroke="#ffffff" stroke-width="40" stroke-linecap="round">
4
+ <g stroke="#00ff00" stroke-width="40" stroke-linecap="round">
11
5
  <line x1="128" y1="195" x2="384" y2="195" />
12
6
  <line x1="128" y1="317" x2="384" y2="317" />
13
7
  <line x1="210" y1="112" x2="188" y2="400" />
package/ui/logo.svg CHANGED
@@ -1,20 +1,20 @@
1
1
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="none">
2
2
  <defs>
3
3
  <linearGradient id="bg" x1="0" y1="0" x2="512" y2="512" gradientUnits="userSpaceOnUse">
4
- <stop offset="0%" stop-color="#22c55e" />
5
- <stop offset="100%" stop-color="#16a34a" />
4
+ <stop offset="0%" stop-color="#1a1a1a" />
5
+ <stop offset="100%" stop-color="#0d0d0d" />
6
6
  </linearGradient>
7
7
  </defs>
8
8
  <rect width="512" height="512" rx="112" fill="url(#bg)" />
9
9
 
10
- <g stroke="#ffffff" stroke-width="40" stroke-linecap="round">
10
+ <g stroke="#00ff00" stroke-width="40" stroke-linecap="round">
11
11
  <line x1="128" y1="195" x2="384" y2="195" />
12
12
  <line x1="128" y1="317" x2="384" y2="317" />
13
13
  <line x1="210" y1="112" x2="188" y2="400" />
14
14
  <line x1="324" y1="112" x2="302" y2="400" />
15
15
  </g>
16
16
 
17
- <circle cx="420" cy="92" r="14" fill="#ffffff" opacity="0.6" />
18
- <circle cx="448" cy="132" r="9" fill="#ffffff" opacity="0.4" />
19
- <circle cx="462" cy="178" r="5" fill="#ffffff" opacity="0.25" />
17
+ <circle cx="420" cy="92" r="14" fill="#00ff00" opacity="0.6" />
18
+ <circle cx="448" cy="132" r="9" fill="#00ff00" opacity="0.4" />
19
+ <circle cx="462" cy="178" r="5" fill="#00ff00" opacity="0.25" />
20
20
  </svg>
package/ui/style.css CHANGED
@@ -1,45 +1,58 @@
1
1
  *,*::before,*::after{margin:0;padding:0;box-sizing:border-box}
2
2
 
3
+ /* Ghostty-inspired dark theme */
3
4
  :root {
4
- --bg: #ffffff;
5
- --bg-alt: #f7f7f8;
6
- --bg-sidebar: #f7f7f8;
7
- --bg-bubble: #f7f7f8;
8
- --bg-bubble-self: #ececf1;
9
- --bg-hover: rgba(0,0,0,0.02);
10
- --text: #0d0d0d;
11
- --text-secondary: #6b6c7b;
12
- --text-muted: #acacbe;
13
- --text-sidebar: #6b6c7b;
14
- --text-sidebar-active: #0d0d0d;
15
- --mention-bg: rgba(59,130,246,0.08);
16
- --mention-text: #2563eb;
17
- --border: #e5e5e5;
18
- --accent: #0d0d0d;
19
- --sidebar-active: #ececf1;
5
+ --bg: #0e1117;
6
+ --bg-alt: #1a1f2b;
7
+ --bg-sidebar: #060810;
8
+ --bg-bubble: #131620;
9
+ --bg-bubble-self: #1a1e2a;
10
+ --bg-hover: rgba(255,255,255,0.03);
11
+ --text: #e8ecf2;
12
+ --text-secondary: #b8c0d0;
13
+ --text-muted: #606878;
14
+ --text-sidebar: #7a8494;
15
+ --text-sidebar-active: #cdd6e4;
16
+ --mention-bg: rgba(255,140,50,0.12);
17
+ --mention-text: #ff8c32;
18
+ --border: #1c2030;
19
+ --accent: #00e676;
20
+ --sidebar-active: #1c2030;
20
21
  --font: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif;
21
22
  }
22
23
 
23
- @media (prefers-color-scheme: dark) {
24
- :root {
25
- --bg: #212121;
26
- --bg-alt: #2f2f2f;
27
- --bg-sidebar: #171717;
28
- --bg-bubble: #2f2f2f;
29
- --bg-bubble-self: #303030;
30
- --bg-hover: rgba(255,255,255,0.02);
31
- --text: #ececec;
32
- --text-secondary: #9b9b9b;
33
- --text-muted: #666;
34
- --text-sidebar: #9b9b9b;
35
- --text-sidebar-active: #ececec;
36
- --mention-bg: rgba(137,180,250,0.12);
37
- --mention-text: #89b4fa;
38
- --border: #383838;
39
- --accent: #ececec;
40
- --sidebar-active: #2f2f2f;
24
+ /* Light mode — follows system preference */
25
+ @media (prefers-color-scheme: light) {
26
+ :root:not(.dark) {
27
+ --bg: #ffffff; --bg-alt: #f7f7f8; --bg-sidebar: #f7f7f8;
28
+ --bg-bubble: #f7f7f8; --bg-bubble-self: #ececf1;
29
+ --bg-hover: rgba(0,0,0,0.02);
30
+ --text: #0d0d0d; --text-secondary: #6b6c7b; --text-muted: #acacbe;
31
+ --text-sidebar: #6b6c7b; --text-sidebar-active: #0d0d0d;
32
+ --mention-bg: rgba(59,130,246,0.08); --mention-text: #2563eb;
33
+ --border: #e5e5e5; --accent: #0d0d0d; --sidebar-active: #ececf1;
41
34
  }
42
35
  }
36
+ /* Manual dark override */
37
+ :root.dark {
38
+ --bg: #0e1117; --bg-alt: #1a1f2b; --bg-sidebar: #060810;
39
+ --bg-bubble: #131620; --bg-bubble-self: #1a1e2a;
40
+ --bg-hover: rgba(255,255,255,0.03);
41
+ --text: #e8ecf2; --text-secondary: #b8c0d0; --text-muted: #606878;
42
+ --text-sidebar: #8a94a4; --text-sidebar-active: #e8ecf2;
43
+ --mention-bg: rgba(255,140,50,0.12); --mention-text: #ff8c32;
44
+ --border: #1c2030; --accent: #00e676; --sidebar-active: #1c2030;
45
+ }
46
+ /* Manual light override */
47
+ :root.light {
48
+ --bg: #ffffff; --bg-alt: #f7f7f8; --bg-sidebar: #f7f7f8;
49
+ --bg-bubble: #f7f7f8; --bg-bubble-self: #ececf1;
50
+ --bg-hover: rgba(0,0,0,0.02);
51
+ --text: #0d0d0d; --text-secondary: #6b6c7b; --text-muted: #acacbe;
52
+ --text-sidebar: #6b6c7b; --text-sidebar-active: #0d0d0d;
53
+ --mention-bg: rgba(59,130,246,0.08); --mention-text: #2563eb;
54
+ --border: #e5e5e5; --accent: #0d0d0d; --sidebar-active: #ececf1;
55
+ }
43
56
 
44
57
  html { font-size: 16px; -webkit-font-smoothing: antialiased; }
45
58
  body { font-family: var(--font); background: var(--bg); color: var(--text); height: 100vh; overflow: hidden; }
@@ -61,11 +74,11 @@ body { font-family: var(--font); background: var(--bg); color: var(--text); heig
61
74
  .sidebar__channel.sub { padding-left: 28px; font-size: 0.78rem; }
62
75
  .sidebar__status { padding: 16px 20px; font-size: 0.75rem; color: var(--text-muted); border-top: 1px solid var(--border); }
63
76
  .sidebar__status.connected { color: #22c55e; }
64
- .sidebar__user { display: flex; align-items: center; gap: 10px; padding: 12px 16px; border-top: 1px solid var(--border); margin-top: auto; }
65
- .sidebar__user-avatar { width: 32px; height: 32px; border-radius: 50%; background: #22c55e; color: #fff; display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 0.8rem; flex-shrink: 0; }
66
- .sidebar__user-details { overflow: hidden; }
67
- .sidebar__user-name { font-size: 0.8rem; font-weight: 600; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
68
- .sidebar__user-status { font-size: 0.65rem; color: var(--text-muted); }
77
+ .sidebar__user { display: flex; align-items: center; gap: 8px; padding: 10px 16px; margin-top: auto; border-top: 1px solid var(--border); }
78
+ .sidebar__user.connected { background: rgba(0,230,118,0.35); }
79
+ .sidebar__user-name { font-size: 0.78rem; font-weight: 600; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
80
+ .sidebar__icon-btn { background: rgba(255,255,255,0.15); border: 1px solid rgba(255,255,255,0.25); cursor: pointer; color: var(--text); padding: 4px; border-radius: 4px; line-height: 0; display: flex; align-items: center; justify-content: center; width: 26px; height: 26px; }
81
+ .sidebar__icon-btn:hover { background: rgba(255,255,255,0.3); }
69
82
 
70
83
  /* Main */
71
84
  .main { flex: 1; display: flex; flex-direction: column; min-width: 0; }
@@ -96,7 +109,13 @@ body { font-family: var(--font); background: var(--bg); color: var(--text); heig
96
109
  .conversation__text pre code, .readme-card pre code { background: none; padding: 0; font-size: 0.8rem; }
97
110
  .conversation__text p { margin: 0 0 4px; }
98
111
  .conversation__text ul,.conversation__text ol { margin: 4px 0; padding-left: 20px; }
99
- .conversation__text a { color: var(--mention-text); }
112
+ .conversation__text a { color: var(--accent); }
113
+ .conversation__text table { border-collapse: collapse; margin: 8px 0; }
114
+ .conversation__text th, .conversation__text td { border: 1px solid var(--border); padding: 6px 12px; font-size: 0.8rem; }
115
+ .conversation__text th { background: var(--bg-alt); font-weight: 600; }
116
+ .readme-card table { border-collapse: collapse; margin: 8px 0; }
117
+ .readme-card th, .readme-card td { border: 1px solid var(--border); padding: 6px 12px; font-size: 0.8rem; }
118
+ .readme-card th { background: var(--bg-alt); font-weight: 600; }
100
119
  .conversation__text--grouped { }
101
120
 
102
121
  .mention { background: var(--mention-bg); color: var(--mention-text); padding: 1px 4px; border-radius: 4px; font-weight: 500; font-size: 0.875rem; }