ScuttleBot

ui: IRC mode conventions in chat user list (#58) - Operators shown with @ prefix, green bold - Agents shown with + prefix - System bots shown in blue (is-bot class) - User list sorted: ops > system bots > agents > regular users - Alphabetical within each tier - Tab completion strips @ and + prefixes for matching Closes #58

lmata 2026-04-04 04:30 trunk
Commit c71a610781e93a16ba9db33b25a26c798f3eff0c5910459d5cc81ba5f9427a73
1 file changed +34 -6
--- internal/api/ui/index.html
+++ internal/api/ui/index.html
@@ -159,10 +159,11 @@
159159
.chat-nicklist.collapsed { width:28px; overflow:hidden; }
160160
.chat-nicklist.collapsed #nicklist-users { display:none; }
161161
.nicklist-head { padding:8px 12px; font-size:11px; text-transform:uppercase; letter-spacing:.06em; color:#8b949e; border-bottom:1px solid #30363d; flex-shrink:0; display:flex; align-items:center; gap:4px; }
162162
.nicklist-nick { padding:5px 12px; font-size:12px; color:#8b949e; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
163163
.nicklist-nick.is-bot { color:#58a6ff; }
164
+.nicklist-nick.is-op { color:#3fb950; font-weight:600; }
164165
.nicklist-nick::before { content:"● "; font-size:8px; vertical-align:middle; }
165166
.chat-new-banner { align-self:center; margin:4px auto 0; background:#1f6feb; color:#fff; border-radius:20px; padding:3px 14px; font-size:12px; cursor:pointer; display:inline-block; white-space:nowrap; }
166167
/* login screen */
167168
.login-screen { position:fixed; inset:0; background:#0d1117; z-index:200; display:flex; align-items:center; justify-content:center; flex-direction:column; }
168169
.login-box { width:340px; }
@@ -1818,16 +1819,43 @@
18181819
const slug = ch.replace(/^#/,'');
18191820
const data = await api('GET', `/v1/channels/${slug}/users`);
18201821
renderNicklist(data.users || []);
18211822
} catch(e) {}
18221823
}
1824
+const SYSTEM_BOTS = new Set(['bridge','oracle','sentinel','steward','scribe','warden','snitch','herald','scroll','systembot','auditbot']);
1825
+const AGENT_PREFIXES = ['claude-','codex-','gemini-','openclaw-'];
1826
+
1827
+function nickTier(nick) {
1828
+ const lower = nick.toLowerCase();
1829
+ // Check if operator (registered as operator type).
1830
+ const agent = allAgents.find(a => a.nick === nick);
1831
+ if (agent && agent.type === 'operator') return 0; // ops
1832
+ if (SYSTEM_BOTS.has(lower)) return 1; // system bots
1833
+ if (AGENT_PREFIXES.some(p => lower.startsWith(p))) return 2; // agents
1834
+ return 3; // regular users
1835
+}
1836
+
1837
+function nickPrefix(nick) {
1838
+ const tier = nickTier(nick);
1839
+ if (tier === 0) return '@';
1840
+ if (tier === 2) return '+';
1841
+ return '';
1842
+}
1843
+
18231844
function renderNicklist(users) {
18241845
const el = document.getElementById('nicklist-users');
1825
- const knownBots = new Set(['bridge','oracle','sentinel','steward','scribe','warden','snitch','herald','scroll','systembot','auditbot','claude']);
1826
- el.innerHTML = users.sort((a,b) => a.localeCompare(b)).map(nick => {
1827
- const isBot = knownBots.has(nick.toLowerCase());
1828
- return `<div class="nicklist-nick${isBot?' is-bot':''}" title="${esc(nick)}">${esc(nick)}</div>`;
1846
+ // Sort: ops > system bots > agents > users, alpha within each tier.
1847
+ const sorted = users.slice().sort((a, b) => {
1848
+ const ta = nickTier(a), tb = nickTier(b);
1849
+ if (ta !== tb) return ta - tb;
1850
+ return a.localeCompare(b);
1851
+ });
1852
+ el.innerHTML = sorted.map(nick => {
1853
+ const tier = nickTier(nick);
1854
+ const prefix = nickPrefix(nick);
1855
+ const cls = tier === 1 ? ' is-bot' : tier === 0 ? ' is-op' : '';
1856
+ return `<div class="nicklist-nick${cls}" title="${esc(nick)}">${prefix}${esc(nick)}</div>`;
18291857
}).join('');
18301858
}
18311859
// Nick colors — deterministic hash over a palette
18321860
const NICK_PALETTE = ['#58a6ff','#3fb950','#ffa657','#d2a8ff','#56d364','#79c0ff','#ff7b72','#a5d6ff','#f0883e','#39d353'];
18331861
function nickColor(nick) {
@@ -1975,12 +2003,12 @@
19752003
19762004
// On first Tab press with this prefix, build candidate list
19772005
if (_tabIdx === -1 || word.toLowerCase() !== _tabPrefix.toLowerCase()) {
19782006
_tabPrefix = word;
19792007
const nicks = Array.from(document.querySelectorAll('#nicklist-users .nicklist-nick'))
1980
- .map(el => el.textContent.replace(/^●\s*/, '').trim())
1981
- .filter(n => n.toLowerCase().startsWith(word.toLowerCase()));
2008
+ .map(el => el.textContent.replace(/^[●@+]\s*/, '').trim())
2009
+ .filter(n => n.toLowerCase().startsWith(word.replace(/^@/, '').toLowerCase()));
19822010
if (!nicks.length) return;
19832011
_tabCandidates = nicks;
19842012
_tabIdx = 0;
19852013
} else {
19862014
_tabIdx = (_tabIdx + 1) % _tabCandidates.length;
19872015
--- internal/api/ui/index.html
+++ internal/api/ui/index.html
@@ -159,10 +159,11 @@
159 .chat-nicklist.collapsed { width:28px; overflow:hidden; }
160 .chat-nicklist.collapsed #nicklist-users { display:none; }
161 .nicklist-head { padding:8px 12px; font-size:11px; text-transform:uppercase; letter-spacing:.06em; color:#8b949e; border-bottom:1px solid #30363d; flex-shrink:0; display:flex; align-items:center; gap:4px; }
162 .nicklist-nick { padding:5px 12px; font-size:12px; color:#8b949e; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
163 .nicklist-nick.is-bot { color:#58a6ff; }
 
164 .nicklist-nick::before { content:"● "; font-size:8px; vertical-align:middle; }
165 .chat-new-banner { align-self:center; margin:4px auto 0; background:#1f6feb; color:#fff; border-radius:20px; padding:3px 14px; font-size:12px; cursor:pointer; display:inline-block; white-space:nowrap; }
166 /* login screen */
167 .login-screen { position:fixed; inset:0; background:#0d1117; z-index:200; display:flex; align-items:center; justify-content:center; flex-direction:column; }
168 .login-box { width:340px; }
@@ -1818,16 +1819,43 @@
1818 const slug = ch.replace(/^#/,'');
1819 const data = await api('GET', `/v1/channels/${slug}/users`);
1820 renderNicklist(data.users || []);
1821 } catch(e) {}
1822 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1823 function renderNicklist(users) {
1824 const el = document.getElementById('nicklist-users');
1825 const knownBots = new Set(['bridge','oracle','sentinel','steward','scribe','warden','snitch','herald','scroll','systembot','auditbot','claude']);
1826 el.innerHTML = users.sort((a,b) => a.localeCompare(b)).map(nick => {
1827 const isBot = knownBots.has(nick.toLowerCase());
1828 return `<div class="nicklist-nick${isBot?' is-bot':''}" title="${esc(nick)}">${esc(nick)}</div>`;
 
 
 
 
 
 
 
1829 }).join('');
1830 }
1831 // Nick colors — deterministic hash over a palette
1832 const NICK_PALETTE = ['#58a6ff','#3fb950','#ffa657','#d2a8ff','#56d364','#79c0ff','#ff7b72','#a5d6ff','#f0883e','#39d353'];
1833 function nickColor(nick) {
@@ -1975,12 +2003,12 @@
1975
1976 // On first Tab press with this prefix, build candidate list
1977 if (_tabIdx === -1 || word.toLowerCase() !== _tabPrefix.toLowerCase()) {
1978 _tabPrefix = word;
1979 const nicks = Array.from(document.querySelectorAll('#nicklist-users .nicklist-nick'))
1980 .map(el => el.textContent.replace(/^●\s*/, '').trim())
1981 .filter(n => n.toLowerCase().startsWith(word.toLowerCase()));
1982 if (!nicks.length) return;
1983 _tabCandidates = nicks;
1984 _tabIdx = 0;
1985 } else {
1986 _tabIdx = (_tabIdx + 1) % _tabCandidates.length;
1987
--- internal/api/ui/index.html
+++ internal/api/ui/index.html
@@ -159,10 +159,11 @@
159 .chat-nicklist.collapsed { width:28px; overflow:hidden; }
160 .chat-nicklist.collapsed #nicklist-users { display:none; }
161 .nicklist-head { padding:8px 12px; font-size:11px; text-transform:uppercase; letter-spacing:.06em; color:#8b949e; border-bottom:1px solid #30363d; flex-shrink:0; display:flex; align-items:center; gap:4px; }
162 .nicklist-nick { padding:5px 12px; font-size:12px; color:#8b949e; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
163 .nicklist-nick.is-bot { color:#58a6ff; }
164 .nicklist-nick.is-op { color:#3fb950; font-weight:600; }
165 .nicklist-nick::before { content:"● "; font-size:8px; vertical-align:middle; }
166 .chat-new-banner { align-self:center; margin:4px auto 0; background:#1f6feb; color:#fff; border-radius:20px; padding:3px 14px; font-size:12px; cursor:pointer; display:inline-block; white-space:nowrap; }
167 /* login screen */
168 .login-screen { position:fixed; inset:0; background:#0d1117; z-index:200; display:flex; align-items:center; justify-content:center; flex-direction:column; }
169 .login-box { width:340px; }
@@ -1818,16 +1819,43 @@
1819 const slug = ch.replace(/^#/,'');
1820 const data = await api('GET', `/v1/channels/${slug}/users`);
1821 renderNicklist(data.users || []);
1822 } catch(e) {}
1823 }
1824 const SYSTEM_BOTS = new Set(['bridge','oracle','sentinel','steward','scribe','warden','snitch','herald','scroll','systembot','auditbot']);
1825 const AGENT_PREFIXES = ['claude-','codex-','gemini-','openclaw-'];
1826
1827 function nickTier(nick) {
1828 const lower = nick.toLowerCase();
1829 // Check if operator (registered as operator type).
1830 const agent = allAgents.find(a => a.nick === nick);
1831 if (agent && agent.type === 'operator') return 0; // ops
1832 if (SYSTEM_BOTS.has(lower)) return 1; // system bots
1833 if (AGENT_PREFIXES.some(p => lower.startsWith(p))) return 2; // agents
1834 return 3; // regular users
1835 }
1836
1837 function nickPrefix(nick) {
1838 const tier = nickTier(nick);
1839 if (tier === 0) return '@';
1840 if (tier === 2) return '+';
1841 return '';
1842 }
1843
1844 function renderNicklist(users) {
1845 const el = document.getElementById('nicklist-users');
1846 // Sort: ops > system bots > agents > users, alpha within each tier.
1847 const sorted = users.slice().sort((a, b) => {
1848 const ta = nickTier(a), tb = nickTier(b);
1849 if (ta !== tb) return ta - tb;
1850 return a.localeCompare(b);
1851 });
1852 el.innerHTML = sorted.map(nick => {
1853 const tier = nickTier(nick);
1854 const prefix = nickPrefix(nick);
1855 const cls = tier === 1 ? ' is-bot' : tier === 0 ? ' is-op' : '';
1856 return `<div class="nicklist-nick${cls}" title="${esc(nick)}">${prefix}${esc(nick)}</div>`;
1857 }).join('');
1858 }
1859 // Nick colors — deterministic hash over a palette
1860 const NICK_PALETTE = ['#58a6ff','#3fb950','#ffa657','#d2a8ff','#56d364','#79c0ff','#ff7b72','#a5d6ff','#f0883e','#39d353'];
1861 function nickColor(nick) {
@@ -1975,12 +2003,12 @@
2003
2004 // On first Tab press with this prefix, build candidate list
2005 if (_tabIdx === -1 || word.toLowerCase() !== _tabPrefix.toLowerCase()) {
2006 _tabPrefix = word;
2007 const nicks = Array.from(document.querySelectorAll('#nicklist-users .nicklist-nick'))
2008 .map(el => el.textContent.replace(/^[●@+]\s*/, '').trim())
2009 .filter(n => n.toLowerCase().startsWith(word.replace(/^@/, '').toLowerCase()));
2010 if (!nicks.length) return;
2011 _tabCandidates = nicks;
2012 _tabIdx = 0;
2013 } else {
2014 _tabIdx = (_tabIdx + 1) % _tabCandidates.length;
2015

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button