Skip to content

Commit c1e3ffe

Browse files
ralyodioclaude
andcommitted
feat(ai-sidebar): IRC slash-commands + status window (v3.6.0)
Previously only /join and /msg worked; everything else (/names, /list, /whois, /me, /nick, /topic, /mode…) was sent to the channel as literal text. Now: - A real command parser: known verbs handled, anything else passed through verbatim as a raw IRC command (e.g. /mode #chan +o nick). - /me sends a proper CTCP ACTION. - A '✻ status' window in the channel sidebar that collects server numerics (LIST/NAMES/WHOIS/MOTD replies, errors) and connection logs — which were previously dropped (system messages with no channel went nowhere). - irc.js surfaces ALL server numerics (was 4xx/5xx only) so command output actually appears. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent b31d527 commit c1e3ffe

27 files changed

Lines changed: 72 additions & 33 deletions

File tree

apps/desktop/extensions/ai-sidebar/chat.js

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,13 @@ if (qo) qo.addEventListener('click', (e) => { e.preventDefault(); chrome.tabs.cr
2323

2424
// --- IRC ------------------------------------------------------------------
2525
const STORE_KEY = 'ircConfig';
26+
const STATUS = '✻ status'; // server/system window (always present, pinned first)
2627
const irc = new IrcClient();
2728
const buffers = new Map(); // channel -> [{from,text,time,...}]
2829
let active = null;
2930

31+
function logStatus(text) { appendMsg({ channel: STATUS, sys: true, text, time: Date.now() }); }
32+
3033
function setStatus(text, kind) { const s = $('irc-status'); s.textContent = text; s.className = 'status ' + (kind || ''); }
3134

3235
function ensureBuffer(chan) {
@@ -72,19 +75,22 @@ function appendMsg(line, store = true) {
7275

7376
irc.addEventListener('status', (e) => {
7477
const { state, error } = e.detail;
75-
if (state === 'connecting') setStatus('Connecting…');
78+
if (state === 'connecting') { setStatus('Connecting…'); logStatus('Connecting…'); }
7679
else if (state === 'connected') {
7780
setStatus('Connected ✓', 'ok');
7881
$('irc-connect').classList.add('hidden');
7982
$('irc-client').classList.remove('hidden');
8083
$('irc-me').textContent = irc.nick;
84+
logStatus(`Connected as ${irc.nick}`);
8185
} else if (state === 'disconnected') {
8286
setStatus('Disconnected', 'err');
87+
logStatus('Disconnected');
8388
} else if (state === 'error') {
8489
setStatus(error || 'error', 'err');
90+
logStatus('Error: ' + (error || 'unknown'));
8591
}
8692
});
87-
irc.addEventListener('joined', (e) => { ensureBuffer(e.detail.channel); if (!active) selectChannel(e.detail.channel); });
93+
irc.addEventListener('joined', (e) => { ensureBuffer(e.detail.channel); if (!active || active === STATUS) selectChannel(e.detail.channel); });
8894
irc.addEventListener('message', (e) => { appendMsg(e.detail); maybeNotify(e.detail); });
8995

9096
// --- web notifications for incoming IRC messages --------------------------
@@ -119,7 +125,8 @@ if (chrome?.notifications?.onClicked) {
119125
}
120126
});
121127
}
122-
irc.addEventListener('system', (e) => { if (e.detail.channel) appendMsg({ channel: e.detail.channel, sys: true, text: e.detail.text }); });
128+
irc.addEventListener('system', (e) => { appendMsg({ channel: e.detail.channel || STATUS, sys: true, text: e.detail.text }); });
129+
irc.addEventListener('names', (e) => logStatus(`Names ${e.detail.channel}: ${(e.detail.names || []).join(' ')}`));
123130
irc.addEventListener('topic', (e) => { if (e.detail.channel === active) $('chan-title').textContent = `${e.detail.channel}${e.detail.topic}`; });
124131

125132
async function doConnect() {
@@ -145,13 +152,42 @@ $('join-form').addEventListener('submit', (e) => { e.preventDefault(); const v =
145152
$('say-form').addEventListener('submit', (e) => {
146153
e.preventDefault();
147154
const v = $('say-input').value;
148-
if (!v.trim() || !active) return;
149-
if (v.startsWith('/join ')) irc.join(v.slice(6).trim());
150-
else if (v.startsWith('/msg ')) { const [, who, ...rest] = v.split(' '); irc.say(who, rest.join(' ')); }
151-
else irc.say(active, v);
155+
if (!v.trim()) return;
156+
if (v[0] === '/') handleCommand(v.slice(1));
157+
else if (active && active !== STATUS) irc.say(active, v);
158+
else logStatus('Not in a channel — use /join #channel');
152159
$('say-input').value = '';
153160
});
154161

162+
// Slash-commands → real IRC. Known verbs get friendly handling; anything else
163+
// is passed through verbatim (e.g. "/mode #chan +o nick", "/who #chan").
164+
function handleCommand(raw) {
165+
const sp = raw.indexOf(' ');
166+
const cmd = (sp === -1 ? raw : raw.slice(0, sp)).toLowerCase();
167+
const rest = sp === -1 ? '' : raw.slice(sp + 1).trim();
168+
const args = rest ? rest.split(/\s+/) : [];
169+
const inChan = active && active !== STATUS;
170+
switch (cmd) {
171+
case 'join': case 'j': if (args[0]) irc.join(args[0]); break;
172+
case 'part': case 'leave': irc.part(args[0] || (inChan ? active : '')); break;
173+
case 'msg': case 'query': if (args[0]) irc.say(args[0], args.slice(1).join(' ')); break;
174+
case 'me':
175+
if (inChan && rest) {
176+
irc.send(`PRIVMSG ${active} :ACTION ${rest}`);
177+
appendMsg({ channel: active, from: irc.nick, text: rest, self: true, time: new Date().toISOString() });
178+
}
179+
break;
180+
case 'nick': if (args[0]) irc.send(`NICK ${args[0]}`); break;
181+
case 'topic': if (inChan) irc.send(`TOPIC ${active}${rest ? ' :' + rest : ''}`); break;
182+
case 'names': irc.send(`NAMES ${args[0] || (inChan ? active : '')}`.trim()); break;
183+
case 'list': irc.send(`LIST ${rest}`.trim()); break;
184+
case 'whois': if (args[0]) irc.send(`WHOIS ${args[0]}`); break;
185+
case 'quit': irc.quit(); break;
186+
case 'raw': case 'quote': if (rest) irc.send(rest); break;
187+
default: irc.send(raw); // generic passthrough — any other IRC command
188+
}
189+
}
190+
155191
// Prefill from saved config — and auto-connect if a full account is set up.
156192
(async () => {
157193
const { [STORE_KEY]: cfg } = await chrome.storage.local.get(STORE_KEY);

apps/desktop/extensions/ai-sidebar/irc.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,10 @@ export class IrcClient extends EventTarget {
147147
this.emit('status', { state: 'error', error: params[0] || 'server error' });
148148
break;
149149
default:
150-
if (/^[45]\d\d$/.test(command)) {
150+
// Surface ALL server numerics (LIST 322, NAMES 366, WHOIS 311-319,
151+
// MOTD 372, errors 4xx/5xx, …) to the status window so commands that
152+
// reply with numerics actually show output.
153+
if (/^\d{3}$/.test(command)) {
151154
this.emit('system', { channel: null, text: params.slice(1).join(' ') });
152155
}
153156
}

apps/desktop/extensions/ai-sidebar/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"manifest_version": 3,
33
"name": "TronBrowser",
4-
"version": "3.5.0",
4+
"version": "3.6.0",
55
"description": "TronBrowser — privacy-first, AI-native. Branded new tab, private search, CoinPay login, and a bring-your-own-keys AI sidebar.",
66
"icons": {
77
"16": "icons/icon-16.png",

apps/desktop/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@tronbrowser/desktop",
3-
"version": "3.5.0",
3+
"version": "3.6.0",
44
"private": true,
55
"description": "Desktop shell for the TronBrowser Chromium fork",
66
"type": "module",

apps/docs/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@tronbrowser/docs",
3-
"version": "3.5.0",
3+
"version": "3.6.0",
44
"private": true,
55
"description": "Documentation site",
66
"type": "module",

apps/extensions/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@tronbrowser/extensions",
3-
"version": "3.5.0",
3+
"version": "3.6.0",
44
"private": true,
55
"description": "TronBrowser extension store — pay $1, list your MV3 extension (tronbrowser.dev/store)",
66
"type": "module",

apps/mobile/app.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"slug": "tronbrowserdev",
55
"owner": "profullstack",
66
"scheme": "tronbrowser",
7-
"version": "3.5.0",
7+
"version": "3.6.0",
88
"orientation": "portrait",
99
"userInterfaceStyle": "dark",
1010
"platforms": [

apps/mobile/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@tronbrowser/mobile",
3-
"version": "3.5.0",
3+
"version": "3.6.0",
44
"private": true,
55
"description": "TronBrowser mobile (Expo / React Native) — Phase 2",
66
"type": "module",

apps/web/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@tronbrowser/web",
3-
"version": "3.5.0",
3+
"version": "3.6.0",
44
"private": true,
55
"description": "TronBrowser marketing site + web dashboard",
66
"type": "module",

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "tronbrowser",
3-
"version": "3.5.0",
3+
"version": "3.6.0",
44
"private": true,
55
"description": "TronBrowser.dev — open-source, privacy-first, AI-native browser",
66
"packageManager": "pnpm@9.12.0",

0 commit comments

Comments
 (0)