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
Commit
c71a610781e93a16ba9db33b25a26c798f3eff0c5910459d5cc81ba5f9427a73
Parent
954037bea92bcc2…
1 file changed
+34
-6
+34
-6
| --- internal/api/ui/index.html | ||
| +++ internal/api/ui/index.html | ||
| @@ -159,10 +159,11 @@ | ||
| 159 | 159 | .chat-nicklist.collapsed { width:28px; overflow:hidden; } |
| 160 | 160 | .chat-nicklist.collapsed #nicklist-users { display:none; } |
| 161 | 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 | 162 | .nicklist-nick { padding:5px 12px; font-size:12px; color:#8b949e; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; } |
| 163 | 163 | .nicklist-nick.is-bot { color:#58a6ff; } |
| 164 | +.nicklist-nick.is-op { color:#3fb950; font-weight:600; } | |
| 164 | 165 | .nicklist-nick::before { content:"● "; font-size:8px; vertical-align:middle; } |
| 165 | 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; } |
| 166 | 167 | /* login screen */ |
| 167 | 168 | .login-screen { position:fixed; inset:0; background:#0d1117; z-index:200; display:flex; align-items:center; justify-content:center; flex-direction:column; } |
| 168 | 169 | .login-box { width:340px; } |
| @@ -1818,16 +1819,43 @@ | ||
| 1818 | 1819 | const slug = ch.replace(/^#/,''); |
| 1819 | 1820 | const data = await api('GET', `/v1/channels/${slug}/users`); |
| 1820 | 1821 | renderNicklist(data.users || []); |
| 1821 | 1822 | } catch(e) {} |
| 1822 | 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 | + | |
| 1823 | 1844 | function renderNicklist(users) { |
| 1824 | 1845 | 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>`; | |
| 1829 | 1857 | }).join(''); |
| 1830 | 1858 | } |
| 1831 | 1859 | // Nick colors — deterministic hash over a palette |
| 1832 | 1860 | const NICK_PALETTE = ['#58a6ff','#3fb950','#ffa657','#d2a8ff','#56d364','#79c0ff','#ff7b72','#a5d6ff','#f0883e','#39d353']; |
| 1833 | 1861 | function nickColor(nick) { |
| @@ -1975,12 +2003,12 @@ | ||
| 1975 | 2003 | |
| 1976 | 2004 | // On first Tab press with this prefix, build candidate list |
| 1977 | 2005 | if (_tabIdx === -1 || word.toLowerCase() !== _tabPrefix.toLowerCase()) { |
| 1978 | 2006 | _tabPrefix = word; |
| 1979 | 2007 | 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())); | |
| 1982 | 2010 | if (!nicks.length) return; |
| 1983 | 2011 | _tabCandidates = nicks; |
| 1984 | 2012 | _tabIdx = 0; |
| 1985 | 2013 | } else { |
| 1986 | 2014 | _tabIdx = (_tabIdx + 1) % _tabCandidates.length; |
| 1987 | 2015 |
| --- 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 |